与 Compose 的区别
M8Test 的声明式UI框架设计参考了 Jetpack Compose,两者的大部分概念相似,以下是主要差异点:
remember
M8Test 中没有 remember 概念,在创建 Slot 的闭包中,可直接通过 Slot.mutableStateOf 创建可变状态,以实现类似 remember 的功能。这是因为 slot 闭包只会执行一次,不受重组影响。
import com.m8test.script.GlobalVariables.*
fun side1Run() {
// 创建一个 ComposeView, 可以通过声明式ui创建脚本界面
_composeView.create {
// 1. 创建 state 必须要在 slot 闭包中,因为这个代码只会执行一次,不会重组
val name = mutableStateOf("")
OutlinedTextField {
// 2. 让 TextField 追踪 name 状态, 当 name 改变时,TextField 会重组
trackSingleState(name)
// 3. 使用 name 状态的值
setValue(name.getValue())
setOnValueChange { newValue ->
// 4. 更新 name 状态的值
name.setValue(newValue)
}
}
}
// 启动一个Activity用于显示脚本界面
_activity.start()
}
//-m8test-remove side1Run();
// 创建一个 ComposeView, 可以通过声明式ui创建脚本界面
$composeView.create { slot ->
// 1. 创建 state 必须要在 slot 闭包中,因为这个代码只会执行一次,不会重组
def name = slot.mutableStateOf("")
slot.OutlinedTextField { textField ->
// 2. 让 TextField 追踪 name 状态, 当 name 改变时,TextField 会重组
textField.trackSingleState(name)
// 3. 使用 name 状态的值
textField.setValue(name.getValue())
textField.setOnValueChange { newValue ->
// 4. 更新 name 状态的值
name.setValue(newValue)
}
}
}
// 启动一个Activity用于显示脚本界面
$activity.start()
// 创建一个 ComposeView, 可以通过声明式ui创建脚本界面
$composeView.create(slot => {
// 1. 创建 state 必须要在 slot 闭包中,因为这个代码只会执行一次,不会重组
const name = slot.mutableStateOf("");
slot.OutlinedTextField(textField => {
// 2. 让 TextField 追踪 name 状态, 当 name 改变时,TextField 会重组
textField.trackSingleState(name);
// 3. 使用 name 状态的值
textField.setValue(name.getValue());
textField.setOnValueChange(newValue => {
// 4. 更新 name 状态的值
name.setValue(newValue);
});
});
});
// 启动一个Activity用于显示脚本界面
$activity.start();
-- 创建一个 ComposeView, 可以通过声明式ui创建脚本界面
_composeView:create(function(slot)
-- 1. 创建 state 必须要在 slot 闭包中,因为这个代码只会执行一次,不会重组
local name = slot:mutableStateOf("")
slot:OutlinedTextField(function(textField)
-- 2. 让 TextField 追踪 name 状态, 当 name 改变时,TextField 会重组
textField:trackSingleState(name)
-- 3. 使用 name 状态的值
textField:setValue(name:getValue())
textField:setOnValueChange(function(newValue)
-- 4. 更新 name 状态的值
name:setValue(newValue)
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 闭包中,因为这个代码只会执行一次,不会重组
$name = $slot->mutableStateOf(javaString(""));
$slot->OutlinedTextField(function ($textField) use ($name) {
// 2. 让 TextField 追踪 name 状态, 当 name 改变时,TextField 会重组
$textField->trackSingleState($name);
// 3. 使用 name 状态的值
$textField->setValue($name->getValue());
$textField->setOnValueChange(function ($newValue) use ($name) {
// 4. 更新 name 状态的值
$name->setValue($newValue);
});
});
});
// 启动一个Activity用于显示脚本界面
$activity->start();
# 导入所需的全局变量
from m8test_java.com.m8test.script.GlobalVariables import _composeView
from m8test_java.com.m8test.script.GlobalVariables import _activity
# 创建一个 ComposeView, 可以通过声明式ui创建脚本界面
_composeView.create(lambda slot: (
# 1. 创建 state 必须要在 slot 闭包中,因为这个代码只会执行一次,不会重组
(name := slot.mutableStateOf("")),
slot.OutlinedTextField(lambda textField: (
# 2. 让 TextField 追踪 name 状态, 当 name 改变时,TextField 会重组
textField.trackSingleState(name),
# 3. 使用 name 状态的值
textField.setValue(name.getValue()),
textField.setOnValueChange(lambda newValue: (
# 4. 更新 name 状态的值
name.setValue(newValue)
))
))
))
# 启动一个Activity用于显示脚本界面
_activity.start()
# encoding: utf-8
# 创建一个 ComposeView, 可以通过声明式ui创建脚本界面
$composeView.create do |slot|
# 1. 创建 state 必须要在 slot 闭包中,因为这个代码只会执行一次,不会重组
name = slot.mutableStateOf("")
slot.OutlinedTextField do |textField|
# 2. 让 TextField 追踪 name 状态, 当 name 改变时,TextField 会重组
textField.trackSingleState(name)
# 3. 使用 name 状态的值
textField.setValue(name.getValue())
textField.setOnValueChange do |newValue|
# 4. 更新 name 状态的值
name.setValue(newValue)
end
end
end
# 启动一个Activity用于显示脚本界面
$activity.start()
@Composable 注解
M8Test 中没有 @Composable 注解,其功能由 Slot 替代: @Composable 注解的方法对应 Slot.xxx 形式。例如,Material Design 3 的 TextField 在 Compose 中是 @Composable 注解的方法,在 M8Test 中对应 Slot.TextField。
若要定义自定义组件(类似 @Composable 函数),可定义一个接收 Slot 对象作为参数的函数。若函数包含多个参数,建议将 Slot 放在首位。
状态
Jetpack Compose 中编译器会自动跟踪状态,而 M8Test 中需要通过 Composable.trackSingleState 显式指定需要跟踪的状态。当状态改变时,M8Test 会检测到变化,使调用 trackSingleState 的 Composable 重组以更新界面。
自定义组件参数
M8Test 中,自定义组件若需在状态改变时重组,必须传递 MutableState<Type> 对象;而 Jetpack Compose 既可以传递 State<Type> 对象,也可以直接传递 Type 对象。
显示和隐藏组件
在 Jetpack Compose 中,可以通过 if (xxx) { XXXComponent() } 的形式控制组件显示状态:当 xxx 为 true 时组件显示,为 false 时组件隐藏。
但在 M8Test 中,无法通过这种方式实现,需要使用 Composable.setVisible(Boolean) 方法控制组件的显示与隐藏:参数为 true 时组件显示,为 false 时组件隐藏。
该方法也可结合状态使用,示例代码如下:
import com.m8test.script.GlobalVariables.*
fun side1Run() {
// 创建一个 ComposeView, 可以通过声明式ui创建脚本界面
_composeView.create {
// 插槽(Slot)是一种内容分发机制。它允许你在封装一个通用组件时,在组件内部预留一些“坑位”或“插槽”,然后让使用该组件的父组件来决定这些“坑位”里到底填充什么内容。
// 创建一个Column,Column是一种垂直布局,可以让其中的组件垂直排列
Column {
// 设置 Column 中 content 插槽的内容
setContent {
// 1. 创建一个可变状态,用于显示/隐藏文本
val showTextComponent = mutableStateOf(true)
// 添加一个文件组件到 Column (垂直布局)中
Text {
// 2. 在组件的某个属性中使用该状态的值
setVisible(showTextComponent.getValue())
setText("我是文本")
// 3. 将状态添加到组件中,这样text组件就会监听该状态的改变,这个是必须的步骤,如果没有此步骤,那么 showTextComponent 的值改变时,composable(text组件)不会重组(界面更新)
trackSingleState(showTextComponent)
}
// 添加一个文本按钮到 Column (垂直布局)中
TextButton {
// 设置文本按钮的content插槽内容
setContent {
// 在插槽中添加一个文本
Text {
setText("点击我")
}
}
// 设置按钮点击事件
setOnClick {
// 4. 当按钮被点击时,更新状态的值,必须要调用 trackSingleState 方法的组件才会重组
showTextComponent.setValue(!showTextComponent.getValue())
}
}
}
}
}
// 启动一个Activity用于显示脚本界面
_activity.start()
}
//-m8test-remove side1Run();
// 创建一个 ComposeView, 可以通过声明式ui创建脚本界面
$composeView.create { slot ->
// 插槽(Slot)是一种内容分发机制。它允许你在封装一个通用组件时,在组件内部预留一些“坑位”或“插槽”,然后让使用该组件的父组件来决定这些“坑位”里到底填充什么内容。
// 创建一个Column,Column是一种垂直布局,可以让其中的组件垂直排列
slot.Column { column ->
// 设置 Column 中 content 插槽的内容
column.setContent { columnSlot ->
// 1. 创建一个可变状态,用于显示/隐藏文本
def showTextComponent = slot.mutableStateOf(true)
// 添加一个文件组件到 Column (垂直布局)中
columnSlot.Text { text ->
// 2. 在组件的某个属性中使用该状态的值
text.setVisible(showTextComponent.getValue())
text.setText("我是文本")
// 3. 将状态添加到组件中,这样text组件就会监听该状态的改变,这个是必须的步骤,如果没有此步骤,那么 showTextComponent 的值改变时,composable(text组件)不会重组(界面更新)
text.trackSingleState(showTextComponent)
}
// 添加一个文本按钮到 Column (垂直布局)中
columnSlot.TextButton { button ->
// 设置文本按钮的content插槽内容
button.setContent { buttonSlot ->
// 在插槽中添加一个文本
buttonSlot.Text { text ->
text.setText("点击我")
}
}
// 设置按钮点击事件
button.setOnClick {
// 4. 当按钮被点击时,更新状态的值,必须要调用 trackSingleState 方法的组件才会重组
showTextComponent.setValue(!showTextComponent.getValue())
}
}
}
}
}
// 启动一个Activity用于显示脚本界面
$activity.start()
// 创建一个 ComposeView, 可以通过声明式ui创建脚本界面
$composeView.create(slot => {
// 插槽(Slot)是一种内容分发机制。它允许你在封装一个通用组件时,在组件内部预留一些“坑位”或“插槽”,然后让使用该组件的父组件来决定这些“坑位”里到底填充什么内容。
// 创建一个Column,Column是一种垂直布局,可以让其中的组件垂直排列
slot.Column(column => {
// 设置 Column 中 content 插槽的内容
column.setContent(columnSlot => {
// 1. 创建一个可变状态,用于显示/隐藏文本
const showTextComponent = slot.mutableStateOf(true);
// 添加一个文件组件到 Column (垂直布局)中
columnSlot.Text(text => {
// 2. 在组件的某个属性中使用该状态的值
text.setVisible(showTextComponent.getValue() == true);
text.setText("我是文本");
// 3. 将状态添加到组件中,这样text组件就会监听该状态的改变,这个是必须的步骤,如果没有此步骤,那么 showTextComponent 的值改变时,composable(text组件)不会重组(界面更新)
text.trackSingleState(showTextComponent);
});
// 添加一个文本按钮到 Column (垂直布局)中
columnSlot.TextButton(button => {
// 设置文本按钮的content插槽内容
button.setContent(buttonSlot => {
// 在插槽中添加一个文本
buttonSlot.Text(text => {
text.setText("点击我");
});
});
// 设置按钮点击事件
button.setOnClick(() => {
// 4. 当按钮被点击时,更新状态的值,必须要调用 trackSingleState 方法的组件才会重组
if (showTextComponent.getValue() == true) {
showTextComponent.setValue(false);
} else {
showTextComponent.setValue(true);
}
});
});
});
});
});
// 启动一个Activity用于显示脚本界面
$activity.start();
-- 创建一个 ComposeView, 可以通过声明式ui创建脚本界面
_composeView:create(function(slot)
-- 插槽(Slot)是一种内容分发机制。它允许你在封装一个通用组件时,在组件内部预留一些“坑位”或“插槽”,然后让使用该组件的父组件来决定这些“坑位”里到底填充什么内容。
-- 创建一个Column,Column是一种垂直布局,可以让其中的组件垂直排列
slot:Column(function(column)
-- 设置 Column 中 content 插槽的内容
column:setContent(function(columnSlot)
-- 1. 创建一个可变状态,用于显示/隐藏文本
local showTextComponent = slot:mutableStateOf(true)
-- 添加一个文件组件到 Column (垂直布局)中
columnSlot:Text(function(text)
-- 2. 在组件的某个属性中使用该状态的值
text:setVisible(showTextComponent:getValue())
text:setText("我是文本")
-- 3. 将状态添加到组件中,这样text组件就会监听该状态的改变,这个是必须的步骤,如果没有此步骤,那么 showTextComponent 的值改变时,composable(text组件)不会重组(界面更新)
text:trackSingleState(showTextComponent)
end)
-- 添加一个文本按钮到 Column (垂直布局)中
columnSlot:TextButton(function(button)
-- 设置文本按钮的content插槽内容
button:setContent(function(buttonSlot)
-- 在插槽中添加一个文本
buttonSlot:Text(function(text)
text:setText("点击我")
end)
end)
-- 设置按钮点击事件
button:setOnClick(function()
-- 4. 当按钮被点击时,更新状态的值,必须要调用 trackSingleState 方法的组件才会重组
showTextComponent:setValue(not showTextComponent:getValue())
end)
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) {
// 插槽(Slot)是一种内容分发机制。它允许你在封装一个通用组件时,在组件内部预留一些“坑位”或“插槽”,然后让使用该组件的父组件来决定这些“坑位”里到底填充什么内容。
// 创建一个Column,Column是一种垂直布局,可以让其中的组件垂直排列
$slot->Column(function ($column) use ($slot) {
// 设置 Column 中 content 插槽的内容
$column->setContent(function ($columnSlot) use ($slot) {
// 1. 创建一个可变状态,用于显示/隐藏文本
$showTextComponent = $slot->mutableStateOf(true);
// 添加一个文件组件到 Column (垂直布局)中
$columnSlot->Text(function ($text) use ($showTextComponent) {
// 2. 在组件的某个属性中使用该状态的值
$text->setVisible($showTextComponent->getValue());
$text->setText(javaString("我是文本"));
// 3. 将状态添加到组件中,这样text组件就会监听该状态的改变,这个是必须的步骤,如果没有此步骤,那么 showTextComponent 的值改变时,composable(text组件)不会重组(界面更新)
$text->trackSingleState($showTextComponent);
});
// 添加一个文本按钮到 Column (垂直布局)中
$columnSlot->TextButton(function ($button) use ($showTextComponent) {
// 设置文本按钮的content插槽内容
$button->setContent(function ($buttonSlot) {
// 在插槽中添加一个文本
$buttonSlot->Text(function ($text) {
$text->setText(javaString("点击我"));
});
});
// 设置按钮点击事件
$button->setOnClick(function () use ($showTextComponent) {
// 4. 当按钮被点击时,更新状态的值,必须要调用 trackSingleState 方法的组件才会重组
$showTextComponent->setValue(!$showTextComponent->getValue());
});
});
});
});
});
// 启动一个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: (
# 插槽(Slot)是一种内容分-发机制。它允许你在封装一个通用组件时,在组件内部预留一些“坑位”或“插槽”,然后让使用该组件的父组件来决定这些“坑位”里到底填充什么内容。
# 创建一个Column,Column是一种垂直布局,可以让其中的组件垂直排列
slot.Column(lambda column: (
# 设置 Column 中 content 插槽的内容
column.setContent(lambda columnSlot: (
# 1. 创建一个可变状态,用于显示/隐藏文本
(showTextComponent := slot.mutableStateOf(True)),
# 添加一个文件组件到 Column (垂直布局)中
columnSlot.Text(lambda text: (
# 2. 在组件的某个属性中使用该状态的值
text.setVisible(showTextComponent.getValue()),
text.setText("我是文本"),
# 3. 将状态添加到组件中,这样text组件就会监听该状态的改变,这个是必须的步骤,如果没有此步骤,那么 showTextComponent 的值改变时,composable(text组件)不会重组(界面更新)
text.trackSingleState(showTextComponent)
)),
# 添加一个文本按钮到 Column (垂直布局)中
columnSlot.TextButton(lambda button: (
# 设置文本按钮的content插槽内容
button.setContent(lambda buttonSlot: (
# 在插槽中添加一个文本
buttonSlot.Text(lambda text: (
text.setText("点击我")
))
)),
# 设置按钮点击事件
button.setOnClick(lambda: (
# 4. 当按钮被点击时,更新状态的值,必须要调用 trackSingleState 方法的组件才会重组
# Groovy的 !someBoolean 对应 Python的 not someBoolean
showTextComponent.setValue(not showTextComponent.getValue())
))
))
))
))
))
# 启动一个Activity用于显示脚本界面
_activity.start()
# encoding: utf-8
# 创建一个 ComposeView, 可以通过声明式ui创建脚本界面
$composeView.create do |slot|
# 插槽(Slot)是一种内容分发机制。它允许你在封装一个通用组件时,在组件内部预留一些“坑位”或“插槽”,然后让使用该组件的父组件来决定这些“坑位”里到底填充什么内容。
# 创建一个Column,Column是一种垂直布局,可以让其中的组件垂直排列
slot.Column do |column|
# 设置 Column 中 content 插槽的内容
column.setContent do |columnSlot|
# 1. 创建一个可变状态,用于显示/隐藏文本
showTextComponent = slot.mutableStateOf(true)
# 添加一个文件组件到 Column (垂直布局)中
columnSlot.Text do |text|
# 2. 在组件的某个属性中使用该状态的值
text.setVisible(showTextComponent.getValue())
text.setText("我是文本")
# 3. 将状态添加到组件中,这样text组件就会监听该状态的改变,这个是必须的步骤,如果没有此步骤,那么 showTextComponent 的值改变时,composable(text组件)不会重组(界面更新)
text.trackSingleState(showTextComponent)
end
# 添加一个文本按钮到 Column (垂直布局)中
columnSlot.TextButton do |button|
# 设置文本按钮的content插槽内容
button.setContent do |buttonSlot|
# 在插槽中添加一个文本
buttonSlot.Text do |text|
text.setText("点击我")
end
end
# 设置按钮点击事件
button.setOnClick do
# 4. 当按钮被点击时,更新状态的值,必须要调用 trackSingleState 方法的组件才会重组
showTextComponent.setValue(!showTextComponent.getValue())
end
end
end
end
end
# 启动一个Activity用于显示脚本界面
$activity.start()
09 December 2025