Jetpack Compose-使用Retrofit2结合Flow进行网络请求-上

前言

Retrofit是现在比较流行的网络请求框架,可以理解为okhttp的加强版,底层封装了Okhttp。

准确来说,Retrofit是一个RESTful的http网络请求框架的封装

因为网络请求工作本质上是由okhttp来完成,而Retrofit负责网络请求接口的封装。

https://square.github.io/retrofit/

代理HTTPS无法请求

Android要配置networkSecurityConfig

查看

https://www.psvmc.cn/article/2019-04-03-win-proxy.html

基本配置

添加网络权限

1
<uses-permission android:name="android.permission.INTERNET"/>

添加依赖

1
2
implementation ("com.squareup.retrofit2:retrofit:2.9.0")
implementation ("com.squareup.retrofit2:converter-gson:2.9.0")

Kotlin整体示例

新版本的Retrofit已经可以方便使用Kotlin的协程。

添加实体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.util.List;

public class ResultListVo<T> {
private int code;
private String msg;
private List<T> obj;

public int getCode() {
return code;
}

public void setCode(int code) {
this.code = code;
}

public String getMsg() {
return msg;
}

public void setMsg(String msg) {
this.msg = msg;
}

public List<T> getObj() {
return obj;
}

public void setObj(List<T> obj) {
this.obj = obj;
}
}

用户

1
data class UserItem(val name: String, val sex: String)

服务接口

添加suspend关键字,返回类型Response<T>

ServiceUser.kt

1
2
3
4
5
6
7
8
9
10
11
package com.xhkjedu.zxs_android.api

import com.xhkjedu.zxs_android.model.ResultListVo
import com.xhkjedu.zxs_android.model.UserItem
import retrofit2.Response
import retrofit2.http.GET

interface ServiceUser {
@GET("userlist.json")
suspend fun getUserList(): Response<ResultListVo<UserItem>>
}

suspend 关键字的核心作用是允许函数暂停协程的执行(而非阻塞线程),并在适当的时候恢复,它是 Kotlin 协程实现 “高效异步编程” 的基础。

Retrofit 支持用 suspend 标记接口方法,直接在协程中调用,无需回调;

管理类

ApiManager.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
import android.annotation.SuppressLint
import com.xhkjedu.zxs_android.common.CommonData
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.security.SecureRandom
import java.security.cert.CertificateException
import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager


object ApiManager {
private lateinit var retrofit: Retrofit
private lateinit var userRetrofit: Retrofit
lateinit var appService: ServiceApp
lateinit var userService: ServiceUser


@SuppressLint("CustomX509TrustManager")
fun getUnsafeOkHttpClient(): OkHttpClient {
try {
// 创建一个信任所有证书的 TrustManager
val trustAllCerts = arrayOf<TrustManager>(

object : X509TrustManager {
override fun checkClientTrusted(
chain: Array<out java.security.cert.X509Certificate>?,
authType: String?
) {
}

override fun checkServerTrusted(
chain: Array<out java.security.cert.X509Certificate>?,
authType: String?
) {
}

override fun getAcceptedIssuers(): Array<java.security.cert.X509Certificate> =
arrayOf()
}
)

// 初始化 SSLContext
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, trustAllCerts, SecureRandom())

// 创建一个允许所有主机名验证的 HostnameVerifier
val allHostsValid = HostnameVerifier { _, _ -> true }

// 创建 OkHttpClient 并配置 SSL 和主机名验证
return OkHttpClient.Builder()
.sslSocketFactory(sslContext.socketFactory, trustAllCerts[0] as X509TrustManager)
.hostnameVerifier(allHostsValid)
.build()
} catch (e: Exception) {
throw RuntimeException(e)
}
}

init {
if (!::retrofit.isInitialized) {
val client = OkHttpClient().newBuilder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.addInterceptor(HeaderInterceptor())
.build();

retrofit = Retrofit.Builder()
.baseUrl(CommonData.apiUrl)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
}

if (!::userRetrofit.isInitialized) {
val client = getUnsafeOkHttpClient().newBuilder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.build();

userRetrofit = Retrofit.Builder()
.baseUrl("https://www.psvmc.cn/")
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
}

if (!::appService.isInitialized) {
appService = retrofit.create(ServiceApp::class.java)
}

if (!::userService.isInitialized) {
userService = userRetrofit.create(ServiceUser::class.java)
}
}
}

简单调用

1
2
3
4
GlobalScope.launch {
var result = ApiManager.appService.getUserList().body()
Log.i("API", Gson().toJson(result))
}

ViewModel中调用

BaseViewModel

如果使用Jetpack Compose

BaseViewModel.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

