Paging library cachedIn() function doesn't work when using function - android

I am a little bit confused. I am trying to implement pagination 3 library. When I working on it I figured something out. If I use pagedItems() method to collect flow cachedIn() is not working. The function sends new request again after device rotated. However, if I use pagedItems value everything is perfectly fine.
The question is I've noticed that when I try to collect flows from these methods after device rotated pagedItems() function's hashcode is changing but pagedItems value's is not. What's the diffrence between them why one of them's hashcode is changing but other's not?
ViewModel
// Working perfectly fine
val pagedItems = useCase.invoke(PAGE_SIZE).cachedIn(viewModelScope)
// Working without caching feature
fun pagedItems(): Flow<PagingData<Result>> {
return useCase.invoke(PAGE_SIZE).cachedIn(viewModelScope)
}
Fragment
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
// It's working fine
launch {
viewModel.pagedItems.collect { pagedData ->
pagingAdapter.submitData(pagedData)
}
}
// It sends request again
launch {
viewModel.pagedItems().collect { pagedData ->
pagingAdapter.submitData(pagedData)
}
}
}

Somewhere in your useCase, you are creating an instance of Pager. In the val case, you do that only once and it's stored in your ViewModel which survives configuration changes. In the function case, you create new instance of Pager every time you call the function, which is every time your Fragment is recreated.

Related

Android Kotlin - How to use flow.CollectLatest{ } in a loop

I need to use flow.collectLatest {} in my fragment in OnViewCreated, and then in a loop under some condition multiple times (I made a filter, so each time different data should be retrieved).
This is my code for calling collectLatest:
viewLifecycleOwner.lifecycleScope.launch {
myViewModel.myFlow.collectLatest { pagingData ->
myAdapter.submitData(pagingData)
myAdapter.notifyDataSetChanged()
}
}
I use this block of code in both onViewCreated and in the loop.
However, it gets called only once, in OnViewCreated.
In the loop, sometimes it gets called, and then it needs 2-3min to retrieve data, but most of the time nothing changes.
I guess it could be an issue related to needing much more time to retrieve data, or it just shouldn't be used this way.
Some of the possible solutions I tried, but didn't work:
using delay
adding flowOn(Despatchers.IO) in the end of the flow
switching flow call to a different thread
You don't need a loop for this, should only collect it on onViewCreated() once time. You should have two flows(one for your filter and another for your data) and to use switcMap(), and your adapter/view should call viewModel to notify any change that to be done.
Here an example:
//you can use any object type for your filter, in this example i used a sealed class
private val _transactionFilter = MutableLiveData<TransactionFilter>(
TransactionFilter.TransactionsByDate(Date())
)
val transactions: LiveData<Data> = _transactionFilter.switchMap { filter ->
//code to return data
}
Kotlin Flow's have a switchMap() too, i used liveData because flow.switchMap was experimental yet.
Another thing: you don't need call notifiyDataSetChanged() when using ListAdapter

Use observe for a variable that updated inside another observe in Kotlin

