InsertKoinIO / koin

Koin - a pragmatic lightweight dependency injection framework for Kotlin & Kotlin Multiplatform
https://insert-koin.io
Apache License 2.0
9.12k stars 721 forks source link

Koin 3.0.1 + Jetpack Compose Navigation - getViewModel() create new ViewModel instance instead of return existing one #1079

Closed glush closed 3 years ago

glush commented 3 years ago

I'd try to use Koin 3.0.1 with new Jetpack Compose Navigation component. I'd pass ViewModel instance to Composable function using "composabe way" -

@Composable fun ComposableFun(viewmodel: MyViewModel = getViewModel()) {...}

If I'm not using Jetpack Compose Navigation Koin works as expected - getViewModel() returns existing ViewModel instance.

When app navigation use Jetpack Compose Navigation component getViewModel() creates new ViewModel instance every time at composable function enter. Same behavior if

val vm : MainViewModel by viewModel()

used inside composable fun.

To Reproduce Here is repo. https://gitlab.com/gLush/koinwithnavigation Initial commit - working sample w/o Navigation component. Last commit - Navigation Component implementation with problem described. You can see how random viewModel stamp changes on screen change. Logcat also shows new VM instance creation.

Expected behavior viewModel: ViewModelType = getViewModel() should return existing ViewModel instance.

Koin project used and used version (please complete the following information): implementation ("io.insert-koin:koin-android-ext:3.0.1") implementation ("io.insert-koin:koin-androidx-compose:3.0.1")

Additional moduleDefinition Add any other moduleDefinition about the problem here. implementation ("androidx.navigation:navigation-compose:1.0.0-alpha10")

jakobmann commented 3 years ago

Got the exact same issue with:

implementation ("io.insert-koin:koin-android-ext:3.1.2")
implementation ("io.insert-koin:koin-androidx-compose:3.1.2")

implementation ("androidx.navigation:navigation-compose:2.4.0-alpha04")
raghavpai commented 3 years ago

Is there any update on this issue?

leungandrew commented 3 years ago

I've worked around this by following suit on how Hilt handles scoping ViewModels to a nav graph. They essentially us a NavBackstackEntry as their ViewModelStoreOwner.

So I created the following function:

@Composable
inline fun <reified VM : ViewModel> getNavGraphViewModel(
    qualifier: Qualifier? = null,
    noinline parameters: ParametersDefinition? = null,
    backStackEntry: NavBackStackEntry
): VM {
    return remember(qualifier,parameters) {
        backStackEntry.getViewModel(qualifier, parameters)
    }
}

And you would pass something like navController.getBackStackEntry("your-root-route-like-the-route-of-your-nav-host")

@Composable
fun HomeScreen(
    navController: NavController,
    vehiclesViewModel: VehiclesViewModel = getNavGraphViewModel(
        backStackEntry = navController.getBackStackEntry("root")
    )
) {
}

where this HomeScreen is a composable in your NavHost, and root is the route of my NavHost

darthkali commented 3 years ago

I run in the same issue!

These are My dependecies:

const val core = "io.insert-koin:koin-core:3.1.1"
const val android = "io.insert-koin:koin-android:3.1.1"
const val compose = "io.insert-koin:koin-androidx-compose:3.1.1"

const val navigation = "androidx.navigation:navigation-compose:2.4.0-alpha04"
burnoo commented 3 years ago

I've released a library - cokoin, which supports Compose Navigation . It is an alternative to org.koin:koin-androidx-compose.

Library Migration Guide

Use Cokoin with Compose Navigation

  1. Add dependency:
    implementation {
    implementation "dev.burnoo:cokoin-jetpack-navigation:0.1.8"
    }
  2. Replace NavHost with KoinNavHost:
    KoinNavHost(navController, startDestination = "1") {
    composable("1") {
        Screen1()
    }
    }
  3. Use getNavController and getNavViewModel inside composables.
    @Composable
    fun Screen1() {
    val navController = getNavController()
    Button(onClick = { navController.navigate("2") }) {
        val navViewModel = getNavViewModel<MainViewModel>()
        //...
    }
    }
