为什么在 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_AmViewMode_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 可能会对您有所帮助