Android:玩转Retrofit+OkHttp+Kotlin协程 网络请求架构

2023-11-09

引言

目前做APP网络API请求Retrofit+OkHttp+Kotlin协程应该是比较流行的,相比之前Retrofit+RxJava 有了太多的优势,Rx可以做的事情,协程一样可以做,而且可以做到更方便,更简洁。还不会用协程的童鞋可以看下这篇[Kotlin:玩转协程],接下来我们进行网络请求框架的实战。

实战

1、引入开源库

在app moudle 下build.gradle引入下列库。

dependencies {
	//OkHttp3
	implementation "com.squareup.okhttp3:okhttp:3.12.0"
    //Retrofit网络请求
    api "com.squareup.retrofit2:retrofit:2.6.2"
    api "com.squareup.retrofit2:converter-gson:2.6.2"
    implementation "com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2"
    //Kotlin Coroutines 协程
    api "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3"
}

最新版本,请查阅官方地址:

2、简单封装

接口请求工厂类 ApiFactory.kt

import com.jakewharton.retrofit2.adapter.kotlin.coroutines.CoroutineCallAdapterFactory
import okhttp3.FormBody
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit

/**
 * 接口请求工厂
 * @author ssq
 */
object ApiFactory {
    // 日志拦截器
    private val mLoggingInterceptor: Interceptor by lazy { LoggingInterceptor() }
    // OkHttpClient客户端
    private val mClient: OkHttpClient by lazy { newClient() }

    /**
     * 创建API Service接口实例
     */
    fun <T> create(baseUrl: String, clazz: Class<T>): T = Retrofit.Builder().baseUrl(baseUrl).client(mClient)
            .addConverterFactory(GsonConverterFactory.create(MGson.getInstance()))
            .addCallAdapterFactory(CoroutineCallAdapterFactory()).build().create(clazz)

    /**
     * OkHttpClient客户端
     */
    private fun newClient(): OkHttpClient = OkHttpClient.Builder().apply {
        connectTimeout(30, TimeUnit.SECONDS)// 连接时间:30s超时
        readTimeout(10, TimeUnit.SECONDS)// 读取时间:10s超时
        writeTimeout(10, TimeUnit.SECONDS)// 写入时间:10s超时
        if (BaseApplication.isDebugMode) addInterceptor(mLoggingInterceptor)// 仅debug模式启用日志过滤器
    }.build()

    /**
     * 日志拦截器
     */
    private class LoggingInterceptor : Interceptor {
        override fun intercept(chain: Interceptor.Chain): Response {
            val builder = StringBuilder()
            val startTime = System.nanoTime()
            val response: Response = with(chain.request()) {
                builder.append(method() + "\n")
                builder.append("Sending request\n" + url())
                if (method() == "POST") {
                    builder.append("?")
                    when (val body = body()) {
                        is FormBody -> {
                            for (j in 0 until body.size()) {
                                builder.append(body.name(j) + "=" + body.value(j))
                                if (j != body.size() - 1) {
                                    builder.append("&")
                                }
                            }
                        }
//                        is MultipartBody -> {}
                    }
                }
                builder.append("\n").append(headers())
                chain.proceed(this)
            }
            builder.append("Received response in " + (System.nanoTime() - startTime) / 1e6 + "ms\n")
            builder.append("code" + response.code() + "\n")
            LogUtil.v(builder.toString())
            return response
        }
    }
}

api接口 ApiServiceKt.kt

/**
 * api接口
 * @author ssq
 * @JvmSuppressWildcards 用来注解类和方法,使得被标记元素的泛型参数不会被编译成通配符?
 */
@JvmSuppressWildcards
interface ApiServiceKt {

    /**
     * 下载文件
     * @param fileUrl 文件地址 (这里的url可以是全名,也可以是基于baseUrl拼接的后缀url)
     * @return
     */
    @Streaming
    @GET
    fun downloadFileAsync(@Url fileUrl: String): Deferred<ResponseBody>

    /**
     * 上传图片
     * @param url 可选,不传则使用默认值
     * @param imgPath 图片路径
     * @param map     参数
     */
    @Multipart
    @POST
    fun uploadImgAsync(@Url url: String = "${ApiConstant.UPLOAD_IMAGE_URL}Upload.php", @PartMap imgPath: Map<String, RequestBody>, @QueryMap map: Map<String, Any>): Deferred<Response<UploadImgEntity>>

