Manually clearing an Android ViewModel? - android

Edit: This question is a bit out of date now that Google has given us the ability to scope ViewModel to navigation graphs. The better approach (rather than trying to clear activity-scoped models) would be to create specific navigation graphs for the right amount of screens, and scope to those.
With reference to the android.arch.lifecycle.ViewModel class.
ViewModel is scoped to the lifecycle of the UI component it relates to, so in a Fragment-based app, that will be the fragment lifecycle. This is a good thing.
In some cases one wants to share a ViewModel instance between multiple fragments. Specifically I am interested in the case where many screens relate to the same underlying data.
(The docs suggest similar approach when multiple related fragments are displayed on the same screen but this can be worked around by using a single host fragment as per answer below.)
This is discussed in the official ViewModel documentation:
ViewModels can also be used as a communication layer between different
Fragments of an Activity. Each Fragment can acquire the ViewModel
using the same key via their Activity. This allows communication
between Fragments in a de-coupled fashion such that they never need to
talk to the other Fragment directly.
In other words, to share information between fragments that represent different screens, the ViewModel should be scoped to the Activity lifecycle (and according to Android docs this can also be used in other shared instances).
Now in the new Jetpack Navigation pattern, it is recommended to use a "One Activity / Many Fragments" architecture. This means that the activity lives for the whole time the app is being used.
i.e. any shared ViewModel instances that are scoped to Activity lifecycle will never be cleared - the memory remains in constant use.
With a view to preserving memory and using as little as required at any point in time, it would be nice to be able to clear shared ViewModel instances when no longer required.
How can one manually clear a ViewModel from it's ViewModelStore or holder fragment?

Quick solution without having to use Navigation Component library:
getActivity().getViewModelStore().clear();
This will solve this problem without incorporating the Navigation Component library. It’s also a simple one line of code. It will clear out those ViewModels that are shared between Fragments via the Activity

If you check the code here you'll find out, that you can get the ViewModelStore from a ViewModelStoreOwner and Fragment, FragmentActivity for example implements, that interface.
Soo from there you could just call viewModelStore.clear(), which as the documentation says:
/**
* Clears internal storage and notifies ViewModels that they are no longer used.
*/
public final void clear() {
for (ViewModel vm : mMap.values()) {
vm.clear();
}
mMap.clear();
}
N.B.: This will clear all the available ViewModels for the specific LifeCycleOwner, this does not allow you to clear one specific ViewModel.

As OP and Archie said, Google has given us the ability to scope ViewModel to navigation graphs. I will add how to do it here if you are using the navigation component already.
You can select all the fragments that needs to be grouped together inside the nav graph and right-click->move to nested graph->new graph
now this will move the selected fragments to a nested graph inside the main nav graph like this:
<navigation app:startDestination="#id/homeFragment" ...>
<fragment android:id="#+id/homeFragment" .../>
<fragment android:id="#+id/productListFragment" .../>
<fragment android:id="#+id/productFragment" .../>
<fragment android:id="#+id/bargainFragment" .../>
<navigation
android:id="#+id/checkout_graph"
app:startDestination="#id/cartFragment">
<fragment android:id="#+id/orderSummaryFragment".../>
<fragment android:id="#+id/addressFragment" .../>
<fragment android:id="#+id/paymentFragment" .../>
<fragment android:id="#+id/cartFragment" .../>
</navigation>
</navigation>
Now, inside the fragments when you initialise the viewmodel do this
val viewModel: CheckoutViewModel by navGraphViewModels(R.id.checkout_graph)
if you need to pass the viewmodel factory(may be for injecting the viewmodel) you can do it like this:
val viewModel: CheckoutViewModel by navGraphViewModels(R.id.checkout_graph) { viewModelFactory }
Make sure its R.id.checkout_graph and not R.navigation.checkout_graph
For some reason creating the nav graph and using include to nest it inside the main nav graph was not working for me. Probably is a bug.
Source: https://medium.com/androiddevelopers/viewmodels-with-saved-state-jetpack-navigation-data-binding-and-coroutines-df476b78144e
Thanks, OP and #Archie for pointing me in the right direction.

