⬆️ ⬇️

Async-Await Android Dive

In the previous article I did a quick overview of async-await on Android. Now it's time to dive a little deeper into the upcoming kotlin version 1.1 functionality.



What is async-await for?



When we are faced with lengthy operations, such as network requests or transactions to the database, we must be sure that the launch takes place in the background thread. If you forget about it, you can get a UI thread lock before the task ends. And while the UI is locked, the user will not be able to interact with the application.



Unfortunately, when we run the task in the background, we cannot use the result right there. For this we need some kind of callback. When the callback is called with the result, only then will we be able to continue, for example, launch another network request.



A simple example of how people come to " callback hell ": a few nested callbacks, everyone is waiting for a call when the replay operation ends.



fun retrieveIssues() { githubApi.retrieveUser() { user -> githubApi.repositoriesFor(user) { repositories -> githubApi.issueFor(repositories.first()) { issues -> handler.post { textView.text = "You have issues!" } } } } } 


This piece of code represents three network requests, where a message is sent to the main thread at the end to update a TextView.



Fix it with async-await



With async-await, you can bring this code to a more imperative style with the same functionality. Instead of sending a callback, you can call the freezing method await , which allows you to use the result just as if it was computed in synchronous code:



 fun retrieveIssues() = asyncUI { val user = await(githubApi.retrieveUser()) val repositories = await(githubApi.repositoriesFor(user)) val issues = await(githubApi.issueFor(repositories.first())) textView.text = "You have issues!" } 


This code still makes three network requests and updates the TextView in the main thread, and does not block the UI!



Wait ... what?



If we use the AsyncAwait-Android library, we will get several methods, two of which are async and await .



The async method allows you to use await and changes the way you get the result. When entering the method, each line will be executed synchronously until it reaches the freeze point (calling the await method). In fact, that's all that async does — it allows you not to move the code into the background thread.



The await method allows you to do things asynchronously. It takes "awaitable" as a parameter, where "awaitable" is some kind of asynchronous operation. When await is called, it is registered to "awaitable" to be notified when the operation is completed, and to return the result to the asyncUI method. When "awaitable" is completed, it will execute the rest of the method, while passing the result to it.



Magic



It all looks like magic, but there is no magic here. In fact, the compiler of the cauldron transforms coroutine (that is within the framework of async ) into a state machine (state machine). Each state of which is part of a coroutine code, where the freeze point (the await call) means the end of the state. When the code passed to await completes, execution proceeds to the next state, and so on.



Consider a simple version of the code presented earlier. We can see which states are created, for this we note every await call:



 fun retrieveIssues() = async { println("Retrieving user") val user = await(githubApi.retrieveUser()) println("$user retrieved") val repositories = await(githubApi.repositoriesFor(user)) println("${repositories.size} repositories") } 


This coroutin has three states:





This code will be compiled into such state-machines (pseudo-byte code):



 class <anonymous_for_state_machine> { // The current state of the machine int label = 0 // Local variables for the coroutine User user = null List<Repository> repositories = null void resume (Object data) { if (label == 0) goto L0 if (label == 1) goto L1 if (label == 2) goto L2 L0: println("Retrieving user") // Prepare for await call label = 1 await(githubApi.retrieveUser(), this) // 'this' is passed as a continuation return L1: user = (User) data println("$user retrieved") label = 2 await(githubApi.repositoriesFor(user), this) return L2: repositories = (List<Repository>) data println("${repositories.size} repositories") label = -1 return } } 


After entering the state machine, label == 0 and the first block of code will be executed. When await is reached, the label will be updated, and the state machine will proceed to the execution of the code passed to await . After that, execution will continue from the point of resume .



After completion of the task sent to await , the state machine's resume (data) method will be called to perform the next part. And this will continue until the last state is reached.



Exception Handling



In the case of the completion of "awaitable" with an error, the state machine will receive a notification of this. In fact, the resume method accepts an additional Throwable parameter, and when the new state is executed, this parameter is checked for null equality. If the parameter is null , then the Throwable is forwarded out.



Therefore, you can use the try / catch statement as usual:



 fun foo() = async { try { await(doSomething()) await(doSomethingThatThrows()) } catch(t: Throwable) { t.printStackTrace() } } 


Multithreading



The await method does not guarantee that awaitable starts in a background thread, but simply registers a listener that responds to awaitable completion. Therefore, awaitable should take care of itself in which thread to start execution.



For example, we sent retrofit.Call <T> to await , call the enqueue () method, and register the listener. Retrofit will take care that the network request is running in the background thread.



 suspend fun <R> await( call: Call<R>, machine: Continuation<Response<R>> ) { call.enqueue( { response -> machine.resume(response) }, { throwable -> machine.resumeWithException(throwable) } ) } 


For convenience, there is one variant of the await method, which accepts the () -> R function and runs it in another thread:



 fun foo() = async<String> { await { "Hello, world!" } } 


async, async <T> and asyncUI



There are three options for the async method.





When using async <T> , you must return a value of type T. The async <T> method itself returns a value of type Task <T> , which, you guessed it, you can send to the await method:



 fun foo() = async { val text = await(bar()) println(text) } fun bar() = async<String> { "Hello world!" } 


Moreover, the asyncUI method ensures that the continuation (code between await ) will occur in the main thread. If you use async or async <T> , then the continuation will occur in the same thread in which the callback was called:



 fun foo() = async { // Runs on calling thread await(someIoTask()) // someIoTask() runs on an io thread // Continues on the io thread } fun bar() = asyncUI { // Runs on main thread await(someIoTask()) // someIoTask() runs on an io thread // Continues on the main thread } 


In custody



As you can see, coroutins provide interesting features and can improve the readability of the code if used correctly. Now they are available in kotlin version 1.1-M02, and you can use the async-await features described in this article using my library on github .



')

Source: https://habr.com/ru/post/314656/



All Articles