I am working on a login screen for a compose multiplatform desktop app. Currently I have a button that looks like this
Button(
onClick = {
if (state.validate()) {
val data = state.getData()
val response = persistence.loginUser(data) { isWaiting -> loading.value = isWaiting } as SimpleResponse
if (response.success) {
loginSuccessText.value = response.message
loginErrorText.value = ""
} else {
loginErrorText.value = response.message
loginSuccessText.value = ""
}
}
},
colors = ButtonDefaults.buttonColors(backgroundColor = Color(AppColours.Wisteria)),
modifier = Modifier.size(100.dp, 40.dp)
) {
if (loading.value) {
CircularProgressIndicator(modifier = Modifier.size(20.dp))
} else Text("Login")
}
Essentially, this button is attached to a form, validates the state of the form, and if everything looks good, it makes a call to persistence.loginUser which attempts to log in the user using the form data.
loginUser looks like this:
fun loginUser(data: Map<String, String>, onDbRequest: (Boolean) -> Unit): Response {
var response : Response?
onDbRequest(true)
runBlocking {
response = dbRequest(
Params.LoginParams(
username = data["username"]!!,
password = data["password"]!!,
)
)
}
onDbRequest(false)
return response!!
}
where dbRequest is a suspend function.
IDEALLY, how it would work is you click the button, this function is called and calls the onDBRequest argument which puts the button into a loading state, then it runs the db request, once the db request is complete it turns off the loading state of the button and returns the data.
Ideally it would also do all of this without blocking the main thread.
The issue is that I have no idea how to return a value from a coroutine without using runBlocking, which I know that you shouldn't use like this because it blocks the main thread. I know people online have said use Mainscope.launch but I can't figure out how to return a value from inside the launch block
This was my attempt to do so:
fun loginUser(data: Map<String, String>, onDbRequest: (Boolean) -> Unit): Response {
var response : Response?
onDbRequest(true)
scope.launch {
response = dbRequest(
Params.LoginParams(
username = data["username"]!!,
password = data["password"]!!,
)
)
onDbRequest(false)
return@launch response!!
}
}
So how can I structure my code to not freeze the UI while also asynchronously executing this database request?
It is technically impossible to wait inline in a non-suspend function without blocking the thread that called it. Usual solution with coroutines is to turn all functions that have to wait into suspend functions, and launch a coroutine somewhere at the beginning, for example when handling an event:
I assume
dbRequest
is a suspend function. If it isn't and it is blocking, then you need to wrap it inwithContext(Dispatchers.IO) {}
.Please note this is a generic solution which can be used with many different frameworks. I'm not very familiar specifically with Compose. Maybe in Compose there are more specific and recommended patterns or utilities for this kind of cases.