У меня есть приложение для Android, которое хранит простые значения с помощью Preferences DataStore. У меня нет экземпляров Proto Datastore.
Проблема в том, что мое приложение совершенно случайно начинает давать сбой при запуске, обычно после слишком много раз очистки пользовательских данных. Я понятия не имею, почему это происходит, поскольку, похоже, это срабатывает даже без вмешательства в мой репозиторий DataStore.
Это MainActivity (единственное действие):
...
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
class MainActivity : ComponentActivity() {
companion object {
var isForeground = false
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
isForeground = true
qrCodeConfiguration()
notificationsConfiguration()
setContent {
val navController = rememberNavController()
val mainviewModel: ComprinhasViewModel = viewModel()
val settingsViewModel: SettingsViewModel = viewModel()
val receiptsViewModel: ReceiptsViewModel = viewModel()
ComprinhasTheme {
NavHost(navController = navController, startDestination = "home") {
navigation(startDestination = "welcome/username", route = "welcome") {
composable("welcome/username") {
UsernameScreen { username, newList ->
navController.navigate(
"welcome/setList/$username/$newList"
)
}
}
composable(
route = "welcome/setList/{username}/{newList}",
arguments = listOf(
navArgument("username") { type = NavType.StringType },
navArgument("newList") { type = NavType.BoolType }
),
enterTransition = {
slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Start)
},
popExitTransition = {
slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.End)
}
) {
val username = it.arguments?.getString("username") ?: ""
val newList = it.arguments?.getBoolean("newList") ?: false
val uiFlow = mainviewModel.uiState
SetListScreen(uiFlow = uiFlow, newList = newList) {listName, listPassword ->
if (newList) {
if (
mainviewModel.createList(username, listName, listPassword)
) {
navController.popBackStack("home", inclusive = false)
}
}
else {
settingsViewModel.updateUserPrefs(username, listName, listPassword)
navController.popBackStack("home", inclusive = false)
}
}
}
}
composable("home") {
HomeScreen(
viewModel = mainviewModel,
toWelcomeScreen = { navController.navigate("welcome") },
toSettingsScreen = { navController.navigate("settings") },
toReceiptsScreen = {
navController.navigate("receipts")
receiptsViewModel.getReceiptsList()
},
showDialog = { navController.navigate("addItem") }
)
}
composable("settings") {
SettingsScreen(
appPreferences = settingsViewModel.appPreferences,
updateUserPrefs = settingsViewModel::updateUserPrefs,
onNavigateBack = navController::popBackStack
)
}
composable("receipts") {
ReceiptsList(
receiptsFlow = receiptsViewModel.receiptsList,
onQrCodeScan = receiptsViewModel::scanQrCode,
uiFlow = receiptsViewModel.uiState,
onNavigateBack = { navController.popBackStack() }
)
}
dialog("addItem") {
InputDialog(
onDismiss = navController::popBackStack,
setValue = {
mainviewModel.addShoppingListItem(ShoppingItem(nomeItem = it, adicionadoPor = settingsViewModel.appPreferences.name))
})
}
}
}
}
}
override fun onStop() {
super.onStop()
isForeground = false
}
private fun qrCodeConfiguration() {
val moduleInstallCLient = ModuleInstall.getClient(this)
val optionalModuleApi = GmsBarcodeScanning.getClient(this)
moduleInstallCLient
.areModulesAvailable(optionalModuleApi)
.addOnSuccessListener {
Toast.makeText(this, "Módulos disponíveis", Toast.LENGTH_SHORT).show()
if (!it.areModulesAvailable()) {
Toast.makeText(this, "QRCode não está presente", Toast.LENGTH_SHORT).show()
val moduleInstallRequest = ModuleInstallRequest.newBuilder()
.addApi(optionalModuleApi)
.build()
moduleInstallCLient.installModules(moduleInstallRequest)
.addOnSuccessListener {
Toast.makeText(this, "Módulo instalado", Toast.LENGTH_SHORT).show()
}
.addOnFailureListener {
Toast.makeText(this, "Falha ao instalar módulo", Toast.LENGTH_SHORT)
.show()
}
} else {
Toast.makeText(this, "QRCode está presente", Toast.LENGTH_SHORT).show()
}
}
.addOnFailureListener {
// TODO: tratar falha na obtencao do leitor de qr code
}
}
private fun notificationsConfiguration() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(
this, Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 101)
}
}
// TODO: separação para melhor UX
val name = "Adição e remoção de itens"
val importance = NotificationManager.IMPORTANCE_DEFAULT
val channel = NotificationChannel("list_notifications", name, importance)
val notificationManager: NotificationManager =
application.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}
Это мой компонуемый HomeScreen:
...
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(
viewModel: ComprinhasViewModel,
toWelcomeScreen: () -> Unit,
toSettingsScreen: () -> Unit,
toReceiptsScreen: () -> Unit,
showDialog: () -> Unit,
) {
val cartList by viewModel.cartList.collectAsState(initial = emptyList())
val shoppingList by viewModel.shoppingList.collectAsState(initial = emptyList())
val homeState by viewModel.uiState.collectAsState(initial = UiState.LOADING)
val scaffoldState = rememberBottomSheetScaffoldState()
val scope = rememberCoroutineScope()
LaunchedEffect(key1 = 1) {
if (viewModel.appPreferences.welcomeScreen) toWelcomeScreen()
else viewModel.getShoppingList()
}
BottomSheetScaffold(
topBar = {
Surface {
TopBar(
showDialog = showDialog,
toReceiptsScreen = toReceiptsScreen,
toSettings = toSettingsScreen,
uiState = homeState
)
}
},
sheetPeekHeight = 115.dp,
scaffoldState = scaffoldState,
sheetContent = {
BottomBar(
cartList = cartList,
onRemoveItem = {
viewModel.removeFromCart(it)
},
onClearCart = {
viewModel.clearCart()
scope.launch { scaffoldState.bottomSheetState.partialExpand() }
},
)
}
) {innerPadding ->
if (homeState == UiState.LOADING) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
CircularProgressIndicator(
modifier = Modifier.width(32.dp)
)
}
}
else {
ShoppingList(
shoppingList = shoppingList,
modifier = Modifier.padding(innerPadding),
onMoveToCart = { viewModel.moveToCart(it) },
onDelete = { viewModel.deleteShoppingItem(it) },
)
}
}
}
Это мой файл репозитория DataStore:
...
data class AppPreferences(
val welcomeScreen: Boolean,
val name: String,
val listId: String,
val listPassword: String,
val lastChanged: Long,
)
class PreferencesRepository(
private val preferencesDatastore: DataStore<Preferences>,
context: Context
) {
private object PreferencesKeys {
val WELCOME_SCREEN = booleanPreferencesKey("welcome_screen")
val USER_NAME = stringPreferencesKey("user_name")
val LIST_ID = stringPreferencesKey("list_id")
val LIST_PASSWORD = stringPreferencesKey("list_password")
val LAST_CHANGED = longPreferencesKey("last_changed")
val UI_STATE = intPreferencesKey("ui_state")
}
val preferencesFlow: Flow<AppPreferences> = preferencesDatastore.data.map { preferences ->
val welcomeScreen = preferences[PreferencesKeys.WELCOME_SCREEN] ?: true
val name = preferences[PreferencesKeys.USER_NAME] ?: ""
val listId = preferences[PreferencesKeys.LIST_ID] ?: ""
val listPassword = preferences[PreferencesKeys.LIST_PASSWORD] ?: ""
val lastChanged = preferences[PreferencesKeys.LAST_CHANGED] ?: -1
AppPreferences(welcomeScreen, name, listId, listPassword, lastChanged)
}
suspend fun updateUiState(state: UiState) {
preferencesDatastore.edit{ it[PreferencesKeys.UI_STATE] = state.ordinal}
}
val uiState: Flow<UiState> = preferencesDatastore.data.map {
val e = it[PreferencesKeys.UI_STATE] ?: UiState.LOADED
enumValues<UiState>().first { it.ordinal == e }
}
suspend fun updateNameAndListId(name: String, listId: String) {
preferencesDatastore.edit { preferences ->
preferences[PreferencesKeys.USER_NAME] = name
preferences[PreferencesKeys.LIST_ID] = listId
}
}
suspend fun updateUserPrefs(name: String, listId: String, listPassword: String) {
preferencesDatastore.edit {preferences ->
preferences[PreferencesKeys.WELCOME_SCREEN] = false
preferences[PreferencesKeys.USER_NAME] = name
preferences[PreferencesKeys.LIST_ID] = listId
preferences[PreferencesKeys.LIST_PASSWORD] = listPassword
}
}
}
И, наконец, моя трассировка стека:
java.util.NoSuchElementException: Array contains no element matching the predicate.
at com.example.comprinhas.data.PreferencesRepository$special$$inlined$map$2$2.emit(Emitters.kt:227)
at kotlinx.coroutines.flow.internal.SafeCollectorKt$emitFun$1.invoke(SafeCollector.kt:15)
at kotlinx.coroutines.flow.internal.SafeCollectorKt$emitFun$1.invoke(SafeCollector.kt:15)
at kotlinx.coroutines.flow.internal.SafeCollector.emit(SafeCollector.kt:87)
at kotlinx.coroutines.flow.internal.SafeCollector.emit(SafeCollector.kt:66)
at androidx.datastore.core.SingleProcessDataStore$data$1$invokeSuspend$$inlined$map$1$2.emit(Collect.kt:137)
at kotlinx.coroutines.flow.FlowKt__LimitKt$dropWhile$1$1.emit(Limit.kt:40)
at kotlinx.coroutines.flow.StateFlowImpl.collect(StateFlow.kt:396)
at kotlinx.coroutines.flow.FlowKt__LimitKt$dropWhile$$inlined$unsafeFlow$1.collect(SafeCollector.common.kt:114)
at androidx.datastore.core.SingleProcessDataStore$data$1$invokeSuspend$$inlined$map$1.collect(SafeCollector.common.kt:114)
at kotlinx.coroutines.flow.FlowKt__CollectKt.emitAll(Collect.kt:109)
at kotlinx.coroutines.flow.FlowKt.emitAll(Unknown Source:1)
at androidx.datastore.core.SingleProcessDataStore$data$1.invokeSuspend(SingleProcessDataStore.kt:117)
at androidx.datastore.core.SingleProcessDataStore$data$1.invoke(Unknown Source:8)
at androidx.datastore.core.SingleProcessDataStore$data$1.invoke(Unknown Source:4)
at kotlinx.coroutines.flow.SafeFlow.collectSafely(Builders.kt:61)
at kotlinx.coroutines.flow.AbstractFlow.collect(Flow.kt:230)
at com.example.comprinhas.data.PreferencesRepository$special$$inlined$map$2.collect(SafeCollector.common.kt:113)
at androidx.compose.runtime.SnapshotStateKt__SnapshotFlowKt$collectAsState$1.invokeSuspend(SnapshotFlow.kt:64)
at androidx.compose.runtime.SnapshotStateKt__SnapshotFlowKt$collectAsState$1.invoke(Unknown Source:8)
at androidx.compose.runtime.SnapshotStateKt__SnapshotFlowKt$collectAsState$1.invoke(Unknown Source:4)
at androidx.compose.runtime.SnapshotStateKt__ProduceStateKt$produceState$3.invokeSuspend(ProduceState.kt:150)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at androidx.compose.ui.platform.AndroidUiDispatcher.performTrampolineDispatch(AndroidUiDispatcher.android.kt:81)
at androidx.compose.ui.platform.AndroidUiDispatcher.access$performTrampolineDispatch(AndroidUiDispatcher.android.kt:41)
at androidx.compose.ui.platform.AndroidUiDispatcher$dispatchCallback$1.run(AndroidUiDispatcher.android.kt:57)
at android.os.Handler.handleCallback(Handler.java:942)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loopOnce(Looper.java:211)
at android.os.Looper.loop(Looper.java:300)
at android.app.ActivityThread.main(ActivityThread.java:8296)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:559)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:954)
Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [androidx.compose.ui.platform.MotionDurationScaleImpl@55e174a, androidx.compose.runtime.BroadcastFrameClock@164a6bb, StandaloneCoroutine{Cancelling}@24a27d8, AndroidUiDispatcher@ac6f731]
Я уже пробовал:
@ianhanniballake, возможно, это так. Я изменил его на val uiState = flowOf(UiState.LOADED), и приложение не вылетало.
Примете ли вы это как решение вашей проблемы или вам нужна дополнительная помощь?
@Левиафан, да, я бы сделал это. Я просто хотел бы получить, если возможно, объяснение, почему произошел сбой на этой строке и только после очистки данных приложения. это из-за UiState.LOADED у оператора Элвиса?
Теперь @ianhanniballake должен преобразовать свой комментарий в ответ, чтобы вы могли его принять. :)
На линии происходит сбой enumValues<UiState>().first { it.ordinal == e }.
Здесь вы сравниваете порядковый номер (то есть просто Int со значением e.
Однако вы используете следующий код для e:
val e = it[PreferencesKeys.UI_STATE] ?: UiState.LOADED
Это означает, что когда it включает ключ для UI_STATE, вы получаете Int, представляющий ранее сохраненный ординал.
Однако, когда ключ еще не сохранен вообще, e становится UiState.LOADED — т. е. фактическим перечислением, а не порядковым номером значения перечисления.
Если вы хотите, чтобы e всегда был порядковым номером, вместо этого вы можете использовать
val e = it[PreferencesKeys.UI_STATE] ?: UiState.LOADED.ordinal
Вы можете редко видеть это при очистке данных, поскольку фактически вы сталкиваетесь с этим только как с состоянием гонки, поскольку как только вы напишете ключ UI_STATE хотя бы один раз, эта ошибка не может появиться (ну, если вы не удалите значение перечисления в обновление приложения - не делайте этого :D)
На линии происходит сбой
enumValues<UiState>().first { it.ordinal == e }. Как работает этот предикат, когда вы попадаете в падеж?: UiState.LOADED? Вы сравниваете порядковый номер с самим фактическим значением, что, похоже, никогда не будет правдой. Если написатьval e = UiState.LOADED, там всегда вылетает?