    /**
     * 通用异步请求 只需要解析BaseBean
     */
    @FormUrlEncoded
    @POST("Interfaces/index")
    fun requestAsync(@FieldMap map: Map<String, Any>): Deferred<Response<BaseBean>>
}

Retrofit 管理类 RetrofitManagerKt.kt

import android.content.Context
import android.webkit.MimeTypeMap
import com.blankj.utilcode.util.SPUtils
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.MediaType
import okhttp3.RequestBody
import okhttp3.ResponseBody
import retrofit2.Response
import java.io.File
import java.net.URLEncoder
import java.util.*

/**
 * Retrofit 管理类
 * @author ssq
 */
object RetrofitManagerKt {
    // 接口API服务
    val apiService by lazy { ApiFactory.create(ApiConstant.BASE_URL, ApiServiceKt::class.java) }

    /**
     * 执行网络请求(结合kotlin 协程使用)
     * @param deferred 请求的接口
     * @param isValidateCode 是否验证code,如:登录是否过期
     * @param context 为null时,登录过期不跳登录页
     */
    suspend fun <T : BaseBean> request(deferred: Deferred<Response<T>>, isValidateCode: Boolean = false, context: Context? = null): T? = withContext(Dispatchers.Default) {
        try {
            val response = deferred.await()
            if (response.isSuccessful) {// 成功
                val body = response.body()
                if (isValidateCode && body != null) {
                    validateCode(context, body.code)
                }
                body ?: throw NullBodyException()
            } else {// 处理Http异常
                ExceptionUtil.catchHttpException(response.code())
                null
            }
        } catch (e: Exception) {
            // 这里统一处理错误
            ExceptionUtil.catchException(e)
            null
        }
    }

    /**
     * 下载文件
     */
    suspend fun downloadFile(fileUrl: String): ResponseBody? = withContext(Dispatchers.IO) {
        try {
            apiService.downloadFileAsync(fileUrl).await()
        } catch (e: Exception) {
            // 这里统一处理错误
            ExceptionUtil.catchException(e)
            null
        }
    }

    /**
     * 上传图片文件
     * @param file 图片文件
     * @param type 用途类型
     */
    suspend fun uploadImage(file: File, type: Int): UploadImgEntity? =
            request(apiService.uploadImgAsync(imgPath = getUploadImgBodyMap(file), map = getUploadImgMap(type)))

    /**
     * 生成上传图片请求的文件参数
     * @param file 上传文件
     */
    fun getUploadImgBodyMap(file: File): HashMap<String, RequestBody> {
        val requestBodyMap = hashMapOf<String, RequestBody>()
        val mimeType = MimeTypeMap.getSingleton()
                .getMimeTypeFromExtension(
                        MimeTypeMap.getFileExtensionFromUrl(file.path)) ?: "image/jpeg"
        val fileBody = RequestBody.create(MediaType.parse(mimeType), file)
        // (注意:okhttp3 请求头不能为中文)如果url参数值含有中文、特殊字符时,需要使用 url 编码。
        requestBodyMap["myfiles\"; filename=\"${URLEncoder.encode(file.name, "utf-8")}"] = fileBody
        return requestBodyMap
    }

    /**
     * 生成上传图片请求参数
     * @param type 用途类型
     */
    fun getUploadImgMap(type: Int): HashMap<String, Any> {
        val map = hashMapOf<String, Any>()
        map["type"] = type
        map["time"] = System.currentTimeMillis()
        return map
    }
}

异常工具类 ExceptionUtil.kt

import android.accounts.NetworkErrorException
import android.content.res.Resources
import androidx.annotation.StringRes
import com.blankj.utilcode.util.ToastUtils
import com.google.gson.stream.MalformedJsonException
import retrofit2.HttpException
import java.net.SocketTimeoutException
import java.net.UnknownHostException

/**
 * 异常工具类
 * @author ssq
 */
object ExceptionUtil {

    /**
     * 处理异常
     */
    fun catchException(e: Exception) {
        e.printStackTrace()
        val msg = when (e) {
            is HttpException -> {
                catchHttpException(e.code())
                return
            }
            is SocketTimeoutException -> R.string.common_error_net_time_out
            is UnknownHostException, is NetworkErrorException -> R.string.common_error_net
            is NullPointerException, is ClassCastException, is Resources.NotFoundException,is MalformedJsonException -> R.string.common_error_do_something_fail
            is NullBodyException -> R.string.common_error_server_body_null
            else -> R.string.common_error_do_something_fail
        }
        ToastUtils.showShort(msg)
    }