I think I have a better solution.
As stated by #Nagy Robi, you could clear the ViewModel by call viewModelStore.clear(). The problem with this is that it will clear ALL the view model scoped within this ViewModelStore. In other words, you won't have control of which ViewModel to clear.
But according to #mikehc here. We could actually create our very own ViewModelStore instead. This will allow us granular control to what scope the ViewModel have to exist.
Note: I have not seen anyone do this approach but I hope this is a valid one. This will be a really good way to control scopes in a Single Activity Application.
Please give some feedbacks on this approach. Anything will be appreciated.
Update:
Since Navigation Component v2.1.0-alpha02, ViewModels could now be scoped to a flow. The downside to this is that you have to implement Navigation Component to your project and also you have no granualar control to the scope of your ViewModel. But this seems to be a better thing.

If you don't want the ViewModel to be scoped to the Activity lifecycle, you can scope it to the parent fragment's lifecycle. So if you want to share an instance of the ViewModel with multiple fragments in a screen, you can layout the fragments such that they all share a common parent fragment. That way when you instantiate the ViewModel you can just do this:
CommonViewModel viewModel = ViewModelProviders.of(getParentFragment()).class(CommonViewModel.class);
Hopefully this helps!

It seems like it has been already solved in the latest architecture components version.
ViewModelProvider has a following constructor:
/**
* Creates {#code ViewModelProvider}, which will create {#code ViewModels} via the given
* {#code Factory} and retain them in a store of the given {#code ViewModelStoreOwner}.
*
* #param owner a {#code ViewModelStoreOwner} whose {#link ViewModelStore} will be used to
* retain {#code ViewModels}
* #param factory a {#code Factory} which will be used to instantiate
* new {#code ViewModels}
*/
public ViewModelProvider(#NonNull ViewModelStoreOwner owner, #NonNull Factory factory) {
this(owner.getViewModelStore(), factory);
}
Which, in case of Fragment, would use scoped ViewModelStore.
androidx.fragment.app.Fragment#getViewModelStore
/**
* Returns the {#link ViewModelStore} associated with this Fragment
* <p>
* Overriding this method is no longer supported and this method will be made
* <code>final</code> in a future version of Fragment.
*
* #return a {#code ViewModelStore}
* #throws IllegalStateException if called before the Fragment is attached i.e., before
* onAttach().
*/
#NonNull
#Override
public ViewModelStore getViewModelStore() {
if (mFragmentManager == null) {
throw new IllegalStateException("Can't access ViewModels from detached fragment");
}
return mFragmentManager.getViewModelStore(this);
}
androidx.fragment.app.FragmentManagerViewModel#getViewModelStore
#NonNull
ViewModelStore getViewModelStore(#NonNull Fragment f) {
ViewModelStore viewModelStore = mViewModelStores.get(f.mWho);
if (viewModelStore == null) {
viewModelStore = new ViewModelStore();
mViewModelStores.put(f.mWho, viewModelStore);
}
return viewModelStore;
}

Im just writing library to address this problem: scoped-vm, feel free to check it out and I will highly appreciate any feedback.
Under the hood, it uses the approach #Archie mentioned - it maintains separate ViewModelStore per scope. But it goes one step further and clears ViewModelStore itself as soon as the last fragment that requested viewmodel from that scope destroys.
I should say that currently whole viewmodel management (and this lib particularly) is affected with a serious bug with the backstack, hopefully it will be fixed.
Summary:
If you care about ViewModel.onCleared() not being called, the best way (for now) is to clear it yourself. Because of that bug, you have no guaranty that viewmodel of a fragment will ever be cleared.
If you just worry about leaked ViewModel - do not worry, they will be garbage collected as any other non-referenced objects. Feel free to use my lib for fine-grained scoping, if it suits your needs.

