5

Google states to use SharedViewModel in case of communication between Fragments by scoping it to the activity.

In a Single Activity Application, this means that the activity will be littered with ViewModels which might not be needed anymore and they will stay there for the whole lifecycle. Considering some example like a extended sign up flow or something considering a few screens.

One way recommended was using a parent Fragment as scope, but that is only possible if there is a parent fragment. It can be just different Fragments.

I came up with the following solution, I want to know how viable or how bad is it and is there any better way around?

Considering I have two Fragments called ExampleOneFragment and ExampleTwoFragment for sake of simplicity and I want them to have a shared scope without actually scoping it to the activity. Let's say I want to update text view in ExampleOneFragment from ExampleTwoFragment so I create a SharedViewModel like this for both

For ExampleOneFragment it will be:

 private val mSharedViewModel by lazy {
    ViewModelProvider(this).get(SharedViewModel::class.java)
}

And For ExampleTwoFragment I came up with this:

private val mSharedViewModel by lazy {
    ViewModelProvider(supportFragmentManager().findFragmentByTag(ExampleOneFragment.TAG) ?: this).get(SharedViewModel::class.java)
}

This seems to be working, but I don't know what kind of issues can this cause.

Some Other Solutions are which I found: According to @mikhehc here We could actually create our very own ViewModelStore instead. This will allow us granular control to what scope the ViewModel have to exist. But I don't understand how to make it work for Fragments?

Secondly, is the hacky way of scoping it to the activity still, but clearing it out via dummy viewmodel by using same key which I found here

Could anyone guide me what is the right approach? I cannot shift to NavGraphs since it is an already up and running project and scoping to activity just feels wrong. Thanks.

che10
  • 2,176
  • 2
  • 4
  • 11
  • Personally I faced with ```onCleared()``` call when have shared a ```ViewModel``` with two fragments. This call clear up my scope and all coroutines tied to it. Despite on ```ViewModel``` has been injected as a ```@Singleton``` over ```dagger```. Passing across your question with intent to learn more about this behaviour. – Gleichmut Aug 01 '23 at 10:52

1 Answers1

2

This seems to be working, but I don't know what kind of issues can this cause.

This code will only work if:

  • An ExampleOneFragment is created first, always
  • It is added to the same FragmentManager that ExampleTwoFragment uses, via a tag of ExampleOneFragment.TAG, always

If either of those assumptions fail, you will wind up with separate viewmodel instances, because supportFragmentManager().findFragmentByTag(ExampleOneFragment.TAG) ?: this will resolve to this.

But I don't understand how to make it work for Fragments?

You would use it the way it is shown in that answer, or by anything else that accepts a ViewModelStoreOwner. In this line:

val viewModel = ViewModelProvider(myApp, viewModelFactory).get(CustomViewModel::class.java)

You would get myApp from your Fragment as:

val myApp = requireContext().application as MyApp

Personally, I think the solution that you pointed to is very risky. The ViewModelStore lives for the entire lifetime of the process and is never cleared. You will wind up sharing your viewmodel across everything, and everything done by that viewmodel is leaked. The concept of creating a custom ViewModelStoreOwner is fine, but you should be doing something to tie the scope of that owner to the relevant lifetime for the viewmodels that it stores. The answer tries to dance around that in its last paragraph; too many developers will ignore this and run into problems.

One way recommended was using a parent Fragment as scope, but that is only possible if there is a parent fragment. It can be just different Fragments.

Your app is not written in a way to make automatic ViewModelStoreOwner management available "out of the box", then.

In the end, you are looking for a ViewModelStoreOwner to be shared between these two fragments. In your first solution, you are trying to hack that by using the ViewModelStoreOwner from one of those fragments, which only works if you can reliably choose which fragment that is. In the solution you pointed to in that other answer, you are trying to hack that by intentionally leaking a ViewModelStoreOwner.

There may be other approaches that you could consider, depending on your circumstances. For example, there may be some option with your dependency inversion framework (Dagger/Hilt, Koin, etc.) to rig up a ViewModelStoreOwner that is tied to a specific pair of fragment instances.

CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
  • Yes, with the second solution, I did not mean to actually scope it to the application lifecycle, I realize that will be even more wrong. What I meant was is there any way to create a ```ViewModelStoreOwner``` that are limited to a scope of Fragments. If so how do I go around doing that? Where do I implement the ```ViewModelStoreOwner``` interface. Also, what do you think about the 2nd approach of clearing out ViewModels. – che10 May 15 '21 at 12:12
  • @che10: "Where do I implement the ViewModelStoreOwner interface" -- on some arbitrary object that you arrange to make available to those two fragments when the pair get created. You would also need to arrange to clear the viewmodels when that pair of fragments is destroyed. For example, there may be some option with your dependency inversion framework (Dagger/Hilt, Koin, etc.) to rig up a `ViewModelStoreOwner` that is tied to a specific pair of fragment instances. – CommonsWare May 15 '21 at 12:20
  • @che10: "Also, what do you think about the 2nd approach of clearing out ViewModels." -- it would not pass a code review on my team. It makes some underlying assumptions about the implementation of the viewmodel system, and those assumptions can change with any given release of those libraries. – CommonsWare May 15 '21 at 12:22
  • So unless I arrange something with DI, which I don't know anything about for now. Using activity scope is the only way for now, or replacing the whole base with new Nav Graphs utility. Thanks for answering. – che10 May 15 '21 at 12:44
  • @che10: "Using activity scope is the only way for now" -- it may be the simplest solution for you. My pair of references to DI started with "For example", because they were examples, and DI is the last sort of 'general purpose' solution I can offer you. You can certainly do something else, but it would be very dependent on how you are creating and destroying those fragments, and so we cannot provide concrete advice on it. – CommonsWare May 15 '21 at 12:47
  • My current structure is just fragments are added on top of each other and all of them are added to back stack. Initially I found out that ```targetFragment.onActivityResult``` got deprecated and I was using this mechanism in a DialogFragment. So I was searching for what is the correct flow to receive results from DialogFragment. SharedViewModel was recommended, but scoping it to activity just for this did not feel right. So rn I am stuck on what approach to take. I could not find any proper replacement for the DialogFragment thing. – che10 May 15 '21 at 13:14
  • @che10: You may be better served sticking with your current `onActivityResult()` approach for a while. With Jetpack libraries, 'deprecated' simply means "this might get removed in the future, and we would prefer that you use something else". You do not have to move to "something else" right away, as you have some control over when you might upgrade to some future library where it actually gets removed. If you are going to be making changes later this year that might help with this problem (adding DI, adding nav graphs, migrating to Jetpack Compose, etc.), worry about the problem then. – CommonsWare May 15 '21 at 13:19
  • Thanks. For later this year I am more worried about the future of Kotlin Synthetics, I know they are already deprecated, and so we have moved onto ViewBinding, but what about old large-scale apps based on Synthetics that are already running and live. I have not been able to find a concrete answer for this. What if one day I update the Kotlin Plugin and it no longer supports the old project because migration for newer projects might be simpler, but for already up and running projects it is not that simple. – che10 May 15 '21 at 13:46
  • @che10: "What if one day I update the Kotlin Plugin and it no longer supports the old project" -- you then roll the Kotlin Plugin version to the version that you had been using, until such time as you are in position to make the necessary changes. – CommonsWare May 15 '21 at 13:48
  • I guess that is the only way then. Thanks a lot man. Thanks for sharing your knowledge. – che10 May 15 '21 at 17:20