从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支持、加证书校验)。

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

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

相关推荐
桂花很香,旭很美4 小时前
智能体技术架构:从分类、选型到落地
人工智能·架构
感谢地心引力5 小时前
安卓、苹果手机无线投屏到Windows
android·windows·ios·智能手机·安卓·苹果·投屏
sxgzzn9 小时前
能源行业智能监测产品与技术架构解析
架构·数字孪生·无人机巡检
xiaoxue..9 小时前
React 手写实现的 KeepAlive 组件
前端·javascript·react.js·面试
快乐非自愿9 小时前
【面试题】MySQL 的索引类型有哪些?
数据库·mysql·面试
小邓吖9 小时前
自己做了一个工具网站
前端·分布式·后端·中间件·架构·golang
南风知我意9579 小时前
【前端面试2】基础面试(杂项)
前端·面试·职场和发展
优雅的潮叭10 小时前
cud编程之 reduce
android·redis·缓存
2601_9496130210 小时前
flutter_for_openharmony家庭药箱管理app实战+用药知识详情实现
android·javascript·flutter
一起养小猫10 小时前
Flutter for OpenHarmony 实战 表单处理与验证完整指南
android·开发语言·前端·javascript·flutter·harmonyos