从0到1搭建Android网络框架:别再让你的请求在"路上迷路"了

前言

作为Android开发者,你是不是也遇到过这样的场景:写个登录功能,复制粘贴了300行网络请求代码;改个接口参数,全项目搜"HttpURLConnection"改到崩溃;用户说"没网的时候APP卡死了",你对着满屏的try-catch陷入沉思...

其实,这些问题的根源都一样:没有一套靠谱的网络框架。

就像盖房子没搭脚手架,砌墙全靠手抓砖------不是不能盖,就是容易塌。今天咱们就来手把手搭一套"抗揍又好改"的Android网络框架,从基础组件到进阶技巧,保证让你的网络请求代码从"一锅粥"变成"精装房"。

先搞懂:网络框架到底要解决啥问题?

在开始敲代码前,咱们得先明确目标:一套好的网络框架,不是为了炫技,而是为了帮你少踩坑。它至少要搞定这几件事:

  • 别重复造轮子:同一个项目里,登录、注册、获取列表的网络请求逻辑不该重复写
  • 主线程不能卡:网络请求是耗时操作,必须扔到子线程,不然ANR警告分分钟找上门
  • 数据解析不头疼:JSON字符串手动转对象?这种体力活就该交给框架
  • 异常能优雅处理:没网、服务器500、数据格式错了,总不能直接抛个红框给用户看吧
  • 缓存要聪明:用户地铁里没网,至少能看到上次加载的内容
  • 扩展性要强:以后想加个"请求重试"、"Token自动刷新",不能改得翻天覆地

带着这些目标,咱们开始"搭骨架"。目前Android网络框架的"黄金组合"是:OkHttp(打工人)+ Retrofit(翻译官)+ 协程(调度员)+ Gson(翻译器)。这四个工具各司其职,配合起来堪称完美。

核心组件:认识这几个"打工人"

1. OkHttp:最靠谱的"快递小哥"

OkHttp是Square公司出的网络库,就像一个经验丰富的快递小哥:能帮你发快递(发起请求)、收快递(处理响应)、遇到堵车绕个路(自动重连)、还能攒一波快递一起送(连接池复用)。

基本用法:先添加依赖(现在都用AndroidX,直接用最新版)

gradle 复制代码
dependencies {
    implementation 'com.squareup.okhttp3:okhttp:4.12.0'
}

然后就能发个简单的GET请求了:

kotlin 复制代码
// 创建OkHttpClient实例(建议全局单例,不然浪费资源)
val okHttpClient = OkHttpClient.Builder().build()

// 创建请求
val request = Request.Builder()
    .url("https://api.example.com/user") // 地址
    .get() // 请求方法
    .build()

// 发请求(注意:不能在主线程!)
okHttpClient.newCall(request).enqueue(object : Callback {
    override fun onFailure(call: Call, e: IOException) {
        // 失败了(没网、超时等)
    }

    override fun onResponse(call: Call, response: Response) {
        // 成功了,response.body?.string()就是返回的字符串
    }
})

但直接用OkHttp太"原始"了:每次请求都要写回调,解析数据还得自己转,所以咱们需要个"翻译官"------Retrofit。

2. Retrofit:OkHttp的"高级秘书"

Retrofit也是Square家的,它的作用是把HTTP接口"翻译"成Java/Kotlin接口,让你用调用函数的方式发请求,不用再手动拼URL、加参数。

添加依赖

gradle 复制代码
dependencies {
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    // 配合Gson解析JSON
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
}

基本用法:三步搞定:

第一步:定义接口(告诉Retrofit"怎么发请求")

kotlin 复制代码
interface ApiService {
    // GET请求:获取用户信息,路径里带个userId参数
    @GET("user/{userId}")
    fun getUserInfo(@Path("userId") userId: String): Call<User>

    // POST请求:登录,参数放请求体里
    @POST("login")
    fun login(@Body loginRequest: LoginRequest): Call<LoginResponse>
}

第二步:创建Retrofit实例(关联OkHttp和解析器)

kotlin 复制代码
val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/") // 基础地址,接口里的路径会拼在后面
    .client(okHttpClient) // 关联OkHttp
    .addConverterFactory(GsonConverterFactory.create()) // 用Gson解析JSON
    .build()

// 创建接口实例
val apiService = retrofit.create(ApiService::class.java)

第三步:调用接口(发请求)

kotlin 复制代码
// 调用登录接口
val loginCall = apiService.login(LoginRequest("username", "password"))
loginCall.enqueue(object : Callback<LoginResponse> {
    override fun onFailure(call: Call<LoginResponse>, t: Throwable) {
        // 失败处理
    }

    override fun onResponse(call: Call<LoginResponse>, response: Response<LoginResponse>) {
        // 成功了,response.body()就是解析好的LoginResponse对象
    }
})

