1. 疑惑
對于 Activity,發(fā)起網(wǎng)絡(luò)請求后得到一個 null,到底應(yīng)不應(yīng)該刷新列表?
或者說,應(yīng)該如何處理 Retrofit 的回調(diào)才顯得合理并且優(yōu)雅呢?
2. 結(jié)論
// 總的原則:
// 1. 返回 null 認(rèn)為是非正常情況,不刷新列表
// 2. 返回非 null 才刷新列表,如果是用戶在 PC 上清空了數(shù)據(jù)(如瀏覽記錄),
// 手機上刷新時需要服務(wù)器返回一個空列表(size 為 0)而不是 null
// 3. ViewModel 中任何 case 都要轉(zhuǎn)調(diào)到 Activity 里去,否則可能出現(xiàn)下拉刷新無法結(jié)束的問題
3. 代碼
這里進行了簡單封裝,參見 SimpleCallback,只需要一句話即可發(fā)起請求并將結(jié)果通過 LiveData 傳遞出去。
Api.mApiService.getTestData().enqueue(SimpleCallback(mTestData))
activity_retrofit_callback.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/linearLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="@android:layout/simple_list_item_1" />
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<Button
android:id="@+id/normal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="normal"
android:textAllCaps="false" />
<Button
android:id="@+id/empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_weight="1"
android:text="empty"
android:textAllCaps="false" />
<Button
android:id="@+id/error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_weight="1"
android:text="error"
android:textAllCaps="false" />
<Button
android:id="@+id/timeout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_weight="1"
android:text="timeout"
android:textAllCaps="false" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
RetrofitCallbackActivity.kt
class RetrofitCallbackActivity : BaseActivity() {
private lateinit var mAdapter: MyAdapter
private val mViewModel: MyViewModel by viewModels()
private var mDataType = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initRetrofit()
initUi()
initViewModel()
}
private fun initViewModel() {
mViewModel.mTestData.observe(this) {
// 總的原則:
// 1. 返回 null 認(rèn)為是非正常情況,不刷新列表
// 2. 返回非 null 才刷新列表,如果是用戶在 PC 上清空了數(shù)據(jù)(如瀏覽記錄),
// 手機上刷新時需要服務(wù)器返回一個空列表(size 為 0)而不是 null
it?.let {
// 針對本身列表為空的情況,要求在 ViewModel 中返回空列表而非 null
mAdapter.setList(it)
}
// TODO 在這里結(jié)束下拉刷新
// 這就要求 ViewModel 在所有 case 下都必須調(diào)用 MutableLiveData#setValue(),
// 否則會出現(xiàn)下拉狀態(tài)無法結(jié)束的問題
}
mViewModel.mResultTestData.observe(this) {
ToastUtils.showShort("code: ${it.code}")
}
}
private fun initUi() {
setContentView(R.layout.activity_retrofit_callback)
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)
})
findViewById<Button>(R.id.normal).setOnClickListener {
mDataType = 0
mViewModel.getTestData()
}
findViewById<Button>(R.id.empty).setOnClickListener {
mDataType = 1
mViewModel.getTestData()
}
findViewById<Button>(R.id.error).setOnClickListener {
mDataType = 2
mViewModel.getResultTestData()
}
findViewById<Button>(R.id.timeout).setOnClickListener {
mDataType = 3
mViewModel.getTestData()
}
}
private fun initRetrofit() {
val client = OkHttpClient.Builder()
.connectTimeout(3, TimeUnit.SECONDS)
.readTimeout(3, TimeUnit.SECONDS)
.writeTimeout(3, TimeUnit.SECONDS)
.addInterceptor { chain ->
// error
if (mDataType == 3) {
throw SocketTimeoutException("timeout occurred.")
}
val body = ResponseBody.create(MediaType.parse("application/json"), mockData())
okhttp3.Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(200)
.body(body)
.message("OK")
.build()
}.build()
Retrofit.Builder()
// 注釋掉 interceptor 后:
// 1. 模擬 failed to connect xxx, 走 onFailure()
// .baseUrl("https://xxx.com/")
// 2. 模擬 404, 走 onResponse()
// 但是 isSuccessful 為 false 且 response.body() 為空且 response.errorBody() 不為空
.baseUrl("https://www.baidu.com/")
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiService::class.java).also {
Api.mApiService = it
}
}
private fun mockData(): String {
when (mDataType) {
// normal
0 -> {
return """
{
"code": 200,
"data": [
{"name": "this is 0"},
{"name": "this is 1"},
{"name-error": "this is 2"},
{"name": "this is 3"},
{"name": "this is 4"}
],
"message": "success"
}
""".trimIndent()
}
// empty
1 -> {
return """
{
"code": 200,
"data": [],
"message": "success"
}
""".trimIndent()
}
// error
2 -> {
return """
{
"code": 500,
"d": [],
"message": "success"
}
""".trimIndent()
}
}
return ""
}
}
class MyViewModel : ViewModel() {
val mTestData = MutableLiveData<List<Item>?>()
fun getTestData() {
Api.mApiService.getTestData().enqueue(SimpleCallback(mTestData))
}
val mResultTestData = MutableLiveData<Result<List<Item>?>>()
fun getResultTestData() {
Api.mApiService.getTestData().enqueue(ResultCallBack(mResultTestData))
}
}
private class MyAdapter :
BaseQuickAdapter<Item, BaseViewHolder>(android.R.layout.simple_list_item_1, null) {
override fun convert(holder: BaseViewHolder, item: Item) {
if (item.name == null) {
holder.setText(android.R.id.text1, "parse error!")
} else {
holder.setText(android.R.id.text1, item.name)
}
}
}
/**
* 通常 UI 界面只需要 data 部分
*/
class SimpleCallback<T>(private val mLiveData: MutableLiveData<T?>) : Callback<Result<T?>> {
override fun onResponse(call: Call<Result<T?>>, response: Response<Result<T?>>) {
mLiveData.value = Api.handleResponse(response)
}
override fun onFailure(call: Call<Result<T?>>, t: Throwable) {
mLiveData.value = null
ToastUtils.showShort(t.message)
}
}
/**
* 如果確實需要 code 和 message 的,可以用該類
*/
class ResultCallBack<T>(private val mLiveData: MutableLiveData<Result<T?>>) : Callback<Result<T?>> {
override fun onResponse(call: Call<Result<T?>>, response: Response<Result<T?>>) {
val result = response.body()
if (result != null) {
mLiveData.value = result
} else {
mLiveData.value = Result(response.message(), response.code(), null)
}
}
override fun onFailure(call: Call<Result<T?>>, t: Throwable) {
mLiveData.value = Result(t.message, -1, null)
ToastUtils.showShort(t.message)
}
}
object Api {
lateinit var mApiService: ApiService
/**
* 根據(jù) response 獲取 Result 中的 data 數(shù)據(jù)
*/
fun <T> handleResponse(response: Response<Result<T?>>): T? {
val result = response.body()
if (result != null) {
// 成功: 如 200,即使成功的情況下 data 也可能為 null:
// 1. 字段解析導(dǎo)致為 null,這是 GsonConverterFactory 解析 json 時導(dǎo)致的
// 2. 服務(wù)端沒直接返回 null,理論上沒數(shù)據(jù)應(yīng)該返回空列表的,
// 但是服務(wù)端硬是返回了 null,這里強制要求服務(wù)端返回空列表,
// 否則客戶端不會認(rèn)為是數(shù)據(jù)清空(如歷史記錄)而會認(rèn)為是發(fā)生了錯誤。
// 同服務(wù)約定 message 不為空時表示需要提示用戶, 也可以約定用 code 來判斷
if (result.code != 200 && !result.message.isNullOrEmpty()) {
ToastUtils.showShort(result.message)
}
// data 可能為 null, 這種情況下會不更新列表
return result.data
} else {
// 失敗: 如 404/500, 這種情況下不更新列表
// 另外:204/205 的情況下,result 也為空
ToastUtils.showShort(response.message())
return null
}
}
}
/**
* 定義實體類 T 時,其中的非基本數(shù)據(jù)類型一定要定義為可空的,
* 因為 GsonConverterFactory 解析 json 時完全有可能對某項數(shù)據(jù)賦值為空,
* 如果又定義為了非空,那么使用的時候就可能發(fā)生空指針。
*/
data class Result<T>(val message: String?, val code: Int, val data: T?)
/**
* 內(nèi)部字段必須定義為可空!
*/
data class Item(val name: String?)
interface ApiService {
@GET("api/test")
fun getTestData(): Call<Result<List<Item>?>>
}
4. 效果

效果.gif
5. 最后

鼓掌.png