第 9 篇|网络请求基础 —— 从 HttpURLConnection 到 OkHttp

网络请求基础 ------ 从 HttpURLConnection 到 OkHttp

掌握 Android 网络请求,实现接口调用、JSON 解析与 UI 更新


哈喽,各位坚持学习的小伙伴们!上一篇我们掌握了 Android 本地存储的各种方案,数据已经能在设备上持久保存了。但一个真正有用的 App,必然要和远程服务器通信 ------ 登录、获取新闻、上传图片、同步数据......这一切都离不开网络请求。

今天,我们就正式进入 Android 网络编程的世界。按照「原理 → 原生实现 → OkHttp 进阶 → Retrofit 入门 → 协程封装 → 架构分层 → 避坑指南 → 综合实战」的路径,带你完整掌握从基础到商业项目主流架构的全链路知识。

我们直接开始!


一、Android 网络请求的两大铁律

在写第一行网络代码之前,必须记住两条铁律,否则 App 分分钟崩溃或无法联网。

1.1 主线程禁止联网 ------ NetworkOnMainThreadException

Android 的 UI 绘制与事件响应全部运行在主线程。如果在主线程执行耗时的网络请求,会造成界面卡顿甚至 ANR。系统从 API 14 开始严格执行检查:只要在主线程发起网络操作,直接抛出 NetworkOnMainThreadException 崩溃。

正确的线程切换思路:

text 复制代码
发起请求 → 工作线程执行网络操作 → 拿到响应数据 → 切回主线程更新 UI

常用实现方式有四种:

  • Thread + runOnUiThread(原生基础,仅用于理解原理)

  • Handler + Message(传统方案,现已少用)

  • Kotlin 协程(现代官方推荐,写法最优雅)

  • OkHttp 异步回调 / Retrofit 协程支持(框架内置,项目常用)

无论哪种方式,最终更新 UI 的代码必须运行在主线程。

1.2 必须声明网络权限 + 适配明文流量

① 声明权限

xml 复制代码
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

② Android 9.0+ 默认禁止明文 HTTP

从 API 28 开始,系统默认只允许 HTTPS,明文 HTTP 会被直接拦截。开发调试时需要连接 http:// 地址时,在 中添加:

xml 复制代码
<application
    android:usesCleartextTraffic="true"
    ...>

⚠️ 正式上线时强烈建议服务器全面升级 HTTPS,移除此配置。


二、原生方案:HttpURLConnection

HttpURLConnection 是 Android SDK 内置的 HTTP 客户端,无需额外依赖,适合理解底层原理。

2.1 GET 请求

kotlin 复制代码
fun httpGet(urlString: String): String {
    var connection: HttpURLConnection? = null
    val result = StringBuilder()
    try {
        val url = URL(urlString)
        connection = url.openConnection() as HttpURLConnection
        connection.requestMethod = "GET"
        connection.connectTimeout = 5000
        connection.readTimeout = 5000

        if (connection.responseCode == HttpURLConnection.HTTP_OK) {
            connection.inputStream.bufferedReader().use { result.append(it.readText()) }
        }
    } catch (e: Exception) {
        e.printStackTrace()
    } finally {
        connection?.disconnect()
    }
    return result.toString()
}

2.2 POST 请求

kotlin 复制代码
fun httpPost(urlString: String, jsonBody: String): String {
    val url = URL(urlString)
    val connection = url.openConnection() as HttpURLConnection
    connection.requestMethod = "POST"
    connection.doOutput = true
    connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8")
    connection.connectTimeout = 5000

    connection.outputStream.use { it.write(jsonBody.toByteArray()) }
    val response = connection.inputStream.bufferedReader().use { it.readText() }
    connection.disconnect()
    return response
}

调用时必须放在子线程,然后切回主线程更新 UI:

kotlin 复制代码
Thread {
    val response = httpGet("https://api.example.com/data")
    runOnUiThread { tvResult.text = response }
}.start()

原生代码繁琐且易出错,实际项目几乎全部使用 OkHttp 及 Retrofit。


三、OkHttp 实战详解

OkHttp 是 Square 公司开源的 HTTP 客户端,是 Android 领域的事实标准,Retrofit 底层同样基于它。

3.1 添加依赖

kotlin 复制代码
dependencies {
    implementation("com.squareup.okhttp3:okhttp:4.12.0")
    implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") // 日志拦截器
}

3.2 同步请求(需在子线程)

kotlin 复制代码
fun okHttpGetSync(url: String): String? {
    val client = OkHttpClient()
    val request = Request.Builder().url(url).build()
    return try {
        client.newCall(request).execute().body?.string()
    } catch (e: IOException) { null }
}

3.3 异步请求(推荐)

