美文网首页
Android Retrofit+Coroutine 的使用

Android Retrofit+Coroutine 的使用

作者: 雁过留声_泪落无痕 | 来源:发表于2021-10-12 18:01 被阅读0次

一、纯 Retrofit 的使用

  1. 参考 Android 关于 Retrofit 回调的正确处理方式 - 简书 (jianshu.com)
    这里使用了 LiveData(因此不需要担心生命周期的问题),同时对返回结果的处理进行了封装,发起请求只需要一句话,得到结果后会自动通知到 LiveData
Api.mApiService.getTestData().enqueue(SimpleCallback(mTestData))

二、Retrofit+Coroutine 的使用

  1. Retrofit 要使用 2.6.0 及以上的版本,其自带 suspend 支持

  2. 使用到了 viewModelScope,不用自己定义 CoroutineScope

  3. 主要是需要对异常做处理,这里封装到了 BaseViewModel 里,也可以用 OkHttp 的 Interceptor 来处理

  4. 使用 Retrofit+Coroutine 的好处:

  • 方便异步代码的编写
  • 可以和 Flow 结合使用
  • 可以并行处理多个请求
  1. 简单使用

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?)
  1. 并行请求

经验证,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
        }
    }
}

相关文章

网友评论

      本文标题:Android Retrofit+Coroutine 的使用

      本文链接:https://www.haomeiwen.com/subject/xqedoltx.html