why i cannot get data from api ? my live data "test" value equal null. how can i solve this?

huangapple go评论63阅读模式
英文:

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时,它里面什么都没有(尚未获取结果的页面),所以它的valuenull。这就是你将其设置为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()
}

所以我们定义了所有可能的过滤类型,以及每个类型所包含的数据。(对于你的情况,你不需要这样做,你可以使用一个带有可为空的statusgender的数据类,根据提供的数据处理它,但这是一个更通用的方法,适用于任何情况)

然后,你需要一个私有的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&lt;DataFilter&gt;() // 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 -&gt;
    when(filter) {
        is DataFilter.Status -&gt; repository.getCharactersByStatus(filter.status)
        is DataFilter.StatusAndGender -&gt; repository.getCharactersByStatusAndGender(filter.status, filter.gender)
        is DataFilter.Gender -&gt; repository.getCharactersByGender(filter.gender)
        DataFilter.All -&gt; 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.

huangapple
  • 本文由 发表于 2023年5月25日 04:08:18
  • 转载请务必保留本文链接:https://go.coder-hub.com/76327073.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定