随着 Coroutines 和 Flow 的普及,Kotlin 发布了 Stateflow API 作为 v1.3.6 的一部分。
在这篇文章中,我们将讨论什么是 StateFlow API 以及它是如何工作的。我们将涵盖,
- 什么是状态流?
- 它与 ConflatedBroadcastChannel 有何不同?
- 如何在 Android 中使用 StateFlow?
那么,让我们开始吧。
什么是状态流?
StateFlow 就像是使用 Kotlin Flow 来管理和表示应用程序中的状态的一种方式。
StateFlow 是一种接口类型,它只是一个只读的并且总是返回更新后的值。为了接收更新的值,我们只需从实现的 Flow 中收集值。
StateFlow 仅在值已更新且不返回相同值时返回。简单来说,考虑两个值x和y,其中x是最初发出的值,y是要发出的值。
StateFlow 确保,如果(x == y)什么都不做,但 if (x !=y)then 只发出新值,即在这种情况下是y 。
注意:常规流是冷的,但 StateFlow 是热的。这意味着常规 Flow 没有最后一个值的概念,只有在收集到它时才会激活,而 StateFlow 有最后一个值的概念,一旦我们创建它就会激活。
我们可以将 StateFlow 视为 RxJava 中的Subject,而将常规 Flow 视为 RxJava 中的普通 observables。
它与 ConflatedBroadcastChannel 有何不同?
在协程中,ConflatedBroadcastChannel用于发出最新的值,并且由多个不同的来源观察到。
StateFlow 几乎相同,它还发出要从流中收集的最新值。它们之间几乎没有显着差异,因为,
- 当从 StateFlow 收集值时,我们总是得到最新的值,因为它总是有一个值,使其读取安全,因为在任何时间点 StateFlow 都会有一个值,不像 ConflatedBroadcastChannel 我们首先需要创建一个没有值的实例并且这就是为什么它不是读取安全的。
- ConflatedBroadcastChannel 实现了 Channel API 来工作,但在 StateFlow API 中,不使用 Channel API,这就是执行速度更快的原因。
- StateFlow 使用运算符distinctUntilChanged的概念,因此它只返回更新的值,因为它过滤掉了相同的值。
如何在 Android 中使用 StateFlow?
我们将通过一个例子来讨论这个问题,从 API 中获取用户列表。让我们分步讨论这个问题。
步骤1:
首先,让我们设置 gradle 文件,我们将在应用程序的 build.gradle 文件中添加协程依赖项,例如,
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6"
步骤2:
我们将使用视图模型创建一个活动。我们将创建 MainActivity 和 MainViewModel。我们还将创建 ApiHelper 类,该类将有一个返回 Flow 的函数,称为 getUsers()。最后,为了管理 API 调用的状态,我们将创建一个资源文件,如下所示,
data class Resource<out T>(val status: Status, val data: T?, val message: String?) {
companion object {
fun <T> success(data: T?): Resource<T> {
return Resource(Status.SUCCESS, data, null)
}
fun <T> error(msg: String, data: T?): Resource<T> {
return Resource(Status.ERROR, data, msg)
}
fun <T> loading(data: T?): Resource<T> {
return Resource(Status.LOADING, data, null)
}
}
}
和状态看起来像,
enum class Status {
SUCCESS,
ERROR,
LOADING
}
步骤3:
现在,我们将设计 MainActivity 的 XML,其中包含一个 recyclerView 和进度条。它看起来像,
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
最后,我们将为 recyclerView 创建一个 Adapter 类。我们将创建一个名为 ApiUserAdapter 的文件,它看起来像,
class ApiUserAdapter(
private val users: ArrayList<ApiUser>
) : RecyclerView.Adapter<ApiUserAdapter.DataViewHolder>() {
class DataViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(user: ApiUser) {
itemView.textViewUserName.text = user.name
itemView.textViewUserEmail.text = user.email
Glide.with(itemView.imageViewAvatar.context)
.load(user.avatar)
.into(itemView.imageViewAvatar)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
DataViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.item_layout, parent,
false
)
)
override fun getItemCount(): Int = users.size
override fun onBindViewHolder(holder: DataViewHolder, position: Int) =
holder.bind(users[position])
fun addData(list: List<ApiUser>) {
users.addAll(list)
}
}
在这里, item_layout.xml 文件看起来像,
<androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="60dp">
<ImageView
android:id="@+id/imageViewAvatar"
android:layout_width="60dp"
android:layout_height="0dp"
android:padding="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/textViewUserName"
style="@style/TextAppearance.AppCompat.Large"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/imageViewAvatar"
app:layout_constraintTop_toTopOf="parent"
tools:text="MindOrks" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/textViewUserEmail"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/textViewUserName"
app:layout_constraintTop_toBottomOf="@+id/textViewUserName"
tools:text="MindOrks" />
</androidx.constraintlayout.widget.ConstraintLayout>
现在,让我们设置 Activity 文件。
步骤4:
让我们更新 MainActivity 中的代码,例如,
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
private lateinit var adapter: ApiUserAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setupUI()
setupViewModel()
setupObserver()
}
}
在这里你可以看到我们有两个变量 viewModel 和 Adapter 类型分别为 MainViewModel 和 ApiUserAdapter。我们在 onCreate() 中也有三个函数调用。
首先,让我们更新 setupUI(),这里将更新代码,例如,
private fun setupUI() {
recyclerView.layoutManager = LinearLayoutManager(this)
adapter =
ApiUserAdapter(
arrayListOf()
)
recyclerView.addItemDecoration(
DividerItemDecoration(
recyclerView.context,
(recyclerView.layoutManager as LinearLayoutManager).orientation
)
)
recyclerView.adapter = adapter
}
现在,我们将在 setupViewModel 函数中设置 viewModel,例如,
private fun setupViewModel() {
viewModel = ViewModelProviders.of(
this,
ViewModelFactory(
ApiHelper()
)
).get(MainViewModel::class.java)
}
这里,MainViewModel 通过 ViewModelFactory 将 ApiHelper 作为构造函数中的参数。
步骤5:
让我们更新 ViewModel 以从 API 获取数据。
@ExperimentalCoroutinesApi
class MainViewModel(
private val apiHelper: ApiHelper
) : ViewModel() {
@ExperimentalCoroutinesApi
private val users = MutableStateFlow<Resource<List<ApiUser>>>(Resource.loading(null))
@ExperimentalCoroutinesApi
fun fetchUsers() {
viewModelScope.launch {
apiHelper.getUsers()
.catch { e ->
users.value = (Resource.error(e.toString(), null))
}
.collect {
users.value = (Resource.success(it))
}
}
}
@ExperimentalCoroutinesApi
fun getUsers(): StateFlow<Resource<List<ApiUser>>> {
return users
}
}
在这里,我们首先创建了一个 MutableStateFlow 类型的变量 user。MutableStateFlowalways 总是取一个默认值,所以这里是加载状态。
然后我们可以,在 fetchUser 函数中进行 API 调用。在 catch 块中,我们将状态更新为错误,在 collect 中,我们将状态值更新为成功并将成功的响应传递给它。
MutableStateFlow 具有读写属性,但 StateFlow 是只读的。因此,我们通过 MutableStateFlow 更新值,但始终通过 StateFlow 收集值。
这里成功的响应是用户列表。现在我们看到了StateFlow 类型的usersof函数。getUsers
步骤6:
现在,我们将调用 Activity 中的 fetchUser 函数来进行 Api 调用,例如,
viewModel.fetchUsers()
现在,观察 setupObservers 内部的变化,我们将从 getUsers 函数中收集数据,例如,
private fun setupObserver() {
lifecycleScope.launch {
val value = viewModel.getUsers()
value.collect {
when (it.status) {
Status.SUCCESS -> {
progressBar.visibility = View.GONE
it.data?.let { users -> renderList(users) }
recyclerView.visibility = View.VISIBLE
}
Status.LOADING -> {
progressBar.visibility = View.VISIBLE
recyclerView.visibility = View.GONE
}
Status.ERROR -> {
//Handle Error
progressBar.visibility = View.GONE
Toast.makeText(
this@SingleNetworkCallActivity,
it.message,
Toast.LENGTH_LONG
).show()
}
}
}
}
}
在这里,在 collect 中,我们获取 Resource 类型的数据,并根据状态,我们将在 renderList() 中设置列表,如下所示,
private fun renderList(users: List<ApiUser>) {
adapter.addData(users)
adapter.notifyDataSetChanged()
}
现在,当我们运行应用程序时,我们会得到所需的输出。
这就是我们如何使用 StateFlow 来管理Android 应用程序中的状态。