Skip to content
Maozy's Blog
Go back

[Jetpack Compose] state读取与追踪

提问:是不是@Composable所定义的组件第一行就一定是那个被监听的可变值?值变动会重新触发函数?

答案是:absolutely no!State 定义在第几行根本不重要,甚至 State 根本不需要定义在这个组件内部。

Compose 的底层逻辑不在于把变量“定义”在哪里,而在于把变量“读取”(Read)在哪里。

通过这个问题需要搞懂 Compose 的两个核心概念:状态读取追踪状态提升 (State Hoisting)(面试高频词)。

Table of contents

Open Table of contents

一、核心机制

Compose 只关心“在哪里使用了数据”

当 Compose 框架执行 @Composable 函数时,它在底层做了一件事:记录作用域内的读取行为

只要在组件的任何位置(第一行、最后一行、或者嵌套的内部组件里)读取了 State 的值(比如调用了 state.value),Compose 就会把这个 State 和当前这个 @Composable 函数绑定在一起。

代码演示:位置完全随意

@Composable
fun RandomPositionComponent() {
    // 第一行可以随便干点别的,完全没关系
    val screenWidth = 1080 
    println("组件开始渲染了")

    // State 甚至可以定义在中间
    var clickCount by remember { mutableStateOf(0) } 

    // 这里甚至可以插一段无关的 UI
    Text("我是无关紧要的标题")

    Button(onClick = { clickCount++ }) {
        // 关键点在这里!!!
        // Compose 发现你在 Text 内部读取了 clickCount 这个状态
        // 它就会记录:当 clickCount 改变时,重新执行这段 UI
        Text("你点击了 $clickCount 次") 
    }
}

二、无状态组件 (Stateless) 与 状态提升 (State Hoisting)

如果不在组件内部定义 State,那该怎么做?

这就引出了 Compose 架构中最核心的思想:单向数据流 (UDF - Unidirectional Data Flow)

在实际的项目研发中,我们反而极力避免把 State 定义在基础组件内部。

我们希望组件是无状态的 (Stateless)

什么是无状态? 就是数据全靠外面传进来,事件全靠回调传出去。它本身像一个没有记忆的打印机,给什么数据就印什么画面。

// 这是一个“无状态”组件,它内部没有任何 remember 和 mutableStateOf
// 它的第一行更没有什么监听的值!
@Composable
fun StatelessLikeButton(
    isLiked: Boolean,         // 状态从外面以参数形式传进来
    onLikeClick: () -> Unit   // 点击事件以 Lambda 形式传出去
) {
    Button(onClick = onLikeClick) {
        Text(if (isLiked) "取消赞" else "点赞") // 这里只负责“读取”外面传来的参数
    }
}

// 这是一个“有状态”的父组件,负责管理数据
@Composable
fun ParentScreen() {
    // 状态被“提升”到了这里定义
    var likedState by remember { mutableStateOf(false) }

    // 将状态向下传递给无状态组件
    StatelessLikeButton(
        isLiked = likedState, 
        onLikeClick = { likedState = !likedState }
    )
}

为什么主流极其看重这种写法? 因为这样的 StatelessLikeButton 非常容易复用,也非常容易写单元测试。

它就像一个纯粹的展示机器,给它什么布尔值,它就长什么样,完全与业务逻辑解耦。

这就叫做状态提升 (State Hoisting) —— 把状态从子组件移到父组件中去管理。

2.1 单向数据流

既然子组件变成了“没有记忆的打印机”,那状态(State)由谁来管呢?

回答:由它的父组件来管。

以前的 Android Java 开发中,如果写一个自定义的 LikeButton 继承自 View,你通常会在这个类里面自己维护一个 private boolean isLiked。点击的时候,按钮内部自己取反,自己 setText()

但这会导致一个大问题:如果其他组件(比如顶部的点赞总数统计)也想知道这个按钮的状态,就非常难同步。

Compose(或是现代前端框架)推崇 “单向数据流 (UDF)” 的架构:

  1. 状态向下流动 (State flows down): 父组件拥有 State,并通过参数(就像那个 boolean isLiked)传递给子组件。
  2. 事件向上冒泡 (Events flow up): 子组件发生点击时,不自己改变 UI,而是通过回调(就像那个 Runnable onLikeClick)大喊一声:“父组件,有人点我了!”。父组件收到消息后,去修改自己手里的 State
  3. 重新渲染: 父组件的 State 变了,Compose 框架自动重新调用所有的 UI 方法,子组件也就拿到了最新的 boolean 值,刷新了显示。

