英文:
why i cannot get data from api ? my live data "test" value equal null. how can i solve this?
问题
When you changed your HomeViewModel
to use a single MutableLiveData
for test
, it worked, but you encountered issues with filtering data. The reason for this difference in behavior is related to how LiveData and its updates work.
In the previous version of your HomeViewModel
, you were assigning a new value to _test
each time you called one of the filtering functions (getByStatusAndGender
, getByStatus
, getByGender
). This means that each time you applied a filter, you were creating a new LiveData instance, and the observers in your HomeFragment
were observing the old instance, which resulted in null data.
In the updated version of your HomeViewModel
, you have a single test
LiveData that you update inside each filtering function. This ensures that your observers in HomeFragment
are always observing the same LiveData instance, and as a result, they receive updates correctly.
If you want to address the filtering issue in the updated version, you should make sure that your filtering logic within each filtering function (getByStatusAndGender
, getByStatus
, getByGender
) correctly filters the data and updates the test
LiveData instance with the filtered data. It's essential to filter the data and assign it to the same test
instance without creating a new LiveData instance each time.
In summary, the key difference is how you handle the test
LiveData instance. In the updated version, you maintain a single instance, and you should ensure that your filtering logic correctly updates this instance with filtered data.
英文:
why i cannot get data from api ? my live data "test" value equal null. how can i solve this?
source code: https://github.com/ElmarShkrv/ChioreRickAndMorty
Paging source:
class HomeFragmentPagingSource(
private val status: String?,
private val gender: String?,
private val rickAndMortyApi: RickAndMortyApi,
) : PagingSource<Int, Characters>() {
override fun getRefreshKey(state: PagingState<Int, Characters>): Int? {
return null
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Characters> {
val pageNumber = params.key ?: 1
return try {
val response = rickAndMortyApi.getAllCharacters(status, gender, pageNumber)
val pagedResponse = response.body()
val data = pagedResponse?.results
var nextPageNumber: Int? = null
if (pagedResponse?.info?.next != null) {
val uri = Uri.parse(pagedResponse.info.next)
val nextPageQuery = uri.getQueryParameter("page")
nextPageNumber = nextPageQuery?.toInt()
}
LoadResult.Page(
data = data.orEmpty(),
prevKey = if (pageNumber == STARTING_PAGE_INDEX) null else pageNumber - 1,
nextKey = nextPageNumber
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
}
Repository:
class HomeRepository @Inject constructor(
private val rickAndMortyApi: RickAndMortyApi
) {
fun getAllCharacters() =
Pager(
config = PagingConfig(
pageSize = 20,
maxSize = 100,
enablePlaceholders = false
),
pagingSourceFactory = { HomeFragmentPagingSource(null, null, rickAndMortyApi) }
).liveData
fun getCharactersbyStatusAndGender(status: String, gender: String) =
Pager(
config = PagingConfig(
pageSize = 20,
maxSize = 100,
enablePlaceholders = false
),
pagingSourceFactory = { HomeFragmentPagingSource(status, gender, rickAndMortyApi) }
).liveData
fun getCharactersByStatus(status: String) =
Pager(
config = PagingConfig(
pageSize = 20,
maxSize = 100,
enablePlaceholders = false
),
pagingSourceFactory = { HomeFragmentPagingSource(status, null, rickAndMortyApi) }
).liveData
fun getCharactersByGender(gender: String) =
Pager(
config = PagingConfig(
pageSize = 20,
maxSize = 100,
enablePlaceholders = false
),
pagingSourceFactory = { HomeFragmentPagingSource(null, gender, rickAndMortyApi) }
).liveData
}
ViewModel:
@HiltViewModel
class HomeViewModel @Inject constructor(
private val repository: HomeRepository,
) : ViewModel() {
private var _test = MutableLiveData<PagingData<Characters>>()
val test: LiveData<PagingData<Characters>> = _test
// var test = MutableLiveData\<PagingData\<Characters\>\>()
var filterValue = MutableLiveData<Array<Int>>()
init {
filterValue.value = arrayOf(0, 0)
}
fun getAllCharacters(): LiveData<PagingData<Characters>> {
val response = repository.getAllCharacters().cachedIn(viewModelScope)
_test.value = response.value
return response
}
fun getByStatusAndGender(status: String, gender: String): LiveData<PagingData<Characters>> {
val response =
repository.getCharactersbyStatusAndGender(status, gender).cachedIn(viewModelScope)
_test.value = response.value
return response
}
fun getByStatus(status: String): LiveData<PagingData<Characters>> {
val response = repository.getCharactersByStatus(status).cachedIn(viewModelScope)
_test.value = response.value
return response
}
fun getByGender(gender: String): LiveData<PagingData<Characters>> {
val response = repository.getCharactersByGender(gender).cachedIn(viewModelScope)
_test.value = response.value
return response
}
FilterFragment:
@AndroidEntryPoint
class FilterFragment : BottomSheetDialogFragment() {
private lateinit var binding: FragmentFilterBinding
private val viewModel by viewModels<HomeViewModel>()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
binding = FragmentFilterBinding.inflate(inflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.apply {
viewModel.filterValue.observe(viewLifecycleOwner) { item ->
chipgroupStatus.setChipChecked(item[0])
radiogroupGender.setButtonChecked(item[1])
}
}
binding.apply {
btnMakeFilter.setOnClickListener {
if (chipgroupStatus.getTextChipChecked()
.isNotEmpty() && radiogroupGender.getTextButtonChecked().isNotEmpty()
) {
viewModel.getByStatusAndGender(
chipgroupStatus.getTextChipChecked(),
radiogroupGender.getTextButtonChecked(),
)
} else {
if (chipgroupStatus.getTextChipChecked().isNotEmpty()) {
viewModel.getByStatus(chipgroupStatus.getTextChipChecked())
} else {
viewModel.getByGender(radiogroupGender.getTextButtonChecked())
}
}
viewModel.filterValue.value = arrayOf(
chipgroupStatus.checkedChipId, radiogroupGender.checkedRadioButtonId
)
findNavController().popBackStack(R.id.homeFragment, false)
}
}
}
}
HomeFragment: (which i want to show data)
@AndroidEntryPoint
class HomeFragment : Fragment() {
private lateinit var binding: FragmentHomeBinding
private lateinit var homeRvAdapter: HomeRvAdapter
private val viewModel by viewModels<HomeViewModel>()
private val TAG = "Home"
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
binding = FragmentHomeBinding.inflate(inflater)
return binding.root
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.getAllCharacters()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setUpHomeRv()
initAdapter()
observeFilteredData()
binding.filterIv.setOnClickListener {
findNavController().navigate(R.id.action_homeFragment_to_filterFragment)
}
}
private fun observeFilteredData() {
viewModel.test.observe(viewLifecycleOwner) { filteredData ->
if (filteredData == null) {
Log.e(TAG, "filterdata equal null")
}
filteredData?.let {
homeRvAdapter.submitData(lifecycle, it)
}
}
}
private fun initAdapter() {
binding.homeRv.adapter = homeRvAdapter.withLoadStateHeaderAndFooter(
header = HomeLoadStateAdapter { homeRvAdapter.retry() },
footer = HomeLoadStateAdapter { homeRvAdapter.retry() },
)
homeRvAdapter.addLoadStateListener { loadState ->
binding.homeRv.isVisible = loadState.source.refresh is LoadState.NotLoading
binding.shimmerLayout.isVisible = loadState.source.refresh is LoadState.Loading
binding.tvHomeSearch.isInvisible = loadState.source.refresh is LoadState.Loading
binding.filterIv.isInvisible = loadState.source.refresh is LoadState.Loading
binding.retryBtn.isVisible = loadState.source.refresh is LoadState.Error
handleError(loadState)
}
binding.retryBtn.setOnClickListener {
homeRvAdapter.retry()
}
}
private fun handleError(loadState: CombinedLoadStates) {
val errorStates = loadState.source.append as? LoadState.Error
?: loadState.source.prepend as? LoadState.Error
errorStates?.let {
Toast.makeText(requireContext(), "${it.error}", Toast.LENGTH_LONG).show()
}
}
private fun setUpHomeRv() {
homeRvAdapter = HomeRvAdapter()
binding.apply {
homeRv.adapter = homeRvAdapter
homeRvAdapter.stateRestorationPolicy =
RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY
homeRv.addItemDecoration(
DefaultItemDecorator(
resources.getDimensionPixelSize(R.dimen.horizontal_margin),
resources.getDimensionPixelSize(R.dimen.vertical_margin)
)
)
}
}
}
when i change viewmodel like this:
@HiltViewModel
class HomeViewModel @Inject constructor(
private val repository: HomeRepository,
) : ViewModel() {
var test = MutableLiveData<PagingData<Characters>>()
var filterValue = MutableLiveData<Array<Int>>()
init {
filterValue.value = arrayOf(0, 0)
}
fun getAllCharacters(): LiveData<PagingData<Characters>> {
val response = repository.getAllCharacters().cachedIn(viewModelScope)
test = response as MutableLiveData<PagingData<Characters>>
return response
}
fun getByStatusAndGender(status: String, gender: String): LiveData<PagingData<Characters>> {
val response =
repository.getCharactersbyStatusAndGender(status, gender).cachedIn(viewModelScope)
test = response as MutableLiveData<PagingData<Characters>>
return response
}
fun getByStatus(status: String): LiveData<PagingData<Characters>> {
val response = repository.getCharactersByStatus(status).cachedIn(viewModelScope)
test = response as MutableLiveData<PagingData<Characters>>
return response
}
fun getByGender(gender: String): LiveData<PagingData<Characters>> {
val response = repository.getCharactersByGender(gender).cachedIn(viewModelScope)
test = response as MutableLiveData<PagingData<Characters>>
return response
}
it worked but i can't filter data why i get data this way but previous way i get null
答案1
得分: 0
你的第二种方法不起作用,因为当你调用过滤函数时,你用一个新的LiveData
对象(不是正在观察的那个对象)替换了test
中保存的LiveData
对象(正在观察的那个)。所以你的观察者永远不会看到更新。
我没有使用过分页库,但我猜想你的第一种方法失败是因为分页是异步发生的。所以当你首次从存储库接收到LiveData
时,它里面什么都没有(尚未获取结果的页面),所以它的value
是null。这就是你将其设置为test
值的原因。结果应该在某个时候传递给该分页器LiveData
,但你已经丢弃了它 - 你没有使用新值更新test
。
解决这个问题有很多方法(我觉得协程可能是最简单的方法),但既然你已经在使用LiveData
,你可以尝试使用switchMap
,它允许你创建一个LiveData
,该LiveData
输出你在其中切换的其他LiveData
对象的结果。
这是通过具有充当控制的LiveData
来实现的 - 当该值更改时,switchMap
调用一个函数,该函数返回一个LiveData
source。所以基本上,你需要一个控制对象,它保存当前的过滤类型,并且该函数需要*与存储库交互以获取一个分页LiveData
。
因为这不完全是你的ViewModel
代码当前的工作方式,你可能需要稍微重写它。首先,让我们获取一个过滤数据对象:
sealed class DataFilter {
object All : DataFilter()
data class StatusAndGender(val status: String, val gender: String) : DataFilter()
data class Status(val status: String) : DataFilter()
data class Gender(val gender: String) : DataFilter()
}
所以我们定义了所有可能的过滤类型,以及每个类型所包含的数据。(对于你的情况,你不需要这样做,你可以使用一个带有可为空的status
和gender
的数据类,根据提供的数据处理它,但这是一个更通用的方法,适用于任何情况)
然后,你需要一个私有的LiveData
来保存当前的过滤类型,以及一个公共函数来设置它:
// 在ViewModel中
private val _filter = MutableLiveData<DataFilter>() // 你可以添加一个默认值,如果需要的话
fun setFilter(filter: DataFilter) {
_filter.value = filter
}
然后,将test
设置为响应对_filter
的更改的内容:
val test = Transformations.switchMap(_filter) { filter ->
when(filter) {
is DataFilter.Status -> repository.getCharactersByStatus(filter.status)
is DataFilter.StatusAndGender -> repository.getCharactersByStatusAndGender(filter.status, filter.gender)
is DataFilter.Gender -> repository.getCharactersByGender(filter.gender)
DataFilter.All -> repository.getAllCharacters()
}.cachedIn(viewModelScope)
}
这基本上就完成了。你可以通过调用例如 setFilter(DataFilter.Status(whatever))
来进行过滤,switchMap
会对更改作出响应,通过调用适当的存储库函数,并使用返回的LiveData
作为test
的新值来源。由于test
本身永远不会被重新分配,观察它的任何对象都将看到所有传递的分页值作为过滤更改。
英文:
Your second way doesn't filter, because when you call your filtering functions you replace the LiveData
object held in test
(the one you're observing) with a new one (which you're not observing). So your observer never sees the updates.
I haven't used the paging library, but I'm assuming your first way is failing because the paging is happening asynchronously. So when you first receive the LiveData
from the repository, there's nothing in it (the page of results hasn't been fetched yet) so its value
is null. And that's what you're setting as the value of test
. A result should come in to that pager LiveData
at some point, but you've thrown it away - you're not updating test
with the new values.
There are a lot of ways to solve this (I feel like coroutines would be the easiest) but since you're already using LiveData
you could try using switchMap
, which lets you create a LiveData
that pumps out the results of other LiveData
objects you switch between.
This works by having a LiveData
which acts as your control - when the value of that changes, switchMap
calls a function which returns a LiveData
source. So basically, you need a control that holds the current type of filtering, and the function needs to *talk to the repo to fetch a pager LiveData
.
Because that's not exactly how your ViewModel
code currently works, you probably need to rewrite it a little. First let's get a filtering data object:
sealed class DataFilter {
object All : DataFilter()
data class StatusAndGender(val status: String, gender: String) : DataFilter()
data class Status(val status: String) : DataFilter()
data class Gender(val gender: String) : DataFilter()
}
So we're defining all the possible filter types, and the data each one holds. (For what you're doing you don't need this, you could use a single data class with nullable status
and gender
and handle it depending on what data is provided, but this is a more general approach that works for anything)
Then you'd have a private LiveData
holding the current filter type, and a public function that lets you set it:
// in the VM again
private _filter = MutableLiveData<DataFilter>() // you could add a default value if you want
fun setFilter(filter: DataFilter) {
_filter.value = filter
}
And then you wire up test
as something that reacts to changes in _filter
:
val test = Transformations.switchMap(_filter) { filter ->
when(filter) {
is DataFilter.Status -> repository.getCharactersByStatus(filter.status)
is DataFilter.StatusAndGender -> repository.getCharactersByStatusAndGender(filter.status, filter.gender)
is DataFilter.Gender -> repository.getCharactersByGender(filter.gender)
DataFilter.All -> repository.getAllCharacters()
}.cachedIn(viewModelScope)
}
And that should basically do it. You filter by calling e.g. setFilter(DataFilter.Status(whatever))
and the switchMap
reacts to the change by calling the appropriate repo function, and using the returned LiveData
as the new source of values for test
. Since test
itself is never reassigned, whatever observes it will see all the paged values piped in as filtering changes.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论