androidkotlinandroid-jetpack-datastore

NoSuchElementException somewhere in DataStore


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:


Solution

  • 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)