Launching a coroutine in ViewModel vs LifeCycleOwner (Activities/fragments) - android

Is it better to launch a coroutine in the ViewModel or to mark the ViewModel function with suspend modifier and launch the coroutine in the activity/fragment itself?
Launching in the ViewModel:
class MainViewModel: ViewModel() {
fun addNewItem(item: Item) {
viewModelScope.launch {
// Add the item to database
}
}
}
class ItemsFragment: Fragment() {
fun onButtonClick() {
viewModel.addNewItem(Item())
}
}
Launching in the LifeCycleOwner:
class MainViewModel: ViewModel() {
suspend fun addNewItem(item: Item) {
// Add the item to database
}
}
class ItemsFragment: Fragment() {
fun onButtonClick() {
lifecycleScope.launchWhenStarted {
viewModel.addNewItem(Item())
}
}
}

It really depends on your use-case and if you'd like to tie the task to the viewModelScope or the view's lifecycleScope.
For better understanding consider the following two examples:
The user triggers a refresh - probably you don't want to tie this to the view's lifecycleScope, since in case of an orientation-change your task will be killed and you'll have to restart the fetching of data again.
Animation or other View related task - suppose you have to do some calculations for an animation that's related to how the view is laid out. In this case after an orientation-change you might have to recalculate things, since the view changed.
Generally speaking you're more likely to bump into the first scenario I believe.
In your case adding an item goes into the 1st use-case, since suppose you're launching from the view directly and using lifecycleScope from Fragment/Actvity, in that case if the suspend saving is still running and an orientation-change happens, the task will get killed and your user will wonder why the item wasn't added.
With that said, db operations generally run so fast that the above scenario will be hard to reproduce, but throw in an api request or delay for testing purposes and you can check out the theory.

Related

How to run CoroutineScope in RecyclerViewCursorAdapter on ViewHolder inside bindCursor{ }

Note: I had implemented Coroutine Single Scope in RecyclerViewCursorAdapter but the app getting too slow when I change it to runBlocking it's working fine
Is there any way to execute CoroutineScope in ViewHolder inside bindCursor{ } on every scroll without impacting on performance ?
I need to execute multiple queries from the database on every scroll and update the UI accordingly.
I am pretty sure, there is a better way of doing what you are doing than to do it in adapter. I don't think that is correct, maybe you can use a callback via an interface or something.
But to return to your question, you should be easily be able to do this using lifeCycleScope
A LifecycleScope is defined for each Lifecycle object. Any coroutine launched in this scope is canceled when the Lifecycle is destroyed.
Since you have not mentioned whether it is an activity or an fragment that you are using, but the logic and functionality remains same for both. I am going to give you an example for Fragment
class MyFragment: Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
// Do stuff here
}
}
}
Just like that you can launch coroutines and it will be handled as per the lifecycle, you may need to pass the viewLifeCycleOwner to your Fragment, although as I said that is not recommended, you are better using an interface of sort and then computing in Fragment itself, but you can get the idea.

Sharing a cold flow of MutableStateFlow in ViewModel between two fragments

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.

run coroutine with parent's ViewModel scope

I just getting started with coroutines and I'm not quite sure whether I'm on the right way using it.
My android app has only 1 activity with several fragments and dialog fragments. I created a feature which asked user if he/she accepts to do something. The app shows a DialogFragment with Yes/No buttons. If user clicks Yes, it closes the dialog and does the job.
I would like to start the heavy job in activity's viewModelScope, so it will continue to execute at background event when user navigates to other fragments.
Parent's ViewModel:
class ActivityViewModel: ViewModel(){
fun doJob(){
viewModelScope.launch{
//Do the heavy job
}
}
}
Dialog Fragment ViewModel:
class DialogViewModel: ViewModel(){
var activityVM: ActivityViewModel
fun onYesClicked(){
activityVM.doJob()
}
}
I guess the job is executed under DialogFragment's ViewModel scope instead of Activity's ViewModel scope. It leads to an issue that when the job runs slower than expected, it's canceled because the dialog is dismissed.
I'm not sure if this is common practice as I can't find any similar discussion. Please help to point me where am I wrong on this code or there is a best practice for this case.
I ended up with a custom coroutine scope on the activity viewModel. By doing this, I manually cancel the coroutines when activity closing event instead of dialog fragment dismissing.
class ActivityViewModel: ViewModel(){
private val mainActivityScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
fun doJob(){
mainActivityScope.launch{
//Do the heavy job
}
}
override fun onCleared() {
super.onCleared()
mainActivityScope.cancel()
}
}
class DialogViewModel: ViewModel(){
var activityVM: ActivityViewModel
fun onYesClicked(){
activityVM.doJob()
}
}

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