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 布局,使用 BottomNavigationNavHost 构建传统的 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 解决了我的问题