Jetpack-Compose 学习笔记(五)—— State 状态是个啥?又是新概念?(上)

简介: Jetpack-Compose 学习笔记(五)—— State 状态是个啥?又是新概念?(上)

系列第五篇,进入 Compose 中有关 State 状态的学习。

前面几篇笔记讲了那么多内容,都是基于静态界面的展示来说的,即给我一个不变的数据,然后将它展示出来。如何在 Compose 中构建一个随数据而变化的动态界面呢?相信看完这篇就知道了。


1、基本知识


众所周知,Compose 彻底舍弃了 xml 文件,我们需要像 Flutter 一样完全用代码去进行界面的编码,这样做很容易会导致一个问题:界面和数据处理逻辑耦合,从而导致 Activity 中代码臃肿且维护性下降。

虽然提出了许多架构思想,如 MVC、MVP、MVVM 等,一定程度上解耦了界面与数据处理逻辑,但是架构本身就具有一定的复杂性,且对于后续维护成本也相对较高,所以 Compose 一开始就将界面与数据分开来,分别称之为 组合State 状态

State 状态:官方文档上说 State 状态是指可以随时间变化的任何值。例如,它可能是存储在 Room 数据库中的值、类的变量,加速度计的当前读数等。 怎么理解这个概念呢?我觉得可以简单理解为:我们要展示给用户看的数据。例如,一个商品的展示页面,其实就是根据数据的不同来展示不同的状态,数据正常、数据错误、空数据等不同的数据就是代表了不同的 State 状态。

组合:按照文档上的意思我觉得可以理解为展示给用户的界面,是由多个组合项(Composable组件)组成。

Event事件:指的是从应用外部生成的输入,用于通知程序的某部分发生了变化。如用户的点击,滑动等操作。所以在 Compose 中,Event 事件一般就是引起 State 状态改变的原因。


2、状态的表示


其实可以换一种说法:Compose 中数据的存储和更新如何处理?目前来看的话,可以用 LiveData、StateFlow、Flow、Observable 等表示。可以看出,这些都是一种可观察数据变化的容器,被它们修饰的对象,我们都可以观察到该对象的变化,从而更新界面。没错,都是使用的观察者模式。

在 Compose 的文档中,ViewModel 被推荐为 State状态的管理对象,从而实现将数据与界面展示的 Activity 分离解耦的目的。


2.1 ViewModel


ViewModel 也是 Jetpack 工具库的成员之一,主要用来存储 UI 展示所需要的数据,谷歌推荐的做法是将 Activity 中的数据都放到 ViewModel 里,而且在 Activity、Fragment 重建时 ViewModel 中的数据是不受影响的。还可以通过 ViewModel 来进行 Activity 与 Fragment 之间,或者 Fragment 与 Fragment 之间的通信。

ViewModel 经常与 LiveData 一起使用,但在 Compose 中,推荐使用 MutableState 来具体存储数据的值。


2.2 MutableState<T>


MutableState<T> 是 Compose 中内置的专门用于存储 State 状态的容器,与 LiveData 一样,它可以观察到存储的值的变化。如果项目不是纯 Compose 代码,建议还是用 LiveData,因为 LiveData 是通用的,而 MutableState<T> 是与 Compose 集成了,所以在 Compose 中使用 MutableState 比 LiveData 更简单。

从这里也可看出,Compose 是推荐将 State 状态设置为可观察的,这样当状态发生更改时,Compose 可以自动重组更新界面。

实际上 MutableState<T> 是个接口:

// code 1
interface MutableState<T>: State<T> {
  override var value: T
}

对 value 进行的任何更改都会自动重组用于读取此状态的所有 Composable 函数,也就是说,value 值改变了之后,所有引用了 value 的 Composable 函数都会重新绘制更新。


3、一个简单例子


先来看看效果:

其中有两个控件,一个是 Text,用于显示输入的内容;另一个是 TextField,相当于 View 体系中的 EditText。可以看出,Text 显示的内容可以随着下面的 TextField 中输入的内容实时更新。

如果是在 View 体系中,一般实现的方法是在 EditText 添加一个 TextWatcher 类用于监听输入事件,然后在 onTextChanged 方法中对 TextView 设置输入的内容即可。

