快速入门
本章节将为你介绍 Ribir 的全部语法和常用的基本概念。
你将了解
- 如何创建和组合 widget
- 如何响应事件并操作数据
- 如何让视图自动响应数据变更
- 如何构建动态 widget
- 如何将自己的数据结构映射为视图
- 如何将内建 widget 当做其它 widget 的一部分来使用
- 如何对状态进行转换,分离和溯源——方便状态的传递和控制视图的更新范围
什么是 widget?
在 Ribir 中,widget 作为核心概念,它是对视图进行描述的基本单元。在形式上它可以是一个按钮,一个文本框,一个列表,一个对话框,甚至是整个应用界面。在代码上,它可以是一个函数,一个闭包或者一个数据对象。
如果你不是特别理解上面的话,不用在意,因为你完全不需要关注 widget 的构建过程,Ribir 也禁止干涉这个过程。你只需明白,Ribir 将所有的 widget 分成四类:
- 函数 widget
ComposewidgetRenderwidgetComposeChildwidget
本章将只会介绍函数 widget 和 Compose widget。因为在大部分场景中这两种 widget 已经足够满足我们的需求了。作为进阶的内容,我们将在纯组合中覆盖 Render widget 和 ComposeChild widget。
注意 Widget 和 widget 的差别,在整个 Ribir 的语境中,widget 是一个泛称,而大写开头的 Widget 是一个具体的 widget,也是所有 widget 构建进入视图的通行证。
函数 widget
返回 Widget 的函数或闭包被称为函数 widget ,一个可以被多次调用的函数 widget ,可以转换为一个 GenWidget, 而我们应用的根 widget 就要求是一个 GenWidget.
通过函数来定义 widget 是最简单快速的一种方式。在体验 Ribir中,你已经见过一个 Hello world! 的函数 widget 了。本节中,我们仍通过 Hello world! 的例子来展开介绍。
通过函数来定义 widget
我们先通过定义一个 hello_world 函数来完成我们的例子。
use ribir::prelude::*;
fn hello_world() -> Widget<'static> {
let mut text = Text::declarer();
text.with_text("Hello World!");
text.finish().into_widget()
}
fn main() {
App::run(hello_world);
}
因为 Text 这个 widget ,只提供了声明式 API 创建一种方法,所以我们需要以 Text::declarer() 来创建它的声明器,并以 finish 来完成创建。紧接着,我们通过 into_widget 来将它转化为 Widget 类型。
对于声明式 widget 我们还可以通过 rdl! 来简化它的写法。
use ribir::prelude::*;
fn hello_world() -> Widget<'static> {
rdl!{ Text { text: "Hello World!" } }
.into_widget()
}
关于 rdl! 的细节,我们将在小节 使用 rdl! 创建对象 中详细介绍,现在先将它暂且放一放。
小提示
框架会给所有类型的 widget 自动实现
into_widget的方法。
闭包和 fn_widget!
因为 hello_world 并没有被其它人调用,所以你可以将它改写成一个闭包:
use ribir::prelude::*;
fn main() {
let hello_world = || {
rdl!{ Text { text: "Hello World!" } }
.into_widget()
};
App::run(hello_world);
}
而对于通过闭包创建函数 widget ,Ribir 提供了一个 fn_widget! 宏来简化这个过程,fn_widget! 除了支持我们本章接下来要讲到的两个语法糖 @ 和 $ 之外,你可以简单认为它会这样展开代码:
move || -> Widget {
{
// 你的代码
}
.into_widget()
}
使用 fn_widget! 改写 hello_world 例子:
use ribir::prelude::*;
fn main() {
App::run(fn_widget! {
rdl!{ Text { text: "Hello World!" } }
});
}
通常声明式 widget 都会提供一个同名宏,这个宏会通过 fn_widget! 来创建以自己为根节点的函数 widget 。
所以,我们的例子可以进一步简化为:
use ribir::prelude::*;
fn main() {
App::run(text! { text: "Hello World!"});
}
这便是我们在体验 Ribir中看到的例子了。
使用 rdl! 创建对象
rdl 是 Ribir Declarative Language 的缩写, rdl! 宏的目的就是帮助你以声明式的方式来创建对象。
注意
rdl!并不关注类型,只在语法层面做处理,所以并不是只有 widget 才可以用它。
声明式创建对象
尽管 rdl! 支持任意 Rust 表达式,但我们所说的声明式创建对象,特指通过结构体字面量的方式:
rdl! {
ObjectType {
... // 字段声明
}
}
当你的表达式是一个结构体字面量时, rdl! 会通过 Declare trait 来创建对象,这就要求你所创建的对象的类型必须继承或实现了 Declare trait。
use ribir::prelude::*;
#[declare]
pub struct Counter {
#[declare(default = 1usize)]
count: usize,
}
fn use_rdl() {
let _ = rdl!{ Counter { } };
}
上面的例子中,Counter 继承了 Declare, 并标记 count 默认值为 1。 所以在 rdl! 中,你可以不用给 count 赋值,rdl! 创建它时会默认赋值为 1。Declare 还有一些其它的特性,我们暂不在这里展开。
表达式创建对象
除了通过结构体字面量的方式创建对象以外,你还可以在 rdl! 中放置任意表达式,rdl!。这种方式的好处是,你可以在 {...} 中写任意代码在创建对象。这在嵌套组合中非常有用,也只在嵌套作为孩子时有必要。下面的例子展示如何在 rdl 中使用表达式创建对象:
use ribir::prelude::*;
let _parent = rdl!{
// 在这里你可以写任意的表达式,表达式的结果将作为孩子
if {
...
} else {
...
}
};
组合 widget
你已经知道如何在 rdl! 中创建 widget 了,我们现在通过将 widget 嵌套在另一个 widget 中来组合出一个简单的计数应用。
你可以在结构体字面量声明的 widget 中嵌入其它 rdl! 作为孩子,注意孩子总是被要求声明在父 widget 属性的后面,这是 rdl! 对格式的强制要求。
use ribir::prelude::*;
fn main() {
let counter = fn_widget! {
rdl!{
Button {
rdl!{ "0" }
}
}
};
App::run(counter);
}
上面的例子中,我们创建了一个 Button, 并给 它组合了一个字符串作为孩子。Button 是在 ribir_widgets 库中已定义好的。
rdl! 也允许你为已创建好的 widget 声明孩子:
use ribir::prelude::*;
fn main() {
let counter = fn_widget! {
let btn = rdl! { Button {} };
rdl!{
(btn) {
rdl!{ "0" }
}
}
};
App::run(counter);
}
注意到 rdl!{ (btn) { ... } } 了吗? 它表示作为父亲的是一个变量而不是类型,所以它不会新建一个 widget ,而是直接使用这个变量来和孩子组合。
小提示
在 Ribir 中,父子的组合并不是任意的,而是有类型限制的,父亲可以约束孩子的类型并给出组合逻辑。这确保了组合的正确性。
在我们上面的例子中,
Button规定好了可以接受两个可选的孩子,一个字符串作为标签,一个Widget作为 icon。为什么 Button 的标签要设计为是一个孩子而不是一个自己的字段呢? 这是因为,如果设计为
Button自己的字段,那么无论Button在使用时是否有一个标签,这个字段都要占用内存。而如果是作为一个孩子,则不存在这个字段内存的开销了。关于如何约束 widget 的孩子类型,我们将在深入 widget 中展开介绍。
@ 语法糖
在组合 widget 的过程中,我们用到了多个 rdl!。一方面,它让你在与 Rust 语法交互时(特别是复杂的例子)能有一个清晰的声明式结构——当你看到 rdl! 时,你就知道一个 widget 节点的组合或创建开始了;另一方面,当每一个节点都用 rdl! 包裹时,它又看上去太冗长了,无法让你一眼看到重点信息。
好在,Ribir 为 rdl! 提供了一个 @ 语法糖,在实际使用的过程中,基本上用的都是 @ 而非 rdl!。总共有三种情况:
@Button {...}作为结构体字面量的语法糖,展开为rdl!{ Button {...} }@ (btn) {...}作为变量结构体字面量的语法糖,展开为rdl!{ (btn) {...} }@ { ... }是表达式的语法糖,展开为rdl!{ ... }
现在用 @ 改写上面的计数器的例子:
use ribir::prelude::*;
fn main() {
App::run(fn_widget! {
@Button {
@ { "0" }
}
});
}
状态——让数据变得可被侦和共享
我们虽然创建了一个计数器,但它总是显示 0,也不响应按钮做任何事情。在这一节中,你将会了解到如何通过状态让你的计数器工作。
状态是一个将数据变得可被侦听和共享的包装器。
状态 = 数据 + 可侦听 + 可共享
一个可交互的 Ribir widget 的完整个生命周期是这样的:
- 将你的数据转换为状态。
- 对状态进行声明式映射构建出视图。
- 在交互过程中,通过状态来修改数据。
- 通过状态接收到数据的变更,根据映射关系点对点更新视图
- 重复步骤 3 和 4 。
现在,让我们引入状态来改造我们的例子。
use ribir::prelude::*;
fn main() {
App::run(fn_widget! {
// 变更 1: 通过 `Stateful::new` 创建一个状态
let count = Stateful::new(0);
@Button {
// 变更 2: 通过点击事件来修改状态
on_tap: move |_| *$write(count) += 1,
// 变更 3: 通过状态来显示数据,并保持视图的持续更新。
// 对于宏或者函数调用,我们可以省略 @ 后面的一对大括号
@ pipe!($read(count).to_string())
}
});
}
通过这 3 处变更,计数器的小例子全部完成了。但是在变更 2 和变更 3 中,有新的东西被引入了 —— $ 和 pipe!。它们是 Ribir 中非常重要的用法,让我们用两个小节来分别展开介绍。
$ 语法糖
在 Ribir 中有两个重要的语法糖,一个是我们之前介绍的 @ 语法糖,另一个就是 $ 语法糖了。
状态的读写引用
$read 表示对状态做读引用, $write 表示对跟随其后的状态做写引用。比如 *$read(count) 将返回 count 的值, 而*$write(count) += 1 则会修改 count 的值加 1。
状态的自动共享
当 $read 或 $write 处在一个 move 闭包中时,它指向的状态会被克隆(读/写),闭包捕获的是状态的克隆,因此 $ 让你可以直接使用一个状态,并轻易的完成共享,而不用去额外的克隆它。
move |_| *$count.write() += 1
大致展开成
{
let count = count.clone_writer();
move |_| *count.write() += 1
}