目前做APP网络API请求Retrofit+OkHttp+Kotlin协程应该是比较流行的,相比之前Retrofit+RxJava 有了太多的优势,Rx可以做的事情,协程一样可以做,而且可以做到更方便,更简洁。还不会用协程的童鞋可以看下这篇[Kotlin:玩转协程],接下来我们进行网络请求框架的实战。
在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"
}
最新版本,请查阅官方地址:
接口请求工厂类 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>
/**
* 地址列表页
* "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()// 取消协程
}
}
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 非常感谢了
MVP架构源码GitHub地址
至此,主要逻辑就是这些!