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()
}
}
Related
I am writing an app for fun. The app launches, does some networking stuff and shows its progress in the MainActivity.
I decided to do the networking stuff in the viewModel so it could survive activity-lifecycle-misery. But in order the show the progress on the UI I need a reference to the activity in the model to call functions on it, but it is considered a fatal mistake beacuse of the lifecycle-misery :D.
So, I've been thinking. If the reference variable in my viewModel is constantly changing as activities come and go and it always points to the current activity, then it should be alright.
So I need to declare a nullable, reassignable activity variable
class MyViewModel: ViewModel() {
var activity: MainActivity? = null
}
The solution, when the activity is created it assigns itself the variable. And when it is destroyed, it clears it with null. (This is similar to subscribing.)
override fun onCreate(savedInstanceState: Bundle?) {
// Initialization
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// My viewModel solution
viewModel = ViewModelProvider(this)[MyViewModel::class.java]
viewModel.activity = this
}
override fun onDestroy() {
viewModel.activity = null
super.onDestroy()
}
If I am right this creates no leaks or runtime errors whatsoever, my networking stuff survives the activity's death and I can conveniently call functions on the currently alive activity.
My Question is: Am I right?
Note: For avoiding thread-misery I use coroutines (with IO dispatchers) in the viewModelScope for networking stuff and call the activity's functions with the Main dispatchers.
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.
I am setting up my Fragment like suggested in Google docs as such:
private var _binding: MyBinding? = null
private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = MyBinding.inflate(inflater, container, false)
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
Now I'm calling a coroutine that to my understanding should be scoped to the lifecycle of this fragment. It has a longer network call, then a success:
lifecycleScope.launch(Dispatchers.Main) {
when (myViewModel.loadFromNetwork(url)) {
true -> responseSuccess()
false -> responseFailure()
}
}
private suspend fun responseSuccess() {
binding.stateSuccess.visibility = View.VISIBLE
// ...
}
Now when I press the Android system-back button while loadFromNetwork is still loading the fragment gets destroyed and onDestroyView() is called. As such binding now is null. I'm getting a kotlin.KotlinNullPointerException. What I not quite getting is why responseSuccess() is still being executed even though I thought that lifecycleScope is specifically meant for these kind of situations. According to Google Docs:
A LifecycleScope is defined for each Lifecycle object. Any coroutine launched in this scope is canceled when the Lifecycle is destroyed.
I understand this code can be fixed by a few changes and some manual null-checks, but I would like to understand how to fix this without boilerplate and in the intended manner. What is the purpose of using lifecycleScope to be lifecycle aware if not exactly this?
Coroutines cancellation is cooperative. It means it's a responsibility of a coroutine itself to check for cancellation. Most (or may be all) suspend operations in the coroutines library check for cancellation, but if you don't call any of them, you need to make your code cancellable as described here.
A better option to work with views in coroutines is to use lifecycle extensions which suspend / cancel the coroutine automatically when the lifecycle state is not in a required state.
Also please note, that cancellation is just a regular CancellationException, so check you don't accidentally catch it.
well it may not be a very clean way of handling it but I suggest to cancel the job in onDestroyView by your own self
define a job in class level like
lateinit var job:Job
then assign it like
job = lifecycleScope.launch(Dispatchers.Main) {
when (myViewModel.loadFromNetwork(url)) {
true -> responseSuccess()
false -> responseFailure()
}
and cancel it in onDestroView method before assigning null to _binding.
override fun onDestroyView() {
super.onDestroyView()
job.cancel()
_binding = null
}
the reason you get NULL POINTER EXCEPTION is that fragments have two lifecycles. 1)lifecycle and view lifecycle. _binding is assigned to null in onDestroyView but fragment lifecycle is still alive so coroutine's job is doing its work, when the network response arrives it run the launch block and wants to access binding object which is null by that time.
You are using lifecycleScope which is different to lifecycle of the fragment's view. So you have to use different scope viewLifecycleOwner.lifecycleScope.launch {}. Btw, the link to Google Docs exactly says so :)
i am using AndroidViewModel and returning stream of data be it Observable or LiveData, so far its going well, i see there is a method in ViewModel class, onCleared() document says
This method will be called when this ViewModel is no longer used and
will be destroyed.It is useful when ViewModel observes some data and
you need to clear this subscription to prevent a leak of this
ViewModel.
I have a scenario where i return Single<ApiResponse> from retrofit do some .map() in ViewModel and return response as Single<ToBeShownOnUiResponse> i subscribe this in View i.e Fragment.
I add subscriber to CompositeDisposable and thereafter clear it onStop fragment. When i navigate from LoginActivity(hold signin/signup/passwordreset fragment) to HomeActivity(hold tablayout with other fragments) i dont see the logs written in onCleared() method of ViewModel class. Is something wrong i am doing or i did a complete mess of it.
My query here is that in what way onCleared() is helpful to me. What separate code or cleanup i should be writing in it?
Usage:
When i need string resource then i use AndroidViewModel(Formatting some api response according to string resource present in xml) and when only api call require i use ViewModel.
One example use case of calling onCleared is when you use a ViewModel for in-app billing. In that case, it is natural to let the BillingClient to persist as long as an activity (In the below example I used Application), for example like this:
class BillingViewModel(application: Application)
: AndroidViewModel(application), PurchasesUpdatedListener, BillingClientStateListener {
private lateinit var playStoreBillingClient: BillingClient
init {
playStoreBillingClient = BillingClient.newBuilder(application.applicationContext)
.enablePendingPurchases()
.setListener(this).build()
playStoreBillingClient.startConnection(this)
}
...
override fun onCleared() {
super.onCleared()
playStoreBillingClient.endConnection()
}
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.