Sharing a cold flow of MutableStateFlow in ViewModel between two fragments - android

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.

Related

Flow re-collecting using repeatOnLifecycle API

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

Observe live data objects in fragment using activity context?

I'm using navigation bottom with shared ViewModel with all fragments inside navigation bottom but it throws this exception when recall fragment second time
java.lang.IllegalArgumentException: Cannot add the same observer with different lifecycles
I have tried to make all observers attached to activity not to it's fragment as below
1-Declare viewModel in fragemt
viewModel = activity?.run {
ViewModelProviders.of(this,viewModelFactory).get(SharedViewModel::class.java)
} ?: throw Exception("Invalid Activity")
2-Observer livedata object
viewModel.msg.observe(activity!!, Observer {
Log.i(TAG,it)
})
3- remove observer
override fun onStop() {
super.onStop()
viewModel.msg.removeObservers(activity!!)
}
This code is working fine with me, but I wondering if my code is correct and working probably?
thanks in advance
It is a common mistake we do while using live-data in fragment. Using this/activity on fragment can be duplicate. You should use viewLifecycleOwner for livedata observing in fragment.
viewModel.msg.observe(viewLifecycleOwner, Observer {
Log.i(TAG,it)
})
For more information, read this article https://medium.com/#cs.ibrahimyilmaz/viewlifecycleowner-vs-this-a8259800367b
You don't need to remove the observer manually.
Why you are adding the observer to the fragment with the activity lifecycle? If you have some logic that needs to be executed when fragment is not active, add it to your activity. So instead of what you have, you need:
viewModel.msg.observe(this, Observer {
Log.i(TAG, it)
})
What happens in your case is that each time you reopen your fragment, you attach a new observer with the same lifecycle, which seems to be an error. Livedata observers were specifically designed so that you don't have to write code for handling lifecycles manually.

LiveDataScope vs ViewModelScope in Android

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.

Is it OK to launch coroutines from Globalscope on Android in certain situations (singletons)?

When launching coroutines from Activities, Fragments or Android Architecture Components ViewModels, it makes total sense to use a coroutine scope that is bound to the lifecycle of that view component in order to avoid leaks and free resources by e.g. canceling the network request when the user leaves the screen.
But there are other situations, where you don't want to cancel a coroutine even when the user leaves the screen like when you are performing a network request for analytics or writing into a database. Is it OK to launch coroutines with GlobalScope in such situations? The objects where these coroutines are launched are mostly Singletons, so they live for the lifetime of the application anyway, so there is no danger of leaks, right?
The Kotlin docs are pretty clear on GlobalScope:
Application code usually should use an application-defined CoroutineScope. Using async or launch on the instance of GlobalScope is highly discouraged.
Is it OK to use GlobalScope in these situations? If not, how should my application-defined CoroutineScope look like?
If you have an asynchronous worker whose lifecycles is truly global (they only die/end when your process dies), using GlobalScope or a similar life-long scope, is fine.
Say, you have an Activity that makes a request, but the actual network-request needs to continue even if the Activity goes away, because you'd like to cache it when the network finally returns a response.
You'll add a CoroutineScope to your Activity/Fragment, or better to your ViewModel and have your code that finally puts stuff on the screen run in that scope. When the Activity/Fragment/ViewModel dies, the scope is canceled and nothing will be attempted to show something on a screen that no longer exists.
However, your Fragment/Activity/ViewModel may talk to a data-source/repository that has a lifecycle that only ends when the process dies. You can switch to a GlobalScope in there so that your network-responses get cached, even when no Activity/Fragment/ViewModel is alive to show the result on the screen.
class MyViewModel(context: CoroutineContext, repo: MyRepository) : ViewModel() {
private val scope = CoroutineScope(context + SuperviserJob())
override fun onCleared() { scope.cancel() }
fun getDataFromNetwork() {
scope.launch {
myLiveData.value = repo.getDataFromNetwork()
}
}
}
// Singleton class
class MyRepositoryImpl(context: CoroutineContext) : MyRepository {
private val scope = CoroutineScope(context + SupervisorJob())
override suspend fun getDataFromNetwork() : String {
return scope.async { // switch scopes
val data = ... fetch data ...
saveInCache(data)
}.await()
}
}
When your ViewModel ends (onCleared is called), the MyRepositoryImpl's getDataFromNetwork still keeps running and will call saveInCache if all goes right. However, the value returned won't be assigned to myLiveData.value because the coroutine of your ViewModel's scope was cancelled.
Given that you're already trying to attach it to application's lifecycle, I'd suggest either passing the scope to your singleton or implementing a coroutinescope by it. Unfortunately, running coroutines on GlobalScope still might end in leaks.
See this great article by Roman Elizarov for more info:
https://medium.com/#elizarov/the-reason-to-avoid-globalscope-835337445abc

MVVM pattern and startActivity

