Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is there a way to have a sharedViewModel for a Fragment's Scope? #593

Closed
arctouch-carlosottoboni opened this issue Oct 3, 2019 · 34 comments

Comments

@arctouch-carlosottoboni
Copy link

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

@arctouch-carlosottoboni arctouch-carlosottoboni changed the title Is there a way to have a sharedViewModel for a Fragment's Scope? Is there a way to have a sharedViewModel for a Fragment's Scope? Oct 3, 2019
@krasavello13
Copy link

krasavello13 commented Oct 4, 2019

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!! })

@arctouch-carlosottoboni
Copy link
Author

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.

@arnaudgiuliani
Copy link
Member

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.

@arctouch-carlosottoboni
Copy link
Author

arctouch-carlosottoboni commented Oct 4, 2019

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.

@arnaudgiuliani
Copy link
Member

More details?

@arctouch-carlosottoboni
Copy link
Author

arctouch-carlosottoboni commented Oct 4, 2019

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.

@arctouch-carlosottoboni
Copy link
Author

@arnaudgiuliani Any idea?

@arnaudgiuliani
Copy link
Member

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

@izackp
Copy link

izackp commented Oct 16, 2019

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.

@arctouch-carlosottoboni
Copy link
Author

arctouch-carlosottoboni commented Nov 5, 2019

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)})

@TheMithuRoy
Copy link

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.

@hendrep
Copy link

hendrep commented Feb 22, 2020

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.

@krasavello13
Copy link

krasavello13 commented Feb 24, 2020

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

@hendrep
Copy link

hendrep commented Feb 24, 2020

@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!!

@krasavello13
Copy link

Oh, excellent
Have a nice coding

@igorka48
Copy link

Will it works with Navigation Library?

@hendrep
Copy link

hendrep commented Feb 25, 2020 via email

@krasavello13
Copy link

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))
}

@igorka48
Copy link

thnx

@krasavello13
Copy link

krasavello13 commented Feb 25, 2020

thnx

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

@SergeyBurlaka
Copy link

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

@lpetroli
Copy link

The feature request #442 would achieve the desired outcome

@aldrinjoemathew
Copy link

aldrinjoemathew commented Mar 29, 2020

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.

@arnaudgiuliani
Copy link
Member

#442 will be considered

@Wokrey
Copy link

Wokrey commented Jun 18, 2020

@aldrinjoemathew use findFragmentByTag(FragmentATag)

@OrhanTozan
Copy link

@Wokrey findFragmentByTag on what? parentFragmentManager?

@Wokrey
Copy link

Wokrey commented Jul 29, 2020

@Wokrey findFragmentByTag on what? parentFragmentManager?

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

@AramKocharyan
Copy link

AramKocharyan commented Dec 4, 2020

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())
}

@Diegolotr99
Copy link

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

I tried this and it was working, BUT it creates two separate instances of the ViewModel....

@Tetr4
Copy link

Tetr4 commented Oct 8, 2021

We solved this in our app for Koin v3.1.2 like this:

import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import org.koin.android.ext.android.getDefaultScope
import org.koin.androidx.viewmodel.ViewModelOwner.Companion.from
import org.koin.androidx.viewmodel.scope.getViewModel
import org.koin.core.parameter.ParametersDefinition
import org.koin.core.qualifier.Qualifier

/** Creates or retrieves a viewmodel, whose lifecycle is scoped to a relative fragment of type [F]. */
inline fun <reified VM : ViewModel, reified F : Fragment> Fragment.sharedViewModel(
    qualifier: Qualifier? = null,
    noinline parameters: ParametersDefinition? = null
): Lazy<VM> = lazy(LazyThreadSafetyMode.NONE) {
    getDefaultScope().getViewModel(qualifier, null, { from(findRelative<F>()) }, VM::class, parameters)
}

/** Finds a relative fragment of type [F] via BFS. */
inline fun <reified F : Fragment> Fragment.findRelative(): F {
    val visited = mutableListOf<Fragment>()
    val queued = mutableListOf(this.rootFragment)
    while (queued.isNotEmpty()) {
        val current = queued.removeFirst()
        when {
            current is F -> return current
            visited.contains(current) -> continue
        }
        visited.add(current)
        queued.addAll(current.childFragmentManager.fragments)
    }
    throw IllegalStateException("Fragment does not have a relative fragment of type ${F::class.java}")
}

val Fragment.rootFragment: Fragment
    get() {
        var root = this
        while (root.parentFragment != null)
            root = root.requireParentFragment()
        return root
    }

Usage example (sharing a viewmodel between a fragment and dialog):

class LoginFragment : Fragment() {
    private val viewModel: LoginViewModel by viewModel()
    // ...
}

class LoginDialog : BottomSheetDialogFragment() {
    private val viewModel by sharedViewModel<LoginViewModel, LoginFragment>()
    // ...
}

Edit: Feel free to use this (Apache 2.0).

@Ahmed-HS
Copy link

We solved this in our app for Koin v3.1.2 like this:

This right here is the most elegant solution for this issue I've seen. Thank you for sharing;)

@clauub
Copy link

clauub commented Oct 19, 2021

@Tetr4 Hi, how can we use this with Navigation Library from Jetpack?

@Tetr4
Copy link

Tetr4 commented Oct 21, 2021

@clauub We are currently using this solution together with the navigation components library. There is nothing special you have to do. If want to scope the viewmodel to a nav host fragment instead, there are some solutions on this thread: #442

@Thanasis17m
Copy link

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>()
} 

I tried this and it was working, BUT it creates two separate instances of the ViewModel....

@Diegolotr99 Hi, this is working for me as well. Are you sure about the two separate instances? Do you know if this is still an issue or could you provide me more info on how you found out about this so I can test this myself? I've used the Android Studio debugger, but to me it seems that there's only one instance created. 🤔

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests