Android 网络请求:多功能网络请求库
介绍
这是一个基于现代Android技术栈的网络请求库示例项目,集成了OkHttp、Retrofit和Kotlin Flow,提供了一套完整的网络请求解决方案。项目展示了如何在Android应用中优雅地处理网络请求,包括基本请求、接口缓存、文件上传下载、断点续传等高级功能。
核心特性
- 现代化架构:基于OkHttp + Retrofit + Kotlin Flow构建
- 双重API风格:支持Flow响应式编程和传统挂起函数两种方式
- 统一错误处理:提供一致的错误处理机制
- 文件操作支持:完整的文件上传、下载和断点续传功能
- 模块化设计:网络层高度封装,便于复用和维护
- 多种数据格式:支持JSON对象、原始字符串等多种响应格式
- 多个服务器支持:支持配置多个服务器地址,请求不同服务器数据
软件架构
app
├── base # 基础组件
├── bean # 数据模型
├── net # 网络层核心
│ ├── api # API接口定义
│ ├── base # 基础响应类
│ └── ext.kt # 扩展函数
├── ui # 界面层
└── vm # ViewModel层
功能演示
项目包含三个主要功能演示页面:
1. Flow风格请求 (FlowRequestActivity)
- 基础响应式请求(BaseResponse格式)
- 对象响应式请求(直接解析为对象)
- 字符串响应式请求(原始数据)
- 组合多个API请求
2. 挂起函数风格请求 (SuspendRequestActivity)
- 传统挂起函数方式请求
- 同步风格的数据获取
- 多请求组合处理
3. 文件操作 (FileOperationActivity)
- 文件选择和上传
- 普通文件下载
- 断点续传下载
- 文件信息获取
使用说明
0.快速引用
repositories {...maven(url = "https://gitee.com/laujiangtao/maven-repo/raw/main/")...
}
dependencies {...implementation("me.laujiangtao.net:easynet:1.0.0")...
}
1. 初始化网络库
在Application中初始化网络模块:
多服务器配置
class MyApplication : Application() {override fun onCreate() {super.onCreate()// 初始化网络模块val server1Config = HttpConfig(serverUrl = server1Url, cacheDir = cacheDir)HttpClient.init(server1Url, server1Config)// 初始化网络模块val server2Config = HttpConfig(serverUrl = server2Url)HttpClient.init(server2Config)// 初始化网络模块HttpClient.init(server3Url)}
}
2. 定义API接口
/*** @author jiangtao on 2025/9/20* 网络请求API接口定义* 定义了应用程序所需的各种网络请求方法*/
interface ApiService {/*** 查询域名的Whois信息* @param domain 域名参数* @return 返回封装了Whois信息的BaseResponse对象*/@GET("/api/whois")@Cacheable(ttl = 30 * 1000,strategy = CacheStrategy.CACHE_FIRST,includeQueryParams = true)suspend fun whois(@Query("domain") domain: String): BaseResponse<Whois?>?/*** 查询城市天气信息* @param city 城市名称参数* @return 返回天气信息对象*/@GET("/api/weather")suspend fun tianqi(@Query("city") city: String): Tianqi/*** 获取美女图片信息(示例接口)* @return 返回任意类型的数据*/@GET("/api/pcmeinvpic")suspend fun pcmeinv(): Any?
}
2.1 动态添加请求头
interface ApiService {@Headers("X-Force-Network: true")@GET("users")suspend fun getUsersForceNetwork(): BaseResponse<List<User>>
}
interface ApiService {@GET("users")suspend fun getUsers(@Header("X-Force-Network") forceNetwork: Boolean = false): BaseResponse<List<User>>
}// 使用时强制从网络获取
apiService.getUsers("true")
3. 创建Repository
/*** @author jiangtao on 2025/9/20* 网络请求仓库类* 继承自NetRepository,提供应用程序所需的网络请求方法* 包含Flow风格和普通挂起函数风格的网络请求方法*/
class MyNetworkRepository : NetworkRepository {/*** 创建单个实例*/private val service = RetrofitClient.create(server1Url)private val apiService1 = service.createService(ApiService::class.java)private val apiService2 = service.createService<ApiService>()//请求返回非json数据private val apiService3 = service.createService<ApiService>(false)/*** 通过 HttpClient 从 Application创建的实例获取*/// 注入具体的API服务private val apiService: ApiService = HttpClient.createService(server1Url)//用于请求返回非json数据private val rawApiService = HttpClient.createService<RawApiService>(server1Url, false)private val fileApiService = HttpClient.createService(server1Url, FileApiService::class.java)// ===================================================================// Flow 风格的网络请求方法// ===================================================================/*** 查询域名信息* @param domain 域名* @return 返回包含Whois信息的Flow*/fun whois(domain: String): Flow<FlowResult<BaseResponse<Whois?>?>> = apiCall {apiService.whois(domain)}/*** 查询天气信息* @param city 城市名* @return 返回包含天气信息的Flow*/fun tianqi(city: String): Flow<FlowResult<Tianqi?>> {return apiCall {apiService.tianqi(city)}}/*** 获取美女图片接口(示例接口)* @return 返回图片数据的Flow*/fun pcmeinv(): Flow<FlowResult<String?>> {return apiCall {rawApiService.pcmeinv()}}// ===================================================================// 普通挂起函数风格的网络请求方法// ===================================================================/*** 同步风格的挂起函数 - 查询域名信息* @param domain 域名* @return 返回Whois信息的BaseResponse包装对象*/suspend fun getWhoIsInfo(domain: String): BaseResponse<Whois?>? {// 这里应该是实际的 API 调用return apiService.whois(domain)}/*** 同步风格的挂起函数 - 查询天气信息* @param city 城市名* @return 返回天气信息对象*/suspend fun getTianqiInfo(city: String): Tianqi? {// 这里应该是实际的 API 调用return apiService.tianqi(city)}/*** 同步风格的挂起函数 - 获取美女图片接口* @return 返回图片数据字符串*/suspend fun getPcmeinvInfo(): String? {// 这里应该是实际的 API 调用return rawApiService.pcmeinv()}suspend fun fetchMultipleDataDirect(domain: String, city: String): MultipleDataResult {// 并行执行所有API调用val whoisResult = apiService.whois(domain)val tianqiResult = apiService.tianqi(city)val pcmeinvResult = rawApiService.pcmeinv()// 等待所有结果val whoisData = whoisResult?.dataval tianqiData = tianqiResultval pcmeinvData = pcmeinvResult// 组合结果并返回return MultipleDataResult(whoisData, tianqiData, pcmeinvData)}/*** 合并多个网络请求结果的Flow方法*/fun fetchMultipleData(domain: String, city: String): Flow<FlowResult<MultipleDataResult?>> =combineApiCalls({ getWhoIsInfo(domain) },{ getTianqiInfo(city) },{ getPcmeinvInfo() },// ... 可以继续添加更多API调用) { results ->// 在这里处理所有结果并组合成最终数据val data1 = results[0] as? BaseResponse<Whois?>?val data2 = results[1] as? Tianqival data3 = results[2]// ... 处理其他结果MultipleDataResult(data1?.data, data2, data3)}/*** 使用Flow方式合并多个网络请求结果*/fun fetchMultipleDataWithFlow(domain: String,city: String): Flow<FlowResult<MultipleDataResult>> =combineFlows(whois(domain),tianqi(city),pcmeinv(),// ... 添加更多Flow) { results ->// 在这里处理所有结果并组合成最终数据val data1 = results[0] as? BaseResponse<Whois?>?val data2 = results[1] as? Tianqival data3 = results[2]// ... 处理其他结果MultipleDataResult(data1?.data, data2, data3)}fun uploadFile(request: FileUploadBean): Flow<FlowResult<String?>> {return apiCall {fileApiService.upload(request.build())}}/*** 普通文件下载* @param fileUrl 文件下载地址* @return Flow<FlowResult<ResponseBody>> 返回文件流的Flow*/fun downloadFile(fileUrl: String): Flow<FlowResult<ResponseBody?>> {return apiCall {fileApiService.downloadFile(fileUrl)}}/*** 带参数的文件下载* @param fileUrl 文件下载地址* @param params 下载参数* @return Flow<FlowResult<ResponseBody>> 返回文件流的Flow*/fun downloadFileWithParams(fileUrl: String,params: Map<String, String>): Flow<FlowResult<ResponseBody?>> {return apiCall {fileApiService.downloadFileWithParams(fileUrl, params)}}/*** 断点续传下载* @param fileUrl 文件下载地址* @param startByte 开始下载的字节位置* @return Flow<FlowResult<ResponseBody>> 返回文件流的Flow*/fun downloadFileWithResume(fileUrl: String,startByte: Long = 0): Flow<FlowResult<ResponseBody?>> {return if (startByte > 0) {// 断点续传val rangeHeader = "bytes=$startByte-"apiCall {fileApiService.downloadFileWithRange(fileUrl, rangeHeader)}} else {// 普通下载apiCall {fileApiService.downloadFile(fileUrl)}}}/*** 获取文件信息(用于断点续传前检查)* @param fileUrl 文件地址* @return Flow<FlowResult<Response>> 返回响应信息的Flow*/fun getFileInfo(fileUrl: String): Flow<FlowResult<Response?>> {return apiCall {fileApiService.getFileInfo(fileUrl)}}
}
4. 在ViewModel中使用
/*** @author jiangtao on 2025/9/20*/
class MyViewModel : ViewModel() {private val repository = MyNetworkRepository()// ===================================================================// Flow 风格的网络请求方法// ===================================================================fun getWhoIs(domain: String, cb: ((resp: FlowResult<BaseResponse<Whois?>?>?) -> Unit)? = null) {viewModelScope.launch(CoroutineExceptionHandler { _, _ ->cb?.invoke(null)}) {repository.whois(domain).collect { result ->cb?.invoke(result)}}}fun getTianqi(city: String, cb: ((resp: FlowResult<Tianqi?>?) -> Unit)? = null) {viewModelScope.launch(CoroutineExceptionHandler { _, _ ->cb?.invoke(null)}) {repository.tianqi(city).collect { result ->cb?.invoke(result)}}}fun getPcmeinvpic(cb: ((resp: FlowResult<String?>?) -> Unit)? = null) {viewModelScope.launch(CoroutineExceptionHandler { _, _ ->cb?.invoke(null)}) {repository.pcmeinv().collect { result ->cb?.invoke(result)}}}// ===================================================================// 普通挂起函数风格的网络请求方法// ===================================================================// 提供同步风格的挂起函数suspend fun getWhoIsSuspend(domain: String): BaseResponse<Whois?>? {return try {// 这里使用 withContext 确保在 IO 线程执行网络请求withContext(Dispatchers.IO) {// 实际调用 API 获取 whois 信息repository.getWhoIsInfo(domain)}} catch (e: Exception) {// 记录错误日志Log.e("UserViewModel", "获取WhoIs信息失败", e)null // 返回 null 表示失败}}suspend fun getTianqiSuspend(city: String): Tianqi? {return try {withContext(Dispatchers.IO) {repository.getTianqiInfo(city)}} catch (e: Exception) {Log.e("UserViewModel", "获取天气信息失败", e)null}}suspend fun getPcmeinvpicSuspend(): String? {return try {withContext(Dispatchers.IO) {repository.getPcmeinvInfo()}} catch (e: Exception) {Log.e("UserViewModel", "获取PC妹纸图片失败", e)null}}// ===================================================================// Flow 风格的多个网络请求合并// ===================================================================suspend fun fetchMultipleDataDirect(domain: String, city: String): MultipleDataResult? {return try {withContext(Dispatchers.IO) {repository.fetchMultipleDataDirect(domain, city)}} catch (e: Exception) {Log.e("UserViewModel", "获取PC妹纸图片失败", e)null}}// 使用 combineResults 方法合并多个网络请求fun fetchMultipleData(domain: String,city: String,cb: ((resp: FlowResult<MultipleDataResult?>?) -> Unit)? = null) {viewModelScope.launch(CoroutineExceptionHandler { _, _ ->cb?.invoke(null)}) {repository.fetchMultipleData(domain, city).collect { result ->cb?.invoke(result)}}}// 使用 combineResultsWithFlow 方法合并多个网络请求fun fetchMultipleDataWithFlow(domain: String,city: String,cb: ((resp: FlowResult<MultipleDataResult?>?) -> Unit)? = null) {viewModelScope.launch(CoroutineExceptionHandler { _, _ ->cb?.invoke(null)}) {repository.fetchMultipleDataWithFlow(domain, city).collect { result ->cb?.invoke(result)}}}fun uploadFile(file: File, params: Map<String, String>, cb: ((resp: FlowResult<String?>?) -> Unit)? = null) {viewModelScope.launch(CoroutineExceptionHandler { _, _ ->cb?.invoke(null)}) {val uploadBean = FileUploadBean(file, params)repository.uploadFile(uploadBean).collect { result ->cb?.invoke(result)}}}fun downloadFile(fileUrl: String, cb: ((resp: FlowResult<ResponseBody?>?) -> Unit)? = null) {viewModelScope.launch(CoroutineExceptionHandler { _, _ ->cb?.invoke(null)}) {repository.downloadFile(fileUrl).collect { result ->cb?.invoke(result)}}}fun downloadFileWithParams(fileUrl: String,params: Map<String, String>,cb: ((resp: FlowResult<ResponseBody?>?) -> Unit)? = null) {viewModelScope.launch(CoroutineExceptionHandler { _, _ ->cb?.invoke(null)}) {repository.downloadFileWithParams(fileUrl, params).collect { result ->cb?.invoke(result)}}}fun downloadFileWithResume(fileUrl: String,startByte: Long = 0,cb: ((resp: FlowResult<ResponseBody?>?) -> Unit)? = null) {viewModelScope.launch(CoroutineExceptionHandler { _, _ ->cb?.invoke(null)}) {repository.downloadFileWithResume(fileUrl, startByte).collect { result ->cb?.invoke(result)}}}
}
5. 在Activity中调用
Flow风格网络请求
/*** @author jiangtao on 2025/9/20*/
class FlowRequestActivity : MyBaseActivity<ActivityFlowRequestBinding>() {private val TAG = "FlowRequestActivity"private lateinit var viewModel: MyViewModelprivate var time: Long = 0private var cost: Long = 0override fun setup(savedInstanceState: Bundle?) {viewModel = ViewModelProvider(this)[MyViewModel::class.java]binding.result.movementMethod = ScrollingMovementMethod.getInstance()setOnClick()}private fun setOnClick() {// ===================================================================// Flow 风格的网络请求方法// ===================================================================binding.flowRequestBaseResponse.setOnClickListener {viewModel.getWhoIs("xxhzm.cn") {handlerData(it as FlowResult)}}binding.flowRequestObject.setOnClickListener {viewModel.getTianqi("上海") {handlerData(it as FlowResult)}}binding.flowRequestString.setOnClickListener {viewModel.getPcmeinvpic() {handlerData(it as FlowResult)}}binding.flowRequestCombinedData1.setOnClickListener {viewModel.fetchMultipleData("xxhzm.cn", "上海") {handlerData(it as FlowResult)}}binding.flowRequestCombinedData2.setOnClickListener {viewModel.fetchMultipleDataWithFlow("xxhzm.cn", "上海") {handlerData(it as FlowResult)}}}/*** 简便起见,返回数据统一处理*/override fun <T> handlerData(result: FlowResult<T?>) {result.handle(onSuccess = {hideProgress()showData(it.toString())},onError = { code, message ->hideProgress()showError("错误: $code, $message")},onException = { e ->hideProgress()showError("异常: ${e.message}")},onPrepare = {showProgress()})}private fun showProgress() {time = System.currentTimeMillis()Log.i(TAG, "showProgress")Toast.makeText(this, "showProgress", Toast.LENGTH_SHORT).show()}private fun hideProgress() {cost = System.currentTimeMillis() - timeLog.i(TAG, "hideProgress")Toast.makeText(this, "hideProgress", Toast.LENGTH_SHORT).show()}private fun showData(str: String?) {Log.i(TAG, "showData: $str")binding.result.text = "耗时: $cost ms\n"val jsonObject: JSONObject = JSONObject(str)binding.result.append(jsonObject.toString(4).toString())}private fun showError(message: String) {Log.e(TAG, "showError: $message")Toast.makeText(this, message, Toast.LENGTH_SHORT).show()}
}
挂起函数风格网络请求
/*** @author jiangtao on 2025/9/20*/
class SuspendRequestActivity : MyBaseActivity<ActivitySuspendRequestBinding>() {private val TAG = "SuspendRequestActivity"private lateinit var viewModel: MyViewModelprivate var time: Long = 0private var cost: Long = 0override fun setup(savedInstanceState: Bundle?) {viewModel = ViewModelProvider(this)[MyViewModel::class.java]binding.result.movementMethod = ScrollingMovementMethod.getInstance()setOnClick()}private fun setOnClick() {// ===================================================================// 普通挂起函数风格的网络请求方法// ===================================================================binding.requestBaseResponse.setOnClickListener {lifecycleScope.launch {showProgress()val response = viewModel.getWhoIsSuspend("xxhzm.cn")showData(response.toString())}}binding.requestObject.setOnClickListener {lifecycleScope.launch {showProgress()val response = viewModel.getTianqiSuspend("上海")showData(response.toString())}}binding.requestString.setOnClickListener {lifecycleScope.launch {showProgress()val response = viewModel.getPcmeinvpicSuspend()showData(response)}}binding.requestCombinedData.setOnClickListener {lifecycleScope.launch {showProgress()val response = viewModel.fetchMultipleDataDirect("xxhzm.cn", "上海")showData(response.toString())}}}private fun showProgress() {time = System.currentTimeMillis()Log.i(TAG, "showProgress")Toast.makeText(this, "showProgress", Toast.LENGTH_SHORT).show()}private fun hideProgress() {cost = System.currentTimeMillis() - timeLog.i(TAG, "hideProgress")Toast.makeText(this, "hideProgress", Toast.LENGTH_SHORT).show()}private fun showData(str: String?) {hideProgress()Log.i(TAG, "showData: $str")binding.result.text = "耗时: $cost ms\n"val jsonObject: JSONObject = JSONObject(str)binding.result.append(jsonObject.toString(4).toString())}
}
文件操作功能
文件上传
val uploadBean = FileUploadBean(file, params)
repository.uploadFile(uploadBean).collect { result ->// 处理上传结果
}
文件下载
/*** @author jiangtao on 2025/9/20*/
class FileOperationActivity : MyBaseActivity<ActivityFileOperationBinding>() {private val TAG = "FileOperationActivity"private lateinit var viewModel: MyViewModelprivate var selectedFileUri: Uri? = nullprivate lateinit var filePickerLauncher: ActivityResultLauncher<Intent>private var time: Long = 0private var cost: Long = 0override fun setup(savedInstanceState: Bundle?) {viewModel = ViewModelProvider(this)[MyViewModel::class.java]binding.result.movementMethod = ScrollingMovementMethod.getInstance()registerFilePickerLauncher()setOnClick()}/*** 注册文件选择器的回调*/private fun registerFilePickerLauncher() {filePickerLauncher =registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->if (result.resultCode == RESULT_OK) {val data = result.dataif (data != null && data.data != null) {selectedFileUri = data.datadisplaySelectedFileInfo(selectedFileUri!!)binding.uploadFileButton.isEnabled = true}}}}private fun setOnClick() {binding.selectFileButton.setOnClickListener { v -> openFilePicker() }binding.uploadFileButton.setOnClickListener { v -> uploadFile(selectedFileUri) }// 添加下载按钮的点击事件binding.downloadFileButton.setOnClickListener {startDownload(false) // 普通下载}binding.resumeDownloadButton.setOnClickListener {startDownload(true) // 断点续传下载}}private fun openFilePicker() {val intent = Intent(Intent.ACTION_GET_CONTENT)intent.type = "*/*" // 可以选择所有类型的文件intent.addCategory(Intent.CATEGORY_OPENABLE)filePickerLauncher.launch(intent)}private fun displaySelectedFileInfo(fileUri: Uri) {try {val cursor = contentResolver.query(fileUri, null, null, null, null)cursor?.use {val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)val sizeIndex = it.getColumnIndex(OpenableColumns.SIZE)if (it.moveToFirst()) {val fileName = it.getString(nameIndex)val fileSize = it.getLong(sizeIndex)binding.selectedFileInfo.text ="文件名: $fileName\n大小: ${formatFileSize(this, fileSize)}"}}} catch (e: Exception) {// 如果无法获取文件信息,则只显示 URIbinding.selectedFileInfo.text = "已选择文件: $fileUri"}}/*** 通过 Uri 获取文件对象*/private fun getFileFromUri(uri: Uri): File? {return try {val cursor = contentResolver.query(uri, null, null, null, null)val fileName = cursor?.use {if (it.moveToFirst()) {val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)it.getString(nameIndex)} else {"unknown_file"}} ?: "unknown_file"// 创建临时文件val tempFile = File(cacheDir, fileName)// 将 Uri 指向的内容复制到临时文件contentResolver.openInputStream(uri)?.use { inputStream ->FileOutputStream(tempFile).use { outputStream ->inputStream.copyTo(outputStream)}}tempFile} catch (e: Exception) {e.printStackTrace()null}}private fun uploadFile(fileUri: Uri?) {if (fileUri == null) {binding.result.text = "请先选择文件\n"return}binding.result.text = "开始处理文件...\n"try {// 获取文件对象val file = getFileFromUri(fileUri)if (file != null && file.exists()) {binding.result.append("文件获取成功!\n")binding.result.append("文件名: ${file.name}\n")binding.result.append("文件路径: ${file.absolutePath}\n")binding.result.append("文件大小: ${android.text.format.Formatter.formatFileSize(this,file.length())}\n")// 在这里可以使用 file 对象进行实际的上传操作// 例如使用 OkHttp、Retrofit 等网络库上传文件binding.result.append("文件准备就绪,可以进行上传操作\n")val params = mutableMapOf<String, String>()params["param1"] = "value1"params["param2"] = "value2"viewModel.uploadFile(file, params) {handlerData(it as FlowResult)}} else {binding.result.append("文件获取失败\n")}} catch (e: Exception) {binding.result.append("文件处理失败: ${e.message}\n")}}/*** 开始下载文件* @param isResume 是否断点续传下载*/private fun startDownload(isResume: Boolean) {val fileUrl = "https://example.com/file.zip"if (isResume) {// 断点续传下载viewModel.downloadFileWithResume(fileUrl, 128) {handlerDownload(it as FlowResult)}} else {// 普通下载viewModel.downloadFile(fileUrl) {handlerDownload(it as FlowResult)}}}private fun handlerDownload(result: FlowResult<ResponseBody?>) {result.handle(onSuccess = {binding.result.text = "下载成功!\n"val responseBody = itresponseBody?.saveFile(".", "file.zip")},onError = { code, message ->binding.result.text = "下载失败!\n${code}, ${message}"},onPrepare = {binding.result.text = "正在下载...\n"},onException = { e -> binding.result.text = "下载异常!\n$e" })}/*** 简便起见,返回数据统一处理*/override fun <T> handlerData(result: FlowResult<T?>) {result.handle(onSuccess = {hideProgress()showData(it.toString())},onError = { code, message ->hideProgress()showError("错误: $code, $message")},onException = { e ->hideProgress()showError("异常: ${e.message}")},onPrepare = {showProgress()})}private fun showProgress() {Log.i(TAG, "showProgress")Toast.makeText(this, "showProgress", Toast.LENGTH_SHORT).show()}private fun hideProgress() {Log.i(TAG, "hideProgress")Toast.makeText(this, "hideProgress", Toast.LENGTH_SHORT).show()}private fun showData(str: String?) {Log.i(TAG, "showData: $str")val jsonObject: JSONObject = JSONObject(str)binding.result.text = jsonObject.toString(4)}private fun showError(message: String) {Log.e(TAG, "showError: $message")Toast.makeText(this, message, Toast.LENGTH_SHORT).show()}
}
项目结构说明
- ApiService.kt - 标准API接口定义
- RawApiService.kt - 原始数据API接口定义
- FileApiService.kt - 文件操作相关API接口
- MyNetworkRepository.kt - 网络请求仓库实现
- MyViewModel.kt - ViewModel层,连接UI和数据
- BaseResponse.kt - 统一响应数据结构
- ext.kt - 扩展函数
依赖技术
- OkHttp - HTTP客户端
- Retrofit - REST API客户端
- Kotlin Coroutines - 协程支持
- Kotlin Flow - 响应式流处理
使用要求
- Android API 21+
- Kotlin 1.5+
- Android Studio Arctic Fox或更高版本