M8Test Help

阶段

与大多数其他界面工具包一样,Compose 会通过几个不同的“阶段”来渲染帧。例如,Android View 系统有 3 个主要阶段:测量、布局和绘制。Compose 和它非常相似,但开头多了一个叫做“组合”的重要阶段。

Compose 文档中的“ Compose 编程思想 ”和“ 状态和 Jetpack Compose ”对“组合”阶段进行了说明。

帧的 3 个阶段

Compose 有 3 个主要阶段:

  1. 组合:要显示什么样的界面。Compose 运行可组合函数并创建界面说明。

  2. 布局:要放置界面的位置。该阶段包含两个步骤:测量和放置。对于布局树中的每个节点,布局元素都会根据 2D 坐标来测量并放置自己及其所有子元素。

  3. 绘制:渲染的方式。界面元素会绘制到画布(通常是设备屏幕)中。

444

这些阶段通常会以相同的顺序执行,让数据能够沿一个方向(从组合到布局,再到绘制)生成帧(也称为单向数据流 )。BoxWithConstraints、LazyColumn 和 LazyRow 是值得注意的特例,其子级的组合取决于父级的布局阶段。

从概念上讲,每个帧都会经历这 3 个阶段;不过,为了优化性能,Compose 会避免在所有这些阶段中重复执行根据相同输入计算出相同结果的工作。如果可以重复使用前面计算出的结果,Compose 会跳过对应的可组合函数;如果没有必要,Compose 界面不会对整个树进行重新布局或重新绘制。Compose 只会执行更新界面所需的最低限度的工作。之所以能够实现这种优化,是因为 Compose 会跟踪不同阶段中的状态读取。

了解阶段

本部分更详细地介绍了如何针对可组合项执行这三个 Compose 阶段。

组合

在组合阶段,Compose 运行时会执行可组合函数,并输出表示界面的树结构。此界面树由包含后续阶段所需的所有信息的布局节点组成,如以下视频所示:

代码和界面树的某个子部分如下所示:

445

在这些示例中,代码中的每个可组合函数都对应于界面树中的单个布局节点。在更复杂的示例中,可组合函数可以包含逻辑和控制流,并根据不同的状态生成不同的树。

布局

在布局阶段,Compose 使用在组合阶段生成的界面树作为输入。布局节点的集合包含确定每个节点在二维空间中的大小和位置所需的所有信息。

在布局阶段,系统会使用以下三步算法遍历树:

  1. 测量子项:节点会测量其子项(如果有)。

  2. 确定自己的尺寸:节点根据这些测量结果确定自己的尺寸。

  3. 放置子节点:每个子节点都相对于节点自身的位置放置。

在此阶段结束时,每个布局节点都具有:

  • 分配的宽度和高度

  • 应绘制到的 x、y 坐标

回顾一下上一部分中的界面树:

445

对于此树,算法的运作方式如下:

  1. Row 测量其子级 Image 和 Column。

  2. 测量 Image。它没有任何子节点,因此会自行确定尺寸,并将尺寸报告给 Row。

  3. 接下来,系统会衡量 Column。它首先测量自己的子项(两个 Text 可组合项)。

  4. 首先测量的是 Text。它没有任何子节点,因此会自行确定尺寸,并将尺寸报告给 Column。

    • 测量第二个 Text。它没有任何子节点,因此会自行决定尺寸并将其报告给 Column。

  5. Column 使用子测量结果来确定自己的大小。它使用最大子宽度和子高度之和。

  6. Column 将其子级放置在相对于自身的位置,并以垂直方式将它们放置在彼此下方。

  7. Row 使用子测量结果来确定自己的大小。它使用最大子项高度及其子项宽度的总和。然后放置其子项。

请注意,每个节点仅访问一次。Compose 运行时只需遍历一次界面树即可测量和放置所有节点,从而提高性能。当树中的节点数量增加时,遍历树所花费的时间会呈线性增加。相比之下,如果每个节点被多次访问,遍历时间会呈指数级增长。

绘制

在绘制阶段,系统会再次从上到下遍历树,并且每个节点依次在屏幕上绘制自身。

使用上一个示例,树内容将按以下方式绘制:

  • Row 会绘制可能包含的任何内容,例如背景颜色。

  • Image 会自行绘制。

  • Column 会自行绘制。

  • 第一个和第二个 Text 分别自行绘制。

状态读取

Compose 会跟踪通过 trackSingleState 方法跟踪状态,通过这项跟踪,Compose 能够在状态的 value 发生变化时重新执行读取程序;此外,我们也是以这项跟踪为基础在 Compose 中实现了对状态的观察。

您通常使用 mutableStateOf() 创建状态, 然后通过 getValue 方法进行访问

import com.m8test.script.GlobalVariables._activity import com.m8test.script.GlobalVariables._composeView fun side1Run() { // 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 _composeView.create { // 1. 创建 state 必须要在 slot 闭包中,因为这个代码只会执行一次,不会重组 val paddingState = mutableStateOf(8) val offsetXState = mutableStateOf(8) Text { // 不要在这里创建 state, 因为这个代码会在重组的时候执行 setText("M8Test") // 2. 跟踪状态,这样 compose 就会在改状态的值改变时自动重组 Text 从而更新界面 trackSingleState(paddingState) setModifier { padding { setAll { dimensions -> // 3. 使用状态值, 在组合阶段,当修饰符被构造时,会读取“paddingState”状态。 “paddingState”状态的改变将触发重组。 dimensions.fromInt(paddingState.getValue()) } } offset { dimensions -> // 3. 使用状态值,在布局阶段的放置步骤中,计算偏移量时会读取 `offsetX` 状态。 `offsetX` 的更改会重新启动布局。 dimensions.createOffset(dimensions.fromInt(offsetXState.getValue()), dimensions.fromInt(0)) } } } } // 启动一个Activity用于显示脚本界面 _activity.start() } //-m8test-remove side1Run();
// 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 $composeView.create { slot -> // 1. 创建 state 必须要在 slot 闭包中,因为这个代码只会执行一次,不会重组 def paddingState = slot.mutableStateOf(8) def offsetXState = slot.mutableStateOf(8) slot.Text { text -> // 不要在这里创建 state, 因为这个代码会在重组的时候执行 text.setText("M8Test") // 2. 跟踪状态,这样 compose 就会在改状态的值改变时自动重组 Text 从而更新界面 text.trackSingleState(paddingState) text.setModifier { modifier -> modifier.padding { paddingValues -> paddingValues.setAll { dimensions -> // 3. 使用状态值, 在组合阶段,当修饰符被构造时,会读取“paddingState”状态。 “paddingState”状态的改变将触发重组。 dimensions.fromInt(paddingState.getValue()) } } modifier.offset { dimensions -> // 3. 使用状态值,在布局阶段的放置步骤中,计算偏移量时会读取 `offsetX` 状态。 `offsetX` 的更改会重新启动布局。 dimensions.createOffset(dimensions.fromInt(offsetXState.getValue()), dimensions.fromInt(0)) } } } } // 启动一个Activity用于显示脚本界面 $activity.start()
// 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 $composeView.create(slot => { // 1. 创建 state 必须要在 slot 闭包中,因为这个代码只会执行一次,不会重组 const paddingState = slot.mutableStateOf(8); const offsetXState = slot.mutableStateOf(8); slot.Text(text => { // 不要在这里创建 state, 因为这个代码会在重组的时候执行 text.setText("M8Test"); // 2. 跟踪状态,这样 compose 就会在改状态的值改变时自动重组 Text 从而更新界面 text.trackSingleState(paddingState); text.setModifier(modifier => { modifier.padding(paddingValues => { paddingValues.setAll(dimensions => { // 3. 使用状态值, 在组合阶段,当修饰符被构造时,会读取“paddingState”状态。 “paddingState”状态的改变将触发重组。 return dimensions.fromInt(paddingState.getValue()); }); }); modifier.offset(dimensions => { // 3. 使用状态值,在布局阶段的放置步骤中,计算偏移量时会读取 `offsetX` 状态。 `offsetX` 的更改会重新启动布局。 return dimensions.createOffset(dimensions.fromInt(offsetXState.getValue()), dimensions.fromInt(0)); }); }); }); }); // 启动一个Activity用于显示脚本界面 $activity.start();
-- 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 _composeView:create(function(slot) -- 1. 创建 state 必须要在 slot 闭包中,因为这个代码只会执行一次,不会重组 local paddingState = slot:mutableStateOf(8) local offsetXState = slot:mutableStateOf(8) slot:Text(function(text) -- 不要在这里创建 state, 因为这个代码会在重组的时候执行 text:setText("M8Test") -- 2. 跟踪状态,这样 compose 就会在改状态的值改变时自动重组 Text 从而更新界面 text:trackSingleState(paddingState) text:setModifier(function(modifier) modifier:padding(function(paddingValues) paddingValues:setAll(function(dimensions) -- 3. 使用状态值, 在组合阶段,当修饰符被构造时,会读取“paddingState”状态。 “paddingState”状态的改变将触发重组。 return dimensions:fromInt(paddingState:getValue()) end) end) modifier:offset(function(dimensions) -- 3. 使用状态值,在布局阶段的放置步骤中,计算偏移量时会读取 `offsetX` 状态。 `offsetX` 的更改会重新启动布局。 return dimensions:createOffset(dimensions:fromInt(offsetXState:getValue()), dimensions:fromInt(0)) end) end) end) end) -- 启动一个Activity用于显示脚本界面 _activity:start()
<?php /** @var m8test_java\com\m8test\script\core\api\ui\compose\ComposeView $composeView */ global $composeView; /** @var m8test_java\com\m8test\script\core\api\ui\Activity $activity */ global $activity; // 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 $composeView->create(function ($slot) { // 1. 创建 state 必须要在 slot 闭包中,因为这个代码只会执行一次,不会重组 $paddingState = $slot->mutableStateOf(8); $offsetXState = $slot->mutableStateOf(8); $slot->Text(function ($text) use ($paddingState, $offsetXState) { // 不要在这里创建 state, 因为这个代码会在重组的时候执行 $text->setText(javaString("M8Test")); // 2. 跟踪状态,这样 compose 就会在改状态的值改变时自动重组 Text 从而更新界面 $text->trackSingleState($paddingState); $text->setModifier(function ($modifier) use ($paddingState, $offsetXState) { $modifier->padding(function ($paddingValues) use ($paddingState) { $paddingValues->setAll(function ($dimensions) use ($paddingState) { // 3. 使用状态值, 在组合阶段,当修饰符被构造时,会读取“paddingState”状态。 “paddingState”状态的改变将触发重组。 return $dimensions->fromInt($paddingState->getValue()); }); }); $modifier->offset(function ($dimensions) use ($offsetXState) { // 3. 使用状态值,在布局阶段的放置步骤中,计算偏移量时会读取 `offsetX` 状态。 `offsetX` 的更改会重新启动布局。 return $dimensions->createOffset($dimensions->fromInt($offsetXState->getValue()), $dimensions->fromInt(0)); }); }); }); }); // 启动一个Activity用于显示脚本界面 $activity->start();
# 导入所需的全局变量 from m8test_java.com.m8test.script.GlobalVariables import _activity from m8test_java.com.m8test.script.GlobalVariables import _composeView # 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 _composeView.create(lambda slot: ( # 1. 创建 state 必须要在 slot 闭包中,因为这个代码只会执行一次,不会重组 (paddingState := slot.mutableStateOf(8)), (offsetXState := slot.mutableStateOf(8)), slot.Text(lambda text: ( # 不要在这里创建 state, 因为这个代码会在重组的时候执行 text.setText("M8Test"), # 2. 跟踪状态,这样 compose 就会在改状态的值改变时自动重组 Text 从而更新界面 text.trackSingleState(paddingState), text.setModifier(lambda modifier: ( modifier.padding(lambda paddingValues: paddingValues.setAll(lambda dimensions: # 3. 使用状态值, 在组合阶段,当修饰符被构造时,会读取“paddingState”状态。 “paddingState”状态的改变将触发重组。 dimensions.fromInt(paddingState.getValue()) ) ), modifier.offset(lambda dimensions: # 3. 使用状态值,在布局阶段的放置步骤中,计算偏移量时会读取 `offsetX` 状态。 `offsetX` 的更改会重新启动布局。 dimensions.createOffset(dimensions.fromInt(offsetXState.getValue()), dimensions.fromInt(0)) ) )) )) )) # 启动一个Activity用于显示脚本界面 _activity.start()
# encoding: utf-8 # 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 $composeView.create do |slot| # 1. 创建 state 必须要在 slot 闭包中,因为这个代码只会执行一次,不会重组 paddingState = slot.mutableStateOf(8) offsetXState = slot.mutableStateOf(8) slot.Text do |text| # 不要在这里创建 state, 因为这个代码会在重组的时候执行 text.setText("M8Test") # 2. 跟踪状态,这样 compose 就会在改状态的值改变时自动重组 Text 从而更新界面 text.trackSingleState(paddingState) text.setModifier do |modifier| modifier.padding do |paddingValues| paddingValues.setAll do |dimensions| # 3. 使用状态值, 在组合阶段,当修饰符被构造时,会读取“paddingState”状态。 “paddingState”状态的改变将触发重组。 dimensions.fromInt(paddingState.getValue()) end end modifier.offset do |dimensions| # 3. 使用状态值,在布局阶段的放置步骤中,计算偏移量时会读取 `offsetX` 状态。 `offsetX` 的更改会重新启动布局。 dimensions.createOffset(dimensions.fromInt(offsetXState.getValue()), dimensions.fromInt(0)) end end end end # 启动一个Activity用于显示脚本界面 $activity.start()

分阶段状态读取

如前所述,Compose 有 3 个主要阶段,并且 Compose 会跟踪在每个阶段中读取到的状态。这样一来,Compose 只需向需要对界面的每个受影响的元素执行工作的特定阶段发送通知即可。

以下部分介绍了每个阶段,并说明在各阶段中读取状态值时分别会发生什么情况。

import com.m8test.script.GlobalVariables._activity import com.m8test.script.GlobalVariables._composeView fun side1Run() { // 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 _composeView.create { // 1. 创建 state 必须要在 slot 闭包中,因为这个代码只会执行一次,不会重组 val paddingState = mutableStateOf(8) val offsetXState = mutableStateOf(8) Text { // 不要在这里创建 state, 因为这个代码会在重组的时候执行 setText("M8Test") // 2. 跟踪状态,这样 compose 就会在改状态的值改变时自动重组 Text 从而更新界面 trackSingleState(paddingState) setModifier { padding { setAll { dimensions -> // 3. 使用状态值, 在组合阶段,当修饰符被构造时,会读取“paddingState”状态。 “paddingState”状态的改变将触发重组。 dimensions.fromInt(paddingState.getValue()) } } offset { dimensions -> // 3. 使用状态值,在布局阶段的放置步骤中,计算偏移量时会读取 `offsetX` 状态。 `offsetX` 的更改会重新启动布局。 dimensions.createOffset(dimensions.fromInt(offsetXState.getValue()), dimensions.fromInt(0)) } } } } // 启动一个Activity用于显示脚本界面 _activity.start() } //-m8test-remove side1Run();
// 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 $composeView.create { slot -> // 1. 创建 state 必须要在 slot 闭包中,因为这个代码只会执行一次,不会重组 def paddingState = slot.mutableStateOf(8) def offsetXState = slot.mutableStateOf(8) slot.Text { text -> // 不要在这里创建 state, 因为这个代码会在重组的时候执行 text.setText("M8Test") // 2. 跟踪状态,这样 compose 就会在改状态的值改变时自动重组 Text 从而更新界面 text.trackSingleState(paddingState) text.setModifier { modifier -> modifier.padding { paddingValues -> paddingValues.setAll { dimensions -> // 3. 使用状态值, 在组合阶段,当修饰符被构造时,会读取“paddingState”状态。 “paddingState”状态的改变将触发重组。 dimensions.fromInt(paddingState.getValue()) } } modifier.offset { dimensions -> // 3. 使用状态值,在布局阶段的放置步骤中,计算偏移量时会读取 `offsetX` 状态。 `offsetX` 的更改会重新启动布局。 dimensions.createOffset(dimensions.fromInt(offsetXState.getValue()), dimensions.fromInt(0)) } } } } // 启动一个Activity用于显示脚本界面 $activity.start()
// 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 $composeView.create(slot => { // 1. 创建 state 必须要在 slot 闭包中,因为这个代码只会执行一次,不会重组 const paddingState = slot.mutableStateOf(8); const offsetXState = slot.mutableStateOf(8); slot.Text(text => { // 不要在这里创建 state, 因为这个代码会在重组的时候执行 text.setText("M8Test"); // 2. 跟踪状态,这样 compose 就会在改状态的值改变时自动重组 Text 从而更新界面 text.trackSingleState(paddingState); text.setModifier(modifier => { modifier.padding(paddingValues => { paddingValues.setAll(dimensions => { // 3. 使用状态值, 在组合阶段,当修饰符被构造时,会读取“paddingState”状态。 “paddingState”状态的改变将触发重组。 return dimensions.fromInt(paddingState.getValue()); }); }); modifier.offset(dimensions => { // 3. 使用状态值,在布局阶段的放置步骤中,计算偏移量时会读取 `offsetX` 状态。 `offsetX` 的更改会重新启动布局。 return dimensions.createOffset(dimensions.fromInt(offsetXState.getValue()), dimensions.fromInt(0)); }); }); }); }); // 启动一个Activity用于显示脚本界面 $activity.start();
-- 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 _composeView:create(function(slot) -- 1. 创建 state 必须要在 slot 闭包中,因为这个代码只会执行一次,不会重组 local paddingState = slot:mutableStateOf(8) local offsetXState = slot:mutableStateOf(8) slot:Text(function(text) -- 不要在这里创建 state, 因为这个代码会在重组的时候执行 text:setText("M8Test") -- 2. 跟踪状态,这样 compose 就会在改状态的值改变时自动重组 Text 从而更新界面 text:trackSingleState(paddingState) text:setModifier(function(modifier) modifier:padding(function(paddingValues) paddingValues:setAll(function(dimensions) -- 3. 使用状态值, 在组合阶段,当修饰符被构造时,会读取“paddingState”状态。 “paddingState”状态的改变将触发重组。 return dimensions:fromInt(paddingState:getValue()) end) end) modifier:offset(function(dimensions) -- 3. 使用状态值,在布局阶段的放置步骤中,计算偏移量时会读取 `offsetX` 状态。 `offsetX` 的更改会重新启动布局。 return dimensions:createOffset(dimensions:fromInt(offsetXState:getValue()), dimensions:fromInt(0)) end) end) end) end) -- 启动一个Activity用于显示脚本界面 _activity:start()
<?php /** @var m8test_java\com\m8test\script\core\api\ui\compose\ComposeView $composeView */ global $composeView; /** @var m8test_java\com\m8test\script\core\api\ui\Activity $activity */ global $activity; // 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 $composeView->create(function ($slot) { // 1. 创建 state 必须要在 slot 闭包中,因为这个代码只会执行一次,不会重组 $paddingState = $slot->mutableStateOf(8); $offsetXState = $slot->mutableStateOf(8); $slot->Text(function ($text) use ($paddingState, $offsetXState) { // 不要在这里创建 state, 因为这个代码会在重组的时候执行 $text->setText(javaString("M8Test")); // 2. 跟踪状态,这样 compose 就会在改状态的值改变时自动重组 Text 从而更新界面 $text->trackSingleState($paddingState); $text->setModifier(function ($modifier) use ($paddingState, $offsetXState) { $modifier->padding(function ($paddingValues) use ($paddingState) { $paddingValues->setAll(function ($dimensions) use ($paddingState) { // 3. 使用状态值, 在组合阶段,当修饰符被构造时,会读取“paddingState”状态。 “paddingState”状态的改变将触发重组。 return $dimensions->fromInt($paddingState->getValue()); }); }); $modifier->offset(function ($dimensions) use ($offsetXState) { // 3. 使用状态值,在布局阶段的放置步骤中,计算偏移量时会读取 `offsetX` 状态。 `offsetX` 的更改会重新启动布局。 return $dimensions->createOffset($dimensions->fromInt($offsetXState->getValue()), $dimensions->fromInt(0)); }); }); }); }); // 启动一个Activity用于显示脚本界面 $activity->start();
# 导入所需的全局变量 from m8test_java.com.m8test.script.GlobalVariables import _activity from m8test_java.com.m8test.script.GlobalVariables import _composeView # 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 _composeView.create(lambda slot: ( # 1. 创建 state 必须要在 slot 闭包中,因为这个代码只会执行一次,不会重组 (paddingState := slot.mutableStateOf(8)), (offsetXState := slot.mutableStateOf(8)), slot.Text(lambda text: ( # 不要在这里创建 state, 因为这个代码会在重组的时候执行 text.setText("M8Test"), # 2. 跟踪状态,这样 compose 就会在改状态的值改变时自动重组 Text 从而更新界面 text.trackSingleState(paddingState), text.setModifier(lambda modifier: ( modifier.padding(lambda paddingValues: paddingValues.setAll(lambda dimensions: # 3. 使用状态值, 在组合阶段,当修饰符被构造时,会读取“paddingState”状态。 “paddingState”状态的改变将触发重组。 dimensions.fromInt(paddingState.getValue()) ) ), modifier.offset(lambda dimensions: # 3. 使用状态值,在布局阶段的放置步骤中,计算偏移量时会读取 `offsetX` 状态。 `offsetX` 的更改会重新启动布局。 dimensions.createOffset(dimensions.fromInt(offsetXState.getValue()), dimensions.fromInt(0)) ) )) )) )) # 启动一个Activity用于显示脚本界面 _activity.start()
# encoding: utf-8 # 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 $composeView.create do |slot| # 1. 创建 state 必须要在 slot 闭包中,因为这个代码只会执行一次,不会重组 paddingState = slot.mutableStateOf(8) offsetXState = slot.mutableStateOf(8) slot.Text do |text| # 不要在这里创建 state, 因为这个代码会在重组的时候执行 text.setText("M8Test") # 2. 跟踪状态,这样 compose 就会在改状态的值改变时自动重组 Text 从而更新界面 text.trackSingleState(paddingState) text.setModifier do |modifier| modifier.padding do |paddingValues| paddingValues.setAll do |dimensions| # 3. 使用状态值, 在组合阶段,当修饰符被构造时,会读取“paddingState”状态。 “paddingState”状态的改变将触发重组。 dimensions.fromInt(paddingState.getValue()) end end modifier.offset do |dimensions| # 3. 使用状态值,在布局阶段的放置步骤中,计算偏移量时会读取 `offsetX` 状态。 `offsetX` 的更改会重新启动布局。 dimensions.createOffset(dimensions.fromInt(offsetXState.getValue()), dimensions.fromInt(0)) end end end end # 启动一个Activity用于显示脚本界面 $activity.start()

第 1 阶段:组合

@Composable 函数或 lambda 代码块中的状态读取会影响组合阶段,并且可能会影响后续阶段。当状态的 value 发生更改时,recomposer 会安排重新运行所有要读取相应状态的 value 的可组合函数。请注意,如果输入未更改,运行时可能会决定跳过部分或全部可组合函数。如需了解详情,请参阅如果输入未更改,则跳过。

根据组合结果,Compose 界面会运行布局和绘制阶段。如果内容保持不变,并且大小和布局也未更改,界面可能会跳过这些阶段。

第 2 阶段:布局

布局阶段包含两个步骤:测量和放置。测量步骤会运行传递给 Layout 可组合项的测量 lambda、LayoutModifier 接口的 MeasureScope.measure 方法,等等。 放置步骤会运行 layout 函数的放置位置块、Modifier.offset { … } 的 lambda 块以及类似函数。

每个步骤的状态读取都会影响布局阶段,并且可能会影响绘制阶段。当状态的 value 发生更改时,Compose 界面会安排布局阶段。如果大小或位置发生更改,界面还会运行绘制阶段。

第 3 阶段:绘制

绘制代码期间的状态读取会影响绘制阶段。常见示例包括 Canvas()、Modifier.drawBehind 和 Modifier.drawWithContent。当状态的 value 发生变化时,Compose 界面只会运行绘制阶段。

446
09 December 2025