kotlin 复制代码
client.newCall(request).enqueue(object : Callback {
    override fun onFailure(call: Call, e: IOException) {
        runOnUiThread { tvResult.text = "请求失败:${e.message}" }
    }
    override fun onResponse(call: Call, response: Response) {
        val body = response.body?.string()
        runOnUiThread { tvResult.text = body ?: "无数据" }
    }
})

⚠️ 注意:异步回调运行在子线程,必须用 runOnUiThread 切回主线程才能更新 UI。

3.4 POST 提交 JSON

kotlin 复制代码
fun okHttpPostJson(url: String, json: String, callback: (String?) -> Unit) {
    val mediaType = "application/json;charset=utf-8".toMediaType()
    val body = json.toRequestBody(mediaType)
    val request = Request.Builder().url(url).post(body).build()

    OkHttpClient().newCall(request).enqueue(object : Callback {
        override fun onFailure(call: Call, e: IOException) { callback(null) }
        override fun onResponse(call: Call, response: Response) { callback(response.body?.string()) }
    })
}

3.5 拦截器(Interceptor)------ OkHttp 的灵魂

统一 Header 拦截器:

kotlin 复制代码
class HeaderInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val newRequest = chain.request().newBuilder()
            .addHeader("App-Version", "1.0.0")
            .addHeader("Authorization", "Bearer your_token_here")
            .build()
        return chain.proceed(newRequest)
    }
}

全局单例 OkHttpClient(整个 App 复用同一个实例,共享连接池):

kotlin 复制代码
val okHttpClient: OkHttpClient = OkHttpClient.Builder()
    .connectTimeout(10, TimeUnit.SECONDS)
    .readTimeout(10, TimeUnit.SECONDS)
    .addInterceptor(HeaderInterceptor())
    .addInterceptor(HttpLoggingInterceptor().apply {
        level = HttpLoggingInterceptor.Level.BODY
    })
    .build()

四、协程版本请求(现代 Android 推荐写法)

传统的回调式写法在多层嵌套时容易陷入「回调地狱」。Kotlin 协程可以用同步代码的写法实现异步逻辑,自动完成线程切换,还能绑定生命周期自动取消。

4.1 封装挂起函数

kotlin 复制代码
suspend fun okHttpGetCoroutine(url: String): String? {
    return withContext(Dispatchers.IO) {
        try {
            val request = Request.Builder().url(url).build()
            okHttpClient.newCall(request).execute().body?.string()
        } catch (e: IOException) { null }
    }
}

4.2 Activity 中调用

kotlin 复制代码
lifecycleScope.launch {
    binding.progressBar.visibility = View.VISIBLE
    val result = okHttpGetCoroutine("https://api.example.com/data")
    binding.tvResult.text = result ?: "请求失败"
    binding.progressBar.visibility = View.GONE
}

协程方案的核心优势:

  • 代码线性流畅,没有嵌套回调
  • withContext 负责子线程执行,自动回主线程
  • lifecycleScope 绑定生命周期,页面销毁时自动取消协程,杜绝内存泄漏

五、Retrofit 快速入门:注解式网络请求框架

Retrofit 同样是 Square 出品,基于 OkHttp 封装的 RESTful 风格网络框架,通过注解定义接口,自动生成请求实现,完美支持协程与自动 JSON 解析。

5.1 引入依赖

kotlin 复制代码
dependencies {
    implementation("com.squareup.retrofit2:retrofit:2.11.0")
    implementation("com.squareup.retrofit2:converter-gson:2.11.0")
}

5.2 定义 API 接口

kotlin 复制代码
interface WeatherApiService {
    @GET("weather")
    suspend fun getWeather(@Query("city") city: String): WeatherResponse
}

5.3 初始化与单例封装

kotlin 复制代码
object RetrofitClient {
    private const val BASE_URL = "https://api.example.com/" // 必须以 / 结尾

    private val retrofit = Retrofit.Builder()
        .baseUrl(BASE_URL)
        .client(okHttpClient) // 复用已配置的 OkHttpClient
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    val weatherApi: WeatherApiService by lazy {
        retrofit.create(WeatherApiService::class.java)
    }
}

5.4 协程方式调用

kotlin 复制代码
lifecycleScope.launch {
    try {
        val response = RetrofitClient.weatherApi.getWeather("南京")
        if (response.code == 200) {
            binding.tvResult.text = "城市:${response.data.city}\n温度:${response.data.temperature}"
        }
    } catch (e: Exception) {
        Toast.makeText(this@MainActivity, "请求失败:${e.message}", Toast.LENGTH_SHORT).show()
    }
}

对比原生 OkHttp,Retrofit + 协程大幅减少了模板代码,无需手动构造请求、读取流、解析 JSON、切换线程。


六、JSON 解析:JSONObject vs Gson vs Moshi

6.1 原生 JSONObject(轻量但不推荐)

kotlin 复制代码
val json = JSONObject(responseBody)
val city = json.getJSONObject("data").getString("city")

