Koin: Is there a way to have a sharedViewModel for a Fragment's Scope?

Created on 3 Oct 2019  Â·  28Comments  Â·  Source: InsertKoinIO/koin

I'm using Android Navigation Component, in a Single-Activity App, there is a ViewModel that I want to be re-created and it's shared between some DialogFragments that compose this specific flow. I want to know if there is a way to have a sharedViewModel tied to this FragmentScope

I would like to use a Scoped sharedViewModel, I think that this will improve the library capacities

As an alternative, I would like to know if there is a way to clear a viewModel instance when the fragment is destroyed

koin-android

Most helpful comment

Hi, currently I'm using: Koin 2.1.0-beta-1
and this version of library have more elegant way to do it.

So, try to do like this :

private val viewModel by lazy { 
     requireParentFragment().getViewModel<MyViewModel>()
} 

To more deep understanding how it works, please check the package org.koin.androidx.viewmodel.ext.android of Koin library

All 28 comments

Yes, you can set the scope like this:
sharedViewModel<ViewModel>(from = {//your scope})

In my case I have ViewPager, and i use the same viewModel for internal Fragments like:
val viewModel by sharedViewModel<MyViewModel>(from = { parentFragment!! })

I'm trying to pass a scope that I've created in the sharedViewModel(from = {}) but it expects a ViewModelStoreOwner.

The way that you uses it, doesn't work for me, once the parentFragment of my DialogFragments is the NavHostFragment from the Navigation

So what I need is to have a way to get a sharedViewModel for a specific scope.

Perhaps more check around a dedicated scope, not something tied to a Fragment nor an activity. You can create a scope instance by hand, and retrieve it elsewhere by its id.

About that, I'm trying to define the scope in my module, like this:

scope(named("fragment_scope")) {
        scoped { factory { MyScopedClass() } }

        viewModel { MyScopedViewModel(get<MyScopedClass>(), get<NotScopedClass>()) }
}

And retrieve it like this:

private val fragmentScope = getKoin().getOrCreateScope("fragment_scope_id", named("fragment_scope"))

private val viewModel: MyScopedViewModel by fragmentScope.viewModel(parentFragment!!)

But it's not working.

More details?

I'm getting an exception when I try to retrieve the viewModel instance from this scope:

private val viewModel: MyScopedViewModel by fragmentScope.viewModel(parentFragment!!)

With this I'm getting this exception:
Unable to instantiate fragment ScopedFragment: calling Fragment constructor caused an exception

The parentFragment is the NavHostFragment for Android Navigation

I'll edit the previous message to add this line.

[EDIT] I Figured out the exception. Looks like I can't pass the parentFragment to the fragmentScope.viewModel() function. Passing this solve the problem. Now I want to have access to the same instance of this created viewModel in my DialogFragment.

Before changing to Android Navigation, it was an Activity that instantiates this viewModel and all the other Fragments that belongs to this flow uses a shared instance of this viewModel, now that I've changed to Android Navigation and my only activity is the Main activity, I want to start a fresh instance of the viewModel for every time that I launches a specific Fragment.

After some digging I found that using a Scope will help me with this problem, and in fact helped, I'm, able to create a new instance of the viewModel every time I launch this Fragment, now I want to know how to retrieve this instance in the other Fragments.

@arnaudgiuliani Any idea?

Can you pass again your complete code, your case is a bit complex :)
Send even a gists

Store a weak reference to the fragment in the activity in the onCreated function.

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        (activity as? MainActivity)?.myFragment= WeakReference(this)
    }

use it to create the sharedViewModel in other fragments

val myFrag= (activity as? MainActivity)?.myFragment?.get() ?: return
viewModel = sharedViewModel<MyViewModel>(from = {myFrag}) { parametersOf(abc) }.value

As long as the fragment is in the back stack I believe its pretty much guaranteed to be alive.

The alternative is to have a regular unscoped (Activity) sharedViewModel then call a reset() and init(my constructor params) function on the model before navigating to the desired fragment

--- Edit
Now that I'm thinking about it, I don't even see what's stopping you from just putting a weak reference of the sharedViewModel on the activity instead.

In the end, sharing data between fragments is just a huge pain on android. And ultimately, dumb.

Can you pass again your complete code, your case is a bit complex :)
Send even a gists

We found out a way to overcome this issue, with NavigationComponent we have access to the ViewModelStoreOwner for a graph, so we created a new graph for this flow in order to get the right ViewModel with:
by sharedViewModel(from = { findNavController().getViewModelStoreOwner(R.id.new_graph)})

I think I'm facing a kinda similar problem as @arctouch-carlosottoboni

I have Fragment A and B. And Fragment A is using ViewModel A and Fragment B is using ViewModel B. (Note: I'll be navigating from Fragment A to B)

So in my 'di' it structured kinda like this.

val module = module {
scope(named("some_name")) {
viewModel { A() }
viewModel { B() }
}
}

I get the viewModel using:
getKoin().createScope("some_unique_id", named("some_name"))

This is so far so good. But here comes the problem.

The moment I want to access the ViewModel A on my Fragment B, I write this code on my Fragment B.

val scope = getKoin().getScope("some_unique_id") // no problem, I get the same scope I get on Fragment A

val expectingSameViewModelA = scope.viewModel(this)
// problem: here I get a new instance of ViewModel A

Note: I'm inside the same activity and navigating Fragment A -> Fragment B

I did fix the problem with a trick, but I think this could be better. I changed my 'di' like this:

Previously:
val module = module {
scope(named("some_name")) {
viewModel { A() }
viewModel { B() }
}
}

Now:
val module = module {
scope(named("some_name")) {
scoped { A() }
viewModel { B() }
}
}

I've replaced the 'viewModel' with 'scoped'. And I can get the ViewModel with the same code previously mentioned.

But now I'm missing out things like: viewModelScope.launch() is not working when I switch tabs on my application (If I replace scope with viewModel I know I won't be facing this issue).

