0

I have a small app where one ViewModel has been shared between Fragment and FragmentDialog. It shares not only logic, but also a state. It works fine until I have added one more fragment which uses the same ViewModel. The issue is when I trigger a method of the ViewModel this method is called, but its viewModelScoped doesn't.

At first I thought it caused by ViewModel's onCleared() call somewhere during runtime. Indeed, onCleared() is called when the first Fragment replaced by the second Fragment. However, when the call of my method is happened ViewModel is still here (not null) and its viewModelScope should be too, isn't it?

Later I noticed a recommendation to pass a parent Activity as a context for a ViewModelFactory - because I have single Activity, ViewModelStoreOwner would be the same for all fragments. It works, but I still concern about root cause of this issue and possible alternative solutions. Single activity is not an always option for many android apps.

public class ViewModelFactory implements ViewModelProvider.Factory {
    private final Map<Class<? extends ViewModel>, Provider<ViewModel>> viewModels;

    @Inject
    public ViewModelFactory(Map<Class<? extends ViewModel>, Provider<ViewModel>> viewModels) {
        this.viewModels = viewModels;
    }

    @Override
    public <T extends ViewModel> T create(Class<T> modelClass) {
        Provider<ViewModel> viewModelProvider = viewModels.get(modelClass);

        if (viewModelProvider == null) {
            throw new IllegalArgumentException("model class " + modelClass + " not found");
        }

        return (T) viewModelProvider.get();
    }
}
@Module
public abstract class ViewModelModule {

    @Binds
    abstract ViewModelProvider.Factory bindViewModelFactory(ViewModelFactory viewModelFactory);

    @Binds
    @IntoMap
    @ViewModelKey(DashboardViewModel.class)
    @Singleton
    abstract ViewModel dashboardViewModel(DashboardViewModel vm);

}
viewModel = ViewModelProvider(requireActivity(), viewModelFactory).get(DashboardViewModel::class.java)
/*DashboardViewModel class*/ {
  fun addItem() {
        Log.d(App.TAG, "Add item has started")
        viewModelScope.launch {
            Log.d(App.TAG, "Add item coroutine has started")
  // ... 
}
    

If you have any good thoughts on this, please share.    
Gleichmut
  • 5,953
  • 5
  • 24
  • 32
  • 1
    One experienced engineer shared some caveats on this path https://stackoverflow.com/questions/67544852/how-to-share-viewmodel-scope-between-a-particular-set-of-fragments-without-usin – Gleichmut Aug 01 '23 at 12:04

1 Answers1

1

You found the root cause. By definition, a ViewModel is scoped either to individual Fragment screens or individual Activity screens depending on whether you pass a Fragment or Activity as the owner parameter of the ViewModelProvider constructor. I say "screens" instead of "instances" because the scope can extend to multiple instances that are occupying the same place in the stack of screens, such as after screen rotations.

The various issues and observations you had were because you were looking at different ViewModel instances, one per Fragment screen.

You said single-Activity apps are not always possible. When this happens, you have to use Intents and/or the relatively new registerForActivityResult API. In most (all?) cases, when you actually need to switch to different activities, there is no need for tight-coupling of behavior between them. You're just passing a few configuration options and getting something back from it. This is even the case in most fragment-to-fragment transitions if you're following principles of single responsibility and modularity as alluded to in the answer you linked in your comment. But I suppose it depends on what type of data you're sharing. It's a common pattern in many of Google's examples to have a singleton repository layer for managing shared model (not UI) data globaly.

Tenfour04
  • 83,111
  • 11
  • 94
  • 154
  • Thank you, you are right when an app switch to multiple activities and fragments scenario, an option to have shared ```ViewModel``` becomes redundant. However, the issue with missed ```viewModelScope``` is still unclear, – Gleichmut Aug 03 '23 at 04:15
  • Do you want to say when I scope my ```ViewModel``` to a ```Fragment```, I get two instances per each ```Fragment```, but when I scope ```ViewModel``` to a common activity, I get a single ```ViewModel``` across fragments? Another question is in case I have two instances of ```ViewModel```, then why ```onCleared()``` call on the first fragment affects ```viewModelScope``` on the second fragment? – Gleichmut Aug 03 '23 at 04:26
  • I don’t know what you mean by a call on one ViewModel affecting a `viewModelScope` on another fragment. What do you mean by the `viewModelScope` being “affected”? You get one view model instance per fragment screen. If two fragment classes use the same ViewModel class but only scoped to fragment instead of activity, they are each looking at a distinct instance of it. – Tenfour04 Aug 03 '23 at 04:35
  • What I have tried to describe is when I open a second fragment and try to add a new item from UI input to a database, the ```ViewModel``` method responsible for this is invoked, but ```viewModelScope``` code block doesn't. The rest details are just a result of my investigation of this issue. – Gleichmut Aug 03 '23 at 04:41
  • This issue has been solved when I use a common ```Activity``` as a ```ViewModelStoreOwner``` (aka I get the same instance of the ```ViewModel```), but if I have a separate instance per fragment in first case, why do I observe such bizarre behaviour? – Gleichmut Aug 03 '23 at 04:43
  • So you're saying if you log something in the coroutine you launch from `viewModelScope` to ensure it is being called, nothing happens when you start the coroutine in the second instance of ViewModel that the second fragment is using? I've never experienced anything like that and could not say why that might happen. – Tenfour04 Aug 03 '23 at 13:20
  • Yes, that's what I have observed. Anyway, thank you for your replies - they helped to better understand some things. – Gleichmut Aug 04 '23 at 10:31