wafer-li commented 3 years ago

Update solution from https://github.com/InsertKoinIO/koin/issues/1079#issuecomment-902215765

At koin 3.1.3 and above, there is no backStackEntry.getViewModel(qualifier, parameters), so the solution above doesn't work.

After looking into the source code of koin, I found that the getViewModel() method from koin 3.1.3 use LocalViewModelStoreOwner.

https://github.com/InsertKoinIO/koin/blob/master/android-compose/koin-androidx-compose/src/main/java/org/koin/androidx/compose/ViewModelComposeExt.kt#L49

Therefore, we could use the CompositionLocal to provide the NavBackStackEntry as ViewModelStoreOwner

composable(Home) { navBackStackEntry ->
    CompositionLocalProvider(LocalViewModelStoreOwner provides navBackStackEntry) {
        HomeScreen(navController = navController, homeViewModel = getViewModel())
    }
}
arnaudgiuliani commented 3 years ago

Last update bring owner param to the getViewModel function:

inline fun <reified T : ViewModel> getViewModel(
    qualifier: Qualifier? = null,
    owner: ViewModelOwner = getComposeViewModelOwner(),
    scope: Scope = GlobalContext.get().scopeRegistry.rootScope,
    noinline parameters: ParametersDefinition? = null,
)

Where ViewModelOwner can be used to pass StoreOwner:

@Composable
fun getComposeViewModelOwner(): ViewModelOwner {
    return ViewModelOwner.from(
        LocalViewModelStoreOwner.current!!,
        LocalSavedStateRegistryOwner.current
    )
}
arnaudgiuliani commented 3 years ago

Documentation need to be updated also.

luangs7 commented 2 years ago

Same problem with 3.2.0-beta-1

arnaudgiuliani commented 2 years ago

there some fixes about VM API that are coming in 3.1.6. It should realign ViewModel API on Google ones

arnaudgiuliani commented 2 years ago

it will be ported to 3.2.x

svenjacobs commented 2 years ago

I'm a bit confused since there are multiple solutions posted here. With Koin 3.2.0 what is the current solution to get a ViewModel instance that is bound to the current NavBackStackEntry when using Navigation Compose? Does it work out of the box?

By the way, the documentation here is outdated for Koin 3.2.0 as it still mentions ViewModelOwner in getViewModel(). This has been replaced with ViewModelStoreOwner.

svenjacobs commented 2 years ago

It seems this works out of the box with Koin 3.2.0 since LocalViewModelStoreOwner inside the composable function of Navigation Compose provides the current NavBackStackEntry.

danieldaeschle commented 1 year ago

I still have this problem and don't know how to solve it.

arnaudgiuliani commented 1 year ago

what version have you @danieldaeschle ?

danieldaeschle commented 1 year ago

Latest koin-androidx-compose 3.4.2

jdavisAR commented 1 year ago

Latest koin-androidx-compose 3.4.2

I'm also experiencing this issue when trying to pass a parameter to a ViewModel.

arnaudgiuliani commented 1 year ago

do you use koinViewModel function?

aungkhanthtoo commented 1 year ago

Add "io.insert-koin:koin-androidx-compose-navigation:3.4.5" and use koinNavViewModel() function. It works!

kepper104 commented 1 year ago

Ran into same problem, using koin-androidx-compose:3.4.6 and compose-destinations:core:1.9.42-beta for navigation, koinNavViewModel() doesn't change anything, everything just returns new instances of my ViewModel

arnaudgiuliani commented 1 year ago

@kepper104 can you provide a new sample project?

scottyab commented 1 year ago

~I'm seeing the same behaviour a ViewModel is created each time. Even after switching to use koinNavViewModel from koin-androidx-compose-navigation v3.4.6~

