快速入门
本章节将为你介绍 Ribir 的全部语法和常用的基本概念。
你将了解
- 如何创建和组合 widget
- 如何响应事件并操作数据
- 如何让视图 自动响应数据变更
- 如何构建动态 widget
- 如何将自己的数据结构映射为视图
- 如何将内建 widget 当做其它 widget 的一部分来使用
- 如何对状态进行转换,分离和溯源——方便状态的传递和控制视图的更新范围
什么是 widget?
在 Ribir 中,widget 作为核心概念,它是对视图进行描述的基本单元。在形式上它可以是一个按钮,一个文本框,一个列表,一个对话框,甚至是整个应用界面。在代码上,它可以是一个函数,一个闭包或者一个数据对象。Ribir 将能通过 &BuildCtx
构建出 Widget
的类型叫做 widget。注意 Widget
和 widget 的差别,在整个 Ribir 的语境中,widget 是一个泛称,而大写开头的 Widget
是一个具体的 widget,也是所有 widget 构建进入视图的通行证。
如果你不是特别理解上面的话,不用在意,因为你完全不需要关注 widget 的构建过程,Ribir 也禁止干涉这个过程。你只需明白,Ribir 将所有的 widget 分成四类:
- 函数 widget
Compose
widgetRender
widgetComposeChild
widget
本章将只会介绍函数 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" }
}
});
}