open class BaseViewModel : ViewModel() {
var zIsShowMsg = mutableStateOf(false)
var zShowMsgContent = mutableStateOf("")

fun mLaunch(block: suspend () -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
try {
block()
} catch (e: Exception) {
withContext(Dispatchers.Main){
zIsShowMsg.value = true
zShowMsgContent.value = "接口请求失败"
}
}
}
}
}

调用

UserViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import android.util.Log
import androidx.compose.runtime.mutableStateListOf
import com.google.gson.Gson
import com.xhkjedu.zxs_android.api.ApiManager
import com.xhkjedu.zxs_android.model.UserItem
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.withContext

class UserViewModel : BaseViewModel() {
private val TAG = "UserViewModel"
val userList = mutableStateListOf<UserItem>()

//加载分组
fun loadUserList() {
mLaunch {
val result = ApiManager.userService.getUserList().body()
Log.i("API", Gson().toJson(result))
withContext(Dispatchers.Main) {
result?.let {
Log.i(TAG, "Thread: " + Thread.currentThread().name)
Log.i(TAG, "UserViewModel: " + Gson().toJson(it))
if (it.code == 0) {
if (it.obj == null) {
it.obj = mutableListOf()
}
userList.addAll(it.obj)
mShowMsg.value = false
} else {
mShowMsg.value = true
mShowMsgContent.value = it.msg
}
}
}
}
}
}

常见配置

请求头设置

HeaderInterceptor.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import android.util.Log
import com.xhkjedu.zxs_android.common.CommonData
import okhttp3.Interceptor
import okhttp3.Response
import java.io.IOException

class HeaderInterceptor : Interceptor {
var TAG: String = "HeaderInterceptor"

@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val original = chain.request()
try {
val mBuilder = original.newBuilder()
if (CommonData.Authorization != "") {
mBuilder.header("Authorization", CommonData.Authorization)
}

val request = mBuilder
.method(original.method, original.body)
.build()

val response = chain.proceed(request)
// 获取响应头
val headers = response.headers
// 处理响应头
headers.forEach { header ->
Log.d("ResponseHeader", "${header.first} ${header.second}")
if (header.first == "Authorization") {
CommonData.Authorization = header.second
}
}
return response
} catch (e: Exception) {
return chain.proceed(original)
}
}
}

其中

intercept 会在每次请求的时候都触发。

超时时间设置

1
2
3
4
5
6
7
8
9
10
11
12
val client = OkHttpClient().newBuilder()
.connectTimeout(10, TimeUnit.SECONDS) // 连接超时时间为 10 秒
.readTimeout(20, TimeUnit.SECONDS) // 读取超时时间为 20 秒
.writeTimeout(20, TimeUnit.SECONDS) // 写入超时时间为 20 秒
.addInterceptor(HeaderInterceptor())
.build();

retrofit = Retrofit.Builder()
.baseUrl(CommonData.apiUrl)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()

这三个超时时间分别是:

  1. 连接超时时间(Connect Timeout):指的是客户端连接到服务器所需的最大时间。

    如果在规定的时间内未能建立连接,则会抛出 java.net.SocketTimeoutException 异常。

  2. 读取超时时间(Read Timeout):指的是客户端从服务器读取数据的最大时间。比如下载。

    如果在规定的时间内未能读取到数据,则会抛出 java.net.SocketTimeoutException 异常。

  3. 写入超时时间(Write Timeout):指的是客户端向服务器发送数据的最大时间。比如上传。

    如果在规定的时间内未能将数据发送完毕,则会抛出 java.net.SocketTimeoutException 异常。

建议:

上传下载和普通接口请求的超时时间分开设置。因为两个的时间差别较大。

信任所有证书

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@SuppressLint("CustomX509TrustManager")
fun getUnsafeOkHttpClient(): OkHttpClient {
try {
// 创建一个信任所有证书的 TrustManager
val trustAllCerts = arrayOf<TrustManager>(

object : X509TrustManager {
override fun checkClientTrusted(
chain: Array<out java.security.cert.X509Certificate>?,
authType: String?
) {
}

override fun checkServerTrusted(
chain: Array<out java.security.cert.X509Certificate>?,
authType: String?
) {
}

override fun getAcceptedIssuers(): Array<java.security.cert.X509Certificate> =
arrayOf()
}
)

// 初始化 SSLContext
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, trustAllCerts, SecureRandom())

// 创建一个允许所有主机名验证的 HostnameVerifier
val allHostsValid = HostnameVerifier { _, _ -> true }

// 创建 OkHttpClient 并配置 SSL 和主机名验证
return OkHttpClient.Builder()
.sslSocketFactory(sslContext.socketFactory, trustAllCerts[0] as X509TrustManager)
.hostnameVerifier(allHostsValid)
.build()
} catch (e: Exception) {
throw RuntimeException(e)
}
}

请求报错

javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found

可以按照上面信任证书

结合Flow

BaseViewModel.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