As it was pointed out it is not possible to clear an individual ViewModel of a ViewModelStore using the architecture components API. One possible solution to this issue is having a per-ViewModel stores that can be safely cleared when necessary:
class MainActivity : AppCompatActivity() {
val individualModelStores = HashMap<KClass<out ViewModel>, ViewModelStore>()
inline fun <reified VIEWMODEL : ViewModel> getSharedViewModel(): VIEWMODEL {
val factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
//Put your existing ViewModel instantiation code here,
//e.g., dependency injection or a factory you're using
//For the simplicity of example let's assume
//that your ViewModel doesn't take any arguments
return modelClass.newInstance()
}
}
val viewModelStore = this#MainActivity.getIndividualViewModelStore<VIEWMODEL>()
return ViewModelProvider(this.getIndividualViewModelStore<VIEWMODEL>(), factory).get(VIEWMODEL::class.java)
}
val viewModelStore = this#MainActivity.getIndividualViewModelStore<VIEWMODEL>()
return ViewModelProvider(this.getIndividualViewModelStore<VIEWMODEL>(), factory).get(VIEWMODEL::class.java)
}
inline fun <reified VIEWMODEL : ViewModel> getIndividualViewModelStore(): ViewModelStore {
val viewModelKey = VIEWMODEL::class
var viewModelStore = individualModelStores[viewModelKey]
return if (viewModelStore != null) {
viewModelStore
} else {
viewModelStore = ViewModelStore()
individualModelStores[viewModelKey] = viewModelStore
return viewModelStore
}
}
inline fun <reified VIEWMODEL : ViewModel> clearIndividualViewModelStore() {
val viewModelKey = VIEWMODEL::class
individualModelStores[viewModelKey]?.clear()
individualModelStores.remove(viewModelKey)
}
}
Use getSharedViewModel() to obtain an instance of ViewModel which is bound to the Activity's lifecycle:
val yourViewModel : YourViewModel = (requireActivity() as MainActivity).getSharedViewModel(/*There could be some arguments in case of a more complex ViewModelProvider.Factory implementation*/)
Later, when it's the time to dispose the shared ViewModel, use clearIndividualViewModelStore<>():
(requireActivity() as MainActivity).clearIndividualViewModelStore<YourViewModel>()
In some cases you would want to clear the ViewModel as soon as possible if it's not needed anymore (e.g., in case of it containing some sensitive user data like username or password). Here's a way of logging the state of individualModelStores upon every fragment switching to help you keep track of shared ViewModels:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (BuildConfig.DEBUG) {
navController.addOnDestinationChangedListener { _, _, _ ->
if (individualModelStores.isNotEmpty()) {
val tag = this#MainActivity.javaClass.simpleName
Log.w(
tag,
"Don't forget to clear the shared ViewModelStores if they are not needed anymore."
)
Log.w(
tag,
"Currently there are ${individualModelStores.keys.size} ViewModelStores bound to ${this#MainActivity.javaClass.simpleName}:"
)
for ((index, viewModelClass) in individualModelStores.keys.withIndex()) {
Log.w(
tag,
"${index + 1}) $viewModelClass\n"
)
}
}
}
}
}

