Android Network Requests: A Practical Guide with Kotlin

举报
yd_217846120 发表于 2026/08/31 14:11:53 2026/08/31
【摘要】 Android Network Requests: A Practical Guide with Kotlin IntroductionNetwork communication is the backbone of almost every modern Android application. Whether you are fetching a list of products, u...

Android Network Requests: A Practical Guide with Kotlin

Introduction

Network communication is the backbone of almost every modern Android application. Whether you are fetching a list of products, uploading a user avatar, or streaming live data, a robust networking layer is essential for a responsive and reliable user experience.

Historically, Android developers juggled HttpURLConnection, AsyncTask, and a fair amount of boilerplate. Today, the Kotlin ecosystem offers elegant, coroutine-friendly solutions that make network code concise, type-safe, and easy to test.

This article walks through the most widely used networking libraries on Android, compares them, and demonstrates how to build a production-ready networking layer using Retrofit + OkHttp + Kotlin Coroutines. We will also touch on Ktor as a modern pure-Kotlin alternative.


The Main Contenders

1. OkHttp

OkHttp is an efficient HTTP client that powers most Android networking stacks under the hood. It supports connection pooling, transparent GZIP, response caching, and HTTP/2. It is the foundation on which Retrofit is built.

2. Retrofit

Retrofit, created by Square, is a type-safe HTTP client for Java and Android. You define an interface describing your API endpoints, and Retrofit generates the implementation at runtime. Combined with coroutines, it turns asynchronous network calls into simple suspend functions.

3. Ktor

Ktor is a Kotlin-first framework from JetBrains. Its HTTP client is fully coroutine-based, multiplatform-ready, and configurable via a plugin pipeline. It is a great choice for new Kotlin Multiplatform projects or teams that want to avoid Java-centric libraries.

Which Should You Choose?

  • Retrofit + OkHttp: The industry default. Massive community, excellent documentation, countless tutorials. Best when you want stability and familiarity.
  • Ktor: Best for Kotlin Multiplatform projects or teams fully committed to a pure-Kotlin toolchain.
  • Raw OkHttp: Best when you need fine-grained control over the wire protocol and do not want an abstraction layer.

Project Setup

Add the required dependencies to your module-level build.gradle.kts:

dependencies {
    // Retrofit
    implementation("com.squareup.retrofit2:retrofit:2.11.0")
    implementation("com.squareup.retrofit2:converter-gson:2.11.0")

    // OkHttp (logging interceptor)
    implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")

    // Coroutines
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")

    // ViewModel + LiveData (for lifecycle-aware state handling)
    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.0")
    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.0")
}

Ensure you have the internet permission in your AndroidManifest.xml:

<uses-permission android:name="android.permission.INTERNET" />

Defining the API Interface

Retrofit maps an annotated Kotlin interface to HTTP calls. Each method describes one endpoint.

interface GithubApi {

    @GET("users/{login}")
    suspend fun getUser(@Path("login") login: String): UserDto

    @GET("search/repositories")
    suspend fun searchRepositories(
        @Query("q") query: String,
        @Query("page") page: Int = 1
    ): RepoSearchResultDto

    @POST("repos/{owner}/{repo}/issues")
    suspend fun createIssue(
        @Path("owner") owner: String,
        @Path("repo") repo: String,
        @Body body: CreateIssueRequest
    ): IssueDto
}

Notice the suspend keyword. Retrofit natively supports coroutines: a suspend function returns the deserialized body directly (or throws an HttpException on failure), so there is no need to wrap the result in Call<T> or Response<T> unless you want access to the raw response.

Data Transfer Objects

data class UserDto(
    val login: String,
    val id: Long,
    val avatarUrl: String,
    val name: String?,
    val publicRepos: Int
)

data class RepoSearchResultDto(
    val totalCount: Int,
    val items: List<RepoDto>
)

data class RepoDto(
    val id: Long,
    val name: String,
    val fullName: String,
    val stargazersCount: Int,
    val owner: UserDto
)

data class CreateIssueRequest(
    val title: String,
    val body: String,
    val labels: List<String> = emptyList()
)

data class IssueDto(
    val number: Int,
    val title: String,
    val state: String,
    val htmlUrl: String
)

Building the Retrofit Instance