再来看一下 Compose 是如何实现这一小功能的 。根据官方推荐,得先有一个 ViewModel 进行状态数据的管理:

// code 2
class ZhuViewModel: ViewModel() {
  // 状态数据初始化,初始化为空字符串
    var inputStr = mutableStateOf("")
    // 状态更新方法,将新输入的内容赋值给 MutableState<T> 对象的 value 值
    fun onInputChange(inputContent: String) {
        inputStr.value = inputContent
    }
}

可以看出,ViewModel 中需要对状态进行初始化,并且提供相应的更新方法。同时 ViewModel 中不会出现任何与界面相关的对象,例如 Activity、Fragment、Context 等,为的就是解耦。

界面代码就是 Composable 函数根据 ViewModel 管理的 State 状态进行展示:

// code 3
class ZhuStateActivity: AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val zhuViewModel by viewModels<ZhuViewModel>()
        setContent {InputShow(zhuViewModel)}
    }
}
@Composable
fun InputShow(viewModel: ZhuViewModel) {
    Column(Modifier.padding(20.dp)) {
        Text(text = viewModel.inputStr.value)
        TextField(
            value = viewModel.inputStr.value,
            onValueChange = { viewModel.onInputChange(it) }
        )
    }
}

TextField 组件相当于 EditText,onValueChange 可获取到用户的输入内容,在这里调用 ViewModel 中更新状态的方法。这样,所有引用了 ViewModel 中 MutableState 类型对象 inputStr 的组合项(Composable 函数),都会自动重绘更新,Text 组件就可以实时更新输入的内容了。


4. remember 关键字


其实在 code 3 中的小功能使用 ViewModel 来管理 State 状态有点小题大做了,可以用 remember 关键字来实现。这个关键字的作用如它的意思一样,“记住” 它所修饰的对象的值。下面的代码就是没有使用 ViewModel 的实现方法:

// code 4
class ZhuStateActivity: AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {InputShow()}
    }
}
@SuppressLint("UnrememberedMutableState")
@Composable
fun InputShow() {
    val inputStr = mutableStateOf("Hello")
    Column(Modifier.padding(20.dp)) {
        Text(text = inputStr.value)
        TextField(
            value = inputStr.value,
            onValueChange = {
                inputStr.value = it
            }
        )
    }
}

这里没有使用 remember 会有红线提醒,我先使用 SuppressLint 去掉了报错,为的只是举个栗子,并且设置了默认展示 “Hello” 文案。运行一下,你会发现,不管输入什么,都只是展示 “Hello”,好像啥也没有发生。。。

这是为啥?加一些 log 看看:

// code 5
@SuppressLint("UnrememberedMutableState")
@Composable
fun InputShow() {
    val inputStr = mutableStateOf("Hello")
    Log.d(TAG, "InputShow: Column inputStr = ${inputStr.value}")
    Column(Modifier.padding(20.dp)) {
        Text(text = inputStr.value)
        TextField(
            value = inputStr.value,
            onValueChange = {
                inputStr.value = it
                Log.d(TAG, "InputShow: onValueChange inputStr = $it")
            }
        )
    }
}

连续输入字母 w、o、r、l、d,打出来的 log 是这样的:

image.png

可见在每次输入之后,都会触发 Composable 函数重新绘制,每次都会重新初始化 inputStr 这个状态,而初始值都是一样的,所以看起来就好像输入不起作用。Composable 函数的重新绘制过程也被称之为 重组

重组:使用新的输入Event事件重新调用可组合项以更新 Compose 树的过程。这一过程会再次运行相同的 Composable 组件进行更新。

顺带说一下,Compose 首次运行渲染 Composable 组件时,会为所有被调用的 Composable 组件构建一个树,然后在重组期间会使用新的 Composable 组件去更新树。

再回到这个例子,使用 remember 关键字就可以避免每次重组时都初始化为初始值。使用后的代码为:

// code 6
@Composable
fun InputShow() {
    val inputStr = remember{ mutableStateOf("Hello") }
    Column(Modifier.padding(20.dp)) {
        Text(text = inputStr.value)
        TextField(
            value = inputStr.value,
            onValueChange = {
                inputStr.value = it
            }
        )
    }
}

