How to share same viewmodel instance in koin compose navigation

305 Views Asked by At

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

There are 3 best solutions below

1
On BEST ANSWER
  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.

3
On

As I see in your code you are creating two separate instances that's why you are facing this issue. there may be two ways to achieve this.

  1. Pass the same ViewModel first screen instance from the first screen navigation time.

  2. you can create one instance on ViewModel inside the navigation graph and use or pass it to both screens. like below


@Composable
fun SimpleNavigation(navController: NavHostController = 
rememberNavController()) {
val viewModel: FirstViewModel = koinNavViewModel()
    NavHost(
        navController = navController,
        startDestination = navController.currentBackStackEntry?.destination?.route ?: "first_screen"
    ) {
        composable("first_screen") {
            
            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 firstName by viewModel.firstName.collectAsState()
            LaunchedEffect(firstName){
                println(">> $firstName")
            }
            Surface {
                Column(Modifier.fillMaxSize()) {
                    firstName?.let { name -> Text(text = name) }
                }
            }
        }
    }
}

This reference is help you link

1
On

Even though the koinNavViewModel() would be unique, your example will never work as you use a sharedFlow. Indeed the sharedFlow doesn't store data in it by default, if you want that behavior you need to add replay=1 or replace it implementation by a stateFlow