I found a simple and fairly elegant way to deal with this issue. The trick is to use a DummyViewModel and model key.
The code works because AndroidX checks the class type of the model on get(). If it doesn't match it creates a new ViewModel using the current ViewModelProvider.Factory.
public class MyActivity extends AppCompatActivity {
private static final String KEY_MY_MODEL = "model";
void clearMyViewModel() {
new ViewModelProvider(this, new ViewModelProvider.NewInstanceFactory()).
.get(KEY_MY_MODEL, DummyViewModel.class);
}
MyViewModel getMyViewModel() {
return new ViewModelProvider(this, new ViewModelProvider.AndroidViewModelFactory(getApplication()).
.get(KEY_MY_MODEL, MyViewModel.class);
}
static class DummyViewModel extends ViewModel {
//Intentionally blank
}
}

In my case, most of the things I observe are related to the Views, so I don't need to clear it in case the View gets destroyed (but not the Fragment).
In the case I need things like a LiveData that takes me to another Fragment (or that does the thing only once), I create a "consuming observer".
It can be done by extending MutableLiveData<T>:
fun <T> MutableLiveData<T>.observeConsuming(viewLifecycleOwner: LifecycleOwner, function: (T) -> Unit) {
observe(viewLifecycleOwner, Observer<T> {
function(it ?: return#Observer)
value = null
})
}
and as soon as it's observed, it will clear from the LiveData.
Now you can call it like:
viewModel.navigation.observeConsuming(viewLifecycleOwner) {
startActivity(Intent(this, LoginActivity::class.java))
}

As I know you can't remove ViewModel object manually by program, but you can clear data that stored in that,for this case you should call onCleared() method manually
for doing this:
Override onCleared() method in that class that is extended from ViewModel class
In this method you can clean data by making null the field that you store data in it
Call this method when you want clear data completely.

Typically you don't clear the ViewModel manually, because it is handled automatically. If you feel the need to clear your ViewModel manually, you're probably doing too much in that ViewModel...
There's nothing wrong with using multiple viewmodels. First one could be scoped to the Activity while another one could be scoped to the fragment.
Try to use the Activity scoped Viewmodel only for things that need to be shared. And put as many things as possible in the Fragment Scoped Viewmodel. The Fragment scoped viewmodel will be cleared when the fragment is destroyed. Reducing the overall memory footprint.

Related

Get parent activity viewModel from Fragment android [duplicate]

After recently migrating from Dagger to Hilt I started observing very strange behavior with respect to ViewModels. Below is the code snippet:
#HiltAndroidApp
class AndroidApplication : Application() {}
#Singleton
class HomeViewModel #ViewModelInject constructor() :
ViewModel() {}
#AndroidEntryPoint
class HomeFragment : Fragment(R.layout.fragment_home) {
private val homeViewModel by viewModels<HomeViewModel>()
override fun onResume() {
super.onResume()
Timber.i("hashCode: ${homeViewModel.hashCode()}")
}
}
#AndroidEntryPoint
class SomeOtherFragment : Fragment(R.layout.fragment_home) {
private val homeViewModel by viewModels<HomeViewModel>()
override fun onResume() {
super.onResume()
Timber.i("hashCode: ${homeViewModel.hashCode()}")
}
}
The value of hashCode isn't consistent in all the fragments. I am unable to figure out what else am I missing for it to generate singleton instance of viewmodel within the activity.
I am using single activity design and have added all the required dependencies.
When you use by viewModels, you are creating a ViewModel scoped to that individual Fragment - this means each Fragment will have its own individual instance of that ViewModel class. If you want a single ViewModel instance scoped to the entire Activity, you'd want to use by activityViewModels
private val homeViewModel by activityViewModels<HomeViewModel>()
What Ian says is correct, by viewModels is the Fragment's extension function, and it will use the Fragment as the ViewModelStoreOwner.
If you need it to be scoped to the Activity, you can use by activityViewModels.
However, you typically don't want Activity-scoped ViewModels. They are effectively global in a single-Activity application.
To create an Activity-global non-stateful component, you can use the #ActivityRetainedScope in Hilt. These will be available to your ViewModels created in Activity or Fragment.
To create stateful retained components, you should rely on ~~#ViewModelInject, and #Assisted~~ #HiltViewModel and #Inject constructor to get a SavedStateHandle.
There is a high likelihood that at that point, instead of an Activity-scoped ViewModel, you really wanted a NavGraph-scoped ViewModel.
To get a SavedStateHandle into a NavGraph-scoped ViewModel inside a Fragment use val vm = androidx.hilt.navigation.fragment.hiltNavGraphViewModels(R.navigation.nav_graph_id).
If you are not using Hilt, then you can use = navGraphViewModels but you can get the SavedStateHandle using either the default ViewModelProvider.Factory, or the CreationExtras.
Here's an alternative solution to what ianhanniballake mentioned. It allows you to share a view model between fragments while not assigning it to the activity, therefore you avoid creating essentially a global view model in a single activity as EpicPandaForce stated. If you're using Navigation component, you can create a nested navigation graph of the fragments that you want to share a view model (follow this guide: Nested navigation graphs)
Within each fragment:
private val homeViewModel: HomeViewModel
by navGraphViewModels(R.id.nested_graph_id){defaultViewModelProviderFactory}
When you navigate out of the nested graph, the view model will be dropped. It will be recreated when you navigate back into the nested graph.
As mentioned by other posts here, using the by activityViewModels<yourClass>() will scope the VM to the entire Activity's lifecycle, making it effectively a global scope, to the entire app, if it's one activity architecture everyone uses and Google recommends.
Clean, minimal solution:
If you're using nav graph scoped viewmodels:
Replace this:
val vm: SomeViewModel by hiltNavGraphViewModels(R.id.nav_vm_id)
with below:
val vm by activityViewModels<SomeViewModel>()
This allows me to use this VM as a sharedviewmodel between the activity and those fragments.
Otherwise even the livedata observers do not work, as it creates new instances and lifecycles that are independent from each other.

Hilt creating different instances of view model inside same activity

After recently migrating from Dagger to Hilt I started observing very strange behavior with respect to ViewModels. Below is the code snippet:
#HiltAndroidApp
class AndroidApplication : Application() {}
#Singleton
class HomeViewModel #ViewModelInject constructor() :
ViewModel() {}
#AndroidEntryPoint
class HomeFragment : Fragment(R.layout.fragment_home) {
private val homeViewModel by viewModels<HomeViewModel>()
override fun onResume() {
super.onResume()
Timber.i("hashCode: ${homeViewModel.hashCode()}")
}
}
#AndroidEntryPoint
class SomeOtherFragment : Fragment(R.layout.fragment_home) {
private val homeViewModel by viewModels<HomeViewModel>()
override fun onResume() {
super.onResume()
Timber.i("hashCode: ${homeViewModel.hashCode()}")
}
}
The value of hashCode isn't consistent in all the fragments. I am unable to figure out what else am I missing for it to generate singleton instance of viewmodel within the activity.
I am using single activity design and have added all the required dependencies.
When you use by viewModels, you are creating a ViewModel scoped to that individual Fragment - this means each Fragment will have its own individual instance of that ViewModel class. If you want a single ViewModel instance scoped to the entire Activity, you'd want to use by activityViewModels
private val homeViewModel by activityViewModels<HomeViewModel>()
What Ian says is correct, by viewModels is the Fragment's extension function, and it will use the Fragment as the ViewModelStoreOwner.
If you need it to be scoped to the Activity, you can use by activityViewModels.
However, you typically don't want Activity-scoped ViewModels. They are effectively global in a single-Activity application.
To create an Activity-global non-stateful component, you can use the #ActivityRetainedScope in Hilt. These will be available to your ViewModels created in Activity or Fragment.
To create stateful retained components, you should rely on ~~#ViewModelInject, and #Assisted~~ #HiltViewModel and #Inject constructor to get a SavedStateHandle.
There is a high likelihood that at that point, instead of an Activity-scoped ViewModel, you really wanted a NavGraph-scoped ViewModel.
To get a SavedStateHandle into a NavGraph-scoped ViewModel inside a Fragment use val vm = androidx.hilt.navigation.fragment.hiltNavGraphViewModels(R.navigation.nav_graph_id).
If you are not using Hilt, then you can use = navGraphViewModels but you can get the SavedStateHandle using either the default ViewModelProvider.Factory, or the CreationExtras.
Here's an alternative solution to what ianhanniballake mentioned. It allows you to share a view model between fragments while not assigning it to the activity, therefore you avoid creating essentially a global view model in a single activity as EpicPandaForce stated. If you're using Navigation component, you can create a nested navigation graph of the fragments that you want to share a view model (follow this guide: Nested navigation graphs)
Within each fragment:
private val homeViewModel: HomeViewModel
by navGraphViewModels(R.id.nested_graph_id){defaultViewModelProviderFactory}
When you navigate out of the nested graph, the view model will be dropped. It will be recreated when you navigate back into the nested graph.
As mentioned by other posts here, using the by activityViewModels<yourClass>() will scope the VM to the entire Activity's lifecycle, making it effectively a global scope, to the entire app, if it's one activity architecture everyone uses and Google recommends.
Clean, minimal solution:
If you're using nav graph scoped viewmodels:
Replace this:
val vm: SomeViewModel by hiltNavGraphViewModels(R.id.nav_vm_id)
with below:
val vm by activityViewModels<SomeViewModel>()
This allows me to use this VM as a sharedviewmodel between the activity and those fragments.
Otherwise even the livedata observers do not work, as it creates new instances and lifecycles that are independent from each other.

injecting viewmodel with navigation-graph scope: NavController is not available before onCreate()

I'm using a navigation-component in my application and also using shared ViewModel between multiple fragments that are in the same graph. Now I want to instantiate the ViewModel with this graph scope with this.
As you know, in fragments we should inject objects ( ViewModel,..etc ) in onAttach:
but when I want to do this (injecting ViewModel with a graph scope in onAttach), this error occurs:
IllegalStateException: NavController is not available before onCreate()
Do you know how I can do this?
In short, you could provide the ViewModel lazily with dagger Provider or Lazy.
The long explanation is:
Your injections points are correct. According to https://dagger.dev/android#when-to-inject
DaggerActivity calls AndroidInjection.inject() immediately in
onCreate(), before calling super.onCreate(), and DaggerFragment does
the same in onAttach().
The problem is some kind of race condition between when Android recreates the Activity and the Fragments attached to the FragmentManger and when the NavController can be provided. More specifically:
one Activity that has Fragments attached is destroyed by the OS (can be reproduced with "don't keep Activities" from "developer settings")
user navigates back to the Activity, OS proceeds to recreate the Activity
Activity calls setContentView while being recreated.
This causes the Fragments in the FragmentManager to be reattached, which involve calling Fragment#onAttach
The Fragment is injected in Fragment#onAttach
Dagger tries to provide the NavController
BUT you cannot get the NavController from the Activity by this point, as Activity#onCreate has not finished yet and you get
IllegalStateException: NavController is not available before onCreate()
The solution I found is to inject provide the NavCotroller or things that depend on the NavController (such as the ViewModel, because Android needs the NavController to get nav-scoped VideModels) lazily. This can be done in two ways:
with Lazy
with Provided
(REF: https://proandroiddev.com/dagger-2-part-three-new-possibilities-3daff12f7ebf)
ie: inject the ViewModel to the Fragment or implementation of navigator like this:
#Inject
lateinit var viewModel: Provider<ViewModel>
then use it like this:
viewModel.get().events.observe(this) {....}
Now, the ViewModel can by provided by Dagger like:
#Provides
fun provideViewModel(
fragment: Fragment,
argumentId: Int
): CreateMyViewModel {
val viewModel: CreateMyViewModel
by fragment.navGraphViewModels(R.id.nested_graph_id)
return viewModel
}
Dagger won't try to resolve the provisioning when the Fragment is injected, but when it's used, hence, the race condition will be solved.
I really hate not being able to use my viewModels directly and need to use Provider, but it's the only workaround I see to solve this issue, which I'm sure it was an oversight by Google (I don't blame them, as keeping track of the absurd lifecycle of Fragment and Activities is so difficult).
...we should inject objects ( ViewModel,..etc ) in onAttach...
Looks like it is currently a no go for such injection with the original by navGraphViewModels(R.id.nav_graph) delegated property provided by androidx.navigation package because from the source code
findNavController().getBackStackEntry(navGraphId) and
public final NavController getNavController() it stated that:
* Returns the {#link NavController navigation controller} for this navigation host.
* This method will return null until this host fragment's {#link #onCreate(Bundle)}
And here are some workarounds:
https://github.com/InsertKoinIO/koin/issues/442

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.

Share ViewModel between fragments that are in different Activity

I have a ViewModel named SharedViewModel:
public class SharedViewModel<T> extends ViewModel {
private final MutableLiveData<T> selected = new MutableLiveData<>();
public void select(T item) {
selected.setValue(item);
}
public LiveData<T> getSelected() {
return selected;
}
}
I've implemented it based on SharedViewModel example on the Google's Arch ViewModel reference page:
https://developer.android.com/topic/libraries/architecture/viewmodel.html#sharing_data_between_fragments
It is very common that two or more fragments in an activity need to communicate with each other. This is never trivial as both
fragments need to define some interface description and the owner
activity must bind the two together. Moreover, both fragments must
handle the case where the other fragment is not yet created or not
visible.
I have two fragments, called ListFragment and DetailFragment.
Until now I used these two fragments inside an activity called MasterActivity, and everything worked well.
I got the ViewModel in ListFragment, selected the value to use it on DetailFragment.
mStepSelectorViewModel = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
However, now, in certain cases, I need that ListFragment (a layout to a different device configuration) will be added to a different activity, called DetailActivity. Is there a way to do that similarly to the above example?
A little late but you can accomplish this using a shared ViewModelStore. Fragments and activities implement the ViewModelStoreOwner interface. In those cases fragments have a store per instance and activities save it in a static member (I guess so it can survive configuration changes).
Getting back to the shared ViewModelStore, let say for example that you want it to be your Application instance. You need your application to implement ViewModelStoreOwner.
class MyApp: Application(), ViewModelStoreOwner {
private val appViewModelStore: ViewModelStore by lazy {
ViewModelStore()
}
override fun getViewModelStore(): ViewModelStore {
return appViewModelStore
}
}
Then in the cases when you know that you need to share ViewModels between activity boundaries you do something like this.
val viewModel = ViewModelProvider(myApp, viewModelFactory).get(CustomViewModel::class.java)
So now it will use the Store defined in your app. That way you can share ViewModels.
Very important. Because in this example the ViewModels live in your application instance they won't be destroyed when the fragment/activity that uses them gets destroyed. So you will have to link them to the lifecycle of the last fragment/activity that will use them, or manually destroy them.
Well, I created a library for this purpose named Vita, You can share ViewModels between activities and even fragments with different host activity:
val myViewModel = vita.with(VitaOwner.Multiple(this)).getViewModel<MyViewModel>()
The created ViewModel in this way stay alive until its last LifeCycleOwner is destroyed.
Also you can create ViewModels with application scope:
val myViewModel = vita.with(VitaOwner.None).getViewModel<MyViewModel>()
And this type of ViewModel will be cleared when user closes app
Give it a try and kindly let me know your feedback:
https://github.com/FarshadTahmasbi/Vita
you can use factory to make viewmodel and this factor will return single object of view model.. As:
class ViewModelFactory() : ViewModelProvider.Factory {
override fun create(modelClass: Class): T {
if (modelClass.isAssignableFrom(UserProfileViewModel::class.java)) {
val key = "UserProfileViewModel"
if(hashMapViewModel.containsKey(key)){
return getViewModel(key) as T
} else {
addViewModel(key, UserProfileViewModel())
return getViewModel(key) as T
}
}
throw IllegalArgumentException("Unknown ViewModel class")
}
companion object {
val hashMapViewModel = HashMap<String, ViewModel>()
fun addViewModel(key: String, viewModel: ViewModel){
hashMapViewModel.put(key, viewModel)
}
fun getViewModel(key: String): ViewModel? {
return hashMapViewModel[key]
}
}
}
In Activity:
viewModelFactory = Injection.provideViewModelFactory(this)
// Initialize Product View Model
userViewModel = ViewModelProviders.of(this, viewModelFactory).get(
UserProfileViewModel::class.java)`
This will provide only single object of UserProfileViewModel which you can share between Activities.
I think we still get confused with the MVVM framework on Android.
For another activity, do not get confused because it must necessarily be the same, why?
This makes sense if it has the same logic (even if the logic could still be abstract in other useful classes), or if the view in the XML is almost identical.
Let's take a quick example:
I create a ViewModel called vmA, and an activity called A and I need the user's data, I will go to insert the repository in vmA of the User.
Now, I need another activity that needs to read user data,
I create another ViewModel called vmB and in it I will call the user repository.
As described, the repository is always the same.
Another way already suggested is to create N instances of the same ViewModel with the implementation of the Factory.
If you want a ViewModel that is shared by all your activities (as opposed to some),
then why not store what you want stored in that ViewModel
inside your Application class?
The trend presented at the last Google I/O seems to be to abandon the concept of Activities in favor of single-activity apps that have a lot of Fragments.
ViewModels are the way to remove the great number of interfaces the activity of an interface formerly had to implement.
Thus this aproach no longer makes for giant and unmaintainable activities.
Here's a link
Hope it helps you. O(∩_∩)O~
In addition:
1) The inspiration for the code came from smart pointer in c++.
2) It will be auto cleared when no activities or fragments references ShareViewModel.
The ShareViewModel # onShareCleared() function will be called at the same time!
You don't need to destroy them manually!
3) If you use dagger2 to inject the ViewModelFactory for share the viewmodel
between two activities (maybe three), Here's sample

Categories

Resources