6.2 Gson(最常用)

kotlin 复制代码
// 定义数据类
data class WeatherResponse(val code: Int, val message: String, val data: WeatherData)
data class WeatherData(val city: String, val temperature: String, val weather: String)

// 一行解析
val weatherResponse = Gson().fromJson(responseBody, WeatherResponse::class.java)

Gson 自动映射 JSON 字段到数据类属性,如字段名不一致可用 @SerializedName 注解。

6.3 Moshi(Kotlin 友好)

与 OkHttp 同属 Square 公司,对 Kotlin 空安全、data class 支持更好。

方案 优点 适用场景
JSONObject 无依赖 极简单 JSON、临时调试
Gson 稳定、生态广 绝大多数项目
Moshi Kotlin 友好、体积小 现代 Kotlin 项目

七、架构升级:ViewModel + Repository 分层

直接把网络请求写在 Activity 中会导致代码臃肿、耦合严重、难以测试。现代 Android 开发推荐 MVVM 架构分层:

层级 职责 生命周期
UI 层(Activity/Fragment) 仅负责 UI 渲染、订阅 ViewModel 状态 与页面一致
ViewModel 层 持有业务逻辑、管理 UI 状态、调用 Repository 长于页面,旋转不销毁
Repository 层 统一管理数据来源,屏蔽底层细节 通常单例

7.1 通用请求状态封装

kotlin 复制代码
sealed class UiState<out T> {
    object Loading : UiState<Nothing>()
    data class Success<T>(val data: T) : UiState<T>()
    data class Error(val msg: String) : UiState<Nothing>()
    object NoNetwork : UiState<Nothing>()
}

7.2 Repository 层

kotlin 复制代码
class WeatherRepository {
    private val api = RetrofitClient.weatherApi
    suspend fun getWeather(city: String): WeatherResponse = api.getWeather(city)
}

7.3 ViewModel 层

kotlin 复制代码
class WeatherViewModel(application: Application) : AndroidViewModel(application) {
    private val repository = WeatherRepository()
    private val _weatherState = MutableStateFlow<UiState<WeatherData>>(UiState.Loading)
    val weatherState: StateFlow<UiState<WeatherData>> = _weatherState.asStateFlow()

    fun queryWeather(city: String) {
        viewModelScope.launch {
            _weatherState.value = UiState.Loading
            if (!NetworkUtil.isNetworkAvailable(getApplication())) {
                _weatherState.value = UiState.NoNetwork
                return@launch
            }
            try {
                val response = repository.getWeather(city)
                if (response.code == 200) _weatherState.value = UiState.Success(response.data)
                else _weatherState.value = UiState.Error(response.message)
            } catch (e: Exception) {
                _weatherState.value = UiState.Error("请求异常:${e.message}")
            }
        }
    }
}

7.4 UI 层订阅状态(使用 repeatOnLifecycle 安全收集)

kotlin 复制代码
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.weatherState.collect { state ->
            when (state) {
                is UiState.Loading -> { /* 显示进度条 */ }
                is UiState.Success -> { /* 展示数据 */ }
                is UiState.Error -> { /* 错误提示 */ }
                is UiState.NoNetwork -> { /* 无网络提示 */ }
            }
        }
    }
}

使用 repeatOnLifecycle 确保页面退到后台时自动停止收集,节省资源。


八、网络状态监听与无网容错处理

8.1 主动查询网络状态

kotlin 复制代码
object NetworkUtil {
    fun isNetworkAvailable(context: Context): Boolean {
        val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        val network = cm.activeNetwork ?: return false
        val capabilities = cm.getNetworkCapabilities(network) ?: return false
        return capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
               capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
    }
}

8.2 无网容错三件套

  1. 前置拦截:请求前判断网络,无网直接返回 NoNetwork 状态

  2. 离线缓存:OkHttp 配置 Cache + 缓存拦截器,无网时自动读取本地缓存

  3. 重试机制:弱网场景提供「点击重试」按钮,由用户主动触发


九、新手必踩坑点清单

坑点 现象 解决
明文 HTTP 被拦截 Cleartext HTTP traffic not permitted 开发期设置 usesCleartextTraffic=true,上线升 HTTPS
异步回调中更新 UI CalledFromWrongThreadException 使用 runOnUiThread 或协程 Dispatchers.Main
response.body?.string() 只能调用一次 第二次调用返回 null 保存到变量中复用
Retrofit BaseUrl 不以 / 结尾 路径拼接错误 确保以 / 结尾:"api.example.com/"
协程异常未捕获 App 直接闪退 网络请求务必用 try-catch 包裹
ViewModel 持有 Activity Context 内存泄漏 使用 AndroidViewModel 持有 Application Context
Flow 未用 repeatOnLifecycle 后台持续收集,浪费资源 使用 repeatOnLifecycle(Lifecycle.State.STARTED)
网络监听未注销 内存泄漏 在 onDestroy 中调用 unregisterNetworkCallback

