I am working on an Android app with Jetpack Compose. Here I need to load data (a list of strings) from the app’s storage to display in a LazyColumn, and allow this list to be updated based on user interaction. Essentially, when the user clicks on an item in this list, that item is moved to the top.
To do this I have a MutableState<Result<SnapshotStateList<String>>?> in a global variable (let’s call it recents). This stores the state of the result of loading the data, where null means that it has not been loaded yet. I also use SnapshotStateList so that I can update it later with a different order of items.
(btw, I’m using a custom Result class, but that’s not relevant)
The function that loads the data to recents executes the loading in a different thread, and sets the MutableState’s value when it is done.
private val recents: MutableState<Result<SnapshotStateList<String>>?> = mutableStateOf(null)
@Composable
fun getRecentlySelectedCurrencies(): State<Result<List<String>>?> {
if (recents.value == null) {
val context = LocalContext.current
rememberWork(recents) {
val file = File(context.filesDir, RECENT_CURRENCIES_FILE_NAME)
file.createNewFile()
file.readText()
.split('\n')
.dropLast(1)
.toMutableStateList()
}
}
@Suppress("UNCHECKED_CAST")
return recents as State<Result<List<String>>?>
}
@Composable
fun <T> rememberWork(
state: MutableState<Result<T>?> = mutableStateOf(null),
executor: Executor = WORKER_THREAD, // Single-threaded Executor Service
task: suspend () -> T
): MutableState<Result<T>?> = remember {
suspend fun runTask()
// Don't allow an exception to terminate the worker thread; gotta catch em all.
= try {
Result.Ok(task())
} catch (e: Throwable) {
Result.Err(e)
}
if (state.value != null) {
throw IllegalArgumentException("Value of 'state' must start as null")
}
executor.execute {
setWorkerThreadId(executor)
runBlocking {
state.value = runTask()
}
}
state
}
Here is the composable with the UI elements. When either the MutableState or SnapshotStateList are updated, this should be recomposed.
@Composable
fun CurrencySelectorButton(...) {
val recentCurrencies by getRecentlySelectedCurrencies()
val orderedCurrencies = remember {
val currencies = Currency.getAvailableCurrencies()
...
}
LazyColumn(
...
) {
when (recentCurrencies) {
// Don't show any extra items, but re-sort the currencies list.
is Result.Ok -> {
// Move around elements in orderedCurrencies.
...
}
// Recent currencies not loaded yet.
// Show loading item at the top.
null -> this.item {
this.LoadingMenuItem()
}
// Show an error item at the top.
is Result.Err -> this.item {
val err = (recentCurrencies as Result.Err).error
this.ErrorMenuItem(err)
}
}
this.items(
items = orderedCurrencies.currencySearchFilter(currencySearchState.text),
key = { currency -> currency.currencyCode }
) { currency ->
...
}
}
}
The problem is that I want to move recents and the function to a different file. But if recents is not in MainActivity.kt, the UI won’t be recomposed when the MutableState is updated and it will think that recentCurrencies is still null. Recomposition only happens when the SnapshotStateList is updated when the user selects an item and it is moved to the front of the list.
This is not a problem with a data race because there is only one line (in rememberWork()) that can set the value of recents, and the value is only set once in the entire runtime of the program. And even if it was, why would putting recents in MainActivity.kt prevent the data race?
I checked if maybe rememberWork() was not setting the value of the MutableState when the task returns, but it definitely is doing that (checked with a print statement).
Also, I tried adding a delay to the file loading task in case this has something to do with thread , and it seems that calling delay(50) with at least 50 milliseconds fixes it. So maybe I was wrong about it not being a data race?
If using a MutableState is not the best way to execute a task in a different thread and get a return value, then what is the right way of doing it?