I’m migrating as much androidMain code to commonMa...
# koin
m
I’m migrating as much androidMain code to commonMain as possible. I’m wondering about viewmodels associated with the
MainActivity
which I currently access using:
Copy code
inline fun <reified T : ViewModel> koinActivityViewModel(
    qualifier: Qualifier? = null,
    key: String? = null,
    scope: Scope = currentKoinScope(),
    noinline parameters: ParametersDefinition? = null,
): T = koinViewModel<T>(
    qualifier = qualifier,
    viewModelStoreOwner = LocalActivity.current as ComponentActivity,
    key = key,
    scope = scope,
    parameters = parameters,
)
Claude is recommending I just use
single { }
for such a view model. Thoughts?
s
Never use
single
for view models, they should init and be cleared in respect with their owner lifecycle (activity in your case)
a
not single, you should use viewModel in CMP
☝️ 1
m
Yes, I've never used single for viewmodel. But what's the alternative? On Android I use the above code so that a viewmodel can be shared between screens in the same activity. Is there some scoping way?
s
There is
viewModel { }
specifically for ViewModels
m
But that's scoped to the screen. I need something that is accessible to all screens (nav routes) in a nav graph
s
scoped to the screen
What do you mean by “screen”? A NavBackStackEntry?
m
Yes
Look for the viewmodelstoreowner
s
There are 3 owners: Activity, Fragment and NanBackStackEntry, If you want a shared viewModel you need to attach it to the Activity, and probably pass it explicitly to your screens
m
You don't need to pass it to the screens, you can just use the above code to inject it. The viewmodelstoreowner is on the activity. My question is how to implement this on iOS
How about:
Copy code
@Composable
inline fun <reified T : ViewModel> koinNavGraphViewModel(
    navController: NavController,
    qualifier: Qualifier? = null,
    noinline parameters: ParametersDefinition? = null,
): T {
    // Get the parent nav graph route automatically
    val navGraphRoute = remember {
        navController.currentBackStackEntry?.destination?.parent?.route
            ?: error("No parent navigation graph found")
    }
    
    val backStackEntry = navController.getBackStackEntry(navGraphRoute)
    
    return koinViewModel(
        viewModelStoreOwner = backStackEntry,
        qualifier = qualifier,
        parameters = parameters,
    )
}
Hmm, that’s not working well for me because I need access to the view models before the NavHost is properly set up. How about using CompositionLocal:
Copy code
@Composable
fun AppScreen() {
    val appViewModelStoreOwner = remember {
        object : ViewModelStoreOwner {
            override val viewModelStore = ViewModelStore()
        }
    }

    DisposableEffect(Unit) {
        onDispose {
            appViewModelStoreOwner.viewModelStore.clear()
        }
    }
    
    CompositionLocalProvider(LocalAppViewModelStoreOwner provides appViewModelStoreOwner) {
        // screen content
    }
}

// CompositionLocal
val LocalAppViewModelStoreOwner = staticCompositionLocalOf<ViewModelStoreOwner?> { null }

@Composable
inline fun <reified T : ViewModel> koinAppViewModel(
    qualifier: Qualifier? = null,
    key: String? = null,
    scope: Scope = currentKoinScope(),
    noinline parameters: ParametersDefinition? = null,
): T {
    return koinViewModel(
        qualifier = qualifier,
        viewModelStoreOwner = LocalAppViewModelStoreOwner.current
            ?: error("No ViewModelStoreOwner provided. Make sure AppScreen provides it."),
        key = key,
        scope = scope,
        parameters = parameters,
    )
}
s
Sorry I’m not quite following what are you trying to achive here? Why do you need to pass
viewModelStoreOwner
explicitly? Isn’t it enought to just
Copy code
@Composable
fun App() {
    AppTheme {

        // If you want to share a view model just instantiate it on the higher level
        // it will be attached to Activity or main UIController
        val sharedViewModel = koinViewModel<SharedViewModel>()

        val navController = rememberNavController()
        NavHost(
            navController = navController,
            startDestination = MainNavKey,
            modifier = Modifier.fillMaxSize(),
        ) {
            composable<MainNavKey> {
                // Then "narrower" view models you instantiate within navigation composable
                val mainViewModel = koinViewModel<MainViewModel>()
                MainScreen(mainViewModel)
            }
        }
    }
}
You don’t need (and shouldn’t) deal with view model store owners yourself
m
It’s possible but I don’t like it all for several reasons. First, you end up passing viewmodels around instead of being able to inject them where needed. Second, it would be a bug to access the viewmodel using
koinViewModel
in the screen where it’s needed, because of the incorrect viewmodelstoreowner. This would be an easy mistake to make.
A much simpler solution is:
Copy code
val LocalAppViewModelStoreOwner = staticCompositionLocalOf<ViewModelStoreOwner?> { null }

@Composable
inline fun <reified T : ViewModel> koinAppViewModel(
    qualifier: Qualifier? = null,
    key: String? = null,
    scope: Scope = currentKoinScope(),
    noinline parameters: ParametersDefinition? = null,
): T {
    val appViewModelStoreOwner = LocalAppViewModelStoreOwner.current
        ?: error("No ViewModelStoreOwner provided. Make sure AppScreen provides it.")
    return koinViewModel(
        qualifier = qualifier,
        viewModelStoreOwner = appViewModelStoreOwner,
        key = key,
        scope = scope,
        parameters = parameters,
    )
}

@Composable
fun AppScreen() {
    CompositionLocalProvider(
        LocalAppViewModelStoreOwner provides LocalViewModelStoreOwner.current,
    ) {
        // content
    }
}

@Composable
fun FooScreen() {
    val fooViewModel: FooViewModel = koinAppViewModel()
    // content
}
Perhaps the upcoming
retain
api could be used for this. https://proandroiddev.com/exploring-retain-api-a-new-way-to-persist-state-in-jetpack-compose-bfb2fe2eae43 Is this something koin would work with?
a
interesting 👍