I recently decided to have a closer look at the new Android Architecture Components that Google released, especially using their ViewModel lifecycle-aware class to a MVVM architecture, and LiveData.
As long as I'm dealing with a single Activity, or a single Fragment, everything is fine.
However, I can't find a nice solution to handle Activity switching.
Say, for the sake of a short example, that Activity A has a button to launch Activity B.
Where would the startActivity() be handled?
Following the MVVM pattern, the logic of the clickListener should be in the ViewModel. However, we want to avoid having references to the Activity in there. So passing the context to the ViewModel is not an option.
I narrowed down a couple of options that seem "OK", but was not able to find any proper answer of "here's how to do it.".
Option 1 : Have an enum in the ViewModel with values mapping to possible routing (ACTIVITY_B, ACTIVITY_C). Couple this with a LiveData.
The activity would observe this LiveData, and when the ViewModel decides that ACTIVITY_C should be launched, it'd just postValue(ACTIVITY_C). Activity can then call startActivity() normally.
Option 2 : The regular interface pattern. Same principle as option 1, but Activity would implement the interface. I feel a bit more coupling with this though.
Option 3 : Messaging option, such as Otto or similar. ViewModel sends a Broadcast, Activity picks it up and launches what it has to. Only problem with this solution is that, by default, you should put the register/unregister of that Broadcast inside the ViewModel. So doesn't help.
Option 4 : Having a big Routing class, somewhere, as singleton or similar, that could be called to dispatch relevant routing to any activity. Eventually via interface? So every activity (or a BaseActivity) would implement
IRouting { void requestLaunchActivity(ACTIVITY_B); }
This method just worries me a bit when your app starts having a lot of fragments/activities (because the Routing class would become humongous)
So that's it. That's my question. How do you guys handle this?
Do you go with an option that I didn't think of?
What option do you consider the most relevant and why?
What is the recommended Google approach?
PS : Links that didn't get me anywhere
1 - Android ViewModel call Activity methods
2 - How to start an activity from a plain non-activity java class?
NSimon, its great that you start using AAC.
I wrote a issue in the aac's-github before about that.
There are several ways doing that.
One solution would be using a
WeakReference to a NavigationController which holds the Context of the Activity. This is a common used pattern for handling context-bound stuff inside a ViewModel.
I highly decline this for several reasons. First: that usually means that you have to keep a reference to your NavigationController which fixes the context leak, but doesnt solve the architecture at all.
The best way (in my oppinion) is using LiveData which is lifecycle aware and can do all the wanted stuff.
Example:
class YourVm : ViewModel() {
val uiEventLiveData = SingleLiveData<Pair<YourModel, Int>>()
fun onClick(item: YourModel) {
uiEventLiveData.value = item to 3 // can be predefined values
}
}
After that you can listen inside your view for changes.
class YourFragmentOrActivity {
//assign your vm whatever
override fun onActivityCreated(savedInstanceState: Bundle?) {
var context = this
yourVm.uiEventLiveData.observe(this, Observer {
when (it?.second) {
1 -> { context.startActivity( ... ) }
2 -> { .. }
}
})
}
}
Take care that ive used a modified MutableLiveData, because else it will always emit the latest result for new Observers which leads to bad behaviour. For example if you change activity and go back it will end in a loop.
class SingleLiveData<T> : MutableLiveData<T>() {
private val mPending = AtomicBoolean(false)
#MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<T>) {
if (hasActiveObservers()) {
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
}
// Observe the internal MutableLiveData
super.observe(owner, Observer { t ->
if (mPending.compareAndSet(true, false)) {
observer.onChanged(t)
}
})
}
#MainThread
override fun setValue(t: T?) {
mPending.set(true)
super.setValue(t)
}
/**
* Used for cases where T is Void, to make calls cleaner.
*/
#MainThread
fun call() {
value = null
}
companion object {
private val TAG = "SingleLiveData"
}
}
Why is that attempt better then using WeakReferences, Interfaces, or any other solution?
Because this event split UI logic with business logic. Its also possible to have multiple observers. It cares about the lifecycle. It doesnt leak anything.
You could also solve it by using RxJava instead of LiveData by using a PublishSubject. (addTo requires RxKotlin)
Take care about not leaking a subscription by releasing it in onStop().
class YourVm : ViewModel() {
var subject : PublishSubject<YourItem> = PublishSubject.create();
}
class YourFragmentOrActivityOrWhatever {
var composite = CompositeDisposable()
onStart() {
YourVm.subject
.subscribe( { Log.d("...", "Event emitted $it") }, { error("Error occured $it") })
.addTo(compositeDisposable)
}
onStop() {
compositeDisposable.clear()
}
}
Also take care that a ViewModel is bound to an Activity OR a Fragment. You can't share a ViewModel between multiple Activities since this would break the "Livecycle-Awareness".
If you need that persist your data by using a database like room or share the data using parcels.
You should call startActivity from activity, not from viewmodel. If you want to open it from viewmodel, you need to create livedata in viewmodel with some navigation parameter and observe on livedata inside the activity
You can extend your ViewModel from AndroidViewModel, which has the application reference, and start the activity using this context.

Categories

Resources