这样就可以正确实现功能了。其实 remember 关键字的使用是由两部分组成:

  1. key arguments:表示这次调用使用的 “键”(key),用圆括号包裹;
  2. calculation :一个 Lambda 表达式,计算得出需要存储的 “值”(value)。

所以,remember 的用法如下所示:

// code 7
remember(key) { calculation: () -> T }

remember 关键字可以为 Composable 组件项提供一个数据存储空间,系统会将由 calculation Lambda 表达式计算得出的值存储到组合树中,只有当 remember 的 “键” 发生变化时,才会重新执行 calculation 计算得出 value 并存储起来;否则还是原来的值。

当然 code 6 中并没有设置 remember 的 key,这种情况下,remember 会默认该 key 没有发生变化,不会重新初始化,而是用之前的值。

需要注意的点: remember 虽然会将数据或对象存储在组合项中,但当调用 remember 的可组合项从组合树中移除后,它会忘记该数据或对象。所以,不要在有添加或移除 Composable 组件的情况下,使用 remember 将重要内容存储在 Composable 组件中,因为添加和移除都会使得数据丢失。

目录
相关文章
|
存储 算法 Android开发
Jetpack-Compose 学习笔记(三)—— Compose 的自定义“View”(下)
Jetpack-Compose 学习笔记(三)—— Compose 的自定义“View”(下)
126 0
|
算法 Android开发 开发者
Jetpack-Compose 学习笔记(三)—— Compose 的自定义“View”(上)
Jetpack-Compose 学习笔记(三)—— Compose 的自定义“View”(上)
191 0
|
Android开发 Kotlin 容器
Jetpack-Compose 学习笔记(二)—— Compose 布局你学会了么?(上)
Jetpack-Compose 学习笔记(二)—— Compose 布局你学会了么?(上)
237 1
|
存储 Android开发
Jetpack-Compose 学习笔记(六)——Compose 主题 Theme 一探究竟,换肤还能如此 Easy & Silky?(下)
Jetpack-Compose 学习笔记(六)——Compose 主题 Theme 一探究竟,换肤还能如此 Easy & Silky?(下)
208 0
|
XML 存储 Android开发
Jetpack-Compose 学习笔记(六)——Compose 主题 Theme 一探究竟,换肤还能如此 Easy & Silky?(上)
Jetpack-Compose 学习笔记(六)——Compose 主题 Theme 一探究竟,换肤还能如此 Easy & Silky?(上)
135 0
|
存储 前端开发 Java
Jetpack-Compose 学习笔记(五)—— State 状态是个啥?又是新概念?(下)
Jetpack-Compose 学习笔记(五)—— State 状态是个啥?又是新概念?(下)
82 0
|
Android开发
Jetpack-Compose 学习笔记(四)—— Intrinsic 固有特性测量是个啥?看完这篇就知道了(下)
Jetpack-Compose 学习笔记(四)—— Intrinsic 固有特性测量是个啥?看完这篇就知道了(下)
110 0
|
开发者
Jetpack-Compose 学习笔记(四)—— Intrinsic 固有特性测量是个啥?看完这篇就知道了(上)
Jetpack-Compose 学习笔记(四)—— Intrinsic 固有特性测量是个啥?看完这篇就知道了(上)
73 0
|
XML API Android开发
Jetpack-Compose 学习笔记(二)—— Compose 布局你学会了么?(下)
Jetpack-Compose 学习笔记(二)—— Compose 布局你学会了么?(下)
180 0
|
6月前
|
存储 安全 Android开发
构建高效的Android应用:Kotlin与Jetpack的结合
【5月更文挑战第31天】 在移动开发的世界中,Android 平台因其开放性和广泛的用户基础而备受开发者青睐。随着技术的进步和用户需求的不断升级,开发一个高效、流畅且易于维护的 Android 应用变得愈发重要。本文将探讨如何通过结合现代编程语言 Kotlin 和 Android Jetpack 组件来提升 Android 应用的性能和可维护性。我们将深入分析 Kotlin 语言的优势,探索 Jetpack 组件的核心功能,并通过实例演示如何在实际项目中应用这些技术。