Jetpack Compose Navigation中如何正确使用Viewmodel
How to use Viewmodel properly in Jetpack Compose Navigation
我目前正在使用 Jetpack Compose 和其他一些 Jetpack 库构建应用程序,
我用 Room 来存储这样的数据
@Dao
interface ClassDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertClassList(classes: List<ClassData>)
@Query("SELECT * FROM ClassData WHERE id=:id")
fun getClassList(id: String): Flow<List<ClassData>>
}
@Database(
entities = [ClassData::class],
version = 1,
exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun classDao(): ClassDao
}
我使用 Repository 进行远程和本地模型集成,就像这样
class ResourceRepository
@Inject
constructor(
private val userPreference: UserPreference,
private val classDao: ClassDao
) {
fun getClassList() = classDao.getClassList(userPreference.getCachedUserId()).flowOn(Dispatchers.IO)
}
并且我像这样使用 Hilt 进行依赖注入
@Module
@InstallIn(SingletonComponent::class)
object PersistenceModule {
@Provides
@Singleton
fun provideAppDatabase(application: Application): AppDatabase {
return Room.databaseBuilder(
application, AppDatabase::class.java, application.getString(R.string.database))
.fallbackToDestructiveMigration()
.build()
}
@Provides
@Singleton
fun provideClassDao(appDatabase: AppDatabase): ClassDao {
return appDatabase.classDao()
}
}
@Module
@InstallIn(ViewModelComponent::class)
object RepositoryModule {
@Provides
@ViewModelScoped
fun provideResourceRepository(
apiService: ApiService,
userPreference: UserPreference,
classDao: ClassDao
): ResourceRepository {
return ResourceRepository(
apiService,
userPreference,
classDao)
}
}
然后我创建 Viewmodel 以与 Composable 通信数据
@HiltViewModel
class MainViewModel @Inject constructor(private val resourceRepository: ResourceRepository) : ViewModel() {
private val _toast: MutableLiveData<String> = MutableLiveData("")
val toast: LiveData<String>
get() = _toast
val classList = resourceRepository.getClassList()
}
然后我使用 Jetpack Compose 和 JetPack Compose Navigation 创建我的 MainActivity
布局,使用 BottomNavigation
和 NavHost
构建传统的 BottomNavigation Activity
@Composable
fun Mobile4Main() {
val viewModel = hiltViewModel<MainViewModel>()
val context = LocalContext.current
LocalLifecycleOwner.current.let { owner ->
viewModel.toast.observe(owner) {
if (it.isNotBlank()) {
ToastUtil.show(context, it)
}
}
}
val navController = rememberNavController()
val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Open))
Scaffold(
scaffoldState = scaffoldState,
topBar = { TopAppBar(title = { Text("Home") }) },
floatingActionButtonPosition = FabPosition.End,
floatingActionButton = {
FloatingActionButton(onClick = { viewModel.getResources() }) {
Icon(Icons.Filled.Refresh, "", tint = MaterialTheme.colors.background)
}
},
bottomBar = { MainBottomNavigation(navController) })
{ innerPadding ->
MainNavHost(navController, viewModel, innerPadding)
}
}
@Composable
fun MainBottomNavigation(
navController: NavHostController
) {
BottomNavigation {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
items.forEach { screen ->
BottomNavigationItem(
icon = { Icon(screen.icon, contentDescription = null) },
label = { Text(stringResource(screen.resourceId)) },
selected = currentRoute == screen.route,
onClick = {
if (currentRoute != screen.route) {
navController.navigate(screen.route) {
navController.graph.startDestinationRoute?.let {
popUpTo(it) {
saveState = true
}
}
launchSingleTop = true
restoreState = true
}
}
})
}
}
}
@Composable
fun MainNavHost(
navController: NavHostController,
mainViewModel: MainViewModel,
innerPadding: PaddingValues
) {
NavHost(
navController,
startDestination = Screen.ClassList.route,
Modifier.padding(innerPadding)
) {
composable(Screen.ClassList.route) {
ClassPage(mainViewModel, Modifier.fillMaxHeight())
}
composable(Screen.ExamList.route) {
ExamPage(mainViewModel, Modifier.fillMaxHeight())
}
composable(Screen.ScoreList.route) {
ScorePage(mainViewModel, Modifier.fillMaxHeight())
}
composable(Screen.Statistics.route) {
StatisticsPage(mainViewModel, Modifier.fillMaxHeight())
}
}
}
sealed class Screen(val route: String, @StringRes val resourceId: Int, val icon: ImageVector) {
object ClassList :
Screen("classList", R.string.class_bottom_navigation_item, Icons.Filled.Class)
object ExamList :
Screen("examList", R.string.exam_bottom_navigation_item, Icons.Filled.Dashboard)
object ScoreList :
Screen("scoreList", R.string.score_bottom_navigation_item, Icons.Filled.Score)
object Statistics :
Screen("statistics", R.string.statistics_bottom_navigation_item, Icons.Filled.Star)
}
val items = listOf(Screen.ClassList, Screen.ExamList, Screen.ScoreList, Screen.Statistics)
其中一个页面是这样的,使用Flow.collectAsState()
将数据流从 Room 转换为 Composable State
@Composable
fun ClassPage(
viewModel: MainViewModel,
modifier: Modifier = Modifier
) {
val classesData by viewModel.classList.collectAsState(listOf())
ClassList(classesData, modifier)
}
@Composable
fun ClassList(classesData: List<ClassData>, modifier: Modifier = Modifier) {
val listState = rememberLazyListState()
Column(
modifier = modifier
.fillMaxWidth()
.background(MaterialTheme.colors.background)
) {
LazyColumn(state = listState, contentPadding = PaddingValues(4.dp)) {
items(
items = classesData,
itemContent = { classData -> ClassItem(classData = classData, selectClass = {}) })
}
}
}
它确实使用 BottomNavigation 构建了一个可行的 MainActivity
,但是当我在 BottomNavigation 按钮之间快速切换时,我的应用程序崩溃了,我得到如下错误日志:
Process: ***, PID: 26668
java.lang.IllegalStateException: You cannot access the NavBackStackEntry's ViewModels until it is added to the NavController's back stack (i.e., the Lifecycle of the NavBackStackEntry reaches the CREATED state).
at androidx.navigation.NavBackStackEntry.getViewModelStore(NavBackStackEntry.kt:174)
at androidx.lifecycle.ViewModelProvider.<init>(ViewModelProvider.java:99)
at androidx.lifecycle.viewmodel.compose.ViewModelKt.get(ViewModel.kt:82)
at androidx.lifecycle.viewmodel.compose.ViewModelKt.viewModel(ViewModel.kt:72)
at androidx.navigation.compose.NavBackStackEntryProviderKt.SaveableStateProvider(NavBackStackEntryProvider.kt:86)
at androidx.navigation.compose.NavBackStackEntryProviderKt.access$SaveableStateProvider(NavBackStackEntryProvider.kt:1)
at androidx.navigation.compose.NavBackStackEntryProviderKt$LocalOwnersProvider.invoke(NavBackStackEntryProvider.kt:51)
at androidx.navigation.compose.NavBackStackEntryProviderKt$LocalOwnersProvider.invoke(NavBackStackEntryProvider.kt:50)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:215)
at androidx.navigation.compose.NavBackStackEntryProviderKt.LocalOwnersProvider(NavBackStackEntryProvider.kt:46)
at androidx.navigation.compose.NavHostKt$NavHost.invoke(NavHost.kt:132)
at androidx.navigation.compose.NavHostKt$NavHost.invoke(NavHost.kt:131)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:116)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
at androidx.compose.animation.CrossfadeKt$Crossfade.invoke(Crossfade.kt:74)
at androidx.compose.animation.CrossfadeKt$Crossfade.invoke(Crossfade.kt:69)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
at androidx.compose.animation.CrossfadeKt.Crossfade(Crossfade.kt:86)
at androidx.navigation.compose.NavHostKt.NavHost(NavHost.kt:131)
at androidx.navigation.compose.NavHostKt$NavHost.invoke(Unknown Source:13)
at androidx.navigation.compose.NavHostKt$NavHost.invoke(Unknown Source:10)
at androidx.compose.runtime.RecomposeScopeImpl.compose(RecomposeScopeImpl.kt:140)
at androidx.compose.runtime.ComposerImpl.recomposeToGroupEnd(Composer.kt:2156)
at androidx.compose.runtime.ComposerImpl.skipCurrentGroup(Composer.kt:2399)
at androidx.compose.runtime.ComposerImpl$doCompose.invoke(Composer.kt:2580)
at androidx.compose.runtime.ComposerImpl$doCompose.invoke(Composer.kt:2573)
at androidx.compose.runtime.SnapshotStateKt.observeDerivedStateRecalculations(SnapshotState.kt:540)
at androidx.compose.runtime.ComposerImpl.doCompose(Composer.kt:2566)
at androidx.compose.runtime.ComposerImpl.recompose$runtime_release(Composer.kt:2542)
at androidx.compose.runtime.CompositionImpl.recompose(Composition.kt:613)
at androidx.compose.runtime.Recomposer.performRecompose(Recomposer.kt:764)
at androidx.compose.runtime.Recomposer.access$performRecompose(Recomposer.kt:103)
at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges.invoke(Recomposer.kt:447)
at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges.invoke(Recomposer.kt:416)
at androidx.compose.ui.platform.AndroidUiFrameClock$withFrameNanos$callback.doFrame(AndroidUiFrameClock.android.kt:34)
at androidx.compose.ui.platform.AndroidUiDispatcher.performFrameDispatch(AndroidUiDispatcher.android.kt:109)
at androidx.compose.ui.platform.AndroidUiDispatcher.access$performFrameDispatch(AndroidUiDispatcher.android.kt:41)
at androidx.compose.ui.platform.AndroidUiDispatcher$dispatchCallback.doFrame(AndroidUiDispatcher.android.kt:69)
2021-08-21 17:45:08.153 26668-26668/com.zjuqsc.mobile4 E/AndroidRuntime: at android.view.Choreographer$CallbackRecord.run(Choreographer.java:970)
at android.view.Choreographer.doCallbacks(Choreographer.java:796)
at android.view.Choreographer.doFrame(Choreographer.java:727)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:957)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:223)
at android.app.ActivityThread.main(ActivityThread.java:7660)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
我用debug断点看看发生了什么,结果是在NavBackStackEntry
达到Lifecycle.State.DESTROYED
状态时用了getViewModelStore
,不知道怎么解决.如果有人能帮助我,我将不胜感激
尝试像 val viewModel by viewModels<MainViewModel>()
那样在主 activity 中初始化您的视图模型
更新到 2.4.0-alpha07
解决了我的问题
我目前正在使用 Jetpack Compose 和其他一些 Jetpack 库构建应用程序,
我用 Room 来存储这样的数据
@Dao
interface ClassDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertClassList(classes: List<ClassData>)
@Query("SELECT * FROM ClassData WHERE id=:id")
fun getClassList(id: String): Flow<List<ClassData>>
}
@Database(
entities = [ClassData::class],
version = 1,
exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun classDao(): ClassDao
}
我使用 Repository 进行远程和本地模型集成,就像这样
class ResourceRepository
@Inject
constructor(
private val userPreference: UserPreference,
private val classDao: ClassDao
) {
fun getClassList() = classDao.getClassList(userPreference.getCachedUserId()).flowOn(Dispatchers.IO)
}
并且我像这样使用 Hilt 进行依赖注入
@Module
@InstallIn(SingletonComponent::class)
object PersistenceModule {
@Provides
@Singleton
fun provideAppDatabase(application: Application): AppDatabase {
return Room.databaseBuilder(
application, AppDatabase::class.java, application.getString(R.string.database))
.fallbackToDestructiveMigration()
.build()
}
@Provides
@Singleton
fun provideClassDao(appDatabase: AppDatabase): ClassDao {
return appDatabase.classDao()
}
}
@Module
@InstallIn(ViewModelComponent::class)
object RepositoryModule {
@Provides
@ViewModelScoped
fun provideResourceRepository(
apiService: ApiService,
userPreference: UserPreference,
classDao: ClassDao
): ResourceRepository {
return ResourceRepository(
apiService,
userPreference,
classDao)
}
}
然后我创建 Viewmodel 以与 Composable 通信数据
@HiltViewModel
class MainViewModel @Inject constructor(private val resourceRepository: ResourceRepository) : ViewModel() {
private val _toast: MutableLiveData<String> = MutableLiveData("")
val toast: LiveData<String>
get() = _toast
val classList = resourceRepository.getClassList()
}
然后我使用 Jetpack Compose 和 JetPack Compose Navigation 创建我的 MainActivity
布局,使用 BottomNavigation
和 NavHost
构建传统的 BottomNavigation Activity
@Composable
fun Mobile4Main() {
val viewModel = hiltViewModel<MainViewModel>()
val context = LocalContext.current
LocalLifecycleOwner.current.let { owner ->
viewModel.toast.observe(owner) {
if (it.isNotBlank()) {
ToastUtil.show(context, it)
}
}
}
val navController = rememberNavController()
val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Open))
Scaffold(
scaffoldState = scaffoldState,
topBar = { TopAppBar(title = { Text("Home") }) },
floatingActionButtonPosition = FabPosition.End,
floatingActionButton = {
FloatingActionButton(onClick = { viewModel.getResources() }) {
Icon(Icons.Filled.Refresh, "", tint = MaterialTheme.colors.background)
}
},
bottomBar = { MainBottomNavigation(navController) })
{ innerPadding ->
MainNavHost(navController, viewModel, innerPadding)
}
}
@Composable
fun MainBottomNavigation(
navController: NavHostController
) {
BottomNavigation {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
items.forEach { screen ->
BottomNavigationItem(
icon = { Icon(screen.icon, contentDescription = null) },
label = { Text(stringResource(screen.resourceId)) },
selected = currentRoute == screen.route,
onClick = {
if (currentRoute != screen.route) {
navController.navigate(screen.route) {
navController.graph.startDestinationRoute?.let {
popUpTo(it) {
saveState = true
}
}
launchSingleTop = true
restoreState = true
}
}
})
}
}
}
@Composable
fun MainNavHost(
navController: NavHostController,
mainViewModel: MainViewModel,
innerPadding: PaddingValues
) {
NavHost(
navController,
startDestination = Screen.ClassList.route,
Modifier.padding(innerPadding)
) {
composable(Screen.ClassList.route) {
ClassPage(mainViewModel, Modifier.fillMaxHeight())
}
composable(Screen.ExamList.route) {
ExamPage(mainViewModel, Modifier.fillMaxHeight())
}
composable(Screen.ScoreList.route) {
ScorePage(mainViewModel, Modifier.fillMaxHeight())
}
composable(Screen.Statistics.route) {
StatisticsPage(mainViewModel, Modifier.fillMaxHeight())
}
}
}
sealed class Screen(val route: String, @StringRes val resourceId: Int, val icon: ImageVector) {
object ClassList :
Screen("classList", R.string.class_bottom_navigation_item, Icons.Filled.Class)
object ExamList :
Screen("examList", R.string.exam_bottom_navigation_item, Icons.Filled.Dashboard)
object ScoreList :
Screen("scoreList", R.string.score_bottom_navigation_item, Icons.Filled.Score)
object Statistics :
Screen("statistics", R.string.statistics_bottom_navigation_item, Icons.Filled.Star)
}
val items = listOf(Screen.ClassList, Screen.ExamList, Screen.ScoreList, Screen.Statistics)
其中一个页面是这样的,使用Flow.collectAsState()
将数据流从 Room 转换为 Composable State
@Composable
fun ClassPage(
viewModel: MainViewModel,
modifier: Modifier = Modifier
) {
val classesData by viewModel.classList.collectAsState(listOf())
ClassList(classesData, modifier)
}
@Composable
fun ClassList(classesData: List<ClassData>, modifier: Modifier = Modifier) {
val listState = rememberLazyListState()
Column(
modifier = modifier
.fillMaxWidth()
.background(MaterialTheme.colors.background)
) {
LazyColumn(state = listState, contentPadding = PaddingValues(4.dp)) {
items(
items = classesData,
itemContent = { classData -> ClassItem(classData = classData, selectClass = {}) })
}
}
}
它确实使用 BottomNavigation 构建了一个可行的 MainActivity
,但是当我在 BottomNavigation 按钮之间快速切换时,我的应用程序崩溃了,我得到如下错误日志:
Process: ***, PID: 26668
java.lang.IllegalStateException: You cannot access the NavBackStackEntry's ViewModels until it is added to the NavController's back stack (i.e., the Lifecycle of the NavBackStackEntry reaches the CREATED state).
at androidx.navigation.NavBackStackEntry.getViewModelStore(NavBackStackEntry.kt:174)
at androidx.lifecycle.ViewModelProvider.<init>(ViewModelProvider.java:99)
at androidx.lifecycle.viewmodel.compose.ViewModelKt.get(ViewModel.kt:82)
at androidx.lifecycle.viewmodel.compose.ViewModelKt.viewModel(ViewModel.kt:72)
at androidx.navigation.compose.NavBackStackEntryProviderKt.SaveableStateProvider(NavBackStackEntryProvider.kt:86)
at androidx.navigation.compose.NavBackStackEntryProviderKt.access$SaveableStateProvider(NavBackStackEntryProvider.kt:1)
at androidx.navigation.compose.NavBackStackEntryProviderKt$LocalOwnersProvider.invoke(NavBackStackEntryProvider.kt:51)
at androidx.navigation.compose.NavBackStackEntryProviderKt$LocalOwnersProvider.invoke(NavBackStackEntryProvider.kt:50)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:215)
at androidx.navigation.compose.NavBackStackEntryProviderKt.LocalOwnersProvider(NavBackStackEntryProvider.kt:46)
at androidx.navigation.compose.NavHostKt$NavHost.invoke(NavHost.kt:132)
at androidx.navigation.compose.NavHostKt$NavHost.invoke(NavHost.kt:131)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:116)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
at androidx.compose.animation.CrossfadeKt$Crossfade.invoke(Crossfade.kt:74)
at androidx.compose.animation.CrossfadeKt$Crossfade.invoke(Crossfade.kt:69)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:107)
at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:34)
at androidx.compose.animation.CrossfadeKt.Crossfade(Crossfade.kt:86)
at androidx.navigation.compose.NavHostKt.NavHost(NavHost.kt:131)
at androidx.navigation.compose.NavHostKt$NavHost.invoke(Unknown Source:13)
at androidx.navigation.compose.NavHostKt$NavHost.invoke(Unknown Source:10)
at androidx.compose.runtime.RecomposeScopeImpl.compose(RecomposeScopeImpl.kt:140)
at androidx.compose.runtime.ComposerImpl.recomposeToGroupEnd(Composer.kt:2156)
at androidx.compose.runtime.ComposerImpl.skipCurrentGroup(Composer.kt:2399)
at androidx.compose.runtime.ComposerImpl$doCompose.invoke(Composer.kt:2580)
at androidx.compose.runtime.ComposerImpl$doCompose.invoke(Composer.kt:2573)
at androidx.compose.runtime.SnapshotStateKt.observeDerivedStateRecalculations(SnapshotState.kt:540)
at androidx.compose.runtime.ComposerImpl.doCompose(Composer.kt:2566)
at androidx.compose.runtime.ComposerImpl.recompose$runtime_release(Composer.kt:2542)
at androidx.compose.runtime.CompositionImpl.recompose(Composition.kt:613)
at androidx.compose.runtime.Recomposer.performRecompose(Recomposer.kt:764)
at androidx.compose.runtime.Recomposer.access$performRecompose(Recomposer.kt:103)
at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges.invoke(Recomposer.kt:447)
at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges.invoke(Recomposer.kt:416)
at androidx.compose.ui.platform.AndroidUiFrameClock$withFrameNanos$callback.doFrame(AndroidUiFrameClock.android.kt:34)
at androidx.compose.ui.platform.AndroidUiDispatcher.performFrameDispatch(AndroidUiDispatcher.android.kt:109)
at androidx.compose.ui.platform.AndroidUiDispatcher.access$performFrameDispatch(AndroidUiDispatcher.android.kt:41)
at androidx.compose.ui.platform.AndroidUiDispatcher$dispatchCallback.doFrame(AndroidUiDispatcher.android.kt:69)
2021-08-21 17:45:08.153 26668-26668/com.zjuqsc.mobile4 E/AndroidRuntime: at android.view.Choreographer$CallbackRecord.run(Choreographer.java:970)
at android.view.Choreographer.doCallbacks(Choreographer.java:796)
at android.view.Choreographer.doFrame(Choreographer.java:727)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:957)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:223)
at android.app.ActivityThread.main(ActivityThread.java:7660)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
我用debug断点看看发生了什么,结果是在NavBackStackEntry
达到Lifecycle.State.DESTROYED
状态时用了getViewModelStore
,不知道怎么解决.如果有人能帮助我,我将不胜感激
尝试像 val viewModel by viewModels<MainViewModel>()
更新到 2.4.0-alpha07
解决了我的问题