下面我們通過一個(gè)簡單的示例,來看看Retrofit結(jié)合Kotlin協(xié)程請(qǐng)求網(wǎng)絡(luò)是怎么開發(fā)的。
需求分析
第一步,產(chǎn)品需求
首先,產(chǎn)品小姐姐給到我們的需求是這樣子的:
- 點(diǎn)擊按鈕,先請(qǐng)求每日一詞接口,獲取每日一詞
- 點(diǎn)擊按鈕,請(qǐng)求翻譯接口,將每日一詞翻譯
第二步,接口定義
因此這個(gè)需求我們需要有兩個(gè)接口:
- 每日一詞接口
- 翻譯接口
具體的接口定義就不寫了
第三步,UI設(shè)計(jì)
具體的布局如下:

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