M8Test Help

与 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() } 的形式控制组件显示状态:当 xxxtrue 时组件显示,为 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