Как правильно использовать Viewmodel в 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
}
и я использую репозиторий для интеграции удаленных и локальных моделей, как это
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, используя
BottomNavigation
с участием
NavHost
для создания традиционного действия BottomNavigation
@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()
для преобразования потока данных из комнаты в составное состояние
@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 = {}) })
}
}
}
И это создало работоспособную
MainActivity
с BottomNavigation, но когда я быстро переключаюсь между кнопками 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$1.invoke(NavBackStackEntryProvider.kt:51)
at androidx.navigation.compose.NavBackStackEntryProviderKt$LocalOwnersProvider$1.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$3.invoke(NavHost.kt:132)
at androidx.navigation.compose.NavHostKt$NavHost$3.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$1$1.invoke(Crossfade.kt:74)
at androidx.compose.animation.CrossfadeKt$Crossfade$1$1.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$4.invoke(Unknown Source:13)
at androidx.navigation.compose.NavHostKt$NavHost$4.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$2$5.invoke(Composer.kt:2580)
at androidx.compose.runtime.ComposerImpl$doCompose$2$5.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$2$2.invoke(Recomposer.kt:447)
at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$2.invoke(Recomposer.kt:416)
at androidx.compose.ui.platform.AndroidUiFrameClock$withFrameNanos$2$callback$1.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$1.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)
Я использую точку останова отладки, чтобы увидеть, что произошло, и оказалось, что она использует
getViewModelStore
когда
NavBackStackEntry
достигать
Lifecycle.State.DESTROYED
состояние, и я понятия не имею, как это исправить. Буду очень признателен, если кто-нибудь сможет мне помочь
2 ответа
Попробуйте инициализировать свою модель просмотра в основном действии, например
val viewModel by viewModels<MainViewModel>()