    /**
     * 处理网络异常
     */
    fun catchHttpException(errorCode: Int) {
        if (errorCode in 200 until 300) return// 成功code则不处理
        showToast(catchHttpExceptionCode(errorCode), errorCode)
    }

    /**
     * toast提示
     */
    private fun showToast(@StringRes errorMsg: Int, errorCode: Int) {
        ToastUtils.showShort("${BaseApplication.instance.getString(errorMsg)}$errorCode ")
    }

    /**
     * 处理网络异常
     */
    private fun catchHttpExceptionCode(errorCode: Int): Int = when (errorCode) {
        in 500..600 -> R.string.common_error_server
        in 400 until 500 -> R.string.common_error_request
        else -> R.string.common_error_request
    }
}

提示文字 string.xml

	<string name="common_error_net">网络异常,请检查网络连接!</string>
    <string name="common_error_net_time_out">网络超时</string>
    <string name="common_error_do_something_fail">操作异常</string>
    <string name="common_error_request">请求错误</string>
    <string name="common_error_server">服务器错误</string>
    <string name="common_error_server_body_null">服务器错误:body为空</string>

3、开始使用

MVP架构模式

  • Activity文件,Activity为主协程域,在界面销毁时取消协程。
/**
 * 地址列表页
 * "CoroutineScope by MainScope()" 协程生命周期管理。在onDestroy()中调用cancel()取消协程。调用launch{}启动协程
 * @author ssq
 */
class AddressListActivity : BaseListActivity<AddressListPresenter>(), CoroutineScope by MainScope(), AddressListContract.View {

	override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mPresenter = AddressListPresenter(this)
        mPresenter?.deleteAddress(data.id, position)
    }
    
    override fun onDestroy() {
        super.onDestroy()
        cancel()// 取消协程
    }
}
  • Presenter文件,从Activity 把协程域传递给Presenter
class AddressListPresenter(scope: CoroutineScope) : BasePresenterKt<AddressListContract.View>(), CoroutineScope by scope, AddressListContract.Presenter {

    override fun deleteAddress(id: String, position: Int) = launch {
        val map = HashMap<String, Any>()
        map["id"] = id
        RetrofitManagerKt.request(RetrofitManagerKt.apiService.requestAsync(map), true, mContext)?.also {
            if (it.code == 0) {
                mView?.deleteAddressSuccess(position)
            } else {
                ToastUtils.showShort(it.message)
            }
        }
    }
}

-------2020年4月25日更新----------
有需要Demo源码的朋友看这里,第三方库版本是基于4月25日最新的
可以将源码下载下来参考,或是直接使用。加快你的项目开发!如果可以顺便点个Star 非常感谢了

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Android:玩转Retrofit+OkHttp+Kotlin协程 网络请求架构 的相关文章

