Mobile Development 16 min read

Using Kotlin Coroutines, Flow, Retrofit, and OkHttp for Network Requests in Android

This article explains how to combine Kotlin coroutines, Flow, Retrofit, and OkHttp to create a clean, reactive, and maintainable network request framework for Android applications, covering concepts, core principles, and practical code examples.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Using Kotlin Coroutines, Flow, Retrofit, and OkHttp for Network Requests in Android

In Android development, network requests are a common task. With the popularity of Kotlin coroutines and Flow, we have new tools to handle network requests elegantly. By combining Retrofit and OkHttp, we can build a powerful, easy‑to‑understand, and maintainable network request framework.

1. Kotlin Coroutines and Flow

Kotlin coroutines provide a lightweight thread‑management mechanism that lets us write asynchronous code in a synchronous style, making code concise and readable.

Flow is Kotlin’s reactive stream library for handling asynchronous, time‑related operations. It is a cold stream that only emits data when collected and supports cancellation and pausing.

Using coroutines and Flow, a network request can be abstracted as a data stream described by a simple function.

1.1 Flow Usage

Flow supports coroutine‑based reactive processing. The basic usage includes creating a Flow with flow and emitting data with emit :

val numbersFlow = flow {
    for (i in 1..3) {
        emit(i)
    }
}

Collecting a Flow is done with collect :

GlobalScope.launch {
    numbersFlow.collect { number ->
        println(number)
    }
}

Transformations such as map and filter can be applied:

val squaresFlow = numbersFlow.map { number ->
    number * number
}

Flows can be combined using operators like combine or zip :

val anotherFlow = flow {
    for (i in 4..6) {
        emit(i)
    }
}
val combinedFlow = numbersFlow.combine(anotherFlow) { a, b ->
    a + b
}

Exception handling is performed with catch and onCompletion :

val errorFlow = flow {
    for (i in 1..3) {
        if (i == 2) {
            throw RuntimeException("Error on $i")
        }
        emit(i)
    }
}.catch { e ->
    println("Caught exception: $e")
}

1.2 Flow Principles

Flow is built on coroutines and operates as a cold stream, emitting data only when collected. Its implementation relies on the kotlinx.coroutines.flow package, with core components such as the Flow interface, the flow builder, the collect terminal operator, transformation operators ( map , filter ), combination operators ( combine , zip ), and exception handling operators ( catch , onCompletion ).

1.3 Example Code

The following example demonstrates creating a Flow, transforming it, combining with another Flow, and handling exceptions:

import kotlinx.coroutines.flow.*
import kotlinx.coroutines.*

fun main() = runBlocking {
    // Create Flow
    val numbersFlow = flow {
        for (i in 1..5) {
            delay(100) // Simulate async work
            emit(i)
        }
    }

    // Transform Flow
    val squaresFlow = numbersFlow.map { number ->
        number * number
    }

    // Collect transformed Flow
    squaresFlow.collect { square ->
        println("Square: $square")
    }

    // Create another Flow
    val anotherFlow = flow {
        for (i in 6..10) {
            delay(100)
            emit(i)
        }
    }

    // Combine Flows
    val combinedFlow = numbersFlow.combine(anotherFlow) { a, b ->
        a + b
    }
    combinedFlow.collect { sum ->
        println("Sum: $sum")
    }

    // Exception handling
    val errorFlow = flow {
        for (i in 1..3) {
            if (i == 2) {
                throw RuntimeException("Error on $i")
            }
            emit(i)
        }
    }.catch { e ->
        println("Caught exception: $e")
    }
    errorFlow.collect { number ->
        println("Number: $number")
    }
}

2. Retrofit and OkHttp

Retrofit is a type‑safe HTTP client that turns API endpoints into Kotlin interfaces using annotations, making network code concise and readable.

OkHttp is a powerful HTTP client that supports HTTP/2, connection pooling, GZIP, caching, and more. Retrofit uses OkHttp internally to perform the actual requests.