Create a singleton object that holds the configured Retrofit instance. Centralizing configuration keeps your app maintainable and makes it easy to swap implementations in tests.

object NetworkModule {

    private const val BASE_URL = "https://api.github.com/"

    private val loggingInterceptor = HttpLoggingInterceptor().apply {
        level = if (BuildConfig.DEBUG) {
            HttpLoggingInterceptor.Level.BODY
        } else {
            HttpLoggingInterceptor.Level.NONE
        }
    }

    private val headerInterceptor = Interceptor { chain ->
        val request = chain.request().newBuilder()
            .header("Accept", "application/vnd.github+json")
            .header("Authorization", "Bearer ${BuildConfig.GITHUB_TOKEN}")
            .build()
        chain.proceed(request)
    }

    private val okHttpClient = OkHttpClient.Builder()
        .addInterceptor(headerInterceptor)
        .addInterceptor(loggingInterceptor)
        .connectTimeout(15, TimeUnit.SECONDS)
        .readTimeout(20, TimeUnit.SECONDS)
        .writeTimeout(20, TimeUnit.SECONDS)
        .build()

    private val retrofit = Retrofit.Builder()
        .baseUrl(BASE_URL)
        .client(okHttpClient)
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    val githubApi: GithubApi = retrofit.create(GithubApi::class.java)
}

Key Points

  • Logging interceptor: Use BODY level only in debug builds to avoid leaking sensitive data in production logs.
  • Header interceptor: Centralizes authentication and content negotiation headers.
  • Timeouts: Always set explicit timeouts. The defaults are generous; tighter limits prevent the UI from hanging on dead connections.

Calling the API from a ViewModel

Network calls should never run on the main thread. With coroutines, launch the request in a lifecycle-aware scope and deliver the result to the UI via StateFlow or LiveData.

class UserViewModel(
    private val api: GithubApi = NetworkModule.githubApi
) : ViewModel() {

    private val _uiState = MutableStateFlow<UserUiState>(UserUiState.Loading)
    val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()

    fun loadUser(login: String) {
        _uiState.value = UserUiState.Loading
        viewModelScope.launch {
            _uiState.value = try {
                val user = api.getUser(login)
                UserUiState.Success(user)
            } catch (e: IOException) {
                UserUiState.Error("Network error: ${e.message}")
            } catch (e: HttpException) {
                UserUiState.Error("Server error: ${e.code()}")
            }
        }
    }
}

sealed interface UserUiState {
    data object Loading : UserUiState
    data class Success(val user: UserDto) : UserUiState
    data class Error(val message: String) : UserUiState
}

Why a Sealed Interface for State?

A sealed state model forces the UI to handle every possible case at compile time. There is no way to forget the loading or error state, which is a common source of crashes and blank screens.


Handling Errors Gracefully

Network code fails in many ways: the device is offline, the server returns 500, the JSON schema changed, or the call timed out. A centralized error handler keeps your ViewModels clean.

sealed class NetworkError : Exception() {
    data object NoConnectivity : NetworkError()
    data class ServerError(val code: Int) : NetworkError()
    data class ParseError(val cause: Throwable) : NetworkError()
    data class Unknown(val cause: Throwable) : NetworkError()
}

suspend fun <T> safeApiCall(block: suspend () -> T): Result<T> {
    return try {
        Result.success(block())
    } catch (e: IOException) {
        Result.failure(NetworkError.NoConnectivity)
    } catch (e: HttpException) {
        Result.failure(NetworkError.ServerError(e.code()))
    } catch (e: JsonSyntaxException) {
        Result.failure(NetworkError.ParseError(e))
    } catch (e: Throwable) {
        Result.failure(NetworkError.Unknown(e))
    }
}

Usage becomes a one-liner in the ViewModel:

viewModelScope.launch {
    when (val result = safeApiCall { api.getUser(login) }) {
        is Result.Success -> _uiState.value = UserUiState.Success(result.getOrThrow())
        is Result.Failure -> _uiState.value = UserUiState.Error(result.exceptionOrNull().toString())
    }
}

Interceptors: The Swiss Army Knife

Interceptors sit between your application code and the network. They are perfect for cross-cutting concerns.

A Retry Interceptor