随机推荐

  • 计算机视觉基础(八)—— LBP特征描述算子之人脸检测

    1 简介 LBP指局部二值模式 Local Binary Pattern 是一种用来描述图像局部特征的算子 具有灰度不变性和旋转不变性等显著优点 LBP常应用于人脸识别和目标检测中 在OpenCV中有使用LBP特征进行人脸识别的接口 也有用
  • python opencv imread()中文路径返回为空None的解决办法 (imdecode函数的使用)

    本质原因并非网上某些地方说的是中文编码UNICODE UTF 8之类的问题 其实是python版opencv不接受NON ASCII的中文路径 为了使用中文路径 可以借助一些其他的库 本文首先介绍一种通过numpy库 毕竟这个是python
  • 编程问题(持续记录)

    问题记录 一 vue问题记录 1 vue elementui项目 build后在ie下报 Promise 未定义错误 2 elementui ie下icon不显示 3 npm run dev 时 报babel runtime core js
  • 两种方法将oracle数据库中的一张表的数据导入到另外一个oracle数据库中

    oracle数据库实现一张表的数据导入到另外一个数据库的表中的方法有很多 在这介绍两个 第一种 把oracle查询的数据导出为sql文件 执行sql文件里的insert语句 如下 第一步 导出sql文件 第二步 用PL Sql Develo
  • 快速幂理解

    2 9 gt gt 2 2 8 2 2 8 gt gt 2 4 4 2 4 4 gt gt 2 16 2 2 16 2 gt gt 2 256 1 思想 通过降阶的方式每次将幂除 2 将底数每次乘方 因为幂不能为奇数 所以如果幂为奇数必须处
  • 数据筛选特征方法-方差法

    在数理统计中 方差是测算随机变量离散趋势最重要 最常用的指标 方差是各变量值与其均值离差平方的平均数 它是测算数值型数据离散程度的最重要的方法 当数据分布比较集中时 各个数据与平均数的差的平方和较小 当数据分布比较分散 即数据在平均数附近波
  • 正确理解层次方框图

    正确的层次方框图示例 图片来源 https blog csdn net qq 15037231 article details 60467793 注意 在网上搜索层次方框图会出现很多类似的图 但很多都是错的 层次方框图用树形结构的一系列多层
  • 重定向与请求转发

    分享一下在华清远见学习的知识点 重定向与请求转发也是面试中容易问到的问题 目录 一 重定向 二 请求转发 三 请求转发与重定向数据传递 四 请求转发与重定向的区别 总结 一 重定向 重定向是指由原请求地址重新定位到某个新地址 原有的 req
  • Java培训班学费一般多少?Java自学成功率是多少?

    Java培训班学费一般多少呢 Java自学成功率是多少 很多想学Java的新手对培训学费很是好奇 同时也想知道自学成功率 毕竟能不花钱就能学好的话 没必要花巨款学习 关于Java培训费用与自学成功率的答案请往下看 Java培训班学费一般多少
  • 鼠标移入背景图片放大(还原)

  • maven 配置 Tomcat7 插件和启动

    如果想使用 在 maven中使用 Tomcat7 来启动的话 可以这样配置
  • ORBSLAM2计算描述子距离——C++ 位操作得到二进制32位int值中有多少个1

    看ORBSLAM2时算描述子之间的距离时看到的神奇的位操作 特此记录一哈 unsigned int v pa pb v v v gt gt 1 0x55555555 v v 0x33333333 v gt gt 2 0x33333333 d
  • 如何解析EML(邮件)格式的文件以及一款小巧的EML邮件阅读工具

    在理解EML格式的时候 先回顾一下历史 这样有助于理解邮件的格式 比如邮件传输时为何会有多种编码方式 此外 理解EML格式也有助于理解HTTP协议 历史溯源 由于历史原因 我们目前看到的大部分的网络协议都是基于ASCII码这种纯文本方式 也
  • 用java写一个权限系统的框架

    Java是一种强大的编程语言 可以用来构建复杂的软件系统 包括权限系统 下面是一个简单的Java权限系统的框架 定义用户和角色 首先需要定义用户和角色 比如管理员 普通用户等 分配权限 为每个角色分配不同的权限 比如管理员有所有权限 普通用
  • 58,滴滴,京东等大厂都在用的抓包工具:Fiddler。软件测试工程师必会技能

    前言 Fiddler是一个http协议调试代理工具 它能够记录并检查所有你的电脑和互联网之间的http通讯 设置断点 查看所有的 进出 Fiddler的数据 例如cookie html js css等文件 换句话说就是Fiddler可以拦截
  • java jdbc 时间段和日期段查询

    首先先创建1000条随机日期数据 package jdbc import java sql Connection import java sql Date import java sql DriverManager import java
  • 顺序表的操作及实现(C++)

    顺序表的操作是大家在学习数据结构的第一个模块 也是最简单的一个 下列是顺序表的六种基本操作详解 希望能对您有所帮助 1 建立一个顺序表 输入n个元素并输出 define MAXSIZE 100 确定最大值 define OK 1 defin
  • 世界十大美女城市排行榜出炉:荷兰首都居首位

    中新网6月4日电 据俄罗斯媒体报道 在旅游旺季到来之际 世界著名的 游客文摘 杂志日前公布了在男性游客心目中拥有世界最漂亮女性的十大城市排行榜 荷兰的阿姆斯特丹 以色列的特拉维夫和加拿大的蒙特利尔位列三甲 俄罗斯首都莫斯科位列第五 高居十大
  • String类

    一 字符串 C语言中 字符串用字符数组来实现 Java语言中 用类来实现 该类共分为两大类 字符串常量 不可修改的字符串 存放在String类的对象中 字符串变量 可以修改的字符串 存放在StringBuffer类的对象中 二 String
  • Android:玩转Retrofit+OkHttp+Kotlin协程 网络请求架构

    文章目录 引言 实战 1 引入开源库 2 简单封装 3 开始使用 MVP架构模式 MVVM架构模式 看 这里 https blog csdn net sange77 article details 103959389 引言 目前做APP网络