M8Test Help

编程思想

Jetpack Compose 是一个适用于 Android 的新式声明性界面工具包。Compose 提供声明性 API,让您可在不以命令方式改变前端视图的情况下呈现应用界面,从而使编写和维护应用界面变得更加容易。此术语需要一些解释说明,它的含义对应用设计非常重要。

声明性编程范式

长期以来,Android 视图层次结构一直可以表示为界面 widget 树。由于应用的状态会因用户交互等因素而发生变化,因此界面层次结构需要进行更新以显示当前数据。最常见的界面更新方式是使用 findViewById() 等函数遍历树,并通过调用 button.setText(String)、container.addChild(View) 或 img.setImageBitmap(Bitmap) 等方法更改节点。这些方法会改变 widget 的内部状态。

手动操纵视图会提高出错的可能性。如果一条数据在多个位置呈现,很容易忘记更新显示它的某个视图。此外,当两项更新以出人意料的方式发生冲突时,也很容易造成异常状态。例如,某项更新可能会尝试设置刚刚从界面中移除的节点的值。一般来说,软件维护的复杂性会随着需要更新的视图数量而增长。

在过去的几年中,整个行业已开始转向声明性界面模型,该模型大大简化了与构建和更新界面关联的工程任务。该技术的工作原理是在概念上从头开始重新生成整个屏幕,然后仅执行必要的更改。此方法可避免手动更新有状态视图层次结构的复杂性。Compose 是一个声明性界面框架。

重新生成整个屏幕所面临的一个难题是,在时间、计算能力和电池用量方面可能成本高昂。为了减少在这方面耗费的资源,Compose 会智能地选择在任何给定时间需要重新绘制界面的哪些部分。这会对您设计界面组件的方式有一定影响,如重组中所述。

简单的可组合函数

使用 Compose,您可以通过定义一组接受数据而发出界面元素的可组合函数来构建界面。一个简单的示例是 Greeting widget,它接受 String 并发出一个显示问候消息的 Text widget。