Update, turns out my issue was probably not a Koin issue. When navigating with the NavHostController I wasn't setting the NavOptions with launchSingleTop = true so new instances of the Screen and ViewModel were being created which makes sense. This is noted in the official docs but was missed when the functionality was added to project. Once I added the below the ViewModel was not recreated each time I navigated to the screen.


  navHostController.navigate(route = navigationState.route.name) {
                    // Pop up to the start destination of the graph to
                    // avoid building up a large stack of destinations
                    // on the back stack as users select items
                    popUpTo(navHostController.graph.findStartDestination().id) {
                        saveState = true
                    }
                    // Avoid multiple copies of the same destination when
                    // reselecting the same item
                    launchSingleTop = true
                    // Restore state when reselecting a previously selected item
                    restoreState = true
                }
vivek-modi commented 9 months ago

I want to use share same viewmodel instance in koin in jetpack compose navigation. I know there is a function in koinViewModel() to get instance of viewModel. I recently saw a Koin Documentation have separate koin-androidx-compose-navigation which gives a koinNavViewModel() function.

build.gradle.kts

dependencies {

implementation("androidx.core:core-ktx:1.12.0")

implementation("io.insert-koin:koin-android:3.4.0")
implementation("io.insert-koin:koin-androidx-workmanager:3.4.0")
implementation("io.insert-koin:koin-androidx-compose:3.4.6")
implementation("io.insert-koin:koin-androidx-compose-navigation:3.4.6")

implementation(platform("androidx.compose:compose-bom:2023.06.01"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.foundation:foundation-layout")
implementation("androidx.compose.material:material")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.runtime:runtime")
implementation("androidx.compose.runtime:runtime-livedata")
implementation("androidx.compose.ui:ui-tooling")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose")
implementation("androidx.activity:activity-compose:1.7.0")
implementation("androidx.lifecycle:lifecycle-runtime-compose:$2.6.2")
implementation("androidx.navigation:navigation-compose:$2.6.0")

testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation(platform("androidx.compose:compose-bom:2023.08.00"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")

} Now I am trying to make a use of One viewmodel to different screens. When I get some data in viewmodel, I stored in SharedFlow and navigate to another screen with same viewmodel instance it gives me variable null.

MainActivity.kt

class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { SimpleComposeNavigationTheme { SimpleNavigation() } } } } SimpleNavigation

@Composable fun SimpleNavigation(navController: NavHostController = rememberNavController()) {

NavHost(
    navController = navController,
    startDestination = navController.currentBackStackEntry?.destination?.route ?: "first_screen"
) {
    composable("first_screen") {
        val viewModel: FirstViewModel = koinNavViewModel()
        Surface {
            Column(Modifier.fillMaxSize()) {
                Button(onClick = { viewModel.updateName("Hello world") }) {
                    Text(text = "Add Name")
                }
               Button(onClick = {
                    navController.navigate("second_screen") {
                        popUpTo(navController.graph.findStartDestination().id) {
                            saveState = true
                        }
                        // Avoid multiple copies of the same destination when
                        // reselecting the same item
                        launchSingleTop = true
                        // Restore state when reselecting a previously selected item
                        restoreState = true
                    }
                }) {
                    Text(text = "Next Screen")
                }
            }
        }
    }
    composable("second_screen") {
        val viewModel: FirstViewModel = koinNavViewModel()
        val firstName by viewModel.firstName.collectAsState()
        LaunchedEffect(firstName){
            println(">> $firstName")
        }
        Surface {
            Column(Modifier.fillMaxSize()) {
                firstName?.let { name -> Text(text = name) }
            }
        }
    }
}

} FirstViewModel.kt

class FirstViewModel : ViewModel() { private val _firstName = MutableStateFlow<String?>(null) val firstName: StateFlow<String?> = _firstName.asStateFlow()

fun updateName(name: String) {
    _firstName.value = name
}

}

sanskar-kumar commented 8 months ago

@vivek-modi Did you found a solution to this?