Retrofit結(jié)合Kotlin協(xié)程請(qǐng)求網(wǎng)絡(luò)最佳實(shí)踐

下面我們通過一個(gè)簡單的示例,來看看Retrofit結(jié)合Kotlin協(xié)程請(qǐng)求網(wǎng)絡(luò)是怎么開發(fā)的。

需求分析

第一步,產(chǎn)品需求

首先,產(chǎn)品小姐姐給到我們的需求是這樣子的:

  1. 點(diǎn)擊按鈕,先請(qǐng)求每日一詞接口,獲取每日一詞
  2. 點(diǎn)擊按鈕,請(qǐng)求翻譯接口,將每日一詞翻譯

第二步,接口定義

因此這個(gè)需求我們需要有兩個(gè)接口:

  1. 每日一詞接口
  2. 翻譯接口

具體的接口定義就不寫了

第三步,UI設(shè)計(jì)

具體的布局如下:

UI

布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <Button
        android:id="@+id/btnDailyWord"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="請(qǐng)求每日一詞接口" />

    <TextView
        android:id="@+id/tvDailyWord"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="每日一詞" />

    <Button
        android:id="@+id/btnTranslate"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="請(qǐng)求翻譯接口" />

    <TextView
        android:id="@+id/tvTranslate"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="翻譯結(jié)果" />

</LinearLayout>

預(yù)備工作

經(jīng)過評(píng)估,我們使用最新版的Retrofit2.9.0,最新版原生支持協(xié)程,不需要額外依賴其他Adapter庫:

    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
    implementation "com.squareup.okhttp3:logging-interceptor:4.9.0"

詳細(xì)的依賴可以參考附錄給出的完整示例。

準(zhǔn)備API接口

由于最新版的Retrofit2.9.0原生支持協(xié)程,接口定義直接寫成掛起函數(shù)就可以了,返回類型直接寫成網(wǎng)絡(luò)數(shù)據(jù)返回類型即可。
然后我們?cè)赾ompanion object域里面創(chuàng)建接口實(shí)現(xiàn)類:

import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Query

interface TranslateService {

    /**
     * 獲取每日一詞接口
     * 新版本的Retrofit支持直接聲明成掛起函數(shù),并且函數(shù)直接返回網(wǎng)絡(luò)返回?cái)?shù)據(jù)
     */
    @GET("dailyword")
    suspend fun requestDailyWord(): BaseResult<String>

    /**
     * 翻譯接口
     */
    @GET("translate")
    suspend fun requestTranslateResult(@Query("input") input: String): BaseResult<String>

    companion object {

        private const val BASE_URL = "http://172.16.47.80:8080/TestServer/"
        private var service: TranslateService? = null

        /**
         * 通過Retrofit的動(dòng)態(tài)代理生成TranslateService實(shí)現(xiàn)類
         */
        fun getApi(): TranslateService {
            if (null == service) {
                val httpLoggingInterceptor =
                    HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BASIC }

                val client = OkHttpClient.Builder()
                    .addInterceptor(httpLoggingInterceptor)
                    .build()

                val retrofit = Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .client(client)
                    .addConverterFactory(GsonConverterFactory.create())
                    .build()

                service = retrofit.create(TranslateService::class.java)
            }

            return service!!
        }
    }

}

其中,返回?cái)?shù)據(jù)類定義如下:

網(wǎng)絡(luò)返回?cái)?shù)據(jù)類

/**
 * 網(wǎng)絡(luò)返回?cái)?shù)據(jù)基類
 */
data class BaseResult<T>(val code: String, val msg: String, val data: T)

實(shí)現(xiàn)ViewModel

創(chuàng)建ViewModel,主要功能是詳情Activity的操作,輸入數(shù)據(jù),請(qǐng)求網(wǎng)絡(luò),返回?cái)?shù)據(jù)。
都有詳細(xì)注釋,直接看代碼:

import androidx.lifecycle.*
import kotlinx.coroutines.launch

class TranslateViewModel : ViewModel() {

    /**
     * 每日一詞LiveData
     */
    val dailyWordLiveData: MutableLiveData<Result<BaseResult<String>>> = MutableLiveData()

    /**
     * 最簡單的無任何輸入的請(qǐng)求
     * 通過擴(kuò)展屬性viewModelScope的launch函數(shù)開啟協(xié)程訪問網(wǎng)絡(luò)并且返回
     */
    fun requestDailyWord() {
        viewModelScope.launch {
            val result = try {
                // 網(wǎng)絡(luò)返回成功
                Result.success(TranslateService.getApi().requestDailyWord())
            } catch (e: Exception) {
                // 網(wǎng)絡(luò)返回失敗
                Result.failure(e)
            }
            // 發(fā)射數(shù)據(jù),之后觀察者就會(huì)收到數(shù)據(jù)
            // 注意這里是主線程,直接用setValue()即可
            dailyWordLiveData.value = result
        }
    }

    /**
     * 翻譯輸入LiveData
     */
    private val inputLiveData: MutableLiveData<String> = MutableLiveData()

