简体   繁体   中英

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

You need to pass the key in ViewModel initialization

I'm not good at Composable but this will resolve your problem

For creating a new instance of ViewModel we need to set Key property

ViewModelProvider(requireActivity()).get(<UniqueKey>, SoundViewModel::class.java)

key – The key to use to identify the ViewModel.

val mViewMode_A = viewModel<SoundViewModel>(key = "NavGraph")
val mViewMode_B = viewModel<SoundViewModel>(key = "ScreenHome")

For Composable this link may help you separateViewmodel

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'

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM