'Why can't I launch viewModel() two times when I use Hilt as DI in an Android Studio project?
I use Hilt as DI in an Android Studio project, and viewModel() will create an instance of SoundViewModel automatically.
Code A works well.
I think viewModel() will create an Singleton of SoundViewModel.
I think mViewMode_A will be assigned to mViewMode_B automatically without creating a new instance in Code B.
I think both mViewMode_A and mViewMode_B will point the same instance in Code B.
But I don't know why I get Result B when I run Code B, could you tell me?
Result B
java.lang.RuntimeException: Cannot create an instance of class info.dodata.soundmeter.presentation.viewmodel.SoundViewModel
Code A
@Composable
fun NavGraph(
mViewModel_A: SoundViewModel = viewModel()
) {
ScreenHome(mViewMode_B = mViewMode1_A)
}
@Composable
fun ScreenHome(
mViewModel_B: SoundViewModel
) {
...
}
@HiltViewModel
class SoundViewModel @Inject constructor(
@ApplicationContext private val appContext: Context,
...
): ViewModel() {
...
}
Code B
@Composable
fun NavGraph(
mViewMode_A: SoundViewModel = viewModel()
) {
ScreenHome()
}
@Composable
fun ScreenHome(
mViewMode_B: SoundViewModel = viewModel() // I think mViewMode_A will be assigned to mViewMode_B automatically without creating a new instnace.
) {
...
}
//The same
Solution 1:[1]
If you want to have a different instance of the same ViewModel type for your composable screens, you need to go like this:
MainActivity:
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
AppTheme {
NavHost(
navController = navController,
startDestination = "screen_a"
) {
composable(route = "screen_a") {
ScreenA {
navController.navigate(route = "screen_b") {
launchSingleTop = true
}
}
}
composable(route = "screen_b") {
ScreenB()
}
}
}
}
}
}
ScreenA:
@Composable
fun ScreenA(
viewModel: MainViewModel = hiltViewModel(),
navToScreenB: () -> Unit
) {
val state by viewModel.state
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = state)
Button(onClick = navToScreenB) {
Text(text = "Nav to Screen B")
}
}
}
ScreenB:
@Composable
fun ScreenB(viewModel: MainViewModel = hiltViewModel()) {
val state by viewModel.state
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = state)
}
}
MainViewModel:
@HiltViewModel
class MainViewModel @Inject constructor() : ViewModel() {
private var _state = mutableStateOf(
value = "random value is: ${Random.nextInt(from = 0, until = 99999999)}"
)
val state: State<String> get() = _state
}
Following this logic, every time you navigate to ScreenB a new instance of a MainViewModel will be generated. This happens because we instantiate a MainViewModel with the hiltViewModel() in the constructor ScreenB (and also on ScreenA).
But if you want to share the same instance of the ViewModel, you need to create it at a level above the composable screens (for example, in MainActivity) and pass the instance on to whoever will use it, like this:
MainActivity:
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
// this will instantiate in normal way, to use in any scope on MainActivity
// private val mainViewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
// this will generate the instance for this setContent composable scope only
val mainViewModel: MainViewModel = hiltViewModel()
AppTheme {
NavHost(
navController = navController,
startDestination = "screen_a"
) {
composable(route = "screen_a") {
ScreenA(viewModel = mainViewModel) {
navController.navigate(route = "screen_b") {
launchSingleTop = true
}
}
}
composable(route = "screen_b") {
ScreenB(viewModel = mainViewModel)
}
}
}
}
}
}
Note: you must choose only one of the MainViewModel initialization examples that I put in MainActivity.
ScreenA:
@Composable
fun ScreenA(
viewModel: MainViewModel,
navToScreenB: () -> Unit
) {
val state by viewModel.state
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = state)
Button(onClick = navToScreenB) {
Text(text = "Nav to Screen B")
}
}
}
ScreenB:
@Composable
fun ScreenB(viewModel: MainViewModel) {
val state by viewModel.state
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = state)
}
}
And that's it, ScreenA and ScreenB have the same instance of a MainViewModel, because this time we didn't create the instance in their constructor, but instead passed on the instance created in MainActivity.
Edit:
I forgot to mention that you will need the following dependencies:
// hilt standard
implementation 'com.google.dagger:hilt-android:2.42'
kapt 'com.google.dagger:hilt-android-compiler:2.42'
// hilt support for compose (to use hiltViewModel())
implementation 'androidx.hilt:hilt-navigation-compose:1.0.0'
// navigation for compose
implementation 'androidx.navigation:navigation-compose:2.4.2'
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 |
