B心智模型

先建立声明式与 UI = f(state),再学积木和状态。看不懂这一章,后面会一直觉得「怪」。

命令式 vs 声明式

概念

命令式:界面是一棵已经建好的 View 树。你改数据后,还要自己找到控件,再 setText / setEnabled / notify… 把变化「推」上去。

声明式:你只描述「当前状态下界面应该长什么样」。状态变了,框架重跑相关的 @Composable,把界面对齐到新描述。

命令式 (XML) 用户点击 代码改 count findViewById tv.setText(新值) 你手动 串联每一步 声明式 (Compose) 状态 count 改变 Composable 函数 Text("$count") 界面自动刷新 框架自动 重跑函数 状态是唯一真相

和 XML 对照

维度XML 命令式Compose 声明式
你写什么怎么改 View界面「是什么」
数据变了自己 setXxx / notify改状态,等重组
找控件findViewById / Binding不需要,直接是参数/表达式
常见坑改了数据忘刷新在 Composable 里乱写副作用

怎么用(最小对照)

XML + 代码
// Activity 里
btn.setOnClickListener {
    count++
    tvCount.text = count.toString()
}
Compose
@Composable
fun CounterDemo() {
    var count by remember { mutableStateOf(0) }
    Column {
        Text("$count")
        Button(onClick = { count++ }) {
            Text("+1")
        }
    }
}
📌 示例边界:上面 Kotlin 片段写在 @Composable fun CounterDemo() 体内。状态与组件不能写在函数外。

易错点

  • 还用「先拿到控件引用再改」的思维——Compose 里没有长期存活的 TextView 句柄给你改。
  • 把声明式理解成「不用管状态」——恰恰相反:状态才是唯一真相,要认真管。
▶ 同一登录按钮:XML 命令刷新 vs Compose 声明

需求:账号、密码都填好后,登录按钮才可点。XML 要监听两个输入框,再手动刷 isEnabled;Compose 让按钮的 enabled 直接读状态表达式。

XML 命令式
etUser.addTextChangedListener { refresh() }
etPwd.addTextChangedListener { refresh() }

fun refresh() {
    btnLogin.isEnabled =
        etUser.text.isNotBlank() &&
        etPwd.text.length >= 6
}
Compose 声明式
@Composable
fun LoginButtonDemo() {
    var user by remember { mutableStateOf("") }
    var pwd by remember { mutableStateOf("") }
    val canLogin = user.isNotBlank() && pwd.length >= 6

    Button(
        onClick = { /* 登录 */ },
        enabled = canLogin
    ) {
        Text("登录")
    }
}

要点:XML 要「记得刷新」;Compose 把「能不能点」写成状态的函数,状态一变按钮外观跟着变。

💡 复制 Compose 侧到 Studio,加 @Previewuser/pwd 初值即可看按钮亮灭。@Preview

UI = f(state)

概念

把界面看成函数:输入是状态,输出是当前 UI 描述。同一份状态,多次运行应得到同一份界面(纯函数直觉)。

核心公式:UI = f(state)
把「同步数据和界面」从你手里移交给框架。重组、状态提升、单向数据流,都是这个公式的推论。

和 XML 对照

  • XML:数据在一边,View 在另一边,靠你写胶水对齐。
  • Compose:数据(状态)进函数,界面是输出;对齐由运行时完成。

怎么用

  1. 先问:这一屏「真相」是哪些字段?(如 userpwdloading
  2. 界面只读这些字段来画;事件里只改字段,不直接「命令某个控件」。
  3. 细节(remember / ViewModel)放到状态与数据流章。

易错点

  • 在 Composable 里改状态又立刻依赖「改完后的副作用顺序」——容易和重组打架。
  • 把业务状态散落在多个互不通信的局部变量里,界面会对不齐。

重组的直觉

概念

重组(Recomposition):状态变化时,Compose 只重跑读过该状态的 Composable,不是整页死刷,也不是「销毁再建整个 Activity」。

State 改变 父 Composable 子 A(读了该 State) 子 B(没读,跳过) 只重跑 子 A Runtime 记下依赖 → 精准重跑 → 跳过无关部分

和 XML 对照

XML 时代Compose
invalidate() / 整段重绘按状态依赖重跑函数
Adapter notifyItemChangedLazy 列表 + 状态变化驱动 item
你决定「刷谁」运行时根据「谁读了状态」决定

怎么用(先有直觉)

  • 把会变的数放进 mutableStateOf(后面章节细讲),在 Text("$count") 里读它——读过的地方才会因它而重组。
  • 没读到该状态的兄弟节点,理想情况下会被跳过。

易错点

  • 以为重组 = 整屏闪一下重画——多数时候只是相关函数再执行一遍。
  • 在 Composable 里直接开网络、注册监听——重组可能反复执行;副作用放到后面的副作用边界章。
  • 状态放得太高:改一个角标,半屏都跟着重跑(状态尽量靠近真正用它的地方)。

组合优于继承

概念

Compose 鼓励用小函数组合出界面:父组件调用子组件,用参数和内容 lambda(插槽)拼装。少去继承一套自定义 View / ViewGroup

和 XML 对照

XML 常见路子

自定义 XxxView 继承 FrameLayout,重写测量/绘制,或再套一层布局 XML。复用靠继承树变深。

Compose 路子

@Composable fun ProfileHeader(...),在需要的地方调用;要变外观就加参数或再包一层 Composable,而不是开子类。

怎么用

@Composable
fun ProfileHeader(name: String, onEdit: () -> Unit) {
    Row {
        Text(name)
        Button(onClick = onEdit) {
            Text("编辑")
        }
    }
}

@Composable
fun ProfileScreen() {
    Column {
        ProfileHeader(name = "Kang", onEdit = { /* ... */ })
        // 其它区块继续组合
    }
}

易错点

  • 把 Activity/Fragment 继承思维搬过来,做一个巨大的「上帝 Composable」——应拆成可组合的小块。
  • 为了「扩展性」过早抽象;先复制再抽,比先造深继承更贴合 Compose。
学完本章:侧栏打开术语表扫一眼「声明式 / 重组 / State」,再进入布局三件套
Android Compose 现代开发知识体系 · 主线必读 / 扩展选读。
术语表常驻;组件与属性先看分讲,再查两张总表。案例默认收起。