import com.m8test.script.GlobalVariables._activity import com.m8test.script.GlobalVariables._composeView import com.m8test.script.core.api.ui.compose.modifier.Modifier import com.m8test.script.core.api.ui.compose.slot.Slot // 垂直方向上显示两个文本组件 private fun Greeting(slot: Slot<out Modifier>, name: String) { slot.Column { // 设置 Column 的修饰符 setModifier { // 这里加个背景色是为了更好的展示效果 background({ setRed(255) }, { shapes -> shapes.getRectangleShape() }) } setContent { Text { // 设置 Text 组件显示的文本 setText("Hello") } Text { // 设置 Text 组件显示的文本 setText(name) } } } } fun side1Run() { // 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 _composeView.create { // 插槽(Slot)是一种内容分发机制。它允许你在封装一个通用组件时,在组件内部预留一些“坑位”或“插槽”,然后让使用该组件的父组件来决定这些“坑位”里到底填充什么内容。 Greeting(this, "M8Test") } // 启动一个Activity用于显示脚本界面 _activity.start() } //-m8test-remove side1Run();
import com.m8test.script.core.api.ui.compose.modifier.Modifier import com.m8test.script.core.api.ui.compose.slot.Slot // 垂直方向上显示两个文本组件 private static def Greeting(Slot<? extends Modifier> slot, String name) { slot.Column { column -> // 设置 Column 的修饰符 column.setModifier { modifier -> // 这里加个背景色是为了更好的展示效果 modifier.background({ it.setRed(255) }, { shapes -> shapes.getRectangleShape() }) } column.setContent { columnSlot -> columnSlot.Text { text -> // 设置 Text 组件显示的文本 text.setText("Hello") } columnSlot.Text { text -> // 设置 Text 组件显示的文本 text.setText(name) } } } } // 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 $composeView.create { slot -> // 插槽(Slot)是一种内容分发机制。它允许你在封装一个通用组件时,在组件内部预留一些“坑位”或“插槽”,然后让使用该组件的父组件来决定这些“坑位”里到底填充什么内容。 Greeting(slot, "M8Test") } // 启动一个Activity用于显示脚本界面 $activity.start()
/** * 垂直方向上显示两个文本组件 * @param {Packages.com.m8test.script.core.api.ui.compose.slot.Slot<Packages.com.m8test.script.core.api.ui.compose.modifier.Modifier>} slot * @param {string} name */ function Greeting(slot, name) { slot.Column(column => { // 设置 Column 的修饰符 column.setModifier(modifier => { // 这里加个背景色是为了更好的展示效果 modifier.background(it => it.setRed(255), shapes => shapes.getRectangleShape()); }); column.setContent(columnSlot => { columnSlot.Text(text => { // 设置 Text 组件显示的文本 text.setText("Hello"); }); columnSlot.Text(text => { // 设置 Text 组件显示的文本 text.setText(name); }); }); }); } // 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 $composeView.create(slot => { // 插槽(Slot)是一种内容分发机制。它允许你在封装一个通用组件时,在组件内部预留一些“坑位”或“插槽”,然后让使用该组件的父组件来决定这些“坑位”里到底填充什么内容。 Greeting(slot, "M8Test"); }); // 启动一个Activity用于显示脚本界面 $activity.start();
-- 垂直方向上显示两个文本组件 local function Greeting(slot, name) slot:Column(function(column) -- 设置 Column 的修饰符 column:setModifier(function(modifier) -- 这里加个背景色是为了更好的展示效果 modifier:background(function(it) it:setRed(255) end, function(shapes) return shapes:getRectangleShape() end) end) column:setContent(function(columnSlot) columnSlot:Text(function(text) -- 设置 Text 组件显示的文本 text:setText("Hello") end) columnSlot:Text(function(text) -- 设置 Text 组件显示的文本 text:setText(name) end) end) end) end -- 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 _composeView:create(function(slot) -- 插槽(Slot)是一种内容分发机制。它允许你在封装一个通用组件时,在组件内部预留一些“坑位”或“插槽”,然后让使用该组件的父组件来决定这些“坑位”里到底填充什么内容。 Greeting(slot, "M8Test") end) -- 启动一个Activity用于显示脚本界面 _activity:start()
<?php use m8test_java\com\m8test\script\core\api\ui\compose\modifier\Modifier; use m8test_java\com\m8test\script\core\api\ui\compose\slot\Slot; /** @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; // 垂直方向上显示两个文本组件 /** @param Slot $slot */ function Greeting($slot, string $name) { $slot->Column(function ($column) use ($name) { // 设置 Column 的修饰符 $column->setModifier(function ($modifier) { // 这里加个背景色是为了更好的展示效果 $modifier->background( function ($it) { $it->setRed(255); }, function ($shapes) { return $shapes->getRectangleShape(); } ); }); $column->setContent(function ($columnSlot) use ($name) { $columnSlot->Text(function ($text) { // 设置 Text 组件显示的文本 $text->setText(javaString("Hello")); }); $columnSlot->Text(function ($text) use ($name) { // 设置 Text 组件显示的文本 $text->setText($name); }); }); }); } // 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 $composeView->create(function ($slot) { // 插槽(Slot)是一种内容分发机制。它允许你在封装一个通用组件时,在组件内部预留一些“坑位”或“插槽”,然后让使用该组件的父组件来决定这些“坑位”里到底填充什么内容。 Greeting($slot, javaString("M8Test")); }); // 启动一个Activity用于显示脚本界面 $activity->start();
# 导入所需的全局变量 from m8test_java.com.m8test.script.GlobalVariables import _activity from m8test_java.com.m8test.script.GlobalVariables import _composeView # 导入所需的Java类 from m8test_java.com.m8test.script.core.api.ui.compose.modifier.Modifier import Modifier from m8test_java.com.m8test.script.core.api.ui.compose.slot.Slot import Slot # 垂直方向上显示两个文本组件 def Greeting(slot: Slot, name: str): slot.Column(lambda column: ( # 设置 Column 的修饰符 column.setModifier(lambda modifier: ( # 这里加个背景色是为了更好的展示效果 # Groovy的 { it.setRed(255) } 对应 lambda it: it.setRed(255) # Groovy的 { shapes -> shapes.getRectangleShape() } 对应 lambda shapes: shapes.getRectangleShape() modifier.background(lambda it: it.setRed(255), lambda shapes: shapes.getRectangleShape()) )), column.setContent(lambda columnSlot: ( columnSlot.Text(lambda text: ( # 设置 Text 组件显示的文本 text.setText("Hello") )), columnSlot.Text(lambda text: ( # 设置 Text 组件显示的文本 text.setText(name) )) )) )) # 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 _composeView.create(lambda slot: ( # 插槽(Slot)是一种内容分发机制。它允许你在封装一个通用组件时,在组件内部预留一些“坑位”或“插槽”,然后让使用该组件的父组件来决定这些“坑位”里到底填充什么内容。 Greeting(slot, "M8Test") )) # 启动一个Activity用于显示脚本界面 _activity.start()
# encoding: utf-8 # 垂直方向上显示两个文本组件 def Greeting(slot, name) slot.Column do |column| # 设置 Column 的修饰符 column.setModifier do |modifier| # 这里加个背景色是为了更好的展示效果 modifier.background(lambda { |it| it.setRed(255) }, lambda { |shapes| shapes.getRectangleShape() }) end column.setContent do |columnSlot| columnSlot.Text do |text| # 设置 Text 组件显示的文本 text.setText("Hello") end columnSlot.Text do |text| # 设置 Text 组件显示的文本 text.setText(name) end end end end # 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 $composeView.create do |slot| # 插槽(Slot)是一种内容分发机制。它允许你在封装一个通用组件时,在组件内部预留一些“坑位”或“插槽”,然后让使用该组件的父组件来决定这些“坑位”里到底填充什么内容。 Greeting(slot, "M8Test") end # 启动一个Activity用于显示脚本界面 $activity.start()
431

关于此函数,有几点值得注意:

  • 此函数接收一个 Slot 类型参数,通过 Slot 可以创建需要的界面元素。

  • 此函数除了接收 Slot 之外还可以接受一些数据, 这些参数可让应用逻辑描述界面。在本例中,我们的 widget 接受一个 String,因此它可以按名称问候用户。

  • 此函数可以在界面中显示文本。为此,它会调用 Text() 可组合函数,该函数实际上会创建文本界面元素。可组合函数通过调用其他可组合函数来发出界面层次结构。

  • 此函数不会返回任何内容。发出界面的 Compose 函数不需要返回任何内容,因为它们描述所需的屏幕状态,而不是构造界面 widget。

  • 此函数快速、幂等且没有附带效应。

    • 使用同一参数多次调用此函数时,它的行为方式相同,并且它不使用其他值,如全局变量或对 random() 的调用。

    • 此函数描述界面而没有任何副作用,如修改属性或全局变量。

    一般来说,出于重组部分所述的原因,所有可组合函数都应使用这些属性来编写。

声明性范式转变

在许多面向对象的命令式界面工具包中,您可以通过实例化 widget 树来初始化界面。您通常通过膨胀 XML 布局文件来实现此目的。每个 widget 都维护自己的内部状态,并且提供 getter 和 setter 方法,允许应用逻辑与 widget 进行交互。

在 Compose 的声明性方法中,widget 相对无状态,并且不提供 setter 或 getter 函数。实际上,widget 不会以对象形式提供。您可以通过调用带有不同实参的同一可组合函数来更新界面。这使得向架构模式提供状态变得很容易,然后,可组合项负责在每次可观察数据更新时将当前应用状态转换为界面。

441

当用户与界面交互时,界面会发起 onClick 等事件。这些事件应通知应用逻辑,应用逻辑随后可以改变应用的状态。当状态发生变化时,系统会使用新数据再次调用可组合函数。这会导致重新绘制界面元素,此过程称为“重组”。

442

重组

在命令式界面模型中,如需更改某个 widget,您可以在该 widget 上调用 setter 以更改其内部状态。在 Compose 中,您可以使用新数据再次调用可组合函数。这样做会导致函数进行重组 -- 系统会根据需要使用新数据重新绘制函数发出的 widget。Compose 框架可以智能地仅重组已更改的组件。

例如,假设有以下可组合函数,它用于显示一个按钮:

import com.m8test.script.GlobalVariables.* import com.m8test.script.core.api.ui.compose.modifier.Modifier import com.m8test.script.core.api.ui.compose.slot.Slot import com.m8test.script.core.api.ui.compose.state.MutableState // 垂直方向上显示两个文本组件 private fun ClickCounter(slot: Slot<out Modifier>, clicks: MutableState<Int>, onClick: () -> Unit) { slot.Button { setOnClick { onClick() } setContent { Text { // 通过trackSingleState方法添加clicks,当clicks状态改变时, Text会重组 trackSingleState(clicks) setText("Click me ${clicks.getValue()}") } } } } fun side1Run() { // 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 _composeView.create { // 插槽(Slot)是一种内容分发机制。它允许你在封装一个通用组件时,在组件内部预留一些“坑位”或“插槽”,然后让使用该组件的父组件来决定这些“坑位”里到底填充什么内容。 val state = mutableStateOf(0) ClickCounter(this, state) { // 这里在按钮点击时会执行 state.setValue(state.getValue() + 1) } } // 启动一个Activity用于显示脚本界面 _activity.start() } //-m8test-remove side1Run();
import com.m8test.script.core.api.ui.compose.modifier.Modifier import com.m8test.script.core.api.ui.compose.slot.Slot import com.m8test.script.core.api.ui.compose.state.MutableState // 垂直方向上显示两个文本组件 private static def ClickCounter(Slot<? extends Modifier> slot, MutableState<Integer> clicks, Closure onClick) { slot.Button { button -> button.setOnClick { onClick() } button.setContent { buttonSlot -> buttonSlot.Text { text -> // 通过trackSingleState方法添加clicks,当clicks状态改变时, Text会重组 text.trackSingleState(clicks) text.setText("Click me " + clicks.value) } } } } // 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 $composeView.create { slot -> // 插槽(Slot)是一种内容分发机制。它允许你在封装一个通用组件时,在组件内部预留一些“坑位”或“插槽”,然后让使用该组件的父组件来决定这些“坑位”里到底填充什么内容。 def state = slot.mutableStateOf(0) ClickCounter(slot, state) { // 这里在按钮点击时会执行 state.setValue(state.getValue() + 1) } } // 启动一个Activity用于显示脚本界面 $activity.start()
/** * @param {Packages.com.m8test.script.core.api.ui.compose.slot.Slot<Packages.com.m8test.script.core.api.ui.compose.modifier.Modifier>} slot * @param {Packages.com.m8test.script.core.api.ui.compose.state.MutableState<number>} clicks * @param {() => void} onClick */ function ClickCounter(slot, clicks, onClick) { slot.Button(button => { button.setOnClick(() => onClick()); button.setContent(buttonSlot => { buttonSlot.Text(text => { // 通过trackSingleState方法添加clicks,当clicks状态改变时, Text会重组 text.trackSingleState(clicks); text.setText("Click me " + clicks.getValue()); }); }); }); } // 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 $composeView.create(slot => { // 插槽(Slot)是一种内容分发机制。它允许你在封装一个通用组件时,在组件内部预留一些“坑位”或“插槽”,然后让使用该组件的父组件来决定这些“坑位”里到底填充什么内容。 const state = slot.mutableStateOf(0); ClickCounter(slot, state, () => { // 这里在按钮点击时会执行 state.setValue(state.getValue() + 1); }); }); // 启动一个Activity用于显示脚本界面 $activity.start();
-- import com.m8test.script.core.api.ui.compose.modifier.Modifier -- import com.m8test.script.core.api.ui.compose.slot.Slot -- import com.m8test.script.core.api.ui.compose.state.MutableState -- (Lua中不需要显式import, require已在别处处理) -- 垂直方向上显示两个文本组件 local function ClickCounter(slot, clicks, onClick) slot:Button(function(button) button:setOnClick(onClick) button:setContent(function(buttonSlot) buttonSlot:Text(function(text) -- 通过trackSingleState方法添加clicks,当clicks状态改变时, Text会重组 text:trackSingleState(clicks) -- 同样使用 tostring() 规则 text:setText("Click me " .. tostring(clicks:getValue())) end) end) end) end -- 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 _composeView:create(function(slot) -- 插槽(Slot)是一种内容分发机制。它允许你在封装一个通用组件时,在组件内部预留一些“坑位”或“插槽”,然后让使用该组件的父组件来决定这些“坑位”里到底填充什么内容。 local state = slot:mutableStateOf(0) ClickCounter(slot, state, function() -- 这里在按钮点击时会执行 state:setValue(state:getValue() + 1) end) end) -- 启动一个Activity用于显示脚本界面 _activity:start()
<?php use m8test_java\com\m8test\script\core\api\ui\compose\modifier\Modifier; use m8test_java\com\m8test\script\core\api\ui\compose\slot\Slot; use m8test_java\com\m8test\script\core\api\ui\compose\state\MutableState; /** @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; // 垂直方向上显示两个文本组件 /** @param Slot $slot */ function ClickCounter($slot, MutableState $clicks, callable $onClick) { $slot->Button(function ($button) use ($onClick, $clicks) { $button->setOnClick(function () use ($onClick) { $onClick(); }); $button->setContent(function ($buttonSlot) use ($clicks) { $buttonSlot->Text(function ($text) use ($clicks) { // 通过trackSingleState方法添加clicks,当clicks状态改变时, Text会重组 $text->trackSingleState($clicks); $text->setText(javaString("Click me ") . $clicks->getValue()); }); }); }); } // 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 $composeView->create(function ($slot) { // 插槽(Slot)是一种内容分发机制。它允许你在封装一个通用组件时,在组件内部预留一些“坑位”或“插槽”,然后让使用该组件的父组件来决定这些“坑位”里到底填充什么内容。 $state = $slot->mutableStateOf(0); ClickCounter($slot, $state, function () use ($state) { // 这里在按钮点击时会执行 $state->setValue($state->getValue() + 1); }); }); // 启动一个Activity用于显示脚本界面 $activity->start();
# 导入所需的全局变量 from m8test_java.com.m8test.script.GlobalVariables import _composeView from m8test_java.com.m8test.script.GlobalVariables import _activity # 导入所需的Java类 from m8test_java.com.m8test.script.core.api.ui.compose.modifier.Modifier import Modifier from m8test_java.com.m8test.script.core.api.ui.compose.slot.Slot import Slot from m8test_java.com.m8test.script.core.api.ui.compose.state.MutableState import MutableState # 导入 typing 以便为回调函数添加类型提示 from typing import Callable # 垂直方向上显示两个文本组件 def ClickCounter(slot: Slot, clicks: MutableState, onClick: Callable): slot.Button(lambda button: ( button.setOnClick(lambda: onClick()), button.setContent(lambda buttonSlot: ( buttonSlot.Text(lambda text: ( # 通过trackSingleState方法添加clicks,当clicks状态改变时, Text会重组 text.trackSingleState(clicks), # 使用f-string进行字符串格式化,这是Python中更现代和推荐的方式 text.setText(f"Click me {clicks.getValue()}") )) )) )) # 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 _composeView.create(lambda slot: ( # 插槽(Slot)是一种内容分发机制。它允许你在封装一个通用组件时,在组件内部预留一些“坑位”或“插槽”,然后让使用该组件的父组件来决定这些“坑位”里到底填充什么内容。 (state := slot.mutableStateOf(0)), # Groovy中的 ClickCounter(slot, state) { ... } 语法糖 # 表示将闭包 { ... } 作为最后一个参数传递给 ClickCounter 函数 # 在Python中,我们需要明确地将lambda作为最后一个参数传入 ClickCounter(slot, state, lambda: ( # 这里在按钮点击时会执行 state.setValue(state.getValue() + 1) )) )) # 启动一个Activity用于显示脚本界面 _activity.start()
# encoding: utf-8 # 垂直方向上显示两个文本组件 # 在 Ruby 中,闭包(Block)通常作为最后一个隐式参数,或者显式声明为 &on_click def ClickCounter(slot, clicks, &on_click) slot.Button do |button| # 在 Ruby 中调用传入的 Block button.setOnClick { on_click.call } button.setContent do |buttonSlot| buttonSlot.Text do |text| # 通过trackSingleState方法添加clicks,当clicks状态改变时, Text会重组 text.trackSingleState(clicks) text.setText("Click me " + clicks.value.to_s) end end end end # 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 $composeView.create do |slot| # 插槽(Slot)是一种内容分发机制。它允许你在封装一个通用组件时,在组件内部预留一些“坑位”或“插槽”,然后让使用该组件的父组件来决定这些“坑位”里到底填充什么内容。 state = slot.mutableStateOf(0) # 在 Ruby 中,大括号或者 do...end 跟在方法调用后面即为传递 Block ClickCounter(slot, state) do # 这里在按钮点击时会执行 state.setValue(state.getValue() + 1) end end # 启动一个Activity用于显示脚本界面 $activity.start()
443

每次点击该按钮时,调用方都会更新 clicks 的值。Compose 会再次调用 lambda 与 Text 函数以显示新值;此过程称为“重组”。不依赖于该值的其他函数不会进行重组。

如前文所述,重组整个界面树在计算上成本高昂,因为会消耗计算能力并缩短电池续航时间。Compose 使用智能重组来解决此问题。

重组是指在输入更改时再次调用可组合函数的过程。当函数的输入更改时,会发生这种情况。当 Compose 根据新输入重组时,它仅调用可能已更改的函数或 lambda,而跳过其余函数或 lambda。通过跳过所有未更改参数的函数或 lambda,Compose 可以高效地重组。

切勿依赖于执行可组合函数所产生的附带效应,因为可能会跳过函数的重组。如果您这样做,用户可能会在您的应用中遇到奇怪且不可预测的行为。附带效应是指对应用的其余部分可见的任何更改。例如,以下操作全部都是危险的附带效应:

  • 写入共享对象的属性

  • 更新 ViewModel 中的可观察项

  • 更新共享偏好设置

可组合函数可能会像每一帧一样频繁地重新执行,例如在呈现动画时。可组合函数应快速执行,以避免在播放动画期间出现卡顿。如果您需要执行成本高昂的操作(例如从共享偏好设置读取数据),请在后台协程中执行,并将值结果作为参数传递给可组合函数。

本文档讨论了您在使用 Compose 时需要注意的事项:

  • 重组会跳过尽可能多的可组合函数和 lambda。

  • 重组是乐观的操作,可能会被取消。

  • 可组合函数可能会像动画的每一帧一样非常频繁地运行。

  • 可组合函数可以并行执行。

  • 可组合函数可以按任何顺序执行。

下面几部分将介绍如何构建可组合函数以支持重组。在每种情况下,最佳实践都是使可组合函数保持快速、幂等且没有附带效应。

重组会跳过尽可能多的内容

如果界面的某些部分无效,Compose 会尽力只重组需要更新的部分。这意味着,它可以跳过某些内容以重新运行单个按钮的可组合项,而不执行界面树中在其上面或下面的任何可组合项。

每个可组合函数和 lambda 都可以自行重组。以下示例演示了在呈现列表时重组如何跳过某些元素:

import com.m8test.script.GlobalVariables.* import com.m8test.script.core.api.ui.compose.modifier.Modifier import com.m8test.script.core.api.ui.compose.slot.Slot import com.m8test.script.core.api.ui.compose.state.MutableState import com.m8test.script.core.api.ui.compose.state.MutableStateList /** * 显示用户可以点击的带有标题的名称列表 */ private fun NamePicker(slot: Slot<out Modifier>, header: MutableState<String>, names: MutableStateList<String>, onNameClick: (String) -> Unit) { slot.Column { setContent { Text { // 当 [header] 发生改变时,它将被重新组合,但当 [names] 发生改变时,它将不会被重新组合 trackSingleState(header) setText(header.getValue()) } HorizontalDivider {} // LazyColumn 是 RecyclerView 的 Compose 版本。 // 传递给 items() 的 lambda 类似于 RecyclerView.ViewHolder LazyColumn { setContent { itemsIndexed(names, null, null) { index, name -> // 当某个项目的 [name] 更新时,该项目的适配器将进行重组。当 [header] 更改时,此适配器不会进行重组 NamePickerItem(this, name, onNameClick) } } } } } } /** * 显示用户可以点击的单个名称。 */ private fun NamePickerItem(slot: Slot<out Modifier>, name: String, onClick: (String) -> Unit) { slot.Text { setText(name) setModifier { clickable(true, null, null) { onClick(name) } } } } fun side1Run() { // 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 _composeView.create { val header = mutableStateOf("Header") val names = mutableStateListOf(_iterables.listOf("Alice", "Tom", "John")) NamePicker(this, header, names) { name -> // 点击时简单打印日志 _console.log(name) } } // 启动一个Activity用于显示脚本界面 _activity.start() } //-m8test-remove side1Run();
import com.m8test.script.core.api.ui.compose.modifier.Modifier import com.m8test.script.core.api.ui.compose.slot.Slot import com.m8test.script.core.api.ui.compose.state.MutableState import com.m8test.script.core.api.ui.compose.state.MutableStateList /** * 显示用户可以点击的带有标题的名称列表 */ private static def NamePicker(Slot<? extends Modifier> slot, MutableState<String> header, MutableStateList<String> names, Closure<String> onNameClick) { slot.Column { column -> column.setContent { columnScopeSlot -> columnScopeSlot.Text { text -> // 当 [header] 发生改变时,它将被重新组合,但当 [names] 发生改变时,它将不会被重新组合 text.trackSingleState(header) text.setText(header.getValue()) } columnScopeSlot.HorizontalDivider {} // LazyColumn 是 RecyclerView 的 Compose 版本。 // 传递给 items() 的 lambda 类似于 RecyclerView.ViewHolder columnScopeSlot.LazyColumn { lazyColumn -> lazyColumn.setContent { lazyListScope -> lazyListScope.itemsIndexed(names, null, null) { lazyItemScopeSlot, index, name -> // 当某个项目的 [name] 更新时,该项目的适配器将进行重组。当 [header] 更改时,此适配器不会进行重组 NamePickerItem(lazyItemScopeSlot, name, onNameClick) } } } } } } /** * 显示用户可以点击的单个名称。 */ private static def NamePickerItem(Slot<? extends Modifier> slot, String name, Closure<String> onClick) { slot.Text { text -> text.setText(name) text.setModifier { modifier -> modifier.clickable(true, null, null) { onClick(name) } } } } // 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 $composeView.create { slot -> def header = slot.mutableStateOf("Header") def names = slot.mutableStateListOf($iterables.listOf("Alice", "Tom", "John")) NamePicker(slot, header, names, ({ name -> // 点击时简单打印日志 $console.log(name) } as Closure<String>)) } // 启动一个Activity用于显示脚本界面 $activity.start()
/** * 显示用户可以点击的带有标题的名称列表 * @param {object} slot * @param {object} header * @param {object} names * @param {function(string): void} onNameClick */ function NamePicker(slot, header, names, onNameClick) { slot.Column(column => { column.setContent(columnScopeSlot => { columnScopeSlot.Text(text => { // 当 [header] 发生改变时,它将被重新组合,但当 [names] 发生改变时,它将不会被重新组合 text.trackSingleState(header); text.setText(header.getValue()); }); columnScopeSlot.HorizontalDivider(() => {}); // LazyColumn 是 RecyclerView 的 Compose 版本。 // 传递给 items() 的 lambda 类似于 RecyclerView.ViewHolder columnScopeSlot.LazyColumn(lazyColumn => { lazyColumn.setContent(lazyListScope => { lazyListScope.itemsIndexed(names, null, null, (lazyItemScopeSlot, index, name) => { // 当某个项目的 [name] 更新时,该项目的适配器将进行重组。当 [header] 更改时,此适配器不会进行重组 NamePickerItem(lazyItemScopeSlot, name, onNameClick); }); }); }); }); }); } /** * 显示用户可以点击的单个名称。 * @param {object} slot * @param {string} name * @param {function(string): void} onClick */ function NamePickerItem(slot, name, onClick) { slot.Text(text => { text.setText(name); text.setModifier(modifier => { modifier.clickable(true, null, null, () => onClick(name)); }); }); } // 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 $composeView.create(slot => { const header = slot.mutableStateOf("Header"); const names = slot.mutableStateListOf($iterables.listOf("Alice", "Tom", "John")); NamePicker(slot, header, names, name => { // 点击时简单打印日志 $console.log(name); }); }); // 启动一个Activity用于显示脚本界面 $activity.start();
--[[ * 显示用户可以点击的单个名称。 ]] local function NamePickerItem(slot, name, onClick) slot:Text(function(text) text:setText(name) text:setModifier(function(modifier) modifier:clickable(true, nil, nil, function() onClick(name) end) end) end) end --[[ * 显示用户可以点击的带有标题的名称列表 ]] local function NamePicker(slot, header, names, onNameClick) slot:Column(function(column) column:setContent(function(columnScopeSlot) columnScopeSlot:Text(function(text) -- 当 [header] 发生改变时,它将被重新组合,但当 [names] 发生改变时,它将不会被重新组合 text:trackSingleState(header) text:setText(tostring(header:getValue())) end) columnScopeSlot:HorizontalDivider(function() end) -- LazyColumn 是 RecyclerView 的 Compose 版本。 -- 传递给 items() 的 lambda 类似于 RecyclerView.ViewHolder columnScopeSlot:LazyColumn(function(lazyColumn) lazyColumn:setContent(function(lazyListScope) lazyListScope:itemsIndexed(names, nil, nil, function(lazyItemScopeSlot, index, name) -- 当某个项目的 [name] 更新时,该项目的适配器将进行重组。当 [header] 更改时,此适配器不会进行重组 NamePickerItem(lazyItemScopeSlot, name, onNameClick) end) end) end) end) end) end -- 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 _composeView:create(function(slot) local header = slot:mutableStateOf("Header") -- 假设 _iterables:listOf 存在,并接收一个 Lua table 来创建 Java List local names = slot:mutableStateListOf(_iterables:listOf("Alice", "Tom", "John")) NamePicker(slot, header, names, function(name) -- 点击时简单打印日志 _console:log(name) end) end) -- 启动一个Activity用于显示脚本界面 _activity:start()
<?php use m8test_java\com\m8test\script\core\api\ui\compose\modifier\Modifier; use m8test_java\com\m8test\script\core\api\ui\compose\slot\Slot; use m8test_java\com\m8test\script\core\api\ui\compose\state\MutableState; use m8test_java\com\m8test\script\core\api\ui\compose\state\MutableStateList; /** @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; /** @var m8test_java\com\m8test\script\core\api\collections\Iterables $iterables */ global $iterables; /** @var m8test_java\com\m8test\script\core\api\console\Console $console */ global $console; /** * 显示用户可以点击的单个名称。 * @param Slot $slot */ function NamePickerItem($slot, string $name, callable $onClick) { $slot->Text(function ($text) use ($name, $onClick) { $text->setText($name); $text->setModifier(function ($modifier) use ($name, $onClick) { $modifier->clickable(true, null, null, function () use ($name, $onClick) { $onClick($name); }); }); }); } /** * 显示用户可以点击的带有标题的名称列表 * @param Slot $slot */ function NamePicker($slot, MutableState $header, MutableStateList $names, callable $onNameClick) { $slot->Column(function ($column) use ($header, $names, $onNameClick) { $column->setContent(function ($columnScopeSlot) use ($header, $names, $onNameClick) { $columnScopeSlot->Text(function ($text) use ($header) { // 当 [header] 发生改变时,它将被重新组合,但当 [names] 发生改变时,它将不会被重新组合 $text->trackSingleState($header); $text->setText($header->getValue()); }); $columnScopeSlot->HorizontalDivider(function () { }); // LazyColumn 是 RecyclerView 的 Compose 版本。 // 传递给 items() 的 lambda 类似于 RecyclerView.ViewHolder $columnScopeSlot->LazyColumn(function ($lazyColumn) use ($names, $onNameClick) { $lazyColumn->setContent(function ($lazyListScope) use ($names, $onNameClick) { $lazyListScope->itemsIndexed($names, null, null, function ($lazyItemScopeSlot, $index, $name) use ($onNameClick) { // 当某个项目的 [name] 更新时,该项目的适配器将进行重组。当 [header] 更改时,此适配器不会进行重组 NamePickerItem($lazyItemScopeSlot, $name, $onNameClick); }); }); }); }); }); } // 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 $composeView->create(function ($slot) { global $iterables, $console; $header = $slot->mutableStateOf(javaString("Header")); $names = $slot->mutableStateListOf($iterables->listOf(javaString("Alice"), javaString("Tom"), javaString("John"))); NamePicker($slot, $header, $names, function ($name) { global $console; // 点击时简单打印日志 $console->log($name); }); }); // 启动一个Activity用于显示脚本界面 $activity->start();
# 导入所需的全局变量 from m8test_java.com.m8test.script.GlobalVariables import _composeView from m8test_java.com.m8test.script.GlobalVariables import _iterables from m8test_java.com.m8test.script.GlobalVariables import _console from m8test_java.com.m8test.script.GlobalVariables import _activity # 导入所需的Java类 from m8test_java.com.m8test.script.core.api.ui.compose.modifier.Modifier import Modifier from m8test_java.com.m8test.script.core.api.ui.compose.slot.Slot import Slot from m8test_java.com.m8test.script.core.api.ui.compose.state.MutableState import MutableState from m8test_java.com.m8test.script.core.api.ui.compose.state.MutableStateList import MutableStateList def NamePicker(slot, header, names, onNameClick): """ 显示用户可以点击的带有标题的名称列表 """ slot.Column(lambda column: ( column.setContent(lambda columnScopeSlot: ( columnScopeSlot.Text(lambda text: ( # 当 [header] 发生改变时,它将被重新组合,但当 [names] 发生改变时,它将不会被重新组合 text.trackSingleState(header), text.setText(header.getValue()) )), # 修复:将 lambda: None 修改为 lambda _: None # 框架会向此回调传递一个参数,即使我们用不到它,也必须定义一个形参来接收,否则会报TypeError columnScopeSlot.HorizontalDivider(lambda _: None), # LazyColumn 是 RecyclerView 的 Compose 版本。 # 传递给 items() 的 lambda 类似于 RecyclerView.ViewHolder columnScopeSlot.LazyColumn(lambda lazyColumn: ( lazyColumn.setContent(lambda lazyListScope: ( lazyListScope.itemsIndexed(names, None, None, lambda lazyItemScopeSlot, index, name: ( # 当某个项目的 [name] 更新时,该项目的适配器将进行重组。当 [header] 更改时,此适配器不会进行重组 NamePickerItem(lazyItemScopeSlot, name, onNameClick) )) )) )) )) )) def NamePickerItem(slot, name, onClick): """ 显示用户可以点击的单个名称。 """ slot.Text(lambda text: ( text.setText(name), text.setModifier(lambda modifier: ( # Groovy的 { onClick(name) } 是一个无参闭包,它调用了外部的onClick函数 # 对应Python的 lambda: onClick(name) modifier.clickable(True, None, None, lambda: onClick(name)) )) )) # 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 _composeView.create(lambda slot: ( (header := slot.mutableStateOf("Header")), (names := slot.mutableStateListOf(_iterables.listOf("Alice", "Tom", "John"))), NamePicker(slot, header, names, # Groovy的 ({ name -> ... } as Closure<String>) 直接变成Python的 lambda name: ... # 点击时简单打印日志 lambda name: _console.log(name) ) )) # 启动一个Activity用于显示脚本界面 _activity.start()
# encoding: utf-8 # 显示用户可以点击的带有标题的名称列表 # onNameClick 在 Ruby 中作为一个 Proc 对象传递 def NamePicker(slot, header, names, onNameClick) slot.Column do |column| column.setContent do |columnScopeSlot| columnScopeSlot.Text do |text| # 当 [header] 发生改变时,它将被重新组合,但当 [names] 发生改变时,它将不会被重新组合 text.trackSingleState(header) text.setText(header.getValue()) end columnScopeSlot.HorizontalDivider {} # LazyColumn 是 RecyclerView 的 Compose 版本。 # 传递给 items() 的 lambda 类似于 RecyclerView.ViewHolder columnScopeSlot.LazyColumn do |lazyColumn| lazyColumn.setContent do |lazyListScope| lazyListScope.itemsIndexed(names, nil, nil) do |lazyItemScopeSlot, index, name| # 当某个项目的 [name] 更新时,该项目的适配器将进行重组。当 [header] 更改时,此适配器不会进行重组 NamePickerItem(lazyItemScopeSlot, name, onNameClick) end end end end end end # 显示用户可以点击的单个名称。 def NamePickerItem(slot, name, onClick) slot.Text do |text| text.setText(name) text.setModifier do |modifier| modifier.clickable(true, nil, nil) { onClick.call(name) } end end end # 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 $composeView.create do |slot| header = slot.mutableStateOf("Header") names = slot.mutableStateListOf($iterables.listOf("Alice", "Tom", "John")) # 使用 lambda 或 proc 创建闭包传递 NamePicker(slot, header, names, lambda { |name| # 点击时简单打印日志 $console.log(name) }) end # 启动一个Activity用于显示脚本界面 $activity.start()

这些作用域中的每一个都可能是在重组期间执行的唯一一个作用域。当 header 发生更改时,Compose 可能会跳至 Column lambda,而不执行它的任何父项。此外,执行 Column 时,如果 names 未更改,Compose 可能会选择跳过 LazyColumn 的项。

同样,执行所有可组合函数或 lambda 都应该没有附带效应。当您需要执行附带效应时,应通过回调触发。

重组是乐观的操作

只要 Compose 认为某个可组合项的参数可能已更改,就会开始重组。重组是乐观的操作,也就是说,Compose 预计会在参数再次更改之前完成重组。如果某个参数在重组完成之前发生更改,Compose 可能会取消重组,并使用新参数重新开始。

取消重组后,Compose 会从重组中舍弃界面树。如有任何附带效应依赖于显示的界面,则即使取消了组合操作,也会应用该附带效应。这可能会导致应用状态不一致。

确保所有可组合函数和 lambda 都幂等且没有附带效应,以处理乐观的重组。

可组合函数可能会非常频繁地运行

在某些情况下,可能会针对界面动画的每一帧运行一个可组合函数。如果该函数执行成本高昂的操作(例如从设备存储空间读取数据),可能会导致界面卡顿。

例如,如果您的 widget 尝试读取设备设置,它可能会在一秒内读取这些设置数百次,这会对应用的性能造成灾难性的影响。

如果您的可组合函数需要数据,它应为相应的数据定义参数。然后,您可以将成本高昂的工作移至组成操作线程之外的其他线程,并使用 mutableStateOf 将相应的数据传递给 Compose。

可组合函数可以并行运行

Compose 可以通过并行运行可组合函数来优化重组。这样一来,Compose 就可以利用多个核心,并以较低的优先级运行可组合函数(不在屏幕上)。

这种优化意味着,可组合函数可能会在后台线程池中执行。 如果某个可组合函数对 ViewModel 调用一个函数,则 Compose 可能会同时从多个线程调用该函数。

为了确保应用正常运行,所有可组合函数都不应有附带效应,而应通过始终在界面线程上执行的 onClick 等回调触发附带效应。

调用某个可组合函数时,调用可能发生在与调用方不同的线程上。这意味着,应避免使用修改可组合 lambda 中的变量的代码,既因为此类代码并非线程安全代码,又因为它是可组合 lambda 不允许的附带效应。

以下示例展示了一个可组合项,它显示一个列表及其项数:

import com.m8test.script.GlobalVariables.* import com.m8test.script.core.api.ui.compose.modifier.Modifier import com.m8test.script.core.api.ui.compose.slot.Slot import com.m8test.script.core.api.ui.compose.state.MutableStateList // 这是正确的版本 private fun ListComposable(slot: Slot<out Modifier>, names: MutableStateList<String>) { slot.Column { setContent { Text { setText("count:${names.getSize()}") } LazyColumn { setContent { itemsIndexed(names, null, null) { index, name -> Text { setText(name) } } } } } } } // 这是错误的版本 private fun ListComposableWithBug(slot: Slot<out Modifier>, names: MutableStateList<String>) { var count = 0 slot.Column { setContent { LazyColumn { setContent { itemsIndexed(names, null, null) { index, name -> Text { setText(name) count++ // 避免附带效应,不应该在这里修改 count 值 } } } } Text { setText("count:$count") } } } } fun side1Run() { // 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 _composeView.create { val names = mutableStateListOf(_iterables.listOf("Alice", "Tom", "John")) ListComposable(this, names) } // 启动一个Activity用于显示脚本界面 _activity.start() } //-m8test-remove side1Run();
import com.m8test.script.core.api.ui.compose.modifier.Modifier import com.m8test.script.core.api.ui.compose.slot.Slot import com.m8test.script.core.api.ui.compose.state.MutableStateList // 这是正确的版本 private static def ListComposable(Slot<? extends Modifier> slot, MutableStateList<String> names) { slot.Column { column -> column.setContent { columnScopeSlot -> columnScopeSlot.Text { text -> text.setText("count:" + names.getSize()) } columnScopeSlot.LazyColumn { lazyColumn -> lazyColumn.setContent { lazyListScope -> lazyListScope.itemsIndexed(names, null, null) { lazyItemScopeSlot, index, name -> lazyItemScopeSlot.Text { text -> text.setText(name) } } } } } } } // 这是错误的版本 private static def ListComposableWithBug(Slot<? extends Modifier> slot, MutableStateList<String> names) { def count = 0 slot.Column { column -> column.setContent { columnScopeSlot -> columnScopeSlot.LazyColumn { lazyColumn -> lazyColumn.setContent { lazyListScope -> lazyListScope.itemsIndexed(names, null, null) { lazyItemScopeSlot, index, name -> lazyItemScopeSlot.Text { text -> text.setText(name) count++ // 避免附带效应,不应该在这里修改 count 值 } } } } columnScopeSlot.Text { text -> text.setText("count:" + count) } } } } // 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 $composeView.create { slot -> def names = slot.mutableStateListOf($iterables.listOf("Alice", "Tom", "John")) ListComposable(slot, names) } // 启动一个Activity用于显示脚本界面 $activity.start()
// 这是正确的版本 function ListComposable(slot, names) { slot.Column(column => { column.setContent(columnScopeSlot => { columnScopeSlot.Text(text => { text.setText("count:" + names.getSize()); }); columnScopeSlot.LazyColumn(lazyColumn => { lazyColumn.setContent(lazyListScope => { lazyListScope.itemsIndexed(names, null, null, (lazyItemScopeSlot, index, name) => { lazyItemScopeSlot.Text(text => { text.setText(name); }); }); }); }); }); }); } // 这是错误的版本 function ListComposableWithBug(slot, names) { let count = 0; slot.Column(column => { column.setContent(columnScopeSlot => { columnScopeSlot.LazyColumn(lazyColumn => { lazyColumn.setContent(lazyListScope => { lazyListScope.itemsIndexed(names, null, null, (lazyItemScopeSlot, index, name) => { lazyItemScopeSlot.Text(text => { text.setText(name); count++; // 避免附带效应,不应该在这里修改 count 值 }); }); }); }); columnScopeSlot.Text(text => { text.setText("count:" + count); }); }); }); } // 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 $composeView.create(slot => { const names = slot.mutableStateListOf($iterables.listOf("Alice", "Tom", "John")); ListComposable(slot, names); }); // 启动一个Activity用于显示脚本界面 $activity.start();
-- 这是正确的版本 local function ListComposable(slot, names) slot:Column(function(column) column:setContent(function(columnScopeSlot) columnScopeSlot:Text(function(text) text:setText("count:" .. names:getSize()) end) columnScopeSlot:LazyColumn(function(lazyColumn) lazyColumn:setContent(function(lazyListScope) lazyListScope:itemsIndexed(names, nil, nil, function(lazyItemScopeSlot, index, name) lazyItemScopeSlot:Text(function(text) text:setText(name) end) end) end) end) end) end) end -- 这是错误的版本 local function ListComposableWithBug(slot, names) local count = 0 slot:Column(function(column) column:setContent(function(columnScopeSlot) columnScopeSlot:LazyColumn(function(lazyColumn) lazyColumn:setContent(function(lazyListScope) lazyListScope:itemsIndexed(names, nil, nil, function(lazyItemScopeSlot, index, name) lazyItemScopeSlot:Text(function(text) text:setText(name) count = count + 1 -- 避免附带效应,不应该在这里修改 count 值 end) end) end) end) columnScopeSlot:Text(function(text) text:setText("count:" .. count) end) end) end) end -- 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 _composeView:create(function(slot) local names = slot:mutableStateListOf(_iterables:listOf("Alice", "Tom", "John")) ListComposable(slot, names) end) -- 启动一个Activity用于显示脚本界面 _activity:start()
<?php use m8test_java\com\m8test\script\core\api\ui\compose\modifier\Modifier; use m8test_java\com\m8test\script\core\api\ui\compose\slot\Slot; use m8test_java\com\m8test\script\core\api\ui\compose\state\MutableStateList; /** @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; /** @var m8test_java\com\m8test\script\core\api\collections\Iterables $iterables */ global $iterables; // 这是正确的版本 /** @param Slot $slot */ function ListComposable($slot, MutableStateList $names) { $slot->Column(function ($column) use ($names) { $column->setContent(function ($columnScopeSlot) use ($names) { $columnScopeSlot->Text(function ($text) use ($names) { $text->setText(javaString("count:") . $names->getSize()); }); $columnScopeSlot->LazyColumn(function ($lazyColumn) use ($names) { $lazyColumn->setContent(function ($lazyListScope) use ($names) { $lazyListScope->itemsIndexed($names, null, null, function ($lazyItemScopeSlot, $index, $name) { $lazyItemScopeSlot->Text(function ($text) use ($name) { $text->setText($name); }); }); }); }); }); }); } // 这是错误的版本 /** @param Slot $slot */ function ListComposableWithBug($slot, MutableStateList $names) { $count = 0; $slot->Column(function ($column) use ($names, &$count) { $column->setContent(function ($columnScopeSlot) use ($names, &$count) { $columnScopeSlot->LazyColumn(function ($lazyColumn) use ($names, &$count) { $lazyColumn->setContent(function ($lazyListScope) use ($names, &$count) { $lazyListScope->itemsIndexed($names, null, null, function ($lazyItemScopeSlot, $index, $name) use (&$count) { $lazyItemScopeSlot->Text(function ($text) use ($name, &$count) { $text->setText($name); $count++; // 避免附带效应,不应该在这里修改 count 值 }); }); }); }); $columnScopeSlot->Text(function ($text) use ($count) { $text->setText(javaString("count:") . $count); }); }); }); } // 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 $composeView->create(function ($slot) { global $iterables; $names = $slot->mutableStateListOf($iterables->listOf(javaString("Alice"), javaString("Tom"), javaString("John"))); ListComposable($slot, $names); }); // 启动一个Activity用于显示脚本界面 $activity->start();
# 导入所需的全局变量 from m8test_java.com.m8test.script.GlobalVariables import _composeView from m8test_java.com.m8test.script.GlobalVariables import _iterables from m8test_java.com.m8test.script.GlobalVariables import _activity # 导入所需的Java类 from m8test_java.com.m8test.script.core.api.ui.compose.modifier.Modifier import Modifier from m8test_java.com.m8test.script.core.api.ui.compose.slot.Slot import Slot from m8test_java.com.m8test.script.core.api.ui.compose.state.MutableStateList import MutableStateList # 这是正确的版本 def ListComposable(slot, names): slot.Column(lambda column: ( column.setContent(lambda columnScopeSlot: ( columnScopeSlot.Text(lambda text: ( text.setText("count:" + str(names.getSize())) )), columnScopeSlot.LazyColumn(lambda lazyColumn: ( lazyColumn.setContent(lambda lazyListScope: ( lazyListScope.itemsIndexed(names, None, None, lambda lazyItemScopeSlot, index, name: ( lazyItemScopeSlot.Text(lambda text: ( text.setText(name) )) )) )) )) )) )) # 这是错误的版本 def ListComposableWithBug(slot, names): # 在Python中,lambda不能直接修改外部作用域的简单类型变量(如整数)。 # 为了模拟Groovy闭包可以直接修改外部变量的行为(附带效应), # 我们使用一个可变对象(如单元素列表)来包装count值。 count = [0] slot.Column(lambda column: ( column.setContent(lambda columnScopeSlot: ( columnScopeSlot.LazyColumn(lambda lazyColumn: ( lazyColumn.setContent(lambda lazyListScope: ( lazyListScope.itemsIndexed(names, None, None, lambda lazyItemScopeSlot, index, name: ( lazyItemScopeSlot.Text(lambda text: ( text.setText(name), # 避免附带效应,不应该在这里修改 count 值 # 这里通过修改列表的第一个元素来模拟 count++ (count.__setitem__(0, count[0] + 1)) )) )) )) )), columnScopeSlot.Text(lambda text: ( # 读取时也从列表中获取 text.setText("count:" + str(count[0])) )) )) )) # 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 _composeView.create(lambda slot: ( (names := slot.mutableStateListOf(_iterables.listOf("Alice", "Tom", "John"))), ListComposable(slot, names) # 如果要测试错误版本,请取消下面一行的注释,并注释掉上面一行 # ListComposableWithBug(slot, names) )) # 启动一个Activity用于显示脚本界面 _activity.start()
# encoding: utf-8 # 这是正确的版本 def ListComposable(slot, names) slot.Column do |column| column.setContent do |columnScopeSlot| columnScopeSlot.Text do |text| text.setText("count:" + names.getSize().to_s) end columnScopeSlot.LazyColumn do |lazyColumn| lazyColumn.setContent do |lazyListScope| lazyListScope.itemsIndexed(names, nil, nil) do |lazyItemScopeSlot, index, name| lazyItemScopeSlot.Text do |text| text.setText(name) end end end end end end end # 这是错误的版本 def ListComposableWithBug(slot, names) count = 0 slot.Column do |column| column.setContent do |columnScopeSlot| columnScopeSlot.LazyColumn do |lazyColumn| lazyColumn.setContent do |lazyListScope| lazyListScope.itemsIndexed(names, nil, nil) do |lazyItemScopeSlot, index, name| lazyItemScopeSlot.Text do |text| text.setText(name) count += 1 # 避免附带效应,不应该在这里修改 count 值 end end end end columnScopeSlot.Text do |text| text.setText("count:" + count.to_s) end end end end # 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 $composeView.create do |slot| names = slot.mutableStateListOf($iterables.listOf("Alice", "Tom", "John")) ListComposable(slot, names) end # 启动一个Activity用于显示脚本界面 $activity.start()

ListComposable 代码没有附带效应,它会将输入列表转换为界面。此代码非常适合显示小列表。不过,如果函数写入局部变量,则这并非线程安全或正确的代码, 也就是 ListComposableWithBug,每次重组时,都会修改 count, 这可以是动画的每一帧,或是在列表更新时。但不管怎样,界面都会显示错误的项数。因此,Compose 不支持这样的写入操作;通过禁止此类写入操作,我们允许框架更改线程以执行可组合 lambda。

可组合函数可以按任何顺序执行

如果您看一下可组合函数的代码,可能会认为这些代码按其出现的顺序运行。但不能保证此属性为 true。如果某个可组合函数包含对其他可组合函数的调用,这些函数可以按任何顺序运行。Compose 可以选择识别出某些界面元素的优先级高于其他界面元素,因而首先绘制这些元素。

例如,假设您有如下代码,用于在标签页布局中绘制三个屏幕:

import com.m8test.script.GlobalVariables.* import com.m8test.script.core.api.ui.compose.modifier.Modifier import com.m8test.script.core.api.ui.compose.slot.Slot private fun StartScreen(slot: Slot<out Modifier>) { // 这里只是简单的显示一个文本,实际可能是一个较为复杂的界面 slot.Text { setText("StartScreen") } } private fun MiddleScreen(slot: Slot<out Modifier>) { // 这里只是简单的显示一个文本,实际可能是一个较为复杂的界面 slot.Text { setText("MiddleScreen") } } private fun EndScreen(slot: Slot<out Modifier>) { // 这里只是简单的显示一个文本,实际可能是一个较为复杂的界面 slot.Text { setText("EndScreen") } } private fun ButtonRow(slot: Slot<out Modifier>) { slot.Row { setModifier { fillMaxWidth(1f) } setHorizontalArrangement { it.getSpaceEvenly() } setContent { StartScreen(this) MiddleScreen(this) EndScreen(this) } } } fun side1Run() { // 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 _composeView.create { ButtonRow(this) } // 启动一个Activity用于显示脚本界面 _activity.start() } //-m8test-remove side1Run();
import com.m8test.script.core.api.ui.compose.modifier.Modifier import com.m8test.script.core.api.ui.compose.slot.Slot private static def StartScreen(Slot<? extends Modifier> slot) { // 这里只是简单的显示一个文本,实际可能是一个较为复杂的界面 slot.Text { text -> text.setText("StartScreen") } } private static def MiddleScreen(Slot<? extends Modifier> slot) { // 这里只是简单的显示一个文本,实际可能是一个较为复杂的界面 slot.Text { text -> text.setText("MiddleScreen") } } private static def EndScreen(Slot<? extends Modifier> slot) { // 这里只是简单的显示一个文本,实际可能是一个较为复杂的界面 slot.Text { text -> text.setText("EndScreen") } } private static def ButtonRow(Slot<? extends Modifier> slot) { slot.Row { row -> row.setModifier { it.fillMaxWidth(1f) } row.setHorizontalArrangement { it.getSpaceEvenly() } row.setContent { rowScopeSlot -> StartScreen(rowScopeSlot) MiddleScreen(rowScopeSlot) EndScreen(rowScopeSlot) } } } // 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 $composeView.create { slot -> ButtonRow(slot) } // 启动一个Activity用于显示脚本界面 $activity.start()
// import com.m8test.script.core.api.ui.compose.modifier.Modifier // import com.m8test.script.core.api.ui.compose.slot.Slot // 注意:上面的 import 语句在 Groovy 中用于类型提示,在 JavaScript 中不需要,因此已注释掉。 function StartScreen(slot) { // 这里只是简单的显示一个文本,实际可能是一个较为复杂的界面 slot.Text(text => { text.setText("StartScreen"); }); } function MiddleScreen(slot) { // 这里只是简单的显示一个文本,实际可能是一个较为复杂的界面 slot.Text(text => { text.setText("MiddleScreen"); }); } function EndScreen(slot) { // 这里只是简单的显示一个文本,实际可能是一个较为复杂的界面 slot.Text(text => { text.setText("EndScreen"); }); } function ButtonRow(slot) { slot.Row(row => { row.setModifier(it => it.fillMaxWidth(1.0)); row.setHorizontalArrangement(it => it.getSpaceEvenly()); row.setContent(rowScopeSlot => { StartScreen(rowScopeSlot); MiddleScreen(rowScopeSlot); EndScreen(rowScopeSlot); }); }); } // 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 $composeView.create(slot => { ButtonRow(slot); }); // 启动一个Activity用于显示脚本界面 $activity.start();
local function StartScreen(slot) -- 这里只是简单的显示一个文本,实际可能是一个较为复杂的界面 slot:Text(function(text) text:setText("StartScreen") end) end local function MiddleScreen(slot) -- 这里只是简单的显示一个文本,实际可能是一个较为复杂的界面 slot:Text(function(text) text:setText("MiddleScreen") end) end local function EndScreen(slot) -- 这里只是简单的显示一个文本,实际可能是一个较为复杂的界面 slot:Text(function(text) text:setText("EndScreen") end) end local function ButtonRow(slot) slot:Row(function(row) row:setModifier(function(it) it:fillMaxWidth(1) end) row:setHorizontalArrangement(function(it) return it:getSpaceEvenly() end) row:setContent(function(rowScopeSlot) StartScreen(rowScopeSlot) MiddleScreen(rowScopeSlot) EndScreen(rowScopeSlot) end) end) end -- 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 _composeView:create(function(slot) ButtonRow(slot) end) -- 启动一个Activity用于显示脚本界面 _activity:start()
<?php use m8test_java\com\m8test\script\core\api\ui\compose\modifier\Modifier; use m8test_java\com\m8test\script\core\api\ui\compose\slot\Slot; /** @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; /** @param Slot $slot */ function StartScreen($slot) { // 这里只是简单的显示一个文本,实际可能是一个较为复杂的界面 $slot->Text(function ($text) { $text->setText(javaString("StartScreen")); }); } /** @param Slot $slot */ function MiddleScreen($slot) { // 这里只是简单的显示一个文本,实际可能是一个较为复杂的界面 $slot->Text(function ($text) { $text->setText(javaString("MiddleScreen")); }); } /** @param Slot $slot */ function EndScreen($slot) { // 这里只是简单的显示一个文本,实际可能是一个较为复杂的界面 $slot->Text(function ($text) { $text->setText(javaString("EndScreen")); }); } /** @param Slot $slot */ function ButtonRow($slot) { $slot->Row(function ($row) { $row->setModifier(function ($it) { $it->fillMaxWidth(1.0); }); $row->setHorizontalArrangement(function ($it) { return $it->getSpaceEvenly(); }); $row->setContent(function ($rowScopeSlot) { StartScreen($rowScopeSlot); MiddleScreen($rowScopeSlot); EndScreen($rowScopeSlot); }); }); } // 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 $composeView->create(function ($slot) { ButtonRow($slot); }); // 启动一个Activity用于显示脚本界面 $activity->start();
# 导入所需的全局变量 from m8test_java.com.m8test.script.GlobalVariables import _activity from m8test_java.com.m8test.script.GlobalVariables import _composeView # 导入所需的Java类 from m8test_java.com.m8test.script.core.api.ui.compose.modifier.Modifier import Modifier from m8test_java.com.m8test.script.core.api.ui.compose.slot.Slot import Slot def StartScreen(slot): # 这里只是简单的显示一个文本,实际可能是一个较为复杂的界面 slot.Text(lambda text: ( text.setText("StartScreen") )) def MiddleScreen(slot): # 这里只是简单的显示一个文本,实际可能是一个较为复杂的界面 slot.Text(lambda text: ( text.setText("MiddleScreen") )) def EndScreen(slot): # 这里只是简单的显示一个文本,实际可能是一个较为复杂的界面 slot.Text(lambda text: ( text.setText("EndScreen") )) def ButtonRow(slot): slot.Row(lambda row: ( row.setModifier(lambda it: it.fillMaxWidth(1.0)), row.setHorizontalArrangement(lambda it: it.getSpaceEvenly()), row.setContent(lambda rowScopeSlot: ( StartScreen(rowScopeSlot), MiddleScreen(rowScopeSlot), EndScreen(rowScopeSlot) )) )) # 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 _composeView.create(lambda slot: ( ButtonRow(slot) )) # 启动一个Activity用于显示脚本界面 _activity.start()
# encoding: utf-8 def StartScreen(slot) # 这里只是简单的显示一个文本,实际可能是一个较为复杂的界面 slot.Text do |text| text.setText("StartScreen") end end def MiddleScreen(slot) # 这里只是简单的显示一个文本,实际可能是一个较为复杂的界面 slot.Text do |text| text.setText("MiddleScreen") end end def EndScreen(slot) # 这里只是简单的显示一个文本,实际可能是一个较为复杂的界面 slot.Text do |text| text.setText("EndScreen") end end def ButtonRow(slot) slot.Row do |row| row.setModifier { |it| it.fillMaxWidth(1.0) } row.setHorizontalArrangement { |it| it.getSpaceEvenly() } row.setContent do |rowScopeSlot| StartScreen(rowScopeSlot) MiddleScreen(rowScopeSlot) EndScreen(rowScopeSlot) end end end # 创建一个 ComposeView, 可以通过声明式ui创建脚本界面 $composeView.create do |slot| ButtonRow(slot) end # 启动一个Activity用于显示脚本界面 $activity.start()

对 StartScreen、MiddleScreen 和 EndScreen 的调用可以按任何顺序进行。这意味着,举例来说,您不能让 StartScreen() 设置某个全局变量(附带效应)并让 MiddleScreen() 利用这项更改。相反,其中每个函数都需要保持独立。

09 December 2025