    /**
     * 翻譯結(jié)果輸出LiveData
     * 通過LiveData的擴(kuò)展函數(shù)switchMap()實(shí)現(xiàn)變換,在下游能夠返回支持協(xié)程的CoroutineLiveData
     * CoroutineLiveData是通過Top-Level函數(shù)里面的liveData()方法來創(chuàng)建,在這里可以傳入閉包,開啟協(xié)程訪問網(wǎng)絡(luò)并且返回
     *
     * 注:
     * 1. LiveDataScope, ViewModelScope和lifecycleScope會(huì)自動(dòng)處理自身的生命周期,在生命周期結(jié)束時(shí)會(huì)自動(dòng)取消沒有執(zhí)行完成的協(xié)程任務(wù)
     * 2. 其中map和switchMap與RxJava中的map和flatMap有點(diǎn)類似
     */
    val translateResult: LiveData<Result<BaseResult<String>>> = inputLiveData.switchMap { input ->
        liveData {
            val result = try {
                // 網(wǎng)絡(luò)返回成功
                Result.success(TranslateService.getApi().requestTranslateResult(input))
            } catch (e: Exception) {
                // 網(wǎng)絡(luò)返回失敗
                Result.failure(e)
            }
            // 發(fā)射數(shù)據(jù),之后觀察者就會(huì)收到數(shù)據(jù)
            emit(result)
        }
    }

    /**
     * 開始翻譯
     */
    fun requestTranslate(input: String) {
        inputLiveData.value = input
    }

}

實(shí)現(xiàn)Activity

創(chuàng)建Activity,主要功能是觀察ViewModel的數(shù)據(jù)返回并展示,響應(yīng)用戶的點(diǎn)擊行為,通知ViewModel去請(qǐng)求網(wǎng)絡(luò):

import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.nan.jetpackprimer.R
/**
 * 導(dǎo)入自動(dòng)生成的視圖注入類,在代碼中可以直接使用控件
 */
import kotlinx.android.synthetic.main.activity_translate.*

/**
 * LiveData結(jié)合協(xié)程
 */
class TranslateActivity : AppCompatActivity() {

    /**
     * 通過ComponentActivity的擴(kuò)展函數(shù)viewModels()方便獲取ViewModel
     */
    private val viewModel: TranslateViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_translate)

        /**
         * 觀察每日一詞結(jié)果
         */
        viewModel.dailyWordLiveData.observe(this) { result ->
            val dailyWordResult = result.getOrNull()
            if (null == dailyWordResult) {
                tvDailyWord.text = "獲取失敗"
                return@observe
            }

            tvDailyWord.text = dailyWordResult.data
        }

        /**
         * 觀察翻譯結(jié)果
         */
        viewModel.translateResult.observe(this) { result ->
            val translateResult = result.getOrNull()
            if (null == translateResult) {
                tvTranslate.text = "翻譯失敗"
                return@observe
            }

            tvTranslate.text = translateResult.data
        }

        /**
         * 按鈕點(diǎn)擊監(jiān)聽
         * 獲取每日一詞
         */
        btnDailyWord.setOnClickListener {
            viewModel.requestDailyWord()
        }

        /**
         * 按鈕點(diǎn)擊監(jiān)聽
         * 獲取EditText輸入并且通知ViewModel開始翻譯
         */
        btnTranslate.setOnClickListener {
            val input = tvDailyWord.text.toString().trim()
            viewModel.requestTranslate(input)
        }
    }

}

服務(wù)端部分代碼

每日一詞接口:

@WebServlet("/dailyword")
public class DailyWordServlet extends BaseJsonServlet {
    @Override
    protected ResponseEntity onHandle(HttpServletRequest req, HttpServletResponse resp) throws Exception {
        ResponseEntity responseEntity = new ResponseEntity();
        responseEntity.code = ResponseCode.OK;
        responseEntity.msg = "成功";
        responseEntity.data = "每天都是好心情";
        return responseEntity;
    }
}

翻譯接口:

@WebServlet("/translate")
public class TranslateServlet extends BaseJsonServlet {
    @Override
    protected ResponseEntity onHandle(HttpServletRequest req, HttpServletResponse resp) throws Exception {
        String input = req.getParameter("input");
        String translateResult = input + " -> " + "Good mood every day";

        ResponseEntity responseEntity = new ResponseEntity();
        responseEntity.code = ResponseCode.OK;
        responseEntity.msg = "Good mood every day";
        responseEntity.data = translateResult;

        return responseEntity;
    }
}

詳情可以參考附錄給出的完整示例。

思考

通過這兩個(gè)例子:

  1. 我們掌握了在ViewModel中最簡單的如何開啟協(xié)程訪問網(wǎng)絡(luò)(無參數(shù)的形式),以及如何響應(yīng)UI層的輸入然后開啟協(xié)程訪問網(wǎng)絡(luò)最終又把返回發(fā)送給UI層(有參數(shù)的形式)
  2. 我們掌握了如何利用JetPack的ViewModel、LiveData、KTX等組件搭建項(xiàng)目架構(gòu)
  3. 這個(gè)例子暫時(shí)不能體現(xiàn)使用協(xié)程的優(yōu)勢,后面讀者可以自己嘗試增加一些諸如鏈?zhǔn)秸?qǐng)求、請(qǐng)求合并、異步處理請(qǐng)求結(jié)果等功能,通過同步的方式去寫異步的代碼,感受一下協(xié)程的強(qiáng)大。另外也可以用RxJava實(shí)現(xiàn)一遍,對(duì)比一下。

附錄

最后,附上完整代碼地址:

客戶端:
https://github.com/huannan/JetpackPrimer/tree/master/app/src/main/java/com/nan/jetpackprimer/livedata/simple4

服務(wù)端:
https://github.com/huannan/Architecture/tree/master/day31_okhttp/TestServer

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容