快速入门
本章节将为你介绍 Ribir 的全部语法和常用的基本概念。
你将了解
- 如何创建和组合 widget
- 如何响应事件并操作数据
- 如何让视图 自动响应数据变更
- 如何构建动态 widget
- 如何将自己的数据结构映射为视图
- 如何将内建 widget 当做其它 widget 的一部分来使用
- 如何对状态进行转换,分离和溯源——方便状态的传递和控制视图的更新范围
什么是 widget?
在 Ribir 中,widget 作为核心概念,它是对视图进行描述的基本单元。在形式上它可以是一个按钮,一个文本框,一个列表,一个对话框,甚至是整个应用界面。在代码上,它可以是一个函数,一个闭包或者一个数据对象。Ribir 将能通过 &BuildCtx 构建出 Widget 的类型叫做 widget。注意 Widget 和 widget 的差别,在整个 Ribir 的语境中,widget 是一个泛称,而大写开头的 Widget 是一个具体的 widget,也是所有 widget 构建进入视图的通行证。
如果你不是特别理解上面的话,不用在意,因为你完全不需要关注 widget 的构建过程,Ribir 也禁止干涉这个过程。你只需明白,Ribir 将所有的 widget 分成四类:
- 函数 widget
ComposewidgetRenderwidgetComposeChildwidget
本章将只会介绍函数 widget 和 Compose widget。因为在大部分场景中这两种 widget 已经足够满足我们的需求了。作为进阶的内容,我们将在深入 widget中覆盖 Render widget 和 ComposeChild widget。
函数 widget
接收 &BuildCtx 作为入参并返回 Widget 的函数或闭包被称为函数 widget。
在没有外部状态依赖的情况下,通过函数来定义 widget 是最简单的一种方式。在创建一个应用中,你已经见过一个 Hello world! 的函数 widget 了。本节中,我们仍通过 Hello world! 的例子来展开介绍。
通过函数来定义 widget
直接通过函数来定义 widget:
use ribir::prelude::*;
fn hello_world(ctx!(): &BuildCtx) -> Widget {
rdl!{ Text { text: "Hello World!" } }
.widget_build(ctx!())
}
fn main() {
App::run(hello_world);
}
首先,你应该发现了在函数签名中参数声明(ctx!(): &BuildCtx)的不同之处,我们用 ctx!() 来作为参数名字,而不是直接给一个名字。这是因为 rdl! 内部会统一通过 ctx!() 作为变量名来引用 &BuildCtx。
接下来一行 rdl!{ Text { text: "Hello World!" } },通过 rdl! 创建了一个内容为 Hello World! 的 Text。关于 rdl! 的细节,你可以先放到一边,将在小节 使用 rdl! 创建对象 中详细介绍。
最后,将 Text 通过 widget_build 方法构建成 Widget,作为函数的返回值。
小提示
Ribir 中有多个过程宏,而 &BuildCtx 常常作为一个需要跨宏使用的变量。为了简化这个传递过程 ,Ribir 在这样的情况下,统一使用
ctx!来命名&BuildCtx,以允许它跨宏使用。所以,你以后会经常看到ctx!这个宏。
闭包和 fn_widget!
因为 hello_world 并没有被其它人调用,所以你可以将它改写成一个闭包:
use ribir::prelude::*;
fn main() {
let hello_world = |ctx!(): &BuildCtx| {
rdl!{ Text { text: "Hello World!" } }
.widget_build(ctx!())
};
App::run(hello_world);
}
对于通过闭包创建函数控件,Ribir 提供了一个 fn_widget! 宏来简化这个过程,fn_widget! 除了支持我们本章接下来要讲到的两个语法糖 @ 和 $ 之外,你可以简单认为它会这样展开代码:
move |ctx!(): &BuildCtx| -> Widget {
{
// 你的代码
}
.widget_build(ctx!())
}
使用 fn_widget! 改写 hello_world 例子:
use ribir::prelude::*;
fn main() {
App::run(fn_widget! {
rdl!{ Text { text: "Hello World!" } }
});
}
你有没有发现,除了没有使用 @ 以为,这个例子和你在创建一个应用中看到的已经一样了。
使用 rdl! 创建对象
rdl 是 Ribir Declarative Language 的缩写, rdl! 宏的目的就是帮助你以声明式的方式来创建对象。
注意
rdl!并不关注类型 ,只在语法层面做处理,所以并不是只有 widget 才可以用它。
声明式创建对象
尽管 rdl! 支持任意 Rust 表达式,但我们所说的声明式创建对象,特指通过结构体字面量的方式。
当你的表达式是一个结构体字面量时, rdl! 会通过 Declare trait 来创建对象,这就要求你所创建的对象的类型必须继承或实现了 Declare trait。
use ribir::prelude::*;
#[derive(Declare)]
pub struct Counter {
#[declare(default = 1usize)]
count: usize,
}
// `rdl!` 需要在一个有可访问的 `ctx!(): &BuildCtx` 的上下文中使用,
// 所以我们用一个带 `ctx!()` 参数的函数来提供这个上下文。
fn use_rdl(ctx!(): &BuildCtx) {
let _ = rdl!{ Counter { } };
}
上面的例子中,Counter 继承了 Declare, 并标记 count 默认值为 1。 所以在 rdl! 中,你可以不用给 count 赋值,rdl! 创建它时会默认赋值为 1。Declare 还有一些其它的特性,我们暂不在这里展开。
组合 widget
你已经知道如何创建一个 widget 了,我们现在通过 widget 嵌套在另一个 widget 中来组合出一个简单的计数应用。
你可以在结构体字面量声明的 widget 中嵌入其它 rdl! 作为孩子,注意孩子总是被要求声明在父 widget 属性的后面,这是 rdl! 对格式的强制要求。
use ribir::prelude::*;
fn main() {
let counter = fn_widget! {
rdl!{
Row {
rdl!{ FilledButton {
rdl! { Label::new("Increment") }
}}
rdl!{ H1 { text: "0" } }
}
}
};
App::run(counter);
}
上面的例子中,我们创建了一个 Row,它有两个子节点,FilledButton 和 H1。这三种 widget 都是 ribir_widgets 库中已定义好的。
rdl! 也允许你为已创建好的 widget 声明孩子:
use ribir::prelude::*;
fn main() {
let counter = fn_widget! {
let row = rdl!{ Row { align_items: Align::Center } };
rdl!{
$row {
rdl!{ FilledButton {
rdl! { Label::new("Increment") }
}}
rdl!{ Text { text: "0" } }
}
}
};
App::run(counter);
}
注意到 rdl!{ $row { ... } } 了吗? 它和结构体字面量语法一样,但是加上 $ 后,它表示作为父亲使一个变量而不是类型,所以它不会新建一个 widget,而是直接使用这个变量来和孩子组合。
小提示
在 Ribir 中,父子的组合并不是任意的,而是有类型限制的,父亲可以约束孩子的类型并给出组合逻辑。这确保了组合的正确性。
在我们上面的例子中,
Row接收任意数目,任意类型的 widget,Text不能接收任何孩子, 而FilledButton则更复杂一点,它允许接收一个Label作为它的文字和一个Svg作为按钮图标。对于如何约束 widget 的孩子类型,我们将在深入 widget中展开介绍。
表达式创建对象
除了通过结构体字面量创建对象以外,你还可以通过 rdl! {...} 包裹任意表达式来创建对象。这种方式的好处是,你可以在 {...} 中写任意代码在创建对象。这在嵌套组合中非常有用,也只在嵌套作为孩子时有必要。下 面的例子展示如何在 rdl 中使用表达式创建对象:
use ribir::prelude::*;
let _ = fn_widget! {
rdl!{ Row {
rdl!{
// 在这里你可以写任意的表达式,表达式的结果将作为孩子
if xxx {
...
} else {
...
}
}
}}
};
到这里,回顾前文的例子:
use ribir::prelude::*;
fn main() {
App::run(fn_widget! {
rdl!{ Text { text: "Hello World!" } }
});
}
相信你应该已经完全理解它了。
@ 语法糖
在组合 widget 的过程中,我们用到了大量的 rdl!。一方面,它让你在与 Rust 语法交互时(特别是复杂的例子)能有一个清晰的声明式结构——当你看到 rdl! 时,你就知道一个 widget 节点的组合或创建开始了;另一方面,当每一个节点都用 rdl! 包裹时,它又看上去太冗长了,无法让你一眼看到重点信息。
好在,Ribir 为 rdl! 提供了一个 @ 语法糖,在实际使用的过程中,基本上用的都是 @ 而非 rdl!。总共有三种情况:
@ Row {...}作为结构体字面量的语法糖,展开为rdl!{ Row {...} }@ $row {...}作为变量结构体字面量的语法糖,展开为rdl!{ $row {...} }@ { ... }是表达式的语法糖,展开为rdl!{ ... }
现在用 @ 改写上面的计数器的例子:
use ribir::prelude::*;
fn main() {
App::run(fn_widget! {
@Row {
@FilledButton {
@ { Label::new("Increment") }
}
@Text { text: "0" }
}
});
}