是不是比直接用OkHttp清爽多了?但回调嵌套还是有点烦,这时候就需要"调度员"------协程登场了。

3. 协程:让异步代码像同步一样好写

如果用Retrofit的回调,多层请求嵌套会写成"回调地狱"(比如先登录,再用token获取用户信息,再获取用户订单...)。而协程能让异步代码线性执行,告别嵌套。

怎么用 :给Retrofit接口加个suspend关键字,就能在协程里直接调用:

kotlin 复制代码
interface ApiService {
    // 加个suspend,变成挂起函数
    @POST("login")
    suspend fun login(@Body loginRequest: LoginRequest): LoginResponse
}

// 在ViewModel里用协程调用
viewModelScope.launch {
    try {
        val loginResponse = apiService.login(LoginRequest("username", "password"))
        // 登录成功,接着做其他事(比如存token)
        val userInfo = apiService.getUserInfo(loginResponse.userId)
        // 获取用户信息成功...
    } catch (e: Exception) {
        // 统一处理异常(网络错、解析错都在这)
    }
}

一行try-catch搞定所有异常,代码从头到尾像同步一样写,这才是开发者该有的待遇!

框架封装:从"能用"到"好用"

光把库串起来还不够,咱们得封装一层,让它更符合项目需求。这部分是框架的"灵魂",直接决定了后续好不好维护。

1. 统一响应格式:别让服务器返回"惊喜"

几乎所有后端接口都会返回统一格式,比如:

json 复制代码
{
  "code": 200, // 状态码:200成功,其他失败
  "message": "success", // 提示信息
  "data": { ... } // 实际数据(成功时才有)
}

如果每个接口都手动判断code,会写吐的。咱们可以封装一个BaseResponse

kotlin 复制代码
// 泛型T表示data的类型
data class BaseResponse<T>(
    val code: Int,
    val message: String,
    val data: T?
)

然后定义一个"成功判断规则",比如code == 200才算成功:

kotlin 复制代码
// 密封类:用于包装请求结果(成功/失败)
sealed class Result<out T> {
    data class Success<out T>(val data: T) : Result<T>()
    data class Failure(val code: Int, val message: String) : Result<Nothing>()
    data class Error(val exception: Exception) : Result<Nothing>()
}

// 扩展函数:把BaseResponse转成Result
suspend fun <T> ApiService.wrapRequest(request: suspend () -> BaseResponse<T>): Result<T> {
    return try {
        val response = request()
        if (response.code == 200) {
            // 成功:如果data为null,可能返回默认值(根据后端情况定)
            Result.Success(response.data ?: throw NullPointerException("data is null"))
        } else {
            // 失败:比如code=401(未登录)、500(服务器错)
            Result.Failure(response.code, response.message)
        }
    } catch (e: Exception) {
        // 异常:网络错、解析错等
        Result.Error(e)
    }
}

这样用起来就爽了:

kotlin 复制代码
// 接口定义时,返回BaseResponse
interface ApiService {
    @POST("login")
    suspend fun login(@Body request: LoginRequest): BaseResponse<LoginData>
}

// 调用时
viewModelScope.launch {
    val result = apiService.wrapRequest { 
        apiService.login(LoginRequest("username", "password")) 
    }
    when (result) {
        is Result.Success -> {
            // 成功,result.data就是LoginData
        }
        is Result.Failure -> {
            // 后端返回的失败,比如"密码错误"
            Toast.makeText(context, result.message, Toast.LENGTH_SHORT).show()
        }
        is Result.Error -> {
            // 异常,比如没网
            if (result.exception is IOException) {
                Toast.makeText(context, "网络不给力", Toast.LENGTH_SHORT).show()
            }
        }
    }
}

从此判断结果只需要一个when,再也不用到处写if (code == 200)了!

2. OkHttp拦截器:给请求"加戏"

拦截器(Interceptor)是OkHttp的"黑科技",能在请求发出去前、响应回来后做些额外操作。比如打印日志、加Token、处理缓存,都靠它。

(1)日志拦截器:调试时的"监控摄像头"

开发时想知道请求参数、响应数据?用HttpLoggingInterceptor

kotlin 复制代码
val loggingInterceptor = HttpLoggingInterceptor { message ->
    Log.d("HttpLog", message) // 打印日志
}.apply {
    level = HttpLoggingInterceptor.Level.BODY // 打印详细日志(包含请求体、响应体)
}

// 给OkHttp添加拦截器
val okHttpClient = OkHttpClient.Builder()
    .addInterceptor(loggingInterceptor)
    .build()

这样Logcat里就会输出完整的请求响应信息,调试时再也不用猜"参数传对了没"。

(2)Token拦截器:自动给请求"戴帽子"

很多接口需要在Header里带Token(比如Authorization: Bearer xxxx),总不能每个接口都手动加吧?用拦截器自动加:

