第5章 Iced框架中的运行时机制
上一章我们用 iced 和 The Elm Architecture 构建了经典计数器界面。我们逐一拆解了它的每个基本组成部分——状态、消息、更新逻辑和视图逻辑。
但现在接下来该做什么呢?没错,我们已经拥有了用户界面的所有基本组件——正如我们在解剖分析中所学到的——但如何让它跑起来,仍不太清晰。
看来我们缺少某个东西,能把所有部分整合起来,让它们协同运行。某个能创建并执行用户界面基本循环的东西——向用户展示控件,并对任何交互做出响应。
这个东西就是运行时(runtime)。可以把它理解为用户界面反馈循环发生的环境。运行时负责循环中的每个环节:初始化状态、生成消息、执行更新逻辑以及运行我们的视图逻辑。
你可以这样想象运行时:它像一台巨大的引擎,却缺少四个基本组件。我们的任务就是把这些组件填上去——然后引擎就能运转了!
一个“神奇”的运行时
为了更好理解一个界面的生命周期,我们不妨探索一下一个基本运行时(虽然它非常“魔法”)的内部机制。
事实上,我们其实已经开始写运行时了!在实现计数器更新逻辑时,我们写了一个很小的测试来模拟用户:
#[test]
fn it_counts_properly() {
let mut counter = Counter { value: 0 };
counter.update(Message::Increment);
counter.update(Message::Increment);
counter.update(Message::Decrement);
assert_eq!(counter.value, 1);
}
从技术上说,这是一个非常简陋的运行时。它初始化了状态、产生了一些交互,并执行了更新逻辑。
当然,这些交互是编造的,而且非常短暂,也没涉及什么视图逻辑——离我们真正想要的目标还差很远。不过这是个不错的开始!让我们一步步扩展它。
初始化状态
我们的小 runtime 已经能正确初始化应用状态了:
// Initialize the state
let mut counter = Counter { value: 0 };
不过,我们可以利用 Default trait 来避免硬编码初始状态。直接派生它就行:
#[derive(Default)]
struct Counter {
value: i64
}
然后在 runtime 里使用 Counter::default:
// Initialize the state
let mut counter = Counter::default();
区别看起来微不足道,但我们其实是在分离关注点——把应用的初始状态放在状态定义旁边,而不是和 runtime 混在一起。这样一来,我们的 runtime 最终或许就能配合任意应用工作了!
显示界面
好了!状态已经初始化完成,接下来呢?在用户能和界面交互之前,我们得先把界面显示出来。
这很简单!只需要在用户使用的操作系统上打开一个窗口,初始化合适的图形后端,然后把视图逻辑返回的 widget 渲染出来——当然还得正确排布!
什么?你完全不知道该怎么做?别担心,我有一个神奇的函数:display。它接收一个界面引用,然后把界面展示给用户。绝对好用!
use magic::display;
// Initialize the state
let mut counter = Counter::default();
// Run our view logic to obtain our interface
let interface = counter.view();
// Display the interface to the user
display(&interface);
看到了吧?很简单!开个玩笑——本章的目的不是教大家图形编程,而是帮助我们更好地理解 runtime 的工作原理。来点魔法也无妨!
收集交互
用户正在使用我们的界面,并已与界面进行了交互。我们需要仔细关注所有的交互操作,并生成相应 消息,即我们组件所定义的消息。
怎么做呢?当然又是靠魔法!我刚刚从我的高帽子里找到了这个 interact 函数——它接收一个界面,并输出对应用户最新交互的 消息。
use magic::{display, interact};
// 初始化状态
let mut counter = Counter::default();
// 运行视图逻辑以获取界面
let interface = counter.view();
// 向用户显示该界面
display(&interface);
// 处理用户交互并获取消息
let messages = interact(&interface);
太棒了!interact 为我们返回了一个 消息列表——可以直接迭代使用。
响应交互
至此,我们已经收集了用户交互并将其转换为一系列 消息。为了正确响应用户,我们需要根据每条 消息 相应地更新 状态。
幸运的是,这一步不再涉及什么魔法技巧——我们只需要使用 更新逻辑:
use magic::{display, interact};
// Initialize the state
let mut counter = Counter::default();
// Run our view logic to obtain our interface
let interface = counter.view();
// Display the interface to the user
display(&interface);
// Process the user interactions and obtain our messages
let messages = interact(&interface);
// Update our state by processing each message
for message in messages {
counter.update(message);
}
这样能确保状态始终与最新的用户交互保持同步。
循环往复
好了!状态已根据用户交互更新完毕。接下来,需要再次向用户显示生成的界面。然后,继续处理后续交互…… 接着,再次更新状态。然后…… 从头再来一遍!
这就是循环!当然,循环本身并不神奇——至少在我们写 Rust 时是这样:
use magic::{display, interact};
// Initialize the state
let mut counter = Counter::default();
// Be interactive. All the time!
loop {
// Run our view logic to obtain our interface
let interface = counter.view();
// Display the interface to the user
display(&interface);
// Process the user interactions and obtain our messages
let messages = interact(&interface);
// Update our state by processing each message
for message in messages {
counter.update(message);
}
}
恭喜!我们刚刚写出了一个功能完备的运行时——当然,那些魔法属性除外。在这里,我们可以清晰地看到 Elm 架构(The Elm Architecture)的每个核心部分如何在应用程序的生命周期中发挥作用。
具体来说,
- 状态 只初始化一次,
- 视图逻辑 在启动时运行一次,之后每当一批交互处理完毕后再运行,
- 而 更新逻辑 会为每一个由交互产生的 消息 执行一次。
冰雪法师
你可能会说:“这确实挺酷,但我又不是什么法师,到现在还是不知道怎么运行我写的那个计数器界面。我还有东西要数呢!”
没问题!iced 内置了一个和我们刚搭建的非常相似的 runtime,它自带了所需的魔法1——所以你不用自己去钻研那些黑魔法。
要运行我们的 Counter,只需调用 run:
use iced::widget::{button, column, text, Column};
pub fn main() -> iced::Result {
iced::run(Counter::update, Counter::view)
}
#[derive(Default)]
struct Counter {
value: i64,
}
#[derive(Debug, Clone, Copy)]
enum Message {
Increment,
Decrement,
}
impl Counter {
fn update(&mut self, message: Message) {
match message {
Message::Increment => {
self.value += 1;
}
Message::Decrement => {
self.value -= 1;
}
}
}
fn view(&self) -> Column<'_, Message> {
column![
button("+").on_press(Message::Increment),
text(self.value),
button("-").on_press(Message::Decrement),
]
}
}
我们只需向runtime提供更新逻辑和视图逻辑——剩下的它会自行搞定!
运行时能够从我们的更新逻辑和视图逻辑的类型签名中,自动推断出状态和消息的类型。状态的初始化利用了Default,正如前文所述。
需要注意的是,run 可能会失败,因此它返回的是 iced::Result。如果仅仅是为了运行应用程序,可以直接在 main 中返回这个结果。
就这样了!去享受计数吧——至少能坚持3亿年。
来自作者的备注
你暂时读到了本书的结尾!
我认为这已经足以作为库基础知识的快速入门介绍。还有更多深入的内容待发掘——但希望你能在此基础上开始动手实验、探索并从中获得乐趣。
本书尚未完全写完——还有许多我想在此涵盖的主题,例如:
- 布局
- 样式
- 并发
- 应用规模化
- 扩展运行时
- 以及更多内容!
在这些章节撰写完成之前,如果你想进一步探索和深入学习,可以查看额外资源章节。
希望你喜欢目前的阅读体验。敬请期待!
— Héctor
-
主要指
winit、softbuffer、wgpu、tiny-skia和cosmic-text。↩