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.
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.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.