I am trying first handle the response from API by using observe. Later after observing the handled variable I want to save it to database.
The variable tokenFromApi is updated inside tokenResponseFromApi's observer. Is it possible to observe tokenFromApi outside the observer of tokenResponseFromApi? When debugged, the code did not enter inside tokenFromApi observer when the app started.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
var tokenResponseFromApi: LiveData<String>? = MutableLiveData<String>()
var tokenFromApi: LiveData<TokenEntity>? = MutableLiveData<TokenEntity>()
tokenResponseFromApi?.observe(viewLifecycleOwner, Observer {
tokenResponseFromApi ->
if (tokenResponseFromApi != null) {
tokenFromApi = viewModel.convertTokenResponseToEntity(tokenResponseFromApi, dh.asDate)
}
})
tokenFromApi?.observe(viewLifecycleOwner, Observer {
tokenFromApi ->
if (tokenFromApi != null) {
viewModel.saveTokenToDB(repo, tokenFromApi)
}
})
}
Your problem is that you're registering the observer on tokenFromApi during setup, and when you get your API response, you're replacing tokenFromApi without registering an observer on it. So if it ever emits a value, you'll never know about it. The only observer you have registered is the one on the discarded tokenFromApi which is never used by anything
Honestly your setup here isn't how you're supposed to use LiveData. Instead of creating a whole new tokenFromApi for each response, you'd just have a single LiveData that things can observe. When there's a new value (like an API token) you set that on the LiveData, and all the observers see it and react to it. Once that's wired up, it's done and it all works.
The way you're doing it right now, you have a data source that needs to be taken apart, replaced with a new one, and then everything reconnected to it - every time there's a new piece of data, if you see what I mean.
Ideally the Fragment is the UI, so it reacts to events (by observing a data source like a LiveData and pushes UI events to the view model (someone clicked this thing, etc). That API fetching and DB storing really belongs in the VM - and you're already half doing that with those functions in the VM you're calling here, right? The LiveDatas belong in the VM because they're a source of data about what's going on inside the VM, and the rest of the app - they expose info the UI needs to react to. Having the LiveData instances in your fragment and trying to wire them up when something happens is part of your problem
Have a look at the App Architecture guide (that's the UI Layer page but it's worth being familiar with the rest), but this is a basic sketch of how I'd do it:
class SomeViewModel ViewModel() {
// private mutable version, public immutable version
private val _tokenFromApi = MutableLiveData<TokenEntity>()
val tokenFromApi: LiveData<TokenEntity> get() = _tokenFromApi
fun callApi() {
// Do your API call here
// Whatever callback/observer function you're using, do this
// with the result:
result?.let { reponse ->
convertTokenResponseToEntity(response, dh.asDate)
}?.let { token ->
saveTokenToDb(repo, token)
_tokenFromApi.setValue(token)
}
}
private fun convertTokenResponseToEntity(response: String, date: Date): TokenEntity? {
// whatever goes on in here
}
private fun saveTokenToDb(repo: Repository, token: TokenEntity) {
// whatever goes on in here too
}
}
so it's basically all contained within the VM - the UI stuff like fragments doesn't need to know anything about API calls, whether something is being stored, how it's being stored. The VM can update one of its exposed LiveData objects when it needs to emit some new data, update some state, or whatever - stuff that's interesting to things outside the VM, not its internal workings. The Fragment just observes whichever one it's interested in, and updates the UI as required.
(I know the callback situation might be more complex than that, like saving to the DB might involve a Flow or something. But the idea is the same - in its callback/result function, push a value to your LiveData as appropriate so observers can receive it. And there's nothing wrong with using LiveData or Flow objects inside the VM, and wiring those up so a new TokenEntity gets pushed to an observer that calls saveTokenToDb, if that kind of pipeline setup makes sense! But keep that stuff private if the outside world doesn't need to know about those intermediate steps

How to know when job from viewModel is done

I am trying to figure out how jobs with coroutines work. Basically, I want to launch this coroutine from FirstFragment and after that navigate to SecondFragment and get notified when this job is done. I call getData() in FirstFragment onViewCreated() and navigate to SecondFragment. Whether I write getData().isCompleted or getData().invokeOnCompletion { } in SecondFragment nothing happens. I don't know if I am missing something or not starting job correctly or something else.
private val _data = MutableStateFlow<GetResource<String>?>(null)
val data: StateFlow<GetResource<String>?> = _data
fun getData() = viewModelScope.launch {
repository.getData().collect {
_data.value = it
}
}
A Flow from a database never completes because it is supposed to monitor the database for changes indefinitely. It only stops when the coroutine is cancelled. Therefore the Job that collects such a Flow will never complete. Also, if you call getData() on the repo again, you are getting a new Flow instance each time.
Regardless of what you're doing, you need to be sure you are using the same ViewModel instance between both fragments by scoping it to the Activity. (Use by activityViewModels() for example.) This is so the viewModelScope won't be cancelled during the transition between Fragments.
If all you need is a single item from the repo one time, probably the simplest thing to do would be to expose a suspend function from the repo instead of a Flow. Then turn it into a Deferred. Maybe by making it a Lazy, you can selectively decide when to start retrieving the value. Omit the lazy if you just want to start retrieving the value immediately when the first Fragment starts.
// In the shared view model:
val data: Deferred<GetResource<String>> by lazy {
viewModelScope.async {
repository.getData() // suspend function returning GetResource<String>
}
}
fun startDataRetrieval() { data } // access the lazy property to start its coroutine
// In second fragment:
lifecycleScope.launch {
val value = mySharedViewModel.data.await()
// do something with value
}
But if you have to have the Flow because you’re using it for other purposes:
If you just want the first available value from the Flow, have the second Fragment monitor your data StateFlow for its first valid value.
lifecycleScope.launch {
val value = mySharedViewModel.data.filterNotNull().first()
// do something with first arrived value
}
And you can use SharedFlow so you don’t have to make the data type nullable. If you do this you can omit filterNotNull() above. In your ViewModel, it’s easier to do this with shareIn than your code that has to use a backing property and manually collect the source.
val data: SharedFlow<GetResource<String>> = repository.getData()
.shareIn(viewModelScope, replay = 1, SharingStarted.Eagerly)
If you need to wait before starting the collection to the SharedFlow, then you could make the property lazy.
Agreed with #Tenfour04 's answer, I would like to contribute a little more.
If you really want to control over the jobs or Structured Concurrency, i would suggest use custom way of handling the coroutine rather than coupled your code with the viewModelScope.
There are couple of things you need to make sure:
1- What happen when cancellation or exception occurrs
2- you have to manage the lifecycle of the coroutine(CoroutineScope)
3- Cancelling scope, depends on usecase like problem facing you are right now
4- Scope of ViewModel e.g: Either it is tied to activity(Shared ViewModel) or for specific fragment.
If you are not handling either of these carefully specifically first 3, your are more likely to leaking the coroutine your are gurenteed gonna get misbehavior of you app.
Whenever you start any coroutine in Custom way you have to make sure, what is going to be the lifecycle, when it gonna end, This is so important, it can cause real problems
Luckily, i have this sample of CustomViewModel using Jobs: Structured Concurrency sample code

Why does by repository Flow not update my viewModels livedata?

So currently I have a Dao with a function that emits a Flow<>
#Query("SELECT * FROM ${Constants.Redacted}")
fun loadAllContacts(): Flow<List<Redacted>>
I am calling this from a repository like so
val loadAllContacts: Flow<List<Redacted>> = contactDao.loadAllContacts()
I am injecting the repository into the viewModel's constructor, and then at the top of my viewModel I have a val like so
val contacts: LiveData<List<Redacted>> = contactRepository.loadAllContacts.asLiveData()
Which is being observed in my Activity like so
viewModel.contacts.observe(this) { contacts ->
viewModel.onContactsChange(contacts)
}
My thinking is that the Flow is converted to a LiveData, and then I can observe this LiveData from my activity and kick off this function to actually update the viewModel upon the data being updated.
For now onContactsChange just looks like
fun onContactsChange(list: List<Redacted>) {
Timber.i("VIEW UPDATE")
}
The problem is that I only see this Timber log upon opening the activity, and never again. I verified that data IS going into my database, and I verified that an insert occurred successfully while the activity & viewModel are open. But I never see the log from onContactsChange again. When I close the activity, and reopen it, I do see my new data, so that is another reason I know my insert is working correctly.
I would like to add that I am using a single instance (singleton) of my repository, and I think I can verify this by the fact that I can see my data at all, at least when the view is first made.
Figured it out:
Note: If your app runs in a single process, you should follow the singleton design pattern when instantiating an AppDatabase object. Each RoomDatabase instance is fairly expensive, and you rarely need access to multiple instances within a single process.
If your app runs in multiple processes, include enableMultiInstanceInvalidation() in your database builder invocation. That way, when you have an instance of AppDatabase in each process, you can invalidate the shared database file in one process, and this invalidation automatically propagates to the instances of AppDatabase within other processes.
It's a little bit hard to follow your question, but I think I see the overall problem with your Flow object not updating the way you want it too.
Following this quick tutorial, it seems that first you should declare your Flow object inside your Repository the same way you're already doing
val loadAllContacts: Flow<List<Redacted>> = contactDao.loadAllContacts()
and have your VM 'subscribe' to it by using the collect coroutine which would then allow you to dump all this data into a MutableLiveData State
data class YourState(..)
val state = MutableLiveData<YourState>()
init {
contactRepository.loadAllContacts().collect {
if (it.isNotEmpty()) {
state.postValue(YourState(
...
)
}
}
}
that your Activity/Fragment could then observe for changes
viewModel.state.observe(.. { state ->
// DO SOMETHING
})
P.S. The tutorial also mentions that because of how Dao's work, you might be getting updates for even the slightest of changes, but that you can use the distinctUntilChanged() Flow extension function to get more specific results.

Hot flow as final observable?

I recently discovered Flows in Kotlin and ever since been trying to optimize the code and making it more readable by avoiding MediatorLiveData and friends.
I try to use Flow in my back ends and finally present it as LiveData to the UI. However, I am having an issue with LiveData unsubscribing after a timeout, which causes the cold flows to recalculate (and make a new network request) each time the timeout is exceeded. Is there any way around it?
Here's the problem:
private val storeProducts = combine(_currentBasket, _currentProduct) { basket, product ->
// network operation
}
val storeProductsLive = storeProducts.asLiveData()
From the above example, my UI observes it as a normal livedata observable. When I rotate my screen, everything works fine and the last value is returned. However, if I navigate to a subfragment and go back after 5s or if I go out of the app and return later, the network operation is done again. the .asLiveData() specifies a default timeout of 5s, after that the Flow is cancelled.
My current workaround:
private val productsState = MutableStateFlow<List<Product>>(emptyList())
init {
viewModelScope.launch {
storeProducts.collect {
productsState.value = it
}
}
}
However, this is quite hacky and it means the observer is always registered, regardless whether the UI actually cares about it. Any way around it?
UPDATE
Within an hour, I found another workaround (described here: https://github.com/Kotlin/kotlinx.coroutines/issues/2140 ):
val storeProductsLive: LiveData<List<Product>> by lazy {
MutableStateFlow<List<Product>>(emptyList()).apply {
viewModelScope.launch {
storeProducts.collect { value = it }
}
}.asLiveData()
}
Is this the recommended way?

Categories

Resources