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
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)
}
}
}
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
}
@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
}
@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)
}
}
}
}
@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)
)
)
}
}
}
@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
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.