It would be great if @arnaudgiuliani can help me tackle this issue.

Yes, you can set the scope like this:
sharedViewModel<ViewModel>(from = {//your scope})

In my case I have ViewPager, and i use the same viewModel for internal Fragments like:
val viewModel by sharedViewModel<MyViewModel>(from = { parentFragment!! })

Does this still work? I have the exact same usecase.
I use Android Navigation Components (so single activity, I don't want to use the Activity as scope becuase the activity is never destroyed)
I have a fragment with a viewpager with fragments. I want the viewpager fragmetns to use the same viewmodel isntance as the parent fragment.
Struggling to get this right.. Any help would be appreciated.
Having a custom scope for this seems overcomplicated? I would just like to use the parent fragment as the scope if possible.

Hi, currently I'm using: Koin 2.1.0-beta-1
and this version of library have more elegant way to do it.

So, try to do like this :

private val viewModel by lazy { 
     requireParentFragment().getViewModel<MyViewModel>()
} 

To more deep understanding how it works, please check the package org.koin.androidx.viewmodel.ext.android of Koin library

@krasavello13 Thank you for the reply.
Unfortunately I get the following error when trying that:
java.lang.ClassCastException: androidx.fragment.app.FragmentViewLifecycleOwner cannot be cast to android.content.ComponentCallbacks

Any ideas what could cause this?

EDIT: Nevermind, I took the code from your original comment from the email notification I got. Your updated code works! Thank you!!

Oh, excellent
Have a nice coding

Will it works with Navigation Library?

Will it works with Navigation Library?

No, its will work only if you have parent and child fragments

For correct work with navigation features (as nested navigation graph)use this extension:

inline fun <reified VM : ViewModel> Fragment.sharedGraphViewModel(
    @IdRes navGraphId: Int,
    qualifier: Qualifier? = null,
    noinline parameters: ParametersDefinition? = null
) = lazy {
    val store = findNavController().getViewModelStoreOwner(navGraphId).viewModelStore
    getKoin().getViewModel(ViewModelParameter(VM::class, qualifier, parameters, null, store, null))
}

thnx

thnx

No problem, if you will have some question, please let me know
Have a nice coding

Store a weak reference to the fragment in the activity in the onCreated function.

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        (activity as? MainActivity)?.myFragment= WeakReference(this)
    }

use it to create the sharedViewModel in other fragments

val myFrag= (activity as? MainActivity)?.myFragment?.get() ?: return
viewModel = sharedViewModel<MyViewModel>(from = {myFrag}) { parametersOf(abc) }.value

As long as the fragment is in the back stack I believe its pretty much guaranteed to be alive.

The alternative is to have a regular unscoped (Activity) sharedViewModel then call a reset() and init(my constructor params) function on the model before navigating to the desired fragment

--- Edit
Now that I'm thinking about it, I don't even see what's stopping you from just putting a weak reference of the sharedViewModel on the activity instead.

In the end, sharing data between fragments is just a huge pain on android. And ultimately, dumb.

looks awful

The feature request https://github.com/InsertKoinIO/koin/issues/442 would achieve the desired outcome

I have 3 fragments in an activity in the order A -> B -> C and I need to use a particular viewmodel shared between two fragments B and C. If I use shared view model in the activity even when I go back to the first fragment (A) the viewmodel instance is retained. How do I tackle this? Can I use scope for this and if so, how? All the fragments are created from the activity, so requireParentFragment won't work.

442 will be considered

@aldrinjoemathew use findFragmentByTag(FragmentATag)

@Wokrey findFragmentByTag on what? parentFragmentManager?

@Wokrey findFragmentByTag on what? parentFragmentManager?

@NahroTo First time I'm hearing about parentFragmentManager). I guess just fragmentManager is enough

Aren't these solutions too complicated? Can't we just use
getKoin().getScope(scopeId).getSource<ParentFragment>().getViewModel<ParentFragmentViewModel>() ?
The only problem is to share the scopeId of the ParentFragment or specify custom scopeId when creating a scope (I prefer this one). Maybe we should move in this direction? In this way, we don't even need navGraphViewModels() anymore, because instead of graphs we have scopes.

UPD:
I've just created the required functions.

How it looks:
In ParentFragment:
private val sharedViewModel: ParentFragmentViewModel by lifecycleScope(SCOPE_ID).viewModel(this)

In ChildFragment:
private val sharedViewMode: ParentFragmentViewModel by sharedViewModel(SCOPE_ID)

How it works:

inline fun <reified T : ViewModel> Fragment.sharedViewModel(scopeId: String): Lazy<T> =
    getKoin().getScope(scopeId).getSource<Fragment>().viewModel()

fun LifecycleOwner.lifecycleScope(scopeId: String): Scope = getOrCreateAndroidScope(scopeId)

private fun LifecycleOwner.getOrCreateAndroidScope(scopeId: String): Scope {
    return getKoin().getScopeOrNull(scopeId) ?: createAndBindAndroidScope(scopeId, getScopeName())
}
Was this page helpful?
0 / 5 - 0 ratings

Related issues

CristianMG picture CristianMG  Â·  3Comments

mubarak1361 picture mubarak1361  Â·  3Comments

sankarsana picture sankarsana  Â·  4Comments

Jeevuz picture Jeevuz  Â·  4Comments

leodeleon22 picture leodeleon22  Â·  4Comments