如何开始使用Kotlin Flows API Zip运营商

312 阅读4分钟

如何开始使用Kotlin Flows API Zip Operator

流量是Kotlin编程语言的一个基本功能。了解它可以帮助你轻松地执行一些网络调用。由于它是建立在coroutines之上的,所以它对管理主线程很有帮助。

在执行网络调用时,Kotlin Flows允许数据的异步排放,因此可以防止线程的阻塞。这将提高应用程序执行网络调用的速度。

Coroutines可以在一些指定的点暂停和恢复,防止线程阻塞。这就是流程的作用,可以防止任务被暂停,因为被暂停的任务可能非常关键。

本教程将介绍如何使用zip 流操作符来执行并行网络调用。我们要使用的API是elephants API。

先决条件

要继续学习本教程,读者需要以下条件。

  • 在你的电脑上安装[Android Studio]。
  • 理解如何使用 [ViewBinding]。
  • [Kotlin]编程语言的基础知识。
  • Kotlin[Coroutines]的基本知识。
  • Android Jetpack组件的基本知识,即Livedata,ViewModelRepository 模式。

什么是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
                }
        }
    }
}

project demo

最后,当两个流量集合被Zip操作符压缩时,两个网络调用都是平行进行的,一旦两个网络调用完成,两个网络调用的结果将在一个回调中返回。因此,两个结果都是一次返回的。

总结

在本教程中,我们学习了如何使用Kotlin flow Zip操作符来执行API中的并行网络调用。我们还学习了如何使用Retrofit库来从API中获取大象。

我们还使用Zip操作符从API中获取数据,并在一个回调中返回结果,提高了远程访问的速度。