一、纯 Retrofit 的使用
- 参考 Android 关于 Retrofit 回调的正确处理方式 - 简书 (jianshu.com)
这里使用了 LiveData(因此不需要担心生命周期的问题),同时对返回结果的处理进行了封装,发起请求只需要一句话,得到结果后会自动通知到 LiveData
Api.mApiService.getTestData().enqueue(SimpleCallback(mTestData))
二、Retrofit+Coroutine 的使用
-
Retrofit 要使用 2.6.0 及以上的版本,其自带 suspend 支持
-
使用到了 viewModelScope,不用自己定义 CoroutineScope
-
主要是需要对异常做处理,这里封装到了 BaseViewModel 里,也可以用 OkHttp 的 Interceptor 来处理
-
使用 Retrofit+Coroutine 的好处:
- 方便异步代码的编写
- 可以和 Flow 结合使用
- 可以并行处理多个请求
- 简单使用
SimpleRetrofitCoroutineActivity.kt
class SimpleRetrofitCoroutineActivity : BaseActivity() {
private lateinit var mAdapter: SimpleAdapter
private val mViewModel: SimpleViewModel by viewModels()
private lateinit var mRefreshLayout: SwipeRefreshLayout
var mError = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initRetrofit()
initUi()
initViewModel()
}
private fun initViewModel() {
mViewModel.mTestData.observe(this) {
mRefreshLayout.isRefreshing = false
it?.let {
mAdapter.setList(it)
}
}
mViewModel.getTestData()
}
private fun initUi() {
setContentView(R.layout.activity_retrofit_coroutine)
val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = SimpleAdapter().apply {
mAdapter = this
}
mAdapter.setEmptyView(TextView(this).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
gravity = Gravity.CENTER
text = "Empty!"
setTextSize(TypedValue.COMPLEX_UNIT_SP, 20f)
})
mRefreshLayout = findViewById(R.id.swipe_refresh)
mRefreshLayout.setOnRefreshListener {
mError = true
mViewModel.getTestData()
}
}
private fun initRetrofit() {
val client = OkHttpClient.Builder()
.connectTimeout(3, TimeUnit.SECONDS)
.readTimeout(3, TimeUnit.SECONDS)
.writeTimeout(3, TimeUnit.SECONDS)
.addInterceptor { chain ->
// delay
Thread.sleep(1500L)
val api = chain.request().url().pathSegments().last()
val body = ResponseBody.create(MediaType.parse("application/json"), mockData(api))
okhttp3.Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(200)
.body(body)
.message("OK")
.build()
}.build()
Retrofit.Builder()
.baseUrl("https://www.baidu.com/")
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiService::class.java).also {
Api.mApiService = it
}
}
private fun mockData(api: String): String {
if (mError) {
return ""
}
when (api) {
"kinds" -> return """
{
"data": [
{
"id": 1,
"name": "fruit"
},
{
"id": 2,
"name": "meat"
}
],
"code": 200,
"message": "OK"
}
""".trimIndent()
}
return ""
}
}
private class SimpleAdapter :
BaseQuickAdapter<Kind, BaseViewHolder>(android.R.layout.simple_list_item_1, null) {
override fun convert(holder: BaseViewHolder, item: Kind) {
if (item.name == null) {
holder.setText(android.R.id.text1, "parse error!")
} else {
holder.setText(android.R.id.text1, item.name)
}
}
}
open class BaseViewModel : ViewModel() {
fun <S, T : MutableLiveData<S?>> myLaunch(
t: T, block: suspend CoroutineScope.() -> Result<S?>
) {
viewModelScope.launch {
try {
val result = block()
if (!result.message.isNullOrEmpty() && result.code != 200) {
ToastUtils.showShort(result.message)
}
t.value = result.data
} catch (e: Exception) {
// 任何异常都会走到这里,如超时、404等
t.value = null
ToastUtils.showShort(e.message)
}
}
}
}
class SimpleViewModel : BaseViewModel() {
val mTestData = MutableLiveData<List<Kind>?>()
fun getTestData() {
myLaunch(mTestData) {
Api.mApiService.getKindsData()
}
}
}
object Api {
lateinit var mApiService: ApiService
}
interface ApiService {
@GET("api/kinds")
suspend fun getKindsData(): Result<List<Kind>?>
}
data class Result<T>(val message: String?, val code: Int, val data: T?)
data class Kind(val id: Int, val name: String?)
- 并行请求
经验证,50个请求大概需要 15s 的样子(单个网络延迟为 1.5S),并行效果有所显现
RetrofitCoroutineActivity.kt
class RetrofitCoroutineActivity : BaseActivity() {
private lateinit var mAdapter: MyAdapter
private val mViewModel: MyViewModel by viewModels()
private lateinit var mRefreshLayout: SwipeRefreshLayout
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initRetrofit()
initUi()
initViewModel()
}
private fun initViewModel() {
mViewModel.mTestData.observe(this) {
mRefreshLayout.isRefreshing = false
it?.let {
mAdapter.setList(it)
}
}
mViewModel.getTestData()
}
private fun initUi() {
setContentView(R.layout.activity_retrofit_coroutine)
val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = MyAdapter().apply {
mAdapter = this
}
mAdapter.setEmptyView(TextView(this).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
gravity = Gravity.CENTER
text = "Empty!"
setTextSize(TypedValue.COMPLEX_UNIT_SP, 20f)
})
mRefreshLayout = findViewById(R.id.swipe_refresh)
mRefreshLayout.setOnRefreshListener {
mViewModel.mError = true
mViewModel.getTestData()
}
}
private fun initRetrofit() {
val client = OkHttpClient.Builder()
.connectTimeout(3, TimeUnit.SECONDS)
.readTimeout(3, TimeUnit.SECONDS)
.writeTimeout(3, TimeUnit.SECONDS)
.addInterceptor { chain ->
// delay
Thread.sleep(1500L)
val api = chain.request().url().pathSegments().last()
val body = ResponseBody.create(MediaType.parse("application/json"), mockData(api))
okhttp3.Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(200)
.body(body)
.message("OK")
.build()
}.build()
Retrofit.Builder()
.baseUrl("https://xxx.com/")
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiService::class.java).also {
Api.mApiService = it
}
}
private fun mockData(api: String): String {
when (api) {
"0" -> return ""
"1" -> return """
{
"data": [
{
"name": "apple"
},
{
"name": "banana"
}
],
"code": 200,
"message": "OK"
}
""".trimIndent()
else -> return """
{
"data": [
{
"name": "pork"
},
{
"name": "beef"
}
],
"code": 200,
"message": "OK"
}
""".trimIndent()
}
}
}
private class MyAdapter :
BaseQuickAdapter<Detail, BaseViewHolder>(android.R.layout.simple_list_item_1, null) {
override fun convert(holder: BaseViewHolder, item: Detail) {
if (item.name == null) {
holder.setText(android.R.id.text1, "parse error!")
} else {
holder.setText(android.R.id.text1, item.name)
}
}
}
class MyViewModel : BaseViewModel() {
var mError = false
val mTestData = MutableLiveData<List<Detail>?>()
fun getTestData() {
// 如果不使用协程,那么可以:
// 1. 考虑开一个单独的线程,但是只能串行逐个请求;
// 2. 考虑开多个线程,并行请求,使用 CountDownLatch 判断请求是否完毕,
// 但是如果同时开 n 多个线程,显然是无法接受的
viewModelScope.launch {
// detailList 必须初始化为 null,当得到了真实的数据后才初始化为非 null
// 否则,如果发生了异常将导致在 UI 上清空了列表,但此时服务端并没有返回一个空列表
var detailList: MutableList<Detail>? = null
val deferredList = mutableListOf<Deferred<Boolean?>>()
(1..50).forEach { id ->
val deferred = myAsync {
LogUtils.d("get detail data, id: $id")
val detailResult = Api.mApiService.getDetailData(if (mError) 0 else id)
// 如果请求发生了异常(如超时),则不会走到这里,
detailResult.data?.let {
if (null == detailList) {
detailList = mutableListOf()
}
detailList!!.addAll(it)
}
}
deferredList.add(deferred)
}
deferredList.awaitAll()
LogUtils.d("get all detail data done")
// detailList 可能为 null,此时不会清空 UI 列表
mTestData.value = detailList
}
}
}
data class Detail(val num: Int, val name: String?)
其中 SimpleRetrofitCoroutineActivity.kt 添加了部分代码
interface ApiService {
@GET("api/kinds")
suspend fun getKindsData(): Result<List<Kind>?>
@GET("api/detail/{id}")
suspend fun getDetailData(@Path("id") id: Int): Result<List<Detail>?>
}
open class BaseViewModel : ViewModel() {
fun <S, T : MutableLiveData<S?>> myLaunch(
t: T, block: suspend CoroutineScope.() -> Result<S?>
) {
viewModelScope.launch {
try {
val result = block()
if (!result.message.isNullOrEmpty() && result.code != 200) {
ToastUtils.showShort(result.message)
}
t.value = result.data
} catch (e: Exception) {
// 任何异常都会走到这里,如超时、404等
t.value = null
LogUtils.d("myLaunch() error: $e")
ToastUtils.showShort(e.message)
}
}
}
fun <T> myAsync(block: suspend CoroutineScope.() -> T): Deferred<T?> {
return viewModelScope.async {
try {
block()
} catch (e: Exception) {
LogUtils.d("myAsync() error: $e")
ToastUtils.showShort(e.message)
}
null
}
}
}
网友评论