如何开始使用Kotlin Flows API Zip Operator
流量是Kotlin编程语言的一个基本功能。了解它可以帮助你轻松地执行一些网络调用。由于它是建立在coroutines之上的,所以它对管理主线程很有帮助。
在执行网络调用时,Kotlin Flows允许数据的异步排放,因此可以防止线程的阻塞。这将提高应用程序执行网络调用的速度。
Coroutines可以在一些指定的点暂停和恢复,防止线程阻塞。这就是流程的作用,可以防止任务被暂停,因为被暂停的任务可能非常关键。
本教程将介绍如何使用zip 流操作符来执行并行网络调用。我们要使用的API是elephants API。
先决条件
要继续学习本教程,读者需要以下条件。
- 在你的电脑上安装[Android Studio]。
- 理解如何使用 [
ViewBinding]。 - [Kotlin]编程语言的基础知识。
- Kotlin[Coroutines]的基本知识。
- Android Jetpack组件的基本知识,即
Livedata,ViewModel和Repository模式。
什么是Kotlin flow
流程是一个可以在一定时间内发出多个值的循环程序。它也可以被定义为Kotlin语言的一个功能,作为一个反应式编程框架。
Kotlin流量运算符
这些是决定一个流的排放会发生什么的操作符。
- filter -> 过滤一个流产生的值。
- map -> 将某个流的值映射到一个新的值。
- onEach -- 它不返回任何正式的值,而是返回之前的流。
- zip -> 是一个流操作符,它通过一个指定的函数将两个流集合的发射合并后发射一个单项。
Flow也有终端操作符,用于开始和终止流程。它们包括,collect,reduce, 和count 。
什么是Zip运算符
Zip操作符是一个流操作符,它通过一个指定的函数将两个流集合的排放合并后,排放出一个单项。理论讲完了,让我们跳进Android studio,动手操作一下💻。
第1步 - 开始使用Android Studio
打开你的Android Studio IDE并创建一个新项目。记住要选择Kotlin语言。
第2步 - 添加依赖性
在你的应用级buld.gardle 文件中,添加以下依赖项。
// Lifecycle
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0"
// Hilt
implementation "com.google.dagger:hilt-android:2.38.1"
kapt "com.google.dagger:hilt-compiler:2.38.1"
// Retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
// Coroutine Lifecycle Scopes
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.0"
//Glide for image loading
implementation 'com.github.bumptech.glide:glide:4.12.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0'
第3步 - 设计用户界面
我们将创建一个简单的界面,在显示大象的图像时包含一个ImageView ,在显示名字时包含一个TextView 。记住要使用一个RecyclerView 。
创建回收器行
进入你的layout 文件夹,创建一个新的布局资源文件,然后粘贴以下代码。
<androidx.cardview.widget.CardView
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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="10dp"
app:cardElevation="15dp"
android:layout_margin="10dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:elevation="10dp">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/image"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:src="@drawable/ic_launcher_background"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/textViewName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#000000"
android:text="TextView"
app:layout_constraintBottom_toTopOf="@+id/textViewStatus"
app:layout_constraintStart_toEndOf="@+id/image"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/textViewStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="16dp"
android:textSize="15sp"
android:textStyle="italic"
android:textColor="@color/black"
android:text="TextView"
app:layout_constraintBottom_toTopOf="@+id/textViewSpecies"
app:layout_constraintStart_toEndOf="@+id/image"
app:layout_constraintTop_toBottomOf="@+id/textViewName" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/textViewSpecies"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:textSize="15sp"
android:textStyle="italic"
android:textColor="@color/black"
android:text="TextView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/image"
app:layout_constraintTop_toBottomOf="@+id/textViewStatus" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
创建RecyclerView布局
添加下面的代码来创建一个RecyclerView 。你可以使用ConstraintLayout 作为你的根布局。
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/elephants_row"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
在我们设置好布局后,我们现在要使用Zip操作符从elephants API中获取数据。
第4步 - 模型类
在这一步,我们将创建一个模型类,包括大象的名字、种类、性别和图片(url)。我们将暂时忽略其他的属性。
import com.google.gson.annotations.SerializedName
class Elephants : ArrayList<Elephants.ElephantsItem>(){
data class ElephantsItem(
@SerializedName("image")
val image: String?,
@SerializedName("name")
val name: String?,
@SerializedName("sex")
val sex: String?,
@SerializedName("species")
val species: String?,
)
}
第5步 - 创建API服务类
这一步包括设计一个ApiService接口,以使用Retrofit库进行API调用。基本URL将是https://elephant-api.herokuapp.com/ ,端点将是elephants/ 。
interface ApiService {
@GET("elephants/")
fun getElephants(): Call<Elephants>
}
object ElephantsApi{
const val BASE_URL = "https://elephant-api.herokuapp.com/"
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
val retrofitService by lazy {
retrofit.create(ApiService::class.java)
}
}
第6步 - RecyclerView适配器类
这个类将负责将数据从API映射到我们的回收器视图。
class ElephantsAdapter : ListAdapter<Elephants.ElephantsItem, ElephantsAdapter.MyViewHolder>(DiffUtilCallback) {
object DiffUtilCallback : DiffUtil.ItemCallback<Elephants.ElephantsItem>() {
override fun areItemsTheSame(
oldItem: Elephants.ElephantsItem,
newItem: Elephants.ElephantsItem
): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(
oldItem: Elephants.ElephantsItem,
newItem: Elephants.ElephantsItem
): Boolean {
return oldItem.id == newItem.id
}
}
inner class MyViewHolder(private val binding: ElephantsRowBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(elephants: Elephants.ElephantsItem?) {
Glide.with(binding.image)
.load(elephants?.image)
.circleCrop()
.into(binding.image)
binding.textViewName.text = ("Name: ${elephants?.name}")
binding.textViewSpecies.text = ("Species: ${elephants?.species}")
binding.textViewStatus.text = ("Sex: ${elephants?.sex}")
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
return MyViewHolder(ElephantsRowBinding.inflate(LayoutInflater.from(parent.context),
parent,
false))
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val elephants = getItem(position)
holder.bind(elephants)
}
}
在下一步,我们将在ViewModels类上下功夫,该类将容纳异步并行网络调用的Zip操作逻辑。
第7步--ViewModel类
我们将创建两个ViewModel类,其中一个类将实现Zip操作逻辑,以允许从API中异步获取数据。一个ViewModel类将包含两个方法,即:getAnElephant() 和getMoreElephants() 。
另一个ViewModel类将有一个zip 操作符,允许使用这两个方法进行平行网络调用。
@HiltViewModel
class MainViewModel @Inject constructor(private val elephantsRepository: ElephantsRepository): ViewModel() {
private val _elephantResult = MutableLiveData<Resource<Elephant>>()
val elephantResult: LiveData<Resource<Elephant>> = _elephantResult
// First method
fun getAnElephant(){
viewModelScope.launch {
_elephantResult.value = Resource.Loading()
_elephantResult.value = elephantsRepository.getAnElephant()
}
}
//Second method
fun getMoreElephants(){
viewModelScope.launch {
_elephantResult.value = Resource.Loading()
_elephantResult.value = elephantsRepository.getAnElephant()
}
}
}
请注意我们是如何在
ElephantsViewModel类中使用zip操作符来结合两个方法(getAnElephant(),getMoreElephants())进行平行网络调用的。
class ElephantsViewModel (
private val elephantsApi: MainViewModel
) : ViewModel() {
private val elephants = MutableLiveData<Resource<List<Elephant>>>()
init {
fetchElephants()
}
private fun fetchElephants() {
viewModelScope.launch {
elephants.postValue(Resource.Loading(null))
elephantsApi.getAnElephant().zip(elephantsApi.getMoreElephants()) { elephantsFromApi, moreElephantsFromApi ->
val allElephantsFromApi = mutableListOf<Elephant>()
allElephantsFromApi.addAll(elephantsFromApi)
allElephantsFromApi.addAll(moreElephantsFromApi)
return@zip allElephantsFromApi
}
.flowOn(Dispatchers.Default)
.catch(e: Exception) {
Log.d(TAG, "fetchElephants: $e.message")
}
.collect {
elephants.value?.data
}
}
}
}

最后,当两个流量集合被Zip操作符压缩时,两个网络调用都是平行进行的,一旦两个网络调用完成,两个网络调用的结果将在一个回调中返回。因此,两个结果都是一次返回的。
总结
在本教程中,我们学习了如何使用Kotlin flow Zip操作符来执行API中的并行网络调用。我们还学习了如何使用Retrofit库来从API中获取大象。
我们还使用Zip操作符从API中获取数据,并在一个回调中返回结果,提高了远程访问的速度。