顺带一提,因为刚学compose 跟 kotlin 不久,上面代码的等同 Java 版本是这样:

// Java 版本的理解方式
public class ComposeUI {

    // StatelessLikeButton 就是一个普通的静态方法
    public static void statelessLikeButton(
        boolean isLiked,             // 纯参数,不是什么监听器
        Runnable onLikeClick         // 纯接口回调
    ) {
        // 创建一个按钮
        Button button = new Button();
        
        // 根据传进来的 boolean 值,决定显示什么字
        if (isLiked) {
            button.setText("取消赞");
        } else {
            button.setText("点赞");
        }

        // 把外面的回调,绑到按钮的点击事件上
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (onLikeClick != null) {
                    onLikeClick.run(); // 告诉外面:我被点啦!
                }
            }
        });
    }
}

2.2 跟传统模式对比

如果只有一个按钮和一个文本,传统的 View 互相调用的确很简单,可以让外层 view 直接持有这个 button,读取其状态。

1. 传统 View 系统的“状态散落”灾难

按照传统做法,按钮自己维护了 isLiked,然后顶部的统计 View 去 get 它的属性,假设界面上有:

  1. 列表里的点赞按钮A
  2. 顶部的点赞总数统计B
  3. 点击进入详情页里的点赞按钮C

在传统模式下,数据同步是灾难级的:

在这个过程中,UI 组件互相持有引用,互相调用方法去修改对方的展现。

状态(数据)散落在各个独立的 View 实例中,就像网状的意大利面条一样死锁在一起。

用后端的思维来理解:

2. 状态提升:引入“唯一可信数据源 (SSOT)”

为了解决这种 UI 状态不同步的灾难,现代前端和 Compose 引入了一个极其重要的架构原则:单一数据源(Single Source of Truth, 简称 SSOT)

3. 单向数据流优雅解决同步

有了单一数据源,之前的复杂场景在 Compose 里就手拿把掐了:

  1. 页面顶层有一个 State,比如 val isLiked = remember { mutableStateOf(false) }
  2. 顶部的点赞总数统计B 读取了这个 State
  3. 列表里的点赞按钮A 也读取了这个 State
  4. 当用户点击按钮A时,A 啥也不干,只是触发回调:onLikeClick()
  5. 父组件收到回调,把唯一的数据源修改了:isLiked.value = true
  6. 见证奇迹的时刻: 因为 Compose 能够精准追踪谁读取了状态,它会自动且同时通知 统计B按钮A 重新渲染!

研发人员根本不需要写任何让 A 找 B、B 找 C 的互相通知代码。

大家都是数据的“下游”,数据源一变,所有用到该数据的 UI 瞬间同步

4. 对比总结

如果问到:“传统 View 这种互相调用的方式有什么不好?为什么 Compose 提倡状态提升?”

传统 View 系统中,View 内部自带状态(如 CheckBox 的 checked 状态)。

在复杂页面中,如果要实现多个 UI 组件之间的状态联动,往往需要互相持有引用并手动调用方法更新,这导致状态分散、数据流向混乱,极易引发 UI 显示不一致的 Bug,就好比系统架构中缺乏统一的数据源。

而 Compose 提倡单一可信数据源 (SSOT)单向数据流 (UDF)。通过状态提升,将状态统一收紧在父组件或 ViewModel 中。底层的 Composable 组件退化为无状态的纯函数,只负责消费数据。这样一来,无论有多少个组件需要联动,只要核心 State 发生改变,Compose 的快照系统就会自动驱动所有依赖该状态的 UI 同步重组。

这从架构根本上消除了 UI 状态不一致的隐患,非常契合复杂业务场景的开发。

三、总结(针对状态定义与追踪)

如果当问到 State 的读取和追踪:

Compose 的响应式机制并不是基于代码声明的位置,而是基于状态的读取追踪(State Read Tracking)。在重组期间,Compose 会自动记录哪些 Composable 函数读取了哪些 State 对象。

在实际架构中,为了保证组件的纯粹性和可复用性,我们通常推崇单向数据流 (UDF)

我们会将 State 从 UI 组件内部抽离出来,通过状态提升 (State Hoisting) 的方式放在父组件或 ViewModel 中管理。

底层的 Composable 最好是无状态的 (Stateless),只接收数据参数和暴露事件 Lambda。

当 ViewModel 中的 State 改变时,会从上到下触发对应作用域的重组。


Share this post on:

Previous Post
[Jetpack Compose] AndroidView
Next Post
[Jetpack Compose] remember