class RetryInterceptor(
    private val maxRetries: Int = 3
) : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {
        var attempt = 0
        var lastError: IOException? = null
        while (attempt < maxRetries) {
            try {
                val response = chain.proceed(chain.request())
                if (response.isSuccessful) return response
                response.close()
            } catch (e: IOException) {
                lastError = e
            }
            attempt++
        }
        throw lastError ?: IOException("Max retries exceeded")
    }
}

A Caching Interceptor

To benefit from OkHttp’s disk cache, you must opt in. Configure a cache directory and a Cache instance, then add an interceptor that respects cache control headers:

val cacheSize = 10L * 1024 * 1024 // 10 MB
val cache = Cache(File(context.cacheDir, "http-cache"), cacheSize)

val cacheInterceptor = Interceptor { chain ->
    val request = chain.request().newBuilder()
        .header("Cache-Control", "public, max-age=60")
        .build()
    chain.proceed(request)
}

Uploading Files with Multipart

File uploads require multipart request bodies. Retrofit makes this straightforward with the @Multipart annotation.

interface UploadApi {

    @Multipart
    @POST("upload")
    suspend fun uploadFile(
        @Part file: MultipartBody.Part,
        @Part("description") description: RequestBody
    ): UploadResponseDto
}

Building the request:

suspend fun uploadAvatar(file: File, description: String) {
    val requestFile = file.asRequestBody("image/jpeg".toMediaTypeOrNull())
    val filePart = MultipartBody.Part.createFormData("file", file.name, requestFile)
    val descPart = description.toRequestBody("text/plain".toMediaTypeOrNull())

    api.uploadFile(filePart, descPart)
}

The Ktor Alternative

For teams embracing Kotlin Multiplatform, Ktor provides a unified, coroutine-native client that works on Android, iOS, and the JVM.

Setup

dependencies {
    implementation("io.ktor:ktor-client-core:2.3.12")
    implementation("io.ktor:ktor-client-android:2.3.12")
    implementation("io.ktor:ktor-client-content-negotiation:2.3.12")
    implementation("io.ktor:ktor-serialization-gson:2.3.12")
    implementation("io.ktor:ktor-client-logging:2.3.12")
}

Configuration and Usage

val client = HttpClient(Android) {
    install(ContentNegotiation) {
        gson()
    }
    install(Logging) {
        level = LogLevel.ALL
    }
    engine {
        connectTimeout = 15_000
        socketTimeout = 20_000
    }
}

suspend fun fetchUser(login: String): UserDto {
    return client.get("https://api.github.com/users/$login").body()
}

Ktor’s pipeline architecture means features like authentication, logging, and serialization are installed as plugins, keeping configuration declarative and composable.


Best Practices Checklist

  1. Never block the main thread. Always use viewModelScope or a custom CoroutineScope tied to a lifecycle owner.
  2. Set explicit timeouts. Relying on defaults leads to unresponsive UIs on flaky networks.
  3. Use a sealed state model. Represent loading, success, and error explicitly so the compiler enforces complete handling.
  4. Centralize configuration. A single NetworkModule makes it trivial to swap mocks in tests.
  5. Log responsibly. Disable verbose logging in release builds to avoid leaking tokens and personal data.
  6. Cache when possible. Disk caching reduces bandwidth and makes the app usable offline.
  7. Handle errors by category. Distinguish connectivity errors from server errors from parse errors so the user sees a meaningful message.
  8. Inject your client. Passing the API interface through the ViewModel constructor enables easy unit testing with fake implementations.
  9. Pin certificates in production. Use CertificatePinner for apps handling sensitive data to prevent man-in-the-middle attacks.
  10. Measure and monitor. Track latency and failure rates in your analytics pipeline to catch regressions early.

Conclusion

A well-structured networking layer is the difference between an app that feels solid and one that randomly breaks on the train. By combining Retrofit’s type safety, OkHttp’s reliability, and Kotlin coroutines’ structured concurrency, you get code that is concise, testable, and resilient.

Start with the basics: a configured Retrofit client, a sealed UI state, and centralized error handling. Add interceptors and caching as your requirements grow. And if you are heading into Kotlin Multiplatform territory, keep Ktor on your radar as a future-proof alternative.

The libraries will evolve, but the principles stay the same: keep it off the main thread, handle every failure mode, and make the network layer as boring and reliable as possible. Boring networking code is the best networking code.

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。