v79
08/30/2023, 6:32 PMrunBlocking{}
and await()
blocks which is working but is probably awful. I'm writing a Jetpack Compose Desktop app, and I'm building a login mechanism which uses AWS Cognito's hosted UI - so my app launches a web browser, the user logs in, and the app waits for the auth code to be returned. I'd appreciate some code review... basic code in thread:v79
08/30/2023, 6:40 PMElevatedButton(
onClick = { onSubmit(SubmitUser(username, password)) },
enabled = formValid.value
) {
Text("Login")
}
THE VIEWMODEL SETS THE MODE TO "busy awaiting auth" and then calls the AUTHORIZATION SERVICE LOGIN suspend function. IT AWAITS THE AUTHORIZATION CODE
fun login(newUser: SubmitUser) {
_mode.value = Mode.BUSY_AWAITING_AUTH
_authCode = null
runBlocking {
val awaitCode = async {
authService.login(newUser)
}
val code = awaitCode.await()
if (code != null) {
// success! we have an auth code and can use it to fetch some data from an API
_mode.value = Mode.VIEWING
service.getData(_authCode!!)
}
}
}
THE AUTH SERVICE LOGIN FUNCTION IS BONKERS
override suspend fun login(user: SubmitUser): String? {
val cognitoUrl =
"https://<<MY-APP>>.<http://auth.eu-west-2.amazoncognito.com/oauth2/authorize|auth.eu-west-2.amazoncognito.com/oauth2/authorize>..."
try {
if (Desktop.isDesktopSupported()) {
val desktop = Desktop.getDesktop()
if (desktop.isSupported(Desktop.Action.BROWSE)) {
println("Browsing to $cognitoUrl")
withContext(<http://Dispatchers.IO|Dispatchers.IO>) {
desktop.browse(URI.create(cognitoUrl))
}
}
}
} catch (ioe: IOException) {
println("Unable to open browser for authentication")
return null
}
// wait indefinitely
while (authCode == null) {
print('.')
}
return authCode
}
And how does authCode
get a value? There's a Ktor server waiting to respond to the callback url specified in cognito - which is <http://localhost:12345/callback>
.v79
08/30/2023, 6:41 PMJeff Lockhart
08/30/2023, 9:08 PMrunBlocking { ... }
to launch a coroutine. Instead, get a coroutine scope for whatever lifecycle your operation should be running in, and then scope.launch { ... }
your coroutine. For an operation tied to your Compose UI's lifecycle, get the scope with rememberCoroutineScope()
.v79
08/31/2023, 3:57 AMJeff Lockhart
08/31/2023, 4:00 AMCoroutineScope
still has the lifecycle of the composable.Jeff Lockhart
08/31/2023, 4:00 AMv79
08/31/2023, 5:47 AMawait()
?
val crScope = rememberCoroutineScope()
LoginDialog(
onDismiss = { },
onSubmit = { crScope.launch { viewModel.login(it, this) } })
}
Thanks for the feedback so far 👍Jeff Lockhart
08/31/2023, 4:21 PMCoroutineScope
either as a parameter or receiver to the function if you need to launch a coroutine in the function, as you're doing with async
. Roman recommends either making a function suspend
or having CoroutineScope
as a parameter or receiver, but not both, in his blog post. In this case, since you aren't awaiting the results of your viewmodel's login()
function from the UI, it doesn't need to be a suspend function. But you also aren't actually doing any parallel work that you need async
for either. You could just launch
a coroutine and do the suspending work, then update your state that your UI reacts to. E.g.:
fun CoroutineScope.login(newUser: SubmitUser) {
_mode.value = Mode.BUSY_AWAITING_AUTH
_authCode = null
launch {
val code = authService.login(newUser)
if (code != null) {
// success! we have an auth code and can use it to fetch some data from an API
_mode.value = Mode.VIEWING
service.getData(_authCode!!)
}
}
}
Alternatively, you could launch the coroutine from your composable and make login a suspend function:
suspend fun login(newUser: SubmitUser) {
_mode.value = Mode.BUSY_AWAITING_AUTH
_authCode = null
val code = authService.login(newUser)
if (code != null) {
// success! we have an auth code and can use it to fetch some data from an API
_mode.value = Mode.VIEWING
service.getData(_authCode!!)
}
}
This pattern is more useful if the viewmodel function is actually returning something to the UI to use, which it's not in this case.Jeff Lockhart
08/31/2023, 4:24 PM