When I collect a StateFlow in an Activity/Fragment using repeatOnLifecycle and then navigate to another activity then go back to the base one then the flow is re-collecting even if I don't update stateFlow.
For example:
in ViewModel
private var _deletionStatusStateFlow = MutableStateFlow(0)
val deletionStatusStateFlow = _deletionStatusStateFlow.asStateFlow()
then i observed it in Fragment:
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED){
deleteAccountViewModel.deletionStatusStateFlow.collect {
if (it == 1){
startActivity(AnyActivity)
}
}
}
}
it keeps open the activity every time I click onBackKey
but if i use LiveData with same example ... the observing block will not execute again (when coming back to the STATRED state in the Fragment -view- )
How do I achieve similar behavior to LiveData in StateFlow?
There's a solution for simple usage: it's when I use flowWithLifecycle(...).distinctUntilChanged()
but this is complex:
val results: StateFlow<SearchResult> =
queryFlow.mapLatest { query ->
repository.search(query)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000L),
initialValue = SearchResult.EMPTY
)
the up stream will be re-generated (and that will cost an upstream repo read)
This is expected behavior because Activities and Fragments can be recreated multiple times. That’s why repeatOnLifecycle exists in the first place.
You need to wrap your data in a class that also has a Boolean property indicating whether the associated navigation event has occurred. The collector in the Activity/Fragment, when it performs the navigation event, should also call a ViewModel function that the ViewModel uses to update the Flow to emit an event where the navigation event is considered handled. This is too complicated for stateIn. You’ll want to use a MutableStateFlow so you can update the values based on the feedback from the Activity.
This process is described in the Android documentation here.
Navigation is often best represented by a regular Flow (or a SharedFlow), rather than a StateFlow.
A navigation event should only be consumed once by collectors, and then discarded, as opposed to UI state which can be re-read as much as you like. If you do not discard the event (a Flow does this for you) you will get the behaviour you are seeing, where the navigation is triggered multiple times.
Considering this, I would expose navigation as a Flow from your view model, separate to any UI state flow you might have.
View Model
// Define our UI state flow
private val _stateFlow = MutableStateFlow(initialState) // Side note: this mutable flow does not need to use "var", we can and should use "val"
val stateFlow: StateFlow<UiState> = _stateFlow.asStateFlow()
// Define our navigation event flow
val navigationFlow: Flow<NavigationEvent> = ... // Logic to construct navigation flow goes here.
// You might use flow { ... } or a MutableSharedFlow<NavigationEvent> depending on your needs, but you only need to expose a Flow.
If you have more than one type of navigation event for this view model, you'll want to have them clearly defined, like this.
Models
sealed interface NavigationEvent {
object GoToXyzActivity : NavigationEvent
}
Now we'll collect the flows we've exposed from the view model in the Activity.
Activity
// Collect our UI state
viewLifecycleOwner.lifecycleScope.launch {
deleteAccountViewModel.stateFlow.collect {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
// Update the view based on the UI state here
}
}
}
// Collect our navigation events
viewLifecycleOwner.lifecycleScope.launch {
// If your navigation doesn't interact with the activity's view,
// we actually don't need to use `repeatOnLifecycle()`, since `startActivity()`
// does not required the view to exist to work - we could be in
// any stage of the activity's `Lifecycle` and that's fine.
deleteAccountViewModel.navigationFlow.collect { navEvent ->
when (navEvent) {
GoToXyzActivity -> startActivity(XyzActivity)
// Other navigation can go here
}
}
}
Just an FYI, this is from the Kotlin docs on StateFlows
Unlike a cold flow built using the flow builder, a StateFlow is hot: collecting from the flow doesn't trigger any producer code.
This means an upstream call to your repo won't be re-triggered as long as you're storing the result in a StateFlow in your view model somewhere.
Try changing lifecycle from started to created
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED)
to this code
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.CREATED)
the former was telling repeatOnLifecycle to repeat collect when lifecycle atleast Started, which mean every time lifecycle on Started State it would re-collect of said flow
unlike repeatOnLifecycle(Lifecycle.State.STARTED), the later would only re-collect flow when lifecycle was on CRATED state, which is only happen on creation of fragment
CMIMW
Related
I've got a problem with a MutableStateFlow property shared between two fragments.
To make it understandable:
I have a BasicViewModel which should be always one instance for both of the fragments because of the nav graph implementation
private val basicViewModel: basicViewModel by navGraphViewModels(R.id.basic_graph) { defaultViewModelProviderFactory }
This ViewModel have a MutableStateFlow property declared like this
private val _basicProperty = MutableStateFlow<BasicClass?>(null)
val basicProperty : Flow<BasicClass?> = _basicId
.filterNotNull()
.flatMapConcat { someRepository.getBasicProperty(it) }
.onEach { _basicProperty.value = it }
.catch { }
Then, I have FragmentA and FragmentB declared in navigation using nav graphs which calls the property similarly, like this
basicViewModel.basicProperty
.filterNotNull()
.mapNotNull { it.innerProperty}
.onEach { doSomething(it) }
.launchIn(viewLifecycleOwner.lifecycleScope)
It all looks fine, but when I navigate to FragmentA the flow of BasicProperty loads (data loads from WebApi) then, I navigate to FragmentB and the flow loads again instead of calling already loaded data which in App it looks kinda lagging because of the reload
Question: What should I do/change to get the already existing data from BasicViewModel in FragmentB?
Your _basicProperty is a hot StateFlow, but you never use it for collecting anything. The property you have exposed, basicProperty, is a cold flow, so each subscriber that collects it will start a new run of the cold flow. Each of these cold flows will be posting their updates to the MutableStateFlow, so at that point, its state is unpredictable because it's showing the latest thing any collector of the shared cold flow is doing.
I think what you want is for there to be one flow of execution that's shared. So you should have a single StateFlow that is performing the connection, like this:
val basicProperty : StateFlow<BasicClass?> = _basicId
.filterNotNull()
.flatMapConcat { someRepository.getBasicProperty(it) }
.catch { }
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
Your original code doesn't do anything to start the flow until each Fragment comes along to collect it. But this code's call to stateIn starts the flow a single time in the viewModelScope (in this case immediately because of the Eagerly parameter).
Now this flow only runs once. You can still have each Fragment run its own downstream flow like you were already doing.
I have some more complex logic for data provided by my ViewModel to the UI, so simply exposing the data via LiveData won't do the job for me. Now I've seen in the Android docs that I can implement Observable on my ViewModel to get the fine-grained control I need.
However in the documentation it also says:
There are situations where you might prefer to use a ViewModel
component that implements the Observable interface over using LiveData
objects, even if you lose the lifecycle management capabilities of
LiveData.
How intelligent is the built-in Android data binding? Will it automatically unregister it's listeners when necessary (e.g. on configuration changes where the View is destroyey) so that I don't have to care about the lost lifecycle capabilities? Or do I have to watch the Lifecycle of the View and unregister it's listeners? (=do manually what LiveData normally does for me).
How intelligent is the built-in Android data binding? Will it automatically unregister it's listeners when necessary (e.g. on configuration changes where the View is destroyey) so that I don't have to care about the lost lifecycle capabilities? Or do I have to watch the Lifecycle of the View and unregister it's listeners? (=do manually what LiveData normally does for me).
So I did some tests. I implemented androidx.databinding.Observable on my ViewModel and did a configuration change with the following log calls:
override fun removeOnPropertyChangedCallback(
callback: androidx.databinding.Observable.OnPropertyChangedCallback?) {
Log.d("APP:EVENTS", "removeOnPropertyChangedCallback " + callback.toString())
}
override fun addOnPropertyChangedCallback(
callback: androidx.databinding.Observable.OnPropertyChangedCallback?) {
Log.d("APP:EVENTS", "addOnPropertyChangedCallback " + callback.toString())
}
I saw that addOnPropertyChangedCallback was invoked for each time my viewmodel was referenced in a layout binding expression. And not once did I see removeOnPropertyChangedCallback invoked. My initial conclusion is that AndroidX databinding is dumb and does not automagically remove the listener.
FYI: the callback type was ViewDataBinding.WeakPropertyListener
However, I took a peek at ViewDataBinding.java source code and found that it is using Weak References to add the listener.
So what this implies, is that upon a configuration change, Android OS should be able to garbage collect your Activity/Fragment because the viewmodel does not have a strong reference.
My advice: Don't add the boilerplate to unregister the listeners. Android will not leak references to your activities and fragments on configuration changes.
Now, if you choose not to use LiveData, consider making your viewmodel implement LifecycleObserver so that you can re-emit the most recent value when your Activity/Fragment goes into the active state. This is the key behavior you lose by not using LiveData. Otherwise, you can emit notifications by using the PropertyChangeRegistry.notifyCallbacks() as mentioned in the documentation you shared at some other time. Unfortunately, I think this can only be used to notify for all properties.
Another thing... while I've not verified the behavior the source code seems to indicate that weak references are used for ObservableField, ObservableList, ObservableMap, etc.
LiveData is different for a couple of reasons:
The documentation for LiveData.observe says that a strong reference is held to both the observer AND the lifecycle owner until the lifecycle owner is destroyed.
LiveData emits differently than ObservableField. LiveData will emit whenever setValue or postValue are called without regard to if the value actually changes. This is not true for ObservableField. For this reason, LiveData can be used to send a somewhat "pseudo-event" by setting the same value more than once. An example of where this can be useful can be found on the Conditional Navigation page where multiple login failures would trigger multiple snackbars.
Nope. ViewModel will not unregister Observable subscription automatically. You can do it manually though. It is pretty easy.
Firstly you create CompositeDisposable
protected var disposables = CompositeDisposable()
Secondly, create your Observable(it may be some request or UI event listener) subscribe to it and assign its result to CompositeDisposable
disposables.add(
someObservable
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ data ->
// update UI or some ObservableFields for view/databinding
}, { exception ->
// handle errors here
})
)
The last thing you should do is to override ViewModel's method onCleared() like this:
override fun onCleared() {
super.onCleared()
disposables.clear()
}
This way all subscription added to your CompositeDisposable will be cleared automatically
Edit
I showed only the example. You may add triggers in onConfigurationChanged or onCreate or onResume to clear subscriptions as well - but it is dependent on specific usecases of an app. I gave just a general one.
Hope it helps.
DataBinding would not do the unregistering for you. Its simply help bind your layout file and the ViewModel. It is the viewModel that will protect you from device's configuration change. You still need to apply onSavedViewState() in your base activity or fragment as viewModel does not cover that. As per unregistering, LiveData does that.
As #Pavio already taught you how to create Observable, that is RxJava working. I would suggest using kotlin's coroutines and viewModel with LiveData to get the best out of your situation. Rx has a learning curve to it, although it does offer hundred of operators for all sorts of operations. If you really want to learn the kotlin way, look into kotlin flows and channels.
If i was in your place, I would solve my problem with ViewModels, LiveData and Coroutines.
I was reading how to use coroutines here https://developer.android.com/topic/libraries/architecture/coroutines. What makes me confused about is the difference between LiveDataScope and ViewModelScope. It sounds like ViewModelScope takes care of lifecycle automatically and you can do network request in the block. When data received from server, post the value to livedata. but then when I continued to read, there's another topic about LiveDataScope which seems redundant to me since you can already accomplish the same result by using ViewModelScope with livedata. What is the main difference between those two? and when should I choose to use one over the other?
Note: This might be late answer for this topic if Author of OP already has understanding about this, But providing some pointers for the referencing comment of #IgorGanapolsky.
Let's see what is the main difference between viewModelScope & LiveDataScope
1. viewModelScope:
Official doc says that, CoroutineScope tied to this ViewModel. This
scope will be canceled when ViewModel will be cleared, i.e
ViewModel.onCleared is called
Meaning that coroutine scope is tied to ViewModel, and once ViewModel gets cleared this scope gets destroyed by cancelling all child coroutine jobs.
Basically, in MVVM pattern we use ViewModel tied to a particular Activity/Fragment. So once that Activity/Fragment gets destroyed, its ViewModel reaches a cleared state. Thus, it cancels all incomplete jobs started by viewModelScope, throwing CancellationException.
So a usecase of viewModelScope is: inside ViewModel when you've got any suspended function to be called and need a CoroutineScope, inspite of making new one you can directly use this one out of the box from viewodel-ktx library.
class SomeViewModel: ViewModel() {
fun someFunction() {
viewModelScope.launch {
callingSomeSuspendedFun()
callingAnotherSuspendedFun()
}
}
}
Note that you don't need to explicitly override onCleared() method of ViewModel to cancel the scope, it does automatically for you, cheers!
2. LiveDataScope:
Now speaking of LiveDataScope, it's actually an interface provided to build better support for LiveData/CoroutineLiveData that can have CoroutineScope out of the box! use livedata-ktx version
Now imagine a situation that you're having a MVVM pattern and wanted to return LiveData from repository to view model. your repository also contains some suspended functions and some coroutine scope.
In that situation when you do some suspended method calls & return the result as live data, there would be some extra work. you'll need transform your data to particular live data after getting it as result. see the example below:
class SomeRepository {
suspended fun someApiCall() : LiveData<Result> {
val result = MutableLiveData<Result>()
someCoroutineScope.launch {
val someData = someOtherCallToGetResult()
result.postValue(someData)
}
return result
}
}
Imagine you had to write above code block due to LiveData didn't had any support for Coroutines ... but until now!
Now you can directly use liveData { } function that returns you LiveData object giving you scope of LiveDataScope in such a way that you can continue your suspended work and emit the result at the same level rather than getting it messy way like above. So above code block can now optimized by following code or better:
class SomeRepository {
suspended fun someApiCall() : LiveData<Result> {
return liveData<Result> {
val someData = someOtherCallToGetResult()
emit(someData)
}
}
}
So use case of liveData would be at repository level when using MVVM pattern if you expose LiveData to viewmodel from respository rather than creating new inside viewmodel. Please note that there's no thumb rule about liveData method shouldn't be used at viewmodel directly. You can if you want to avoid viewModelScope completely.
TL;DR
Check out the liveData method,
Doc states that, The liveData building block serves as a
structured concurrency primitive between coroutines and LiveData. The code block starts executing when LiveData becomes
active and is automatically canceled after a configurable timeout when
the LiveData becomes inactive. If it is canceled before completion,
it is restarted if the LiveData becomes active again. If it
completed successfully in a previous run, it doesn't restart. Note
that it is restarted only if canceled automatically. If the block is
canceled for any other reason (e.g. throwing a
CancelationException), it is not restarted.
I hope that make sense!
The names imply what they actually are:
A ViewModelScope is defined for each ViewModel in your app. Any
coroutine launched in this scope is automatically canceled if the
ViewModel is cleared.
This means that you can do some tasks(like continuous processing) in a coroutine that is in the scope of the ViewModel. The advantage is that you don't have to care anymore when the ViewModel will be stopped to stop your coroutine (this is a big pain when working with global things like java threads). The lifecycle of the ViewModel is related to when an activity is ended.
The LiveDataScope is used for emitting values in the scope of a LiveData object. This means that as long as the LiveData object is alive and there are subscribers that coroutine will work, however once all the subscribers are out the coroutine will stop. This coroutine also restarts once the LiveData is active again.
Basically these are 2 coroutine contexts each responsible for the lifecycle of its element.
PS:
It sounds like ViewModelScope takes care of lifecycle automatically
and you can do network request in the block.
First of all, network requests cannot be done from the Main thread, you usually do them from IO scope, you can read more here. The second thing is that you should take a look at the lifecycle of the ViewModel compared to Activity if you want to understand why LiveDataScope is usually combined with ViewModelScope, you can read about that here.
The short answer to your question is that you cannot be sure that the view is created from the ViewModelScope so if you want to push some updates to UI you should push them as long as someone is subscribed to LiveData, this is where the LiveDataScope comes into play.
this one question has been bothering me for 6 months, it is like a missing peace.. So, I really like LiveData and use it a lot, perhaps too much. Basically, all our fragments adding and removing is managed by LiveData. I have done it for several reasons:
We need to remove fragments in some cases, after onPause has occurred (odd, but a must for our use case).
We have only a single activity with fragments.
I have created a specific navigationViewModel which is shared across all fragments and is created in activity.
I add, remove fragments in this manner:
//ViewModel
...
val addFragmentNr3 = SingleLiveEvent<Boolean>()
//Activity or some fragment calls this:
navigationViewModel.addFragmentNr3.value = true
Then I observe LiveData in Activity and handling transition:
navigationViewModel.addFragmentNr3.observe(this, Observer { response ->
if (response != null) {
if (response) {
router.addFragmentNr3(supportFragmentManager)
}
}
})
Then router handles it:
fun addFragmentNr3(supportFragmentManager: FragmentManager) {
val fragmentNr3 = FragmentNr3()
supportFragmentManager.beginTransaction().replace(R.id.root_layout, fragmentNr3, FRAGMENT_NR_3.commit()}
In my honest opinion this should definitely prevent from this crash:java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
However, it does occur in our crash analytics.. It occurs rarely after more complex logic (like updating livedata after onActivityResult), but it does occur...
My main question is: Isn't it is a case, that LiveData handles such scenarios and would emit results only when it safe to perform operations? If not, it means my logic is bad and this approach is complete failure.
P.S. I would like to use navigation library, but as I said we have to manually remove some fragments after user goes to background, or uses split mode and etc.
LiveData does not know whether an action is safe to perform or not.
onSaveInstanceState() is called sometime before onStop() for Android version below P. So there is a small chance that the observer gets notified after onSaveInstanceState() is called.
According to doc, it turned out that onSaveInstanceState() should mark the lifecycle as CREATED and observers are not supposed to be called after onSaveInstanceState().
Suggestion on how to fix it.
One way is to use Android Navigation component and let it handle all of the fragment transaction.
If this is not feasible--like op's case--I suggests just using .commitAllowingStateLoss().
fun addFragmentNr3(supportFragmentManager: FragmentManager) {
val fragmentNr3 = FragmentNr3()
supportFragmentManager.beginTransaction().replace(R.id.root_layout, fragmentNr3, FRAGMENT_NR_3
.commitAllowingStateLoss()}
Now, if you search on the internet there will be dozens of articles warning how using .commitAllowingStateLoss() is bad. I believe these claims are no longer applicable to modern Android development where view restoration does not rely on saved bundles. If you are building an Android application with view models, you hardly need to rely on the Android framework to do the saving. In a proper MVVM application, the view should be designed in a way that it can restore its complete state based on its view models, and view models only.
I'm struggling with the correct way of refreshing data on the master-detail view using Architecture Components. I have a single-top master activity that displays a list of favourite movies. When I go to details view, add/remove movie from favourites and close the details view the master view's the data stays unsync. I initialize view model in the onCreate method:
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
moviesViewModel = ViewModelProviders
.of(this, viewModelFactory)
.get(MoviesViewModel.class);
moviesViewModel.getMovies(FAVOURITES).observe(this, this::show);
}
View Model is also quite straight forward:
#NonNull
LiveData<Resource<List<Movie>>> getMovies(Criterion criterion) {
movieRepository.fetchBy(criterion)
.doOnSubscribe(it -> moviesLiveData.setValue(Resource.loading()))
.subscribe(
movies -> moviesLiveData.setValue(Resource.success(movies)),
throwable -> moviesLiveData.setValue(Resource.error(throwable))
);
The simplest solution would be to force to refresh the view every time it becomes active:
#Override
public void onResume() {
super.onResume();
moviesViewModel.getMovies(FAVOURITES).observe(this, this::show);
}
But I don't really like this approach since it will result in a refresh when screen orientation changes and also when an activity comes from the background.
It's also possible to start details activity for result and refresh data only when it has changed, but I also don't think this is how it should be done this way in the reactive approach.
Moreover, I was thinking about subscribing for database changes in the cotent resolver and updating the Flowable with new content everytime data changes, but I'm convinced whether it will work since when it the change occurs the live data observer (master view) is in pause mode so it will not be notified, am I right?
There are a few ways you can manage refreshing your master view.
You could use a shared ViewModel for this situation. If both the master and detail view are fragments, all you have to do is use the same viewmodel, passing in the activity or the containing fragment as the scope, eg ViewModelProviders.of(parentFragment!!, viewModelFactory). Then your moviesLiveData can be observed by both master and detail pages. **Note that the latest version of Navigation allows you more finely defined scopes by tying the ViewModel to a specific nav graph.
If you are using activities for your pages, you can pass the movies results information back to the master page via startActivityForResult, setResult in the detail screen, and retrieve the information in onActivityResult
You can cache the result of the network request somewhere other than the viewmodel. Typically you might have a Singleton repository that retrieves your data. You could use LiveData in the same pattern as with ViewModels.
You can also observe your database as you suggest. Don't worry about it being in pause mode! That's the point of LiveData, it stores the latest update and delivers it again whenever something observes it.
You could use livedata also for the database in order to update changes from db -> vm -> ui