网络请求基础 ------ 从 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 无网容错三件套
-
前置拦截:请求前判断网络,无网直接返回 NoNetwork 状态
-
离线缓存:OkHttp 配置 Cache + 缓存拦截器,无网时自动读取本地缓存
-
重试机制:弱网场景提供「点击重试」按钮,由用户主动触发
九、新手必踩坑点清单
| 坑点 | 现象 | 解决 |
|---|---|---|
| 明文 HTTP 被拦截 | Cleartext HTTP traffic not permitted | 开发期设置 usesCleartextTraffic=true,上线升 HTTPS |
| 异步回调中更新 UI | CalledFromWrongThreadException | 使用 runOnUiThread 或协程 Dispatchers.Main |
| response.body?.string() 只能调用一次 | 第二次调用返回 null | 保存到变量中复用 |
| Retrofit BaseUrl 不以 / 结尾 | 路径拼接错误 | 确保以 / 结尾:"https://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 能够与手机硬件深度交互。
✨ 如果本文对你有帮助,欢迎点赞 、收藏,让更多零基础的小伙伴少走弯路!