本文已参与「新人创作礼」活动,一起开启掘金创作之路。
大家周末愉快,今天我们来学点轻松的知识。用协程 + Retrofit 写一个 Demo,进行一次简单的网络请求。
一、效果图
最终效果图如下:
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,感兴趣的读者可以自行尝试。