kotlin 复制代码
class TokenInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        // 1. 获取原始请求
        val originalRequest = chain.request()
        
        // 2. 从本地获取Token(比如从SharedPreferences里读)
        val token = SPUtils.getString("token", "")
        
        // 3. 如果有Token,给请求加Header
        val newRequest = if (token.isNotEmpty()) {
            originalRequest.newBuilder()
                .header("Authorization", "Bearer $token")
                .build()
        } else {
            originalRequest // 没Token就用原始请求
        }
        
        // 4. 继续处理请求
        return chain.proceed(newRequest)
    }
}

// 添加到OkHttp
val okHttpClient = OkHttpClient.Builder()
    .addInterceptor(TokenInterceptor())
    .addInterceptor(loggingInterceptor) // 注意顺序:Token拦截器先加,日志才能打印出带Token的请求
    .build()

更牛的是,还能处理Token过期:如果接口返回401(Token失效),自动刷新Token后重试请求。这个逻辑稍微复杂点,需要用"拦截器+协程"配合,核心思路是:

  1. 拦截到401响应
  2. 用刷新Token的接口获取新Token
  3. 用新Token重新构建请求,再次发起
  4. 把新Token存到本地

(代码太长,这里就不贴了,核心是用chain.proceed(newRequest)重试)

3. 缓存策略:没网也能"看历史"

用户没网的时候,能显示上次加载的数据,体验会好很多。OkHttp的缓存机制可以帮我们实现这个功能。

配置缓存

kotlin 复制代码
// 1. 指定缓存目录和大小(比如50MB)
val cacheDir = context.cacheDir // 应用缓存目录
val cache = Cache(cacheDir, 50 * 1024 * 1024L) // 50MB

// 2. 写个缓存拦截器,定义缓存规则
class CacheInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        var request = chain.request()
        
        // 如果没网,强制使用缓存(缓存有效期60秒,可自定义)
        if (!NetworkUtils.isConnected(context)) {
            request = request.newBuilder()
                .cacheControl(CacheControl.FORCE_CACHE)
                .build()
        }
        
        val response = chain.proceed(request)
        
        // 如果有网,缓存有效期设为0(不缓存);没网则延长缓存时间
        if (NetworkUtils.isConnected(context)) {
            val maxAge = 0 // 0秒内不缓存,直接请求网络
            response.newBuilder()
                .header("Cache-Control", "public, max-age=$maxAge")
                .removeHeader("Pragma") // 清除旧的缓存头
                .build()
        } else {
            val maxStale = 60 * 60 * 24 // 没网时,缓存有效期24小时
            response.newBuilder()
                .header("Cache-Control", "public, only-if-cached, max-stale=$maxStale")
                .removeHeader("Pragma")
                .build()
        }
        return response
    }
}

// 3. 给OkHttp添加缓存和拦截器
val okHttpClient = OkHttpClient.Builder()
    .cache(cache)
    .addInterceptor(CacheInterceptor()) // 应用拦截器:处理缓存逻辑
    .addNetworkInterceptor(CacheInterceptor()) // 网络拦截器:配合缓存使用
    .build()

这里要注意:addInterceptor(应用拦截器)和addNetworkInterceptor(网络拦截器)的区别:前者不管有没有网都会执行,后者只在有网络请求时执行。缓存逻辑需要两者配合。

4. 单例管理:别让实例"满天飞"

OkHttp、Retrofit这些实例创建成本很高,最好全局单例。可以用Dagger或Hilt(Google推荐的依赖注入库)来管理,这里用Hilt举个例子:

kotlin 复制代码
// 1. 定义OkHttp单例
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Singleton
    @Provides
    fun provideOkHttpClient(context: Context): OkHttpClient {
        return OkHttpClient.Builder()
            .cache(Cache(context.cacheDir, 50 * 1024 * 1024L))
            .addInterceptor(TokenInterceptor())
            .addInterceptor(loggingInterceptor)
            .addInterceptor(CacheInterceptor())
            .addNetworkInterceptor(CacheInterceptor())
            .connectTimeout(10, TimeUnit.SECONDS) // 连接超时
            .readTimeout(10, TimeUnit.SECONDS) // 读取超时
            .build()
    }

    // 2. 定义Retrofit单例
    @Singleton
    @Provides
    fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com/")
            .client(okHttpClient)
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(CoroutineCallAdapterFactory()) // 支持协程
            .build()
    }

    // 3. 提供ApiService
    @Singleton
    @Provides
    fun provideApiService(retrofit: Retrofit): ApiService {
        return retrofit.create(ApiService::class.java)
    }
}

然后在需要用的地方直接注入:

kotlin 复制代码
class UserRepository @Inject constructor(
    private val apiService: ApiService
) {
    // 直接用apiService发请求
    suspend fun login(username: String, password: String) = apiService.wrapRequest {
        apiService.login(LoginRequest(username, password))
    }
}

