Kotlin 协程 (七) ——— 协程 + Retrofit 实战

1,689 阅读5分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

大家周末愉快,今天我们来学点轻松的知识。用协程 + Retrofit 写一个 Demo,进行一次简单的网络请求。

一、效果图

最终效果图如下:

Retrofit + 协程

UI 很简单,一个 REQUEST 按钮和一个简单的文本。点击按钮发起网络请求,这里我调用的是鸿洋大神提供的 Wanandroid API,获取其首页文章数据。获取到数据后,在 TextView 上显示其中第一篇文章的标题。

这个 Demo 采用了 Coroutine + Retrofit + ViewModel + LiveData + DataBinding,并且使用了 MVVM 架构,可以说是麻雀虽小,五脏俱全。

协程 + Retrofit 的方式使得网络请求的代码非常简洁,MVVM 的架构使得其拓展性很强,接下来我们就来一步步地完成这个 Demo。

注:简单说一下 MVC、MVP、MVVM 的区别,他们是三种不断演进的架构:

  • MVC 的特点是 Model 和 View 层职责分离;Model、View、Controller 三者之间可以相互交流。
  • MVP 的特点是严格规定 Model 和 View 层不能直接交流,必须通过 Presenter 层间接交流,并且规定 Model 和 View 与 Presenter 之间必须通过接口交流,以增强其复用性。
  • MVVM 的特点是 Model 层和 View 层不能直接交流,并且 Model 层和 View 层通过与 ViewModel 双向绑定的方式完成交流,减少了 MVP 架构中的接口,绑定的方式利用了观察者模式。

二、准备工作

用 Android Studio 新建一个项目,我将其命名为 CoroutineDemo。

然后需要做一些准备条件,首先是在 AndroidManifest 中添加网络请求权限:

<uses-permission android:name="android.permission.INTERNET" />

然后在 app/build.gradle 中导入相关依赖库:

// LiveData
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1"
implementation 'androidx.fragment:fragment-ktx:1.4.1'
// Coroutine
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0'
// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.7.2'
implementation 'com.squareup.retrofit2:converter-gson:2.7.2'

这里我们导入了 LiveData、ViewModel、Coroutine、Retrofit 相关依赖库。其中,引入 Retrofit 库时,还添加了 converter-gson 库,这个库用来解析接口返回的 json 数据。

接下来仍然是在 app/build.gradle 中,开启 DataBinding 和 Java8 支持:

android {
    ...
    dataBinding {
        enabled true
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}

OK,这样准备工作就算完成了。

三、View 层:编写布局

布局文件比较简单,一个 Button 加一个 TextView:

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="16dp"
    tools:context=".MainActivity">
    <Button
        android:id="@+id/btnRequest"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Request"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
    <TextView
        android:id="@+id/tvResult"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

这里使用了约束布局,Button 位于顶部居中的位置,TextView 位于中间位置。

四、Model 层:编写 Retrofit API

Retrofit 是一个封装了 OkHttp 的库,它通过注解的方式来配置网络请求,内部通过动态代理的方式读取注解中的配置,生成一个网络请求的代理类,达到了封装网络请求细节的目的,非常好用。这里就不细讲了:

interface ArticleApi {
    @GET("article/list/{page}/json")
    suspend fun getHomeArticles(@Path("page") page: Int): Response
}

需要注意的是,我们给这个接口方法添加了 suspend 关键字,这样的话,我们就可以在协程中直接调用此 API 来获得其返回数据。这是 Retrofit 内部添加的协程支持。

这里我们声明了一个 getHomeArticles() 方法,调用的是 WanAndroid 开放 API 中的 "article/list/{page}/json" 接口,page 由调用处传递进来,请求方式是 GET。

其中,Response 是接口返回的数据类型,这个类需要根据返回数据的格式生成:

data class Response(val data: Articles)

data class Articles(
    val curPage: Int = 0,
    val datas: MutableList<ArticleBean> = mutableListOf(),
    val offset: Int = 0,
    val over: Boolean = false,
    val pageCount: Int = 0,
    val size: Int = 0,
    val total: Int = 0
)

data class ArticleBean(
    val apkLink: String,
    val audit: Int,
    val author: String,
    val canEdit: Boolean,
    val chapterId: Int,
    val chapterName: String,
    var collect: Boolean,
    val courseId: Int,
    val desc: String,
    val descMd: String,
    val envelopePic: String,
    val fresh: Boolean,
    val id: Int,
    val link: String,
    val niceDate: String,
    val niceShareDate: String,
    val origin: String,
    val prefix: String,
    val projectLink: String,
    val publishTime: Long,
    val selfVisible: Int,
    val shareDate: Long,
    val shareUser: String,
    val superChapterId: Int,
    val superChapterName: String,
    val tags: List<Tag>,
    val title: String,
    val type: Int,
    val userId: Int,
    val visible: Int,
    val zan: Int
)

data class Tag(
    val name: String = "",
    val url: String = ""
)

返回数据示例:www.wanandroid.com/article/lis…

五、Model 层:编写仓库类,获取数据

接下来我们编写一个 ArticleRepository 类,这个类负责调用 Retrofit API,获取返回数据:

class ArticleRepository {
    private val retrofit = Retrofit.Builder()
        .baseUrl("https://www.wanandroid.com/")
        .client(OkHttpClient.Builder().addInterceptor {
            it.proceed(it.request()).apply {
                Log.d("~~~", "request ${code()}")
            }
        }.build())
        // 将返回的数据转换为String
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    suspend fun getArticle(): String {
        val articleApi = retrofit.create(ArticleApi::class.java)
        val response = articleApi.getHomeArticles(0)
        return response.data.datas.first().title
    }
}

在这个类中,我们声明了一个 retrofit 对象,它的 baseUrl 设置为 "https://www.wanandroid.com/",client 中添加了一个拦截器,打印了一下 Http 请求返回的 code,正常情况下这个 code 应该是 200。再通过 GsonConverterFactory 将返回的 json 数据自动解析成对象。

然后我们还写了一个 getArticle() 函数,这个函数调用了我们刚才写的 ArticleApi,获取到返回结果后,返回 Response 中的第一篇文章的标题。

六、ViewModel 层:编写 MainViewModel,并通过 LiveData 绑定 Model 层和 ViewModel 层

按照 MVVM 的架构,这个 ArticleRepository 类应该在 ViewModel 中使用:

class MainViewModel : ViewModel() {
    private val articleRepository = ArticleRepository()
    val articlesLiveData = MutableLiveData<String>()

    fun getArticle() {
        viewModelScope.launch {
            articlesLiveData.value = articleRepository.getArticle()
        }
    }
}

在 ViewModel 中,我们声明了一个 articlesLiveData 变量,这个变量用于存储 ArticleRepository 返回的数据结果。

getArticle() 方法中,通过 viewModelScope 启动一个协程,调用 articleRepository.getArticle() 函数发起网络请求,获取到返回结果后,将其设置到 articlesLiveData 中。

七、通过 DataBinding 绑定 View 和 ViewModel

在布局中,通过 DataBinding 将 ViewModel 绑定到 UI 控件上:

<?xml version="1.0" encoding="utf-8"?>
<layout 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">

    <data>

        <variable
            name="mainViewModel"
            type="com.example.coroutine.viewModel.MainViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        ...>

        <Button
            ...
            android:onClick="@{view -> mainViewModel.getArticle()}"
            />

        <TextView
            ...
            android:text="@{mainViewModel.articlesLiveData}"
            />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

可以看到,我们将 mainViewModel.getArticle() 方法绑定到了 Button 的点击事件中,将 mainViewModel.articlesLiveData 数据绑定到了 TextView 上。

八、View 层:MainActivity 中使用 DataBinding

编辑 MainActivity:

class MainActivity : AppCompatActivity() {
    private val mainViewModel: MainViewModel by viewModels()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
        binding.lifecycleOwner = this
        binding.mainViewModel = mainViewModel
    }
}

在 MainActivity 中,声明了 mainViewModel,并将其设置到了 DataBinding 中。

这样,就完成了我们一开始的效果图。

附:源码

源码已上传至 github:github.com/wkxjc/Corou…

这个 Demo 是按照标准 MVVM 架构搭建的,可以很轻松地实现拓展。只要将获取到的数据展示到 RecyclerView 中,添加下拉刷新、上拉加载等功能,再改改 UI 样式,就可以生成一个很漂亮的 app,感兴趣的读者可以自行尝试。