Retrofit结合Kotlin协程请求网络最佳实践

下面我们通过一个简单的示例,来看看Retrofit结合Kotlin协程请求网络是怎么开发的。

需求分析

第一步,产品需求

首先,产品小姐姐给到我们的需求是这样子的:

  1. 点击按钮,先请求每日一词接口,获取每日一词
  2. 点击按钮,请求翻译接口,将每日一词翻译

第二步,接口定义

因此这个需求我们需要有两个接口:

  1. 每日一词接口
  2. 翻译接口

具体的接口定义就不写了

第三步,UI设计

具体的布局如下:

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="请求每日一词接口" />

    <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="请求翻译接口" />

    <TextView
        android:id="@+id/tvTranslate"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="翻译结果" />

</LinearLayout>

预备工作

经过评估,我们使用最新版的Retrofit2.9.0,最新版原生支持协程,不需要额外依赖其他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"

详细的依赖可以参考附录给出的完整示例。

准备API接口

由于最新版的Retrofit2.9.0原生支持协程,接口定义直接写成挂起函数就可以了,返回类型直接写成网络数据返回类型即可。
然后我们在companion object域里面创建接口实现类:

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支持直接声明成挂起函数,并且函数直接返回网络返回数据
     */
    @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的动态代理生成TranslateService实现类
         */
        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!!
        }
    }

}

其中,返回数据类定义如下:

网络返回数据类

/**
 * 网络返回数据基类
 */
data class BaseResult<T>(val code: String, val msg: String, val data: T)

实现ViewModel

创建ViewModel,主要功能是详情Activity的操作,输入数据,请求网络,返回数据。
都有详细注释,直接看代码:

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

class TranslateViewModel : ViewModel() {

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

    /**
     * 最简单的无任何输入的请求
     * 通过扩展属性viewModelScope的launch函数开启协程访问网络并且返回
     */
    fun requestDailyWord() {
        viewModelScope.launch {
            val result = try {
                // 网络返回成功
                Result.success(TranslateService.getApi().requestDailyWord())
            } catch (e: Exception) {
                // 网络返回失败
                Result.failure(e)
            }
            // 发射数据,之后观察者就会收到数据
            // 注意这里是主线程,直接用setValue()即可
            dailyWordLiveData.value = result
        }
    }

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

    /**
     * 翻译结果输出LiveData
     * 通过LiveData的扩展函数switchMap()实现变换,在下游能够返回支持协程的CoroutineLiveData
     * CoroutineLiveData是通过Top-Level函数里面的liveData()方法来创建,在这里可以传入闭包,开启协程访问网络并且返回
     *
     * 注:
     * 1. LiveDataScope, ViewModelScope和lifecycleScope会自动处理自身的生命周期,在生命周期结束时会自动取消没有执行完成的协程任务
     * 2. 其中map和switchMap与RxJava中的map和flatMap有点类似
     */
    val translateResult: LiveData<Result<BaseResult<String>>> = inputLiveData.switchMap { input ->
        liveData {
            val result = try {
                // 网络返回成功
                Result.success(TranslateService.getApi().requestTranslateResult(input))
            } catch (e: Exception) {
                // 网络返回失败
                Result.failure(e)
            }
            // 发射数据,之后观察者就会收到数据
            emit(result)
        }
    }

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

}

实现Activity

创建Activity,主要功能是观察ViewModel的数据返回并展示,响应用户的点击行为,通知ViewModel去请求网络:

import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.nan.jetpackprimer.R
/**
 * 导入自动生成的视图注入类,在代码中可以直接使用控件
 */
import kotlinx.android.synthetic.main.activity_translate.*

/**
 * LiveData结合协程
 */
class TranslateActivity : AppCompatActivity() {

    /**
     * 通过ComponentActivity的扩展函数viewModels()方便获取ViewModel
     */
    private val viewModel: TranslateViewModel by viewModels()

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

        /**
         * 观察每日一词结果
         */
        viewModel.dailyWordLiveData.observe(this) { result ->
            val dailyWordResult = result.getOrNull()
            if (null == dailyWordResult) {
                tvDailyWord.text = "获取失败"
                return@observe
            }

            tvDailyWord.text = dailyWordResult.data
        }

        /**
         * 观察翻译结果
         */
        viewModel.translateResult.observe(this) { result ->
            val translateResult = result.getOrNull()
            if (null == translateResult) {
                tvTranslate.text = "翻译失败"
                return@observe
            }

            tvTranslate.text = translateResult.data
        }

        /**
         * 按钮点击监听
         * 获取每日一词
         */
        btnDailyWord.setOnClickListener {
            viewModel.requestDailyWord()
        }

        /**
         * 按钮点击监听
         * 获取EditText输入并且通知ViewModel开始翻译
         */
        btnTranslate.setOnClickListener {
            val input = tvDailyWord.text.toString().trim()
            viewModel.requestTranslate(input)
        }
    }

}

服务端部分代码

每日一词接口:

@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;
    }
}

详情可以参考附录给出的完整示例。

思考

通过这两个例子:

  1. 我们掌握了在ViewModel中最简单的如何开启协程访问网络(无参数的形式),以及如何响应UI层的输入然后开启协程访问网络最终又把返回发送给UI层(有参数的形式)
  2. 我们掌握了如何利用JetPack的ViewModel、LiveData、KTX等组件搭建项目架构
  3. 这个例子暂时不能体现使用协程的优势,后面读者可以自己尝试增加一些诸如链式请求、请求合并、异步处理请求结果等功能,通过同步的方式去写异步的代码,感受一下协程的强大。另外也可以用RxJava实现一遍,对比一下。

附录

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

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

服务端:
https://github.com/huannan/Architecture/tree/master/day31_okhttp/TestServer

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,999评论 4 368
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,102评论 1 302
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,709评论 0 250
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,439评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,846评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,881评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,062评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,783评论 0 205
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,517评论 1 248
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,762评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,241评论 1 264
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,568评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,236评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,145评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,941评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,965评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,802评论 2 275