快速入门
本章节将为你介绍 Ribir 的全部语法和常用的基本概念。
你将了解
- 如何创建和组合 widget
- 如何响应事件并操作数据
- 如何让视图自动响应数据变更
- 如何构建动态 widget
- 如何将自己的数据结构映射为视图
- 如何将内建 widget 当做其它 widget 的一部分来使用
- 如何对状态进行转换,分离和溯源——方便状态的传递和控制视图的更新范围
什么是 widget?
在 Ribir 中,widget 作为核心概念,它是对视图进行描述的基本单元。在形式上它可以是一个按钮,一个文本框,一个列表,一个对话框,甚至是整个应用界面。在代码上,它可以是一个函数,一个闭包或者一个数据对象。
如果你不是特别理解上面的话,不用在意,因为你完全不需要关注 widget 的构建过程,Ribir 也禁止干涉这个过程。你只需明白,Ribir 将所有的 widget 分成四类:
- 函数 widget
Compose
widgetRender
widgetComposeChild
widget
本章将只会介绍函数 widget 和 Compose
widget。因为在大部分场景中这两种 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::*;
#[derive(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: 通过 `State::value` 创建一个状态
let count = State::value(0);
@Button {
// 变更 2: 通过点击事件来修改状态
on_tap: move |_| *$count.write() += 1,
// 变更 3: 通过状态来显示数据,并保持视图的持续更新。
// 对于宏或者函数调用,我们可以省略 @ 后面的一对大括号
@ pipe!($count.to_string())
}
});
}
通过这 3 处变更,计数器的小例子全部完成了。但是在变更 2 和变更 3 中,有新的东西被引入了 —— $
和 pipe!
。它们是 Ribir 中非常重要的用法,让我们用两个小节来分别展开介绍。
$ 语法糖
在 Ribir 中有两个重要的语法糖,一个是我们之前介绍的 @ 语法糖,另一个就是 $
语法糖了。
状态的读写引用
$
表示对跟随其后的状态做读或写引用。比如 $count
表示对 count
状态的读引用,而当其后多跟随一个write()
调用时,则表示对 count
状态的写引用,如 $count.write()
。
除了 write
以外, Ribir 还有一个 silent
写引用,通过 silent
写引用修改数据不会触发视图更新。
状态的 $
语法糖展开逻辑为:
$counter.write()
展开为counter.write()
$counter.silent()
展开为counter.silent()
$counter
展开为counter.read()
状态的自动共享
当 $
处在一个 move
闭包中时,它指向的状态会被克隆(读/写),闭包捕获的是状态的克隆,因此 $
让你可以直接使用一个状态,并轻易的完成共享,而不用去额外的克隆它。
move |_| *$count.write() += 1
大致展开成
{
let count = count.clone_writer();
move |_| *count.write() += 1
}
语法糖展开的优先级
还记得我们在组合-widget中也同样用到了 $
吗?
比如 rdl!{ $btn { ... } }
或者 @ $btn { ... }
,这可不是对状态数据的引用哦。因为 rdl!
赋予了它其它的语义——通过变量声明父 widget。
无论是 @
还是 $
,它们首先应遵循它们所在宏的语义,其次才是一个 Ribir 语法糖。当我们在一个非 Ribir 提供的宏中使用 @
或 $
时,它们就不再是 Ribir 的语法糖,因为外部宏很可能为它们赋予了特殊的语义。比如:
use ribir::prelude::*;
fn_widget!{
user_macro! {
// `@` 此时不是语法糖,它的语义取决于 `user_macro!` 的实现
@Button { ... }
}
}