I have an Android application that stores simple values using Preferences DataStore. I do not have any instances of Proto Datastore.
The problem is, completely randomly my app starts crashing when launching, usually after cleaning user data too many times. I don't have any clue why this is happening since it seems to trigger even without messing with my DataStore repository.
This is the MainActivity (the ony Activity):
...
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)
}
}
This is my HomeScreen composable:
...
@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) },
)
}
}
}
This is my DataStore repository file:
...
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
}
}
}
And finally, my stacktrace:
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]
I already tried:
It is crashing at the line enumValues<UiState>().first { it.ordinal == e }
.
What you're doing there is comparing the ordinal (i.e., just an Int
with the value of e
.
You're using the following code for e
though:
val e = it[PreferencesKeys.UI_STATE] ?: UiState.LOADED
This means when it
includes the key for UI_STATE
, you get an Int
representing the oridinal that you previously saved.
However, when the key hasn't yet been saved at all, e
becomes UiState.LOADED
- i.e., the actual enum itself and not the ordinal of an enum value.
If you want e
to always be an ordinal, you could instead use
val e = it[PreferencesKeys.UI_STATE] ?: UiState.LOADED.ordinal
You may only see this rarely when clearing data since you're effectively only running into this as a race condition, since as soon as you write the UI_STATE
key at least once, this error cannot appear (well, unless you remove an enum value in an app update - don't do that :D)