M8Test Help

架构

构建 Compose 界面

在 Compose 中,界面是不可变的,在绘制后无法进行更新。您可以控制的是界面的状态。每当界面的状态发生变化时,Compose 都会重新创建界面树中已更改的部分。可组合项可以接受状态并公开事件,例如 TextField 接受值并公开请求回调处理程序更改值的回调 onValueChange。

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()

由于可组合项接受状态并公开事件,因此单向数据流模式非常适合 Jetpack Compose。本指南将重点介绍如何在 Compose 中实现单向数据流模式,如何实现事件和状态容器,以及如何在 Compose 中使用 ViewModel。

单向数据流

单向数据流 (UDF) 是一种设计模式,在该模式下状态向下流动,事件向上流动。通过采用单向数据流,您可以将在界面中显示状态的可组合项与应用中存储和更改状态的部分分离开来。

使用单向数据流的应用的界面更新循环如下所示:

  1. 事件:界面的某一部分生成一个事件,并将其向上传递,例如将按钮点击传递给 ViewModel 进行处理;或者从应用的其他层传递事件,如指示用户会话已过期。

  2. 更新状态:事件处理脚本可能会更改状态。

  3. 显示状态:状态容器向下传递状态,界面显示此状态。

447

使用 Jetpack Compose 时遵循此模式可带来下面几项优势:

  • 可测试性:将状态与显示状态的界面分离开来,更方便单独对二者进行测试。

  • 状态封装:因为状态只能在一个位置进行更新,并且可组合项的状态只有一个可信来源,所以不太可能由于状态不一致而出现 bug。

  • 界面一致性:通过使用可观察的状态容器,例如 StateFlow 或 LiveData,所有状态更新都会立即反映在界面中。

Jetpack Compose 中的单向数据流

可组合项基于状态和事件进行工作。例如,只有在更新其 value 参数并公开 onValueChange 回调(这是一个请求将值更改为新值的事件)时,TextField 才会更新。Compose 将 State 对象定义为值容器,而对状态值的更改会触发重组。

TextField 可组合项的值的类型为 String,因此该值可以来自任意位置,包括来自硬编码值、ViewModel 或从父级可组合项传入。您不必将它保存在 State 对象中,但在调用 onValueChange 时需要更新该值。

定义可组合项参数

在定义可组合项的状态参数时,您应牢记以下问题:

  • 可组合项的可重用性或灵活性如何?

  • 状态参数如何影响此可组合项的性能?

为了促进分离和重复使用,每个可组合项都应包含尽可能少的信息。例如,构建可组合项以保存新闻报道的标题时,最好仅传递需要显示的信息,而不是整篇新闻报道:

有时,使用独立参数还能提高性能,例如,如果 News 包含的不仅仅是 title 和 subtitle 的信息,每当有 News 的新实例传入 Header( news) 时,即使 title 和 subtitle 没有变化,可组合项也将重组。

请仔细考虑传入的参数数量。如果一个函数拥有过多参数,会降低该函数的工效,因此在这种情况下,建议您将这些参数分到一个类下。

Compose 中的事件

应用的每项输入都应表示为事件:点按、文本更改,甚至计时器或其他更新。当这些事件更改界面的状态时,ViewModel 应负责处理这些事件并更新界面状态。

界面层绝不应更改事件处理脚本之外的状态,因为这样做可能会导致应用出现不一致和 bug。

最好为状态和事件处理脚本 lambda 传递不可变值。此方法具有以下优势:

  • 提升可重用性。

  • 确保您的界面不会直接更改状态的值。

  • 避免并发问题,因为您可确保不会从其他线程修改状态。

  • 通常情况下,还可以降低代码的复杂性。

例如,接受 String 和 lambda 作为参数的可组合项可以从许多上下文中调用,并且可重用性较高。假设应用中的顶部应用栏始终显示文本并包含返回按钮。您可以定义一个更通用的 MyAppTopAppBar 可组合项,该可组合项用于接收文本和返回按钮句柄作为参数:

09 December 2025