十、综合小案例:天气预报 App(MVVM 架构)

结合所学,开发一个完整的天气查询应用:输入城市名 → 显示 Loading → 请求 API → 解析 JSON → 展示结果,支持错误与无网状态处理。

10.1 布局文件(activity_weather.xml)

xml 复制代码
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="20dp">

    <EditText
        android:id="@+id/etCity"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="请输入城市名称,如:南京" />

    <Button
        android:id="@+id/btnQuery"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="查询天气" />

    <ProgressBar
        android:id="@+id/progressBar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:visibility="gone" />

    <TextView
        android:id="@+id/tvResult"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:textSize="16sp" />
</LinearLayout>

10.2 核心代码(ViewModel + StateFlow + repeatOnLifecycle)

kotlin 复制代码
class WeatherActivity : AppCompatActivity() {
    private lateinit var binding: ActivityWeatherBinding
    private val viewModel: WeatherViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityWeatherBinding.inflate(layoutInflater)
        setContentView(binding.root)

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.weatherState.collect { state ->
                    when (state) {
                        is UiState.Loading -> {
                            binding.progressBar.visibility = View.VISIBLE
                            binding.tvResult.text = ""
                        }
                        is UiState.Success -> {
                            binding.progressBar.visibility = View.GONE
                            val w = state.data
                            binding.tvResult.text = "城市:${w.city}\n温度:${w.temperature}\n天气:${w.weather}\n湿度:${w.humidity}"
                        }
                        is UiState.Error -> {
                            binding.progressBar.visibility = View.GONE
                            Toast.makeText(this@WeatherActivity, state.msg, Toast.LENGTH_SHORT).show()
                        }
                        is UiState.NoNetwork -> {
                            binding.progressBar.visibility = View.GONE
                            binding.tvResult.text = "当前无网络,请检查连接后重试"
                        }
                    }
                }
            }
        }

        binding.btnQuery.setOnClickListener {
            val city = binding.etCity.text.toString().trim()
            if (city.isEmpty()) {
                Toast.makeText(this, "请输入城市名称", Toast.LENGTH_SHORT).show()
                return@setOnClickListener
            }
            viewModel.queryWeather(city)
        }
    }
}

提示:示例 API 地址为占位,实际开发可替换为和风天气、OpenWeatherMap 等开放接口。


十一、总结与下篇预告

今天我们系统掌握了 Android 网络请求的完整知识体系:

  • ✅ 两大铁律:主线程不能联网,须声明权限并适配明文流量。

  • ✅ HttpURLConnection:理解底层原理,实际项目少用。

  • ✅ OkHttp 实战:同步/异步请求、拦截器添加 Header、全局单例配置。

  • ✅ 协程封装:挂起函数 + lifecycleScope,告别回调地狱。

  • ✅ Retrofit 入门:注解式接口,自动解析 JSON,与协程完美配合。

  • ✅ JSON 解析:Gson 最常用,Moshi 对 Kotlin 更友好。

  • ✅ MVVM 分层:ViewModel + Repository + StateFlow,解耦 UI 与数据。

  • ✅ 网络容错:网络检测、离线缓存、差异化错误提示。

  • ✅ 避坑指南:覆盖明文限制、线程安全、内存泄漏等 8 个核心坑点。

  • ✅ 综合案例:MVVM 架构天气预报 App,完整覆盖全链路。

掌握这些内容后,你已经具备独立开发商业级网络请求功能的能力。下一篇,我们将学习 权限处理与相机相册调用,搞定运行时权限、拍照、选图与文件存储,让你的 App 能够与手机硬件深度交互。


✨ 如果本文对你有帮助,欢迎点赞 、收藏,让更多零基础的小伙伴少走弯路!

相关推荐
传奇开心果编程4 小时前
【Compose Multiplatform 跨端开发学与练】第3课 布局与组件
android·windows·学习·ui·ios·kotlin·composer
事圆则缓5 小时前
Kotlin 入门与面试:从空安全、扩展函数到协程
安全·面试·kotlin
传奇开心果编程6 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程8 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程8 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程8 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
传奇开心果编程8 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer
熊猫钓鱼>_>1 天前
Kotlin Multiplatform for OpenHarmony 实战:为 Reaktive 实现响应式原语适配(完整版 · 含摘要目录与技术图表)
华为·kotlin·大模型·ai编程·harmonyos·适配·reaktive
李游Leo1 天前
HarmonyOS 7 InsightBoard 多形态适配实录 06:ArkUI × 多设备回归:布局抖动、监听释放与全形态验收【鸿蒙心迹】
回归·kotlin·harmonyos
点燃大海1 天前
手机给平板当键盘?
kotlin