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

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

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


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

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

我们直接开始!


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

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

1.1 主线程禁止联网 —— NetworkOnMainThreadException

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

正确的线程切换思路:

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

常用实现方式有四种:

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

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

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

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

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

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

① 声明权限

<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:// 地址时,在 中添加:

<application
    android:usesCleartextTraffic="true"
    ...>

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


二、原生方案:HttpURLConnection

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

2.1 GET 请求

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 请求

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:

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

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


三、OkHttp 实战详解

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

3.1 添加依赖

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

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

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 异步请求(推荐)

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

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 拦截器:

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 复用同一个实例,共享连接池):

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 封装挂起函数

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 中调用

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 引入依赖

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

5.2 定义 API 接口

interface WeatherApiService {
    @GET("weather")
    suspend fun getWeather(@Query("city") city: String): WeatherResponse
}

5.3 初始化与单例封装

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 协程方式调用

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(轻量但不推荐)

val json = JSONObject(responseBody)
val city = json.getJSONObject("data").getString("city")

6.2 Gson(最常用)

// 定义数据类
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稳定、生态广绝大多数项目
MoshiKotlin 友好、体积小现代 Kotlin 项目

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

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

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

7.1 通用请求状态封装

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 层

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

7.3 ViewModel 层

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 安全收集)

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 主动查询网络状态

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
异步回调中更新 UICalledFromWrongThreadException使用 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)

<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)

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 能够与手机硬件深度交互。


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

内容概要:本文探讨了SLAM(实时定位与地图构建)技术在芯片行业后道封装测试环节的应用,聚焦于晶圆缺陷检测机器人导航系统。文章介绍了视觉SLAM的核心概念与关键技术,包括特征点法(如ORB-SLAM)、关键帧、词袋模型和本质矩阵,并详细分析了基于OpenCV实现的ORB特征提取与匹配、RANSAC位姿估计的C++代码。该系统利用视觉传感器构建三维稀疏点云地图,实现机器人在复杂环境中的自主导航与缺陷精准定位。未来趋势指向多模态融合(视觉-激光-IMU)和语义级高精地图与数字孪生平台的集成。; 适合人群:具备计算机视觉或机器人相关基础知识,从事智能制造、自动化巡检、SLAM算法研发的技术人员及工程师,尤其适用于工作1-3年、希望深入理解SLAM工业落地实践的研发人员。; 使用场景及目标:①应用于芯片封测车间的自动巡检机器人,解决设备密集、线缆交错环境下的高精度导航问题;②实现缺陷图像与空间坐标的绑定,提升运维效率与智能化水平;③为SLAM在工业场景中的鲁棒性优化提供可复用的技术路径。; 阅读建议:此资源结合理论与代码实践,建议读者在掌握相机模型、特征提取等前置知识的基础上,动手运行并调试文中代码,深入理解RANSAC、回环检测等关键步骤在真实工业场景中的作用机制,并结合实际硬件系统进行拓展研究。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Android 开发之道

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值