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

引言

目前做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)
            }
        }
    }
}

至此,主要逻辑就是这些!

发布了63 篇原创文章 · 获赞 67 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/sange77/article/details/102575852