为什么在 Android Studio 项目中使用 Hilt 作为 DI 时无法启动 viewModel() 两次?
Why can't I launch viewModel() two times when I use Hilt as DI in an Android Studio project?
我在 Android Studio 项目中使用 Hilt 作为 DI,viewModel()
将自动创建 SoundViewModel
的实例。
代码 A 运行良好。
我认为 viewModel()
会创建一个 SoundViewModel
的单例。
我认为 mViewMode_A
将自动分配给 mViewMode_B
而无需在代码 B 中创建新实例。
我认为 mViewMode_A
和 mViewMode_B
都指向代码 B 中的同一个实例。
但是我不知道为什么我在运行代码B时得到结果B,你能告诉我吗?
结果B
java.lang.RuntimeException: Cannot create an instance of class info.dodata.soundmeter.presentation.viewmodel.SoundViewModel
代码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() {
...
}
代码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
如果您想为可组合屏幕使用同一 ViewModel 类型的不同实例,则需要这样做:
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()
}
}
}
}
}
}
屏幕A:
@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")
}
}
}
屏幕B:
@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
}
按照这个逻辑,每次导航到 ScreenB 时,都会生成一个 MainViewModel 的新实例。发生这种情况是因为我们在构造函数 ScreenB(以及 ScreenA)中使用 hiltViewModel() 实例化了 MainViewModel。
但是,如果您想共享 ViewModel 的同一个实例,则需要在可组合屏幕之上的级别(例如,在 MainActivity 中)创建它,并将该实例传递给将要使用它的任何人,就像这样:
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)
}
}
}
}
}
}
注意:您必须只选择我放在MainActivity中的MainViewModel初始化示例之一。
屏幕A:
@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")
}
}
}
屏幕B:
@Composable
fun ScreenB(viewModel: MainViewModel) {
val state by viewModel.state
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = state)
}
}
就是这样,ScreenA 和 ScreenB 有相同的 MainViewModel 实例,因为这次我们没有在它们的构造函数中创建实例,而是传递了在 MainActivity 中创建的实例。
编辑:
我忘了说你需要以下 依赖项:
// 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'
ViewModel初始化时需要传key
我不擅长组合,但这会解决你的问题
为了创建 ViewModel 的新实例,我们需要设置 Key 属性
ViewModelProvider(requireActivity()).get(<UniqueKey>, SoundViewModel::class.java)
key – 用于识别 ViewModel 的键。
val mViewMode_A = viewModel<SoundViewModel>(key = "NavGraph")
val mViewMode_B = viewModel<SoundViewModel>(key = "ScreenHome")
对于 Composable,此 link 可能会对您有所帮助
我在 Android Studio 项目中使用 Hilt 作为 DI,viewModel()
将自动创建 SoundViewModel
的实例。
代码 A 运行良好。
我认为 viewModel()
会创建一个 SoundViewModel
的单例。
我认为 mViewMode_A
将自动分配给 mViewMode_B
而无需在代码 B 中创建新实例。
我认为 mViewMode_A
和 mViewMode_B
都指向代码 B 中的同一个实例。
但是我不知道为什么我在运行代码B时得到结果B,你能告诉我吗?
结果B
java.lang.RuntimeException: Cannot create an instance of class info.dodata.soundmeter.presentation.viewmodel.SoundViewModel
代码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() {
...
}
代码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
如果您想为可组合屏幕使用同一 ViewModel 类型的不同实例,则需要这样做:
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()
}
}
}
}
}
}
屏幕A:
@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")
}
}
}
屏幕B:
@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
}
按照这个逻辑,每次导航到 ScreenB 时,都会生成一个 MainViewModel 的新实例。发生这种情况是因为我们在构造函数 ScreenB(以及 ScreenA)中使用 hiltViewModel() 实例化了 MainViewModel。
但是,如果您想共享 ViewModel 的同一个实例,则需要在可组合屏幕之上的级别(例如,在 MainActivity 中)创建它,并将该实例传递给将要使用它的任何人,就像这样:
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)
}
}
}
}
}
}
注意:您必须只选择我放在MainActivity中的MainViewModel初始化示例之一。
屏幕A:
@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")
}
}
}
屏幕B:
@Composable
fun ScreenB(viewModel: MainViewModel) {
val state by viewModel.state
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = state)
}
}
就是这样,ScreenA 和 ScreenB 有相同的 MainViewModel 实例,因为这次我们没有在它们的构造函数中创建实例,而是传递了在 MainActivity 中创建的实例。
编辑:
我忘了说你需要以下 依赖项:
// 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'
ViewModel初始化时需要传key
我不擅长组合,但这会解决你的问题
为了创建 ViewModel 的新实例,我们需要设置 Key 属性
ViewModelProvider(requireActivity()).get(<UniqueKey>, SoundViewModel::class.java)
key – 用于识别 ViewModel 的键。
val mViewMode_A = viewModel<SoundViewModel>(key = "NavGraph")
val mViewMode_B = viewModel<SoundViewModel>(key = "ScreenHome")
对于 Composable,此 link 可能会对您有所帮助