2.1 Retrofit Usage

Creating a Retrofit instance:

val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .client(OkHttpClient())
    .addConverterFactory(GsonConverterFactory.create())
    .build()

Defining an API interface:

interface ApiService {
    @GET("user/{id}")
    suspend fun getUser(@Path("id") id: Int): User
}

Creating the service and calling the API:

val apiService = retrofit.create(ApiService::class.java)
GlobalScope.launch {
    val user = apiService.getUser(1)
    println("User: $user")
}

2.2 Retrofit Principles

Retrofit works by generating a dynamic proxy for the API interface. Annotations such as @GET , @POST , and @Path describe request details. Converters (e.g., GsonConverterFactory , MoshiConverterFactory ) transform HTTP responses into Kotlin objects, while an OkHttpClient handles the actual network I/O.

2.3 Example Code

import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Path
import kotlinx.coroutines.runBlocking
import okhttp3.OkHttpClient

data class User(val id: Int, val name: String)

interface ApiService {
    @GET("user/{id}")
    suspend fun getUser(@Path("id") id: Int): User
}

fun main() = runBlocking {
    val retrofit = Retrofit.Builder()
        .baseUrl("https://api.example.com/")
        .client(OkHttpClient())
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    val apiService = retrofit.create(ApiService::class.java)
    val user = apiService.getUser(1)
    println("User: $user")
}

3. Combining Coroutines, Flow, Retrofit, and OkHttp

First, configure OkHttp and Retrofit:

val okHttpClient = OkHttpClient.Builder()
    .connectTimeout(15, TimeUnit.SECONDS)
    .readTimeout(15, TimeUnit.SECONDS)
    .writeTimeout(15, TimeUnit.SECONDS)
    .build()

val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .client(okHttpClient)
    .addConverterFactory(GsonConverterFactory.create())
    .addCallAdapterFactory(CoroutineCallAdapterFactory())
    .build()

Define the API interface:

interface ApiService {
    @GET("user/{id}")
    suspend fun getUser(@Path("id") id: Int): User
}

Wrap the network call in a Flow:

fun getUser(id: Int): Flow
= flow {
    val apiService = retrofit.create(ApiService::class.java)
    val user = apiService.getUser(id)
    emit(user)
}.catch { e ->
    // Handle exception
}

Collect the Flow, for example in a ViewModel:

viewModelScope.launch {
    getUser(1).collect { user ->
        // Process user data
    }
}

4. Q&A

4.1 Is it necessary to wrap a coroutine with Flow?

Directly using a coroutine may be sufficient for simple cases, but Flow offers advantages such as clear data‑flow representation, built‑in exception handling ( catch , onCompletion ), a rich set of operators ( map , filter , combine ), back‑pressure support, and flexible concurrency control ( buffer , flatMapMerge , flatMapLatest ). When complex data streams or reactive patterns are needed, Flow is a better choice.

4.2 Can Flow be defined directly in ApiService?

It is possible to declare a Flow return type in the Retrofit service, but this expands the service’s responsibility, making it harder to maintain and test. A custom CallAdapter (e.g., FlowCallAdapterFactory ) would be required. In practice, separating network request logic from Flow handling keeps the code modular.

interface ApiService {
    @GET("user/{id}")
    suspend fun getUser(@Path("id") id: Int): Flow
}
class FlowCallAdapter
: CallAdapter
> {
    // ... implementation ...
}

class FlowCallAdapterFactory : CallAdapter.Factory() {
    // ... implementation ...
}
val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .client(okHttpClient)
    .addConverterFactory(GsonConverterFactory.create())
    .addCallAdapterFactory(FlowCallAdapterFactory())
    .build()

5. Summary

By combining coroutines, Flow, Retrofit, and OkHttp, we can implement network requests in a clean, reactive manner. Abstracting requests as data streams simplifies code, improves readability, and enhances maintainability.

androidNetworkKotlinCoroutinesOkHttpRetrofitFlow
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.