Search code examples
androidkotlinandroid-recyclerviewandroid-viewmodelandroid-paging-3

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


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


Solution

  • 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.