用Hilt管理后,再也不用手动new实例了,还能保证全局只有一个实例,内存更省。

进阶技巧:让框架更"抗揍"

1. 请求重试:失败了别轻易放弃

网络偶尔抽风很正常,给重要请求加个重试机制很有必要。可以用拦截器实现:

kotlin 复制代码
class RetryInterceptor(private val maxRetry: Int = 3) : Interceptor {
    private var retryCount = 0 // 已重试次数

    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        var response: Response? = null
        try {
            response = chain.proceed(request)
        } catch (e: Exception) {
            // 如果失败,且没到最大重试次数,就重试
            while (retryCount < maxRetry) {
                retryCount++
                response = chain.proceed(request)
                if (response.isSuccessful) { // 成功了就退出循环
                    break
                }
            }
        }
        return response ?: throw IOException("重试$maxRetry次后仍失败")
    }
}

注意:不是所有请求都适合重试(比如POST提交订单,重试可能导致重复下单),所以最好在拦截器里加个判断,比如只重试GET请求。

2. 大文件下载:进度监听不能少

下载文件时,用户肯定想知道进度。OkHttp可以通过ResponseBodysource()方法监听进度:

kotlin 复制代码
// 定义下载进度回调
interface DownloadListener {
    fun onProgress(progress: Int) // 0-100
    fun onSuccess(file: File)
    fun onFailure(e: Exception)
}

// 下载接口
interface ApiService {
    @GET
    @Streaming // 流式下载(大文件必须加,不然会把整个文件读到内存)
    suspend fun downloadFile(@Url url: String): ResponseBody
}

// 下载实现
suspend fun downloadFile(url: String, savePath: String, listener: DownloadListener) {
    try {
        val responseBody = apiService.downloadFile(url)
        val totalLength = responseBody.contentLength() // 总大小
        var currentLength = 0L // 已下载大小
        val inputStream = responseBody.byteStream()
        val file = File(savePath)
        val outputStream = FileOutputStream(file)
        val buffer = ByteArray(1024 * 8)
        var len: Int
        while (inputStream.read(buffer).also { len = it } != -1) {
            outputStream.write(buffer, 0, len)
            currentLength += len
            // 计算进度并回调
            val progress = (currentLength * 100 / totalLength).toInt()
            listener.onProgress(progress)
        }
        outputStream.flush()
        inputStream.close()
        outputStream.close()
        listener.onSuccess(file)
    } catch (e: Exception) {
        listener.onFailure(e)
    }
}

关键是加@Streaming注解,不然大文件会直接塞满内存,导致OOM。

总结

到这里,咱们的网络框架就搭得差不多了。总结一下,它包含这些部分:

  • 底层引擎:OkHttp负责实际的网络通信,处理连接、缓存、拦截器
  • 接口封装:Retrofit把HTTP接口转成Kotlin接口,配合Gson自动解析JSON
  • 线程调度:协程解决异步回调问题,让代码更清爽
  • 统一处理:BaseResponse+Result封装响应,统一判断成功失败
  • 功能增强:拦截器处理Token、日志、缓存;重试机制提高稳定性
  • 依赖管理:Hilt管理单例,避免资源浪费

这套框架就像一个"网络请求管家":你只需要告诉它"我要登录,参数是xxx",它就会自动帮你发请求、处理异常、刷新Token、缓存数据,最后把整理好的结果给你。

最后说句大实话:框架不是越复杂越好,适合自己项目的才是最好的。

刚开始可以先搭个基础版,随着项目需求增加再慢慢加功能(比如加RxJava支持、加证书校验)。

毕竟,能解决问题的框架,才是好框架~

祝你从此告别"网络请求恐惧症",代码越写越顺!

相关推荐
拉不动的猪21 分钟前
前端三大权限场景全解析:设计、实现、存储与企业级实践
前端·javascript·面试
花花鱼34 分钟前
android room中实体类变化以后如何迁移
android
语落心生35 分钟前
Apache Geaflow推理框架Geaflow-infer 解析系列(四)依赖管理
架构
云渠道商yunshuguoji1 小时前
亚马逊云渠道商:如何用 EC2 Auto Scaling 轻松应对流量洪峰?
架构
Jomurphys1 小时前
设计模式 - 适配器模式 Adapter Pattern
android
雨白1 小时前
电子书阅读器:解析 EPUB 底层原理与实战
android·html
g***B7381 小时前
Kotlin协程在Android中的使用
android·开发语言·kotlin
A***27951 小时前
Kotlin反射机制
android·开发语言·kotlin
2501_916007472 小时前
iOS 应用性能测试的工程化流程,构建从指标采集到问题归因的多工具协同测试体系
android·ios·小程序·https·uni-app·iphone·webview