androidkotlinandroid-jetpack-composekoinandroid-jetpack-compose-navigation

How to share same viewmodel instance in koin compose navigation


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

Solution

    1. You can create a viewmodel instance in parent and pass it as parameter in both screens.

    2. 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.