Network call interface in Kotlin
When starting with Kotlin on Android, the first thing I needed to do was to fetch data over the internet. I knew that coroutines provide a built-in way for asynchronous work (no need for AsyncTask or RxJava!), but how do I define my network call interfaces?
The wrong way: Async-style functions
You might think that you need something that you can call from anywhere and just doesn't block the main thread. Googling and StackOverflowing around, you might end up with this easy (but wrong) solution.
fun callServiceAsync(): Deferred<MyResult> = GlobalScope.async(Dispatchers.IO) {
// Make your blocking network call here (e.g. with OkHttp)
}
GlobalScope.launch {
val result = callServiceAsync().await()
// Do something with it
}
Awesome, right? You use Dispatchers.IO
so you are not blocking the main thread. And you can call this from anywhere.
Unfortunately, using GlocalScrope
is an anti-pattern in Kotlin and not recommended for various reasons (e.g. not bound to any job and will continue running even if not needed).
The recommended way: Structured concurrency with async
The ideal way is to create coroutines and use them inside ContextScopes
that are bounded to the lifecycle of a "parent" element (e.g. your ViewModel
). With this approach, the active coroutines will be cancelled when they are not needed anymore (e.g. when the user navigates to another screen). Also, you make sure that you don't have common memory leaks (e.g. network calls that hanged and no one stopped them) in your app.
suspend fun callService(): MyResult = withContext(Dispatchers.IO) {
// Make your blocking network call here (e.g. with OkHttp)
}
viewModelScope.launch {
val result = callService()
// Do something with it
}
A common use case is to make multiple network calls before processing/combining your results to something the user can see. If you just call suspend
functions one after the other they will be executed sequentially.
viewModelScope.launch {
val result1 = callService1()
val result2 = callService2()
// Combine/process the 2 results
}
callService1()
will finish, then callService2()
will start) To run them in parallel, use the async
coroutine builder. Execution starts as soon as the async
block is defined.
viewModelScope.launch {
val result1 = async { callService1() }
val result2 = async { callService2() }
result1.await()
result2.await()
// Combine/process the 2 results
}
callService1()
and callService2()
will start at the same time)The experimental way: Flows
If you need to do complicated compositions of the results, returning the result as a Flow might be suitable. Flows allow you the flexibility to compose and transform the return results in a fluent way.
Didn't try it yet in a project of mine, but seems like a promising approach.
fun callService(): Flow<MyResult> = flow {
// Make your blocking network call here (e.g. with OkHttp)
}.flowOn(Dispatchers.IO)
viewModelScope.launch {
callService().transform {
// Do stuff with the response
}
.zip(callService2())
.collect {
// Combine/process the 2 results
}
}
Happy network calling!