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") }) {
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 = MutableSharedFlow<String?>()
val firstName: SharedFlow<String?> = _firstName.asSharedFlow()
fun updateName(name: String) {
viewModelScope.launch {
_firstName.emit(name)
}
}
}
SampleApplication.kt
class SampleApplication : Application() {
override fun onCreate() {
super.onCreate()
initializeDependencyInjection()
}
private fun initializeDependencyInjection() {
startKoin {
androidLogger(Level.ERROR)
modules(
listOf(simpleModule)
)
}
}
}
val simpleModule = module {
viewModelOf(::FirstViewModel)
}
What is the benefits of using koinNavViewModel()
if we cannot share the same instance in different screens of compose through navigation.
I know there is another question for using another class for storing all these data and retrieve data. I want better appoarch through koin. Thanks
UPDATE
I don't want to make the viewmodel in global level and passed the instance to in each function.
UPDATE 1
i tried hiren-rafaliya and benjytec suggestion and it works
@Composable
fun SimpleNavigation(navController: NavHostController = rememberNavController()) {
NavHost(navController = navController, startDestination = "screenA", route = "parentRoute") {
composable("screenA") {
// create backstack entry from parent route which was passed in NavHost
val backStackEntry = remember(it) { navController.getBackStackEntry("parentRoute") }
// pass the backstack entry as viewModelStoreOwner
val viewModel: MainViewModel = koinNavViewModel(viewModelStoreOwner = backStackEntry)
ScreenA(viewModel) {
navController.navigate("screenB")
}
}
composable("screenB") {
// create backstack entry from parent route which was passed in NavHost
val backStackEntry = remember(it) { navController.getBackStackEntry("parentRoute") }
// pass the backstack entry as viewModelStoreOwner
val viewModel: MainViewModel = koinNavViewModel(viewModelStoreOwner = backStackEntry)
ScreenB(viewModel) {
navController.navigate("screenA")
}
}
}
}
@Composable
fun ScreenA(viewModel: MainViewModel, onNavigate: () -> Unit) {
val firstName by viewModel.firstName
Column {
Text(text = "SCREEN A")
Text(text = "MainViewModel.firstname = $firstName")
Button(onClick = {
viewModel.updateName("ABC")
}) {
Text(text = "MainViewModel.firstname = ABC")
}
Button(onClick = onNavigate) {
Text(text = "Go to SCREEN B")
}
}
}
@Composable
fun ScreenB(viewModel: MainViewModel, onNavigate: () -> Unit) {
val firstName by viewModel.firstName
Column {
Text(text = "SCREEN B")
Text(text = "firstname = $firstName")
Button(onClick = {
viewModel.updateName("DEF")
}) {
Text(text = "MainViewModel.firstname = DEF")
}
Button(onClick = onNavigate) {
Text(text = "Go to SCREEN A")
}
}
}
class MainViewModel : ViewModel() {
private val _firstName = mutableStateOf<String?>(null)
val firstName: State<String?> = _firstName
fun updateName(name: String) {
viewModelScope.launch {
_firstName.value = name
}
}
}
You can create a viewmodel instance in parent and pass it as parameter in both screens.
Provide a viewModelStoreOwner when creating a viewmodel instance so that koin will persist the existing viewmodel till viewModelStoreOwner gets destroyed.
class MainActivity : ComponentActivity() {
@SuppressLint("UnrememberedGetBackStackEntry")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AndroidkoinsharedvmTheme {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "screenA", route = "parentRoute") {
composable("screenA") {
// create backstack entry from parent route which was passed in NavHost
val backStackEntry = remember { navController.getBackStackEntry("parentRoute") }
// pass the backstack entry as viewModelStoreOwner
val viewModel: MainViewModel = koinNavViewModel(viewModelStoreOwner = backStackEntry)
ScreenA(viewModel) {
navController.navigate("screenB")
}
}
composable("screenB") {
// create backstack entry from parent route which was passed in NavHost
val backStackEntry = remember { navController.getBackStackEntry("parentRoute") }
// pass the backstack entry as viewModelStoreOwner
val viewModel: MainViewModel = koinNavViewModel(viewModelStoreOwner = backStackEntry)
ScreenB(viewModel) {
navController.navigate("screenA")
}
}
}
}
}
}
}
@Composable
fun ScreenA(viewModel: MainViewModel, onNavigate: () -> Unit) {
Column {
Text(text = "SCREEN A")
Text(text = "MainViewModel.firstname = ${viewModel.firstName.value}")
Button(onClick = {
viewModel.updateName("ABC")
}) {
Text(text = "MainViewModel.firstname = ABC")
}
Button(onClick = onNavigate) {
Text(text = "Go to SCREEN B")
}
}
}
@Composable
fun ScreenB(viewModel: MainViewModel, onNavigate: () -> Unit) {
Column {
Text(text = "SCREEN B")
Text(text = "MainViewModel.firstname = ${viewModel.firstName.value}")
Button(onClick = {
viewModel.updateName("DEF")
}) {
Text(text = "MainViewModel.firstname = DEF")
}
Button(onClick = onNavigate) {
Text(text = "Go to SCREEN A")
}
}
}
class MainViewModel : ViewModel() {
var firstName = mutableStateOf("")
fun updateName(name: String) = viewModelScope.launch {
firstName.value = name
}
}
This will return the same instance of viewmodel in both screens because both of them have a same viewModelStoreOwner which is route from NavHost.
Koin will persist this viewmodel untill this parent Navhost gets destroyed from memory.