I have an Activity that is responsible for creating a new entry in the database. In this activity, a button launches a coroutine to do all the database-related stuff, and when it's done, navigate to another Activity where you overview your vault.
Today is my first experience with coroutines, so I don't really understand how things work. I tried different combinations of making functions suspend and not, running in GlobalScope, running in composable-aware coroutine scope etc. The built-in Gemini in Android Studio was, as always, more of a problem than of a helper.
Below is my code.
The button:
val localctx = LocalContext.current
val coroutineScope = rememberCoroutineScope()
Button(
onClick = {
var newEntryUuid = Uuid.random()
val newEntryUuidClone = newEntryUuid
coroutineScope.launch(Dispatchers.IO) {
if (newEntryViewModel.selectedEntryType == EntryTypes.Card)
newEntryUuid = newEntryViewModel.pushNewEntry(card = newEntryViewModel.createCard(), context = localctx)
if (newEntryViewModel.selectedEntryType == EntryTypes.Account)
newEntryUuid = newEntryViewModel.pushNewEntry(account = newEntryViewModel.createAccount(), context = localctx)
newEntryViewModel.entryCreated.value = newEntryUuid != newEntryUuidClone
}
},
enabled = newEntryViewModel.allRequiredFieldsAreFilled,
colors = ButtonColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
disabledContainerColor = Color.LightGray,
disabledContentColor = Color.DarkGray
),
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp)
) {
Text(stringResource(R.string.continue_button))
}
Spacer(
modifier = Modifier.height(80.dp)
)
}
The pushNewEntry functions:
@OptIn(DelicateCoroutinesApi::class)
suspend fun pushNewEntry(account: Account, context: Context): Uuid {
return withContext(Dispatchers.IO) {
val database = DatabaseProvider.getDatabase(context)
val newAccUuid = AccountManager.createAccount(
database = database,
account = account,
encryptionKey = VaultHandler().getEncryptionKey(context)
)
if (selectedFolderUuid != null) {
FolderManager.performEntryFolderOper(
database = database,
operation = FolderManager.EntryFolderOperations.Add,
entryUuid = newAccUuid,
targetFolderUuid = selectedFolderUuid as Uuid
)
}
newAccUuid
}
}
// the very same thing but with a card type
I'm so so sorry for the distraction to some I may've raised >:3 The problem was NOT here. This code is working perfectly fine. My issue was in the Composables. I used default Kotlin variables and not mutableStates, therefore Composables didn't display whatever I've put into the DB. So I've spent 4 hours to chase a non-existent problem.
I would recommend move this code:
var newEntryUuid = Uuid.random()
val newEntryUuidClone = newEntryUuid
coroutineScope.launch(Dispatchers.IO) {
if (newEntryViewModel.selectedEntryType == EntryTypes.Card)
newEntryUuid = newEntryViewModel.pushNewEntry(card = newEntryViewModel.createCard(), context = localctx)
if (newEntryViewModel.selectedEntryType == EntryTypes.Account)
newEntryUuid = newEntryViewModel.pushNewEntry(account = newEntryViewModel.createAccount(), context = localctx)
newEntryViewModel.entryCreated.value = newEntryUuid != newEntryUuidClone
}
to a new method at your viewmodel do to you already have one.
And because you're already updating this value:
newEntryViewModel.entryCreated.value
doing it at your VM will be easier, consistent and testeable, because your logic will be separated from your view.
then on your button now you'll only need to pass the method as parameter:
Button(
onClick = newEntryViewModel::yourMethodToPushEntry
)
therefore your composable doesn't need to worry about manage coroutines.
you can launch it at your viewmodel using viewmodelScope.launch {} yes without the dispatcher because your method:
suspend fun pushNewEntry(
is already a suspend fun and its handling the need of move the context to IO Dispatchers.
Cheers!