I really struggle with coroutines, so I've cobbled...
# coroutines
v
I really struggle with coroutines, so I've cobbled together some
runBlocking{}
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:
JETPACK COMPOSE LOGIN BUTTON calls a function on a 'VIEWMODEL'
Copy code
ElevatedButton(
      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
Copy 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
Copy code
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>
.
Anyway, I'd really appreciate some feedback and a better way of doing this. I feel like I am missing some proper understanding.
j
In general, you should avoid
runBlocking { ... }
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()
.
1
v
Does that apply to Desktop as well? Since my 'view model' is just a plain class, there's no android lifecycle in the project.
j
Yes, the Compose UI
CoroutineScope
still has the lifecycle of the composable.
Then just make your viewmodel's function a suspend function.
v
And then pass the scope down to my function so that I can use
await()
?
Copy code
val crScope = rememberCoroutineScope()
LoginDialog(
   onDismiss = { },
   onSubmit = { crScope.launch { viewModel.login(it, this) } })
}
Thanks for the feedback so far 👍
j
Yes, you can pass the
CoroutineScope
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.:
Copy code
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:
Copy code
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.
I highly recommend Roman's blog posts on structured concurrency and other coroutine topics on his blog.