open class BaseViewModel : ViewModel() {
var mShowMsg = mutableStateOf(false)
var mShowMsgContent = mutableStateOf("")
var isLoading = mutableStateOf(false)

fun mLaunch(block: suspend () -> Unit) {
viewModelScope.launch {
try {
block()
} catch (e: Exception) {
withContext(Dispatchers.Main) {
e.printStackTrace()
isLoading.value = false
mShowMsg.value = true
mShowMsgContent.value = "数据加载失败"
}
}
}
}
}

UserViewModel.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import android.util.Log
import androidx.compose.runtime.mutableStateListOf
import com.google.gson.Gson
import com.xhkjedu.zxs_android.api.ApiManager
import com.xhkjedu.zxs_android.model.UserItem
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.withContext

class UserViewModel : BaseViewModel() {
private val TAG = "UserViewModel"
val userList = mutableStateListOf<UserItem>()

fun loadUserListUseFlow() {
mLaunch {
flow {
Log.i(TAG, "Thread-Flow: " + Thread.currentThread().name)
emit(ApiManager.userService.getUserList().body())
}
.flowOn(Dispatchers.IO)
.catch { e ->
e.printStackTrace()
}.collect {
Log.i(TAG, "Thread-Collect: " + Thread.currentThread().name)
it?.let {
Log.i(TAG, "UserViewModel: " + Gson().toJson(it))
if (it.code == 0) {
if (it.obj == null) {
it.obj = mutableListOf()
}
userList.addAll(it.obj)
mShowMsg.value = false
} else {
mShowMsg.value = true
mShowMsgContent.value = it.msg
}
}

}
}
}
}

服务参数

Query参数

1
2
3
4
@GET("/user/list")
suspend fun apiUserList(
@Query("page") page: Int, @Query("limit") limit: Int
): Response<ResultVo<Collection<UserModel>>>

Path参数

1
2
@GET("users/{userId}")
fun getUser(@Path("userId") userId: Int): Response<ResultVo<UserModel>>

Body参数

数据类

首先定义数据类:

1
2
3
4
5
data class UserRequest(
val username: String,
val password: String,
val age: Int
)

然后在 Retrofit 接口中使用 @Body 注解:

1
2
3
4
5
6
7
8
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.POST

interface ApiService {
@POST("user/login")
suspend fun login(@Body request: UserRequest): Response<UserModel>
}

MutableMap

1
2
3
4
interface ApiService {
@POST("user/login")
suspend fun login(@Body params: Map<String, @JvmSuppressWildcards Any>): Response<UserModel>
}

使用时

1
2
3
4
5
6
val params = mapOf(
"username" to "test",
"password" to "123456",
"rememberMe" to true
)
apiService.login(params)

报错

Parameter type must not include a type variable or wildcard: java.util.Map<java.lang.String, ?>

原因是:

因为参数Map的value类型Any,在Java中这个value的类型是Object,可以被Retrofit识别,但对于kotlin来说,retrofit会把Any识别成 ?,所以就报出了错误。

解决方法

Any前要添加@JvmSuppressWildcards

请求体是空

如果请求体要求是{}

不传参数的话,请求体是空,不是{},写成Map<String, Any?>这样也不行,会提示不知道值的类型。

我们可以写成这样

1
2
@POST("/api/sys/facility/resource/queryList")
suspend fun apiSysFacilityResourceQuerylist(@Body params: Map<String, String>): Response<ResultVo<Collection<TopMenuItem>>>

调用

1
ApiManager.menuService.apiSysFacilityResourceQuerylist(mapOf())

匿名对象

1
2
3
4
interface ApiService {
@POST("user/info")
suspend fun updateInfo(@Body params: Any): Response<UserModel>
}

使用时

1
2
3
4
5
6
7
apiService.updateInfo(
object {
val nickname = "新昵称"
val age = 25
val phone = "13800138000"
}
)

Form 表单参数

@Field 强制依赖 @FormUrlEncoded,会自动设置请求头为表单类型。

单个参数

1
2
3
4
5
6
@FormUrlEncoded
@POST("/user/login")
suspend fun apiUserLogin(
@Field("username") username: String,
@Field("password") password: String
): Response<ResultVo<UserModel>>

表单Map

1
2
3
4
5
interface ApiService {
@FormUrlEncoded
@POST("user/register")
suspend fun register(@FieldMap params: Map<String, String>): Response<RegisterResponse>
}

使用时

1
2
3
4
5
6
val params = mapOf(
"username" to "test",
"password" to "123456",
"email" to "test@example.com"
)
apiService.register(params)

MutiPart

1
2
3
4
5
6
7
8
interface ApiService {
@Multipart
@POST("user/update")
suspend fun updateUser(
@Part("username") username: RequestBody,
@Part avatar: MultipartBody.Part // 文件部分
): Response<UserResponse>
}