From 92625b35f43dfbfe43d134ab08e0fb8918a99231 Mon Sep 17 00:00:00 2001 From: Adoo Date: Tue, 19 Dec 2023 17:59:45 +0800 Subject: [PATCH] =?UTF-8?q?docs(ribir):=20=E2=9C=8F=EF=B8=8F=20the=20engli?= =?UTF-8?q?sh=20version=20of=20quick=20start?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/src/state.rs | 6 +- docs/en/get_started/quick_start.md | 743 ++++++++++++++++++ docs/en/introduction.md | 6 +- .../non-intrusive_developing.md | 1 + ...53\351\200\237\345\205\245\351\227\250.md" | 156 ++-- "docs/zh/\347\256\200\344\273\213.md" | 4 +- 6 files changed, 838 insertions(+), 78 deletions(-) create mode 100644 docs/en/get_started/quick_start.md create mode 100644 docs/en/practice_todos_app/non-intrusive_developing.md diff --git a/core/src/state.rs b/core/src/state.rs index 62aef7ee7..a1f3fb508 100644 --- a/core/src/state.rs +++ b/core/src/state.rs @@ -93,9 +93,9 @@ pub trait StateWriter: StateReader { /// Return a new writer that be part of the origin writer by applying a /// function to the contained value. /// - /// This writer share the same data with the origin writer. But has it's own - /// notifier. When modifies across the return writer, the downstream - /// subscribed on the origin state will not be notified. + /// The return writer share the same data with the origin writer. But modify + /// the data through the return writer will not trigger the views depend on + /// the origin writer to update. /// /// If you want a new writer that has same notifier with the origin writer, /// you can use `map_writer(...)`. diff --git a/docs/en/get_started/quick_start.md b/docs/en/get_started/quick_start.md new file mode 100644 index 000000000..17fb1c603 --- /dev/null +++ b/docs/en/get_started/quick_start.md @@ -0,0 +1,743 @@ +# Quick Start + +This chapter will introduce you to all the syntax and basic concepts of Ribir. + +> You will learn +> +> - How to create and compose widgets +> - How to respond to events and operate data +> - How to make the view automatically respond to data changes +> - How to build dynamic widgets +> - How to map your own data structure to a view +> - How to use built-in widgets as part of other widgets +> - How to convert, separate and trace original state -- to facilitate the transfer of state and control the scope of view updates + +## What is a widget? + +In Ribir, the widget is the basic unit for describing the view. In form, it can be a button, a text, a list, a dialog, or even the entire application interface. In code, it can be a function, a closure, or a data object. The type that Ribir can build `Widget` through `&BuildCtx` is called widget. Note the difference between `Widget` and widget, in the context of the entire Ribir, widget is a generic term, and the capitalized `Widget` is a specific widget, which is also the pass for all widgets to enter the view. + +If you don't understand the above words very well, don't worry, because you don't need to care about the construction process of the widget at all, and Ribir also prohibits developer interference in this process. You only need to understand that Ribir divides all widgets into four categories: + +- function widget +- `Compose` widget +- `Render` widget +- `ComposeChild` widget + +This chapter will only introduce function widget and `Compose` widget. Because in most scenarios, these two widgets are enough to meet our needs. As advanced content, we will cover `Render` widgets and `ComposeChild` widgets in [Widget In-depth](./widget-in-depth.md). + +## Function widget + +The function or closure that accepts `&BuildCtx` as the input parameter and returns the `Widget` is called a function widget. + +A function widget is the simplest way to define a widget without external state dependencies. In [Creating an application](./creating_an_application.md), you have seen a function widget of `Hello world!`. In this section, we will continue to introduce it through the example of `Hello world!`. + + +### Define widget through function + +A function widget can be defined directly through a function: + +```rust +use ribir::prelude::*; + +fn hello_world(ctx!(): &BuildCtx) -> Widget { + rdl!{ Text { text: "Hello World!" } } + .widget_build(ctx!()) +} + +fn main() { + App::run(hello_world); +} +``` + +At first, you should find the difference in the parameter declaration (`ctx!(): &BuildCtx`) in the function signature. We use `ctx!()` as the parameter name instead of directly giving a name. This is because `rdl!` will unify `ctx!()` as the variable name to refer to `&BuildCtx` inside. + +Then, you can see the next line `rdl!{ Text { text: "Hello World!" } }`, which creates a `Text` with the content `Hello World!` through `rdl!`. The details of `rdl!` will be put aside for now, and will be introduced in detail in the section [Creating objects using `rdl!`](#creating-objects-using-rdl). + +Finally, build `Text` into `Widget` through the `widget_build` method as the return value of the function. + + +> Tip +> +> There are multiple procedural macros in Ribir, and `&BuildCtx` is often used as a variable that needs to be passed across macros. In order to simplify this passing process, Ribir uses `ctx!` as the variable name in this case to allow it to be used across macros. So, you will often see the macro `ctx!` in the future. + +### Closure and `fn_widget!` + +Because `hello_world` is not called by anyone else, you can rewrite it as a closure: + +```rust +use ribir::prelude::*; + +fn main() { + let hello_world = |ctx!(): &BuildCtx| { + rdl!{ Text { text: "Hello World!" } } + .widget_build(ctx!()) + }; + App::run(hello_world); +} +``` + +For function widgets created through closure, Ribir provides a `fn_widget!` macro to simplify this process. Except for the two syntactic sugars `@` and `$` that we will talk about later in this chapter, you can simply think it will expand the code like this: + +``` rust ignore +move |ctx!(): &BuildCtx| -> Widget { + { + // Your code + } + .widget_build(ctx!()) +} +``` + +The `hello_world` example is rewritten with `fn_widget`!`: + + +```rust +use ribir::prelude::*; + +fn main() { + App::run(fn_widget! { + rdl!{ Text { text: "Hello World!" } } + }); +} +``` +Do you notice that except for not using `@`, this example is already the same as what you saw in [Creating an application](./creating_an_application.md). + +## Creating objects using `rdl!` + +`rdl` is the abbreviation of Ribir Declarative Language, and the purpose of the `rdl!` macro is to help you create objects in a declarative way. + +> Notice +> +> `rdl!` does not care about types, it only does processing at the syntax level, so it is not only widgets that can use it. + +### Declarative creation of objects + +Although `rdl!` supports any Rust expression, but what we mean by declarative creation of objects, specifically refers to the way of creating objects through structure literals. + +When your expression is a structure literal, `rdl!` will create an object through the `Declare` trait, which requires that the type of the object you create must inherit or implement the `Declare` trait. + + +```rust +use ribir::prelude::*; + +#[derive(Declare)] +pub struct Counter { + #[declare(default = 1usize)] + count: usize, +} +// `rdl!` only allow to be used in a context with `ctx!(): &BuildCtx` accessible. +// So we use a function with `ctx!()` parameter to provide this context. +fn use_rdl(ctx!(): &BuildCtx) { + let _ = rdl!{ Counter { } }; +} +``` + +In the above example, `Counter` inherits `Declare` and marks the default value of `count` as `1`. So in `rdl!`, you don't need to assign a value to `count`, `rdl!` will assign it a default value of `1` when creating it. `Declare` has some other features, which we will not expand here. + +## Composing widgets + +You already know how to create a widget, and now we will compose a simple counter application by nesting widgets in another widget. + +You can nest additional `rdl!` instances as children within the widget declared by the structure literal. Please note that child widgets must always be declared after the parent widget's properties. This is a formatting requirement of `rdl!`. + + +```rust +use ribir::prelude::*; + +fn main() { + let counter = fn_widget! { + rdl!{ + Row { + rdl!{ FilledButton { + rdl! { Label::new("Increment") } + }} + rdl!{ H1 { text: "0" } } + } + } + }; + + App::run(counter); +} +``` + +In the above example, we created a `Row` with two child nodes, `FilledButton` and `H1`. These three widgets are already defined in the `ribir_widgets` library. + +`rdl!` also allows you to declare children for widgets that have already been created: + +```rust +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); +} +``` + +Do you notice the `rdl! { $row { ... } }`? It is the same as the structure literal syntax, but with `$` in front of it, it means that it is a variable rather than a type, so it will not create a new widget, but directly use this variable to compose with the child. + +> Tip +> +> In Ribir, the composition of parent and child widgets is not arbitrary, but subject to type constraints. The parent can restrict the type of the child and implement the composition logic, ensuring the correctness of the composition. +> +> In our example above, `Row` accepts any number and any type of widget, `Text` cannot accept any children, and `FilledButton` is a bit more complicated, it allows to accept a `Label` as its text and a `Svg` as the button icon. +> +> For how to constrain the child type of the widget, we will introduce it in [Widget In-depth](./widget-in-depth.md). + +### Creating objects through expressions + +Except for creating objects through structure literals, you can also create objects by wrapping any expression with `rdl! {...}`. The advantage of this approach is that you can write any code in `{...}` to create objects. This is very useful in nested composition, and it is only necessary when nesting as a child. The following example shows how to use expressions to create objects in `rdl`: + +```rust ignore +use ribir::prelude::*; + +let _ = fn_widget! { + rdl!{ Row { + rdl!{ + // you can write any expression here, the result of the expression will be the child + if xxx { + ... + } else { + ... + } + } + }} +}; +``` + +At this point, let's review the previous example: + +```rust +use ribir::prelude::*; + +fn main() { + App::run(fn_widget! { + rdl!{ Text { text: "Hello World!" } } + }); +} +``` + +I believe you should have fully understood it. + +## The `@` syntactic sugar + +In the process of composing widgets, we use a lot of `rdl!`. It allows you to have a clear declarative structure when interacting with Rust syntax (especially complex examples)-when you see `rdl!`, you know that the composition or creation of a widget node has begun; on the other hand, when each node is wrapped with `rdl!`, it looks too long to see the key information at a glance. + +Fortunately, Ribir offers a syntactic sugar, `@`, as an alternative to `rdl!`. In practice, we almost always use `@` instead of `rdl!`. There are three use cases: + +- `@ Row {...}` as a syntactic sugar for structure literals, expanded to `rdl!{ Row {...} }` +- `@ $row {...}` as a syntactic sugar for variable structure literals, expanded to `rdl!{ $row {...} }` +- `@ {...}` as a syntactic sugar for expressions, expanded to `rdl!{ ... }` + +Now let's rewrite the previous example of Counter using `@`: + +```rust +use ribir::prelude::*; + +fn main() { + App::run(fn_widget! { + @Row { + @FilledButton { + @ { Label::new("Increment") } + } + @Text { text: "0" } + } + }); +} +``` +## State -- make data watchable and shareable + +Although we have created a counter, it always shows `0` and does not respond to the button. In this section, you will learn how to make your counter work through state. + +The state is a wrapper that makes data watchable and shareable. + +`State = Data + Watchable + Shareable` + +The complete life cycle of an interactive Ribir widget is as follows: + +1. Convert your data to a state. +2. Declaratively map the state to build the view. +3. During the interaction, modify the data through the state. +4. Receive data changes through the state, and update the view point-to-point according to the mapping relationship. +5. Repeat steps 3 and 4. + +![lifecycle](../../assets/data-flows.svg) + +Now, let's improve our example by introducing the state. + +```rust +use ribir::prelude::*; + +fn main() { + App::run(fn_widget! { + // Change 1: Create a state through `State::new` + let count = State::value(0); + + @Row { + @FilledButton { + // Change 2: increase the count by 1 when the button is clicked + on_tap: move |_| *$count.write() += 1, + @ { Label::new("Increment") } + } + // Change 3: display the count through the state, and keep the view continuously updated. + @H1 { text: pipe!($count.to_string()) } + } + }); +} +``` + +Through the above three changes, the Counter example is complete. But in changes 2 and 3, new things have been introduced -- `$` and `pipe!`. They are very important usages in Ribir, let's introduce them in two sections. + +## The `$` syntactic sugar + +There are two important syntactic sugars in Ribir, one is the [@ syntactic sugar](#the-@-syntactic-sugar) we introduced earlier, and the other is the `$` syntactic sugar. + +### Read and write references to state + +`$` represents a read or write reference to the state that follows it. For example, `$count` represents a read reference to the `count` state, and when it is followed by a `write()` call, it represents a write reference to the `count` state, such as `$count.write()`. + +Except for `write`, Ribir also has a `silent` write reference, modifying data through `silent` write reference will not trigger view updates. + +The `$` syntactic sugar for a state is expanded to: + +- `$counter.write()` expand to `counter.write()` +- `$counter.silent()` expand to `counter.silent()` +- `$counter` expand to `counter.read()` + +### Automatic sharing of state + +When `$` is in a `move` closure, the state it points to will be cloned (read/write), and the closure captures the clone of the state, so `$` allows you to directly use a state and easily complete sharing without having to clone it separately. + +```rust ignore +move |_| *$count.write() += 1 +``` + +Roughly expanded to: + +```rust ignore +{ + let count = count.clone_writer(); + move |_| *count.write() += 1 +} +``` + +### The priority of syntactic sugar expansion + +Do you remember that we also used `$` in [Composing widgets](#composing-widgets)? For example, `rdl!{ $row { ... } }` or `@$row { ... }`, this is not a reference to state data. Because `rdl!` gives it a different semantics -- declare the parent widget through a variable. + +No matter `@` or `$`, they should first follow the semantics of the macro they are in, and then as a syntactic sugar of Ribir. When we use `@` or `$` in a macro that is not provided by Ribir, they no longer be a syntactic sugar of Ribir, because the external macro may use them with special semantics. For example: + +```rust ignore +use ribir::prelude::*; + +fn_widget!{ + user_macro! { + // `@` is not a syntactic sugar here, its semantics + // depends on the implementation of `user_macro!` + @Row { ... } + } +} +``` + +## `Pipe` stream -- keep responding to data + +A `Pipe` stream is a continuously updated data stream with an initial value. It can be decomposed into an initial value and an RxRust stream -- the RxRust stream can be subscribed. It is also the only channel for Ribir to update data changes to the view. + +Ribir provides a `pipe!` macro to help you quickly create a `Pipe` stream. It accepts an expression and monitors all states marked with `$` in the expression to trigger the recalculation of the expression. + +In the following example, `sum` is a `Pipe` stream of the sum of `a` and `b`. Whenever `a` or `b` changes, `sum` can send the latest result to its downstream. + +```rust +use ribir::prelude::*; + +let a = State::value(0); +let b = State::value(0); + +let sum = pipe!(*$a + *$b); +``` + +When declaring an object, you can initialize its property with a `Pipe` stream, so that its property will continue to change with this `Pipe` stream. As we have seen in [State -- make data watchable and shareable](#state----make-data-watchable-and-shareable) + +```rust ignore + @Text { text: pipe!($count.to_string()) } +``` + +### Dynamically render different widgets + + +At this point, all the structures of the views you create are static, and only the properties will change with the data, but the structure of the widget will not change with the data. You can also create a continuously changing widget structure through the `Pipe` stream. + +Suppose you have a counter that doesn't display the count with numbers, but instead uses red squares to represent the count: + + + +The code: + +```rust +use ribir::prelude::*; + +fn main() { + App::run( fn_widget! { + let counter = State::value(0); + + @Row { + @FilledButton { + on_tap: move |_| *$counter.write() += 1, + @ { Label::new("Increment") } + } + @ { + pipe!(*$counter).map(move |counter| { + (0..counter).map(move |_| { + @Container { + margin: EdgeInsets::all(2.), + size: Size::new(10., 10.), + background: Color::RED + } + }) + }) + } + } + }); +} +``` + +### Try to keep `pipe!` containing the smallest expression + +While `pipe!` can hold any expression, it's best to keep it minimal and use `map` for transformations. This makes it easier to track changes in `pipe!` and avoids unnecessary dependencies in complex expressions. So, in the example above, we write: + + +```rust ignore +pipe!(*$counter).map(move |counter| { + (0..counter).map(move |_| { + @Container { + margin: EdgeInsets::all(2.), + size: Size::new(10., 10.), + background: Color::RED + } + }) +}) +``` + +instead of: + +```rust ignore +pipe!{ + (0..*$counter).map(move |_| { + @Container { + margin: EdgeInsets::all(2.), + size: Size::new(10., 10.), + background: Color::RED + } + }) +} +``` + +### Chain RxRust operators on `Pipe` stream + +The update push of the `Pipe` stream is built on top of the RxRust stream, so the `Pipe` also provides the `value_chain` method for you to operate on the RxRust stream. Therefore, you can use RxRust operators such as `filter`, `debounce` `distinct_until_change` and other operations to reduce the frequency of updates. + +Let's say you have a simple auto-sum example: + +```rust +use ribir::prelude::*; + +fn main() { + App::run(fn_widget! { + let a = State::value(0); + let b = State::value(0); + + @Column { + @Text { text: pipe!($a.to_string()) } + @Text { text: pipe!($b.to_string()) } + @Text { + text: pipe!((*$a + *$b).to_string()) + .value_chain(|s| s.distinct_until_changed().box_it()), + on_tap: move |_| { + *$a.write() += 1; + *$b.write() -= 1; + } + } + } + }); +} +``` + +In the above example, the first two `Text` will be updated with the modification of `a` and `b`, even if the values of `a` and `b` do not change -- such as setting the same value to them. The last `Text` filters out duplicate updates through `distinct_until_changed`, and it will only be updated when the sum of `a` and `b` changes. + +So, when we click on the last `Text`, only the first two `Text` will be marked as updated, and the last `Text` will not. + +> Tip +> +> In general, to identify the dynamic parts of the view, simply look for where `pipe!` is used. + + +## `watch!` watches for modifications to expressions + +`watch!` is a macro that watches for modifications in expressions. It accepts an expression and monitors all states marked with `$` in the expression to trigger the recalculation of the expression and push the latest result to the downstream subscriber. + +Both `watch!` and `pipe!` watch changes in expressions and have similar syntax. However, `pipe!` comes with an initial value, acting more like a continuously changing value rather than a simple subscribable data stream. On the other hand, `watch!` is purely a subscribable data stream. As a result, the output of `pipe!` can be used to initialize widget properties, while the output of `watch!` cannot. + + +In short: + +- `pipe!` = (Initial Value + RxRust Stream) +- `watch!` = RxRust Stream + +Of course, you can also use `watch!` to implement your counter: + +```rust +use ribir::prelude::*; + +fn main() { + App::run(fn_widget! { + let count = State::value(0); + let display = @H1 { text: "0" }; + + watch!(*$count).subscribe(move |v| { + $display.write().text = v.to_string().into(); + }); + + @Row { + @FilledButton { + on_tap: move |_| *$count.write() += 1, + @ { Label::new("Increment") } + } + @{ display } + } + }); +} +``` + +## `Compose` widget -- describe your data structure + +Typically, in complex real-world scenarios, you can't complete all development tasks just by creating some local data and using simple function widgets. You need your own data structures and use `Compose` widgets to map your data structures to the view. + +Using the `Compose` widget, the Counter example can be rewritten as: + +```rust +use ribir::prelude::*; + +struct Counter(usize); + +impl Counter { + fn increment(&mut self) { + self.0 += 1; + } +} + +impl Compose for Counter { + fn compose(this: impl StateWriter) -> impl WidgetBuilder { + fn_widget! { + @Row { + @FilledButton { + on_tap: move |_| $this.write().increment(), + @ { Label::new("Increment") } + } + @H1 { text: pipe!($this.0.to_string()) } + } + } + } +} + +fn main() { + App::run(fn_widget!{ Counter(0) }); +} + +``` + +In the above example, when you implement `Compose` for `Counter`, `Counter` and all writable states of `Counter` are valid widgets. + +## Built-in widgets + +Ribir provides a set of built-in widgets that allow you to configure basic styles, respond to events, manage lifecycles, and more. The key difference between built-in widgets and regular widgets is that when you create a widget declaratively, you can use the fields and methods of the built-in widget as if they were your own. Ribir will handle the creation and composition of the built-in widgets for you. + + +Let's take `Margin` as an example. Suppose you want to set a 10-pixel blank margin for a `Text`, the code is as follows: + + +```rust +use ribir::prelude::*; + +fn main() { + App::run(fn_widget! { + // Declare `Margin` as the parent of `Text` + @Margin { + margin: EdgeInsets::all(10.), + @Text { text: "Hello World!" } + } + }); +} +``` + +But you don't have to explicitly declare a `Margin`, you can write it directly as: + +```rust +use ribir::prelude::*; + +fn main() { + App::run(fn_widget! { + // Use the `Margin::margin` field directly in `Text` + @Text { + margin: EdgeInsets::all(10.), + text: "Hello World!" + } + }); +} +``` + +When you create a widget declaratively, you can directly access the fields of the built-in widget, even if you don't explicitly declare them (if you use them in your code, the corresponding built-in widget will be created). For example: + +```rust +use ribir::prelude::*; + +fn main() { + App::run(fn_widget! { + // `margin` is not declared + let mut hello_world = @Text { text: "Hello World!" }; + // But you can still access the `margin` field, + // It's created with default value when you use it. + $hello_world.write().margin = EdgeInsets::all(10.); + hello_world + }); +} +``` + +Refer to [Built-in widget list](../../builtin_widget/declare_builtin_fields.md) for a list of all built-in fields and methods that can be used as extensions. + + +## Map, Split and trace the original state + +From the previous sections, you have learned: + +- Modifying the data of the state will cause the dependent view to be updated directly +- You can use `Compose` to map the data to view + +Suppose `AppData` is the data of your entire application, you can use `Compose` to map it to the view. However, if `AppData` is complex, using only one `Compose` to map the view of the entire application will be a disaster in code organization; and the entire application view only depends on one state, which will cause any modification to `AppData` to update all dynamic parts of the view. In most cases, this will cause your application to not get the best interactive performance. + +Fortunately, for state management, Ribir provides a mechanism for transformation, splitting, and tracing the origin state. It allows you to start with a complete application state, and then map or split that state into smaller sub-states. These sub-states can be further mapped or split. Within these sub-states, you can use the tracing mechanism to identify their origin state. + +### Map and split, convert state to sub-state + +The **map** is to transform a parent state into a sub-state, and the sub-state has the same data as the parent state. Modifying the parent state is equivalent to modifying the sub-state, and vice versa. It only reduces the visible scope of the data, making it easier for you to use or pass only part of the state. + +The **split** is to separate a sub-state from a parent state. The parent and child state also share the same data. The difference is that modifying data through the sub-state will not trigger the views dependent on the parent state to update, and modifying data through the parent state will cause the split sub-state to be invalidated. + +What you need to note is that whether it's **map** or **split**, the parent and child state share the same data. Therefore, their modifications to the data will affect each other, but the scope of data modifications they push may be different. + +Read the following example carefully to help you better understand how state **map** and **split** work: + +```rust +use ribir::prelude::*; + +struct AppData { + count: usize, +} + +let state = State::value(AppData { count: 0 }); +let map_count = state.map_writer(|d| &d.count, |d| &mut d.count); +let split_count = state.split_writer(|d| &d.count, |d| &mut d.count); + +watch!($state.count).subscribe(|_| println!("Parent data")); +watch!(*$map_count).subscribe(|_| println!("Child(map) data")); +watch!(*$split_count).subscribe(|_| println!("Child(split) data")); +state + .raw_modifies() + .filter(|s| s.contains(ModifyScope::FRAMEWORK)) + .subscribe(|_| println!("Parent framework")); +map_count + .raw_modifies() + .filter(|s| s.contains(ModifyScope::FRAMEWORK)) + .subscribe(|_| println!("Child(map) framework")); +split_count + .raw_modifies() + .filter(|s| s.contains(ModifyScope::FRAMEWORK)) + .subscribe(|_| println!("Child(split) framework")); + +// Modify data through the split sub-state, the data modification push to both the parent and child state subscribers. +// But only the split sub-state subscribers are pushed framework notifications. +*split_count.write() = 1; +AppCtx::run_until_stalled(); +// Print: +// Parent data +// Child(map) data +// Child(split) data +// Child(split) framework + +// When data is modified through the parent state, both the data modification and framework notifications are pushed to the subscribers of the parent and child states. However, the split sub-state becomes invalidated. +state.write().count = 3; +// The push is asynchronous, forcing the push to be sent immediately +AppCtx::run_until_stalled(); +// Print: +// Parent data +// Child(map) data +// Parent framework +// Child(map) framework + +// Modify data through the map sub-state, the data modification push to both the parent and child state subscribers. +*map_count.write() = 2; +AppCtx::run_until_stalled(); +// Print: +// Parent data +// Child(map) data +// Parent framework +// Child(map) framework +``` + +Because data modification notifications are sent out asynchronously in batches, in the example, for ease of understanding, we call `AppCtx::run_until_stalled()` after each data modification to force the notifications to be sent. However, this should not appear in your actual code. + + +If the reader and writer that you map or split from are on the same path, you can use `map_writer!` and `split_writer!` provided by Ribir to simplify your code: + +```rust ignore +// let map_count = state.map_writer(|d| &d.count, |d| &mut d.count) +let map_count = map_writer!($state.count); +// let split_count = state.split_writer(|d| &d.count, |d| &mut d.count); +let split_count = split_writer!($state.count); +``` + +If you only want to get a read-only sub-state, you can use `map_reader` to convert: + +```rust ignore +let count_reader = state.map_reader(|d| &d.count); +``` + +However, Ribir does not provide a `split_reader`, because splitting a read-only sub-state is equivalent to converting a read-only sub-state. + + +### The origin state of the sub-state + +Any state can get where it comes from through `origin_reader` and `origin_writer`. The origin state of the root state is itself, and the origin state of the sub-state is where it splits from. + +```rust +use ribir::prelude::*; + +struct AppData { + count: usize, +} + +let state: State = State::value(AppData { count: 0 }); +let split_count = split_writer!($state.count); + +// the root state's origin state is itself +let _: &State = state.origin_reader(); +let _: &State = state.origin_writer(); + +// the sub-state's origin state is where it splits from +let _: &Writer = split_count.origin_reader(); +let _: &Writer = split_count.origin_writer(); +``` + +## The next step + +You have mastered all the syntax and basic concepts needed to develop a Ribir application. It's time to put them into practice by [Practice: Todos application](../practice_todos_app/non-intrusive_developing.md). + diff --git a/docs/en/introduction.md b/docs/en/introduction.md index a292d0b2c..6d96f46b5 100644 --- a/docs/en/introduction.md +++ b/docs/en/introduction.md @@ -66,10 +66,10 @@ fn_widget!{ The above example shows the way of combining built-in widgets. Even if `Text` does not have a `margin` field, you can still use the `Margin::margin` and compose it with `Text` to form a new widget. `Margin` will only be created when a widget uses the `margin` field, otherwise, there will be no overhead. -**Digestion of composite widgets**: When describing the view of the data, in addition to some basic widgets, most widgets are composed of other widgets. For example, a `Button` is composed of `Text`, `Icon` or `BoxDecoration`, etc. The `Button` itself is not a view element, we call this type of widget a composite widget. Composite widgets will be digested during view construction. They are like a function and are called once during view construction to build the final view and create the corresponding update logic, and do not exist in the final view. +**Digestion of compose widget**: When describing the view of the data, in addition to some basic widgets, most widgets are composed of other widgets. For example, a `Button` is a composition of `Text, `Icon`, `BoxDecoration` and other widgets. +The `Button` itself is not a visual element, but a compose widget. During the view construction, compose widgets are processed. They are similar to a function that is invoked once during view construction to build the final view and establish the corresponding update logic. They do not persist in the final view. -**Only state with write sources are real state**: Unlike other declarative frameworks that add fields to widgets to control widget updates. Ribir is non-intrusive. Ribir treats the entire widget as a state to control updates. -At the same time, it provides the ability to split the state, so that the local view can directly depend on the modification of part of the data to update (introduced in detail in the subsequent tutorial). Another big difference is that stateful and stateless can be converted to each other. If a state has no write source, it will degenerate into stateless, because no one will update it. For example: +**Sateful without writing source will convert to Stateless**: Unlike other declarative frameworks that add fields to widgets to control widget updates. Ribir is non-intrusive. Ribir treats the entire widget as a state to control updates. It provides the ability to split the state so that the local view can directly depend on the modification of part of the data to update (introduced in detail in the subsequent tutorial). Another big difference is that stateful and stateless can be converted to each other. If a state has no write source, it will degenerate into stateless, because no one will update it. For example: ```rust use ribir::prelude::*; diff --git a/docs/en/practice_todos_app/non-intrusive_developing.md b/docs/en/practice_todos_app/non-intrusive_developing.md new file mode 100644 index 000000000..91f12c935 --- /dev/null +++ b/docs/en/practice_todos_app/non-intrusive_developing.md @@ -0,0 +1 @@ +# Coming soon diff --git "a/docs/zh/\345\277\253\351\200\237\344\270\212\346\211\213/\345\277\253\351\200\237\345\205\245\351\227\250.md" "b/docs/zh/\345\277\253\351\200\237\344\270\212\346\211\213/\345\277\253\351\200\237\345\205\245\351\227\250.md" index 686da073b..8ef2c71e7 100644 --- "a/docs/zh/\345\277\253\351\200\237\344\270\212\346\211\213/\345\277\253\351\200\237\345\205\245\351\227\250.md" +++ "b/docs/zh/\345\277\253\351\200\237\344\270\212\346\211\213/\345\277\253\351\200\237\345\205\245\351\227\250.md" @@ -12,7 +12,7 @@ > - 如何将内建 widget 当做其它 widget 的一部分来使用 > - 如何对状态进行转换,分离和溯源——方便状态的传递和控制视图的更新范围 -## 什么是 widget +## 什么是 widget? 在 Ribir 中,widget 作为核心概念,它是对视图进行描述的基本单元。在形式上它可以是一个按钮,一个文本框,一个列表,一个对话框,甚至是整个应用界面。在代码上,它可以是一个函数,一个闭包或者一个数据对象。Ribir 将能通过 `&BuildCtx` 构建出 `Widget` 的类型叫做 widget。注意 `Widget` 和 widget 的差别,在整个 Ribir 的语境中,widget 是一个泛称,而大写开头的 `Widget` 是一个具体的 widget,也是所有 widget 构建进入视图的通行证。 @@ -49,8 +49,7 @@ fn main() { } ``` - -首先,你应该发现了在函数签名中参数声明(`ctx!(): &BuildCtx`)的特别之处,我们用 `ctx!()` 来作为参数名字,而不是直接给一个名字。这是因为 `rdl!` 内部会统一通过 `ctx!()` 作为变量名来引用 `&BuildCtx`。 +首先,你应该发现了在函数签名中参数声明(`ctx!(): &BuildCtx`)的不同之处,我们用 `ctx!()` 来作为参数名字,而不是直接给一个名字。这是因为 `rdl!` 内部会统一通过 `ctx!()` 作为变量名来引用 `&BuildCtx`。 接下来一行 `rdl!{ Text { text: "Hello World!" } }`,通过 `rdl!` 创建了一个内容为 `Hello World!` 的 `Text`。关于 `rdl!` 的细节,你可以先放到一边,将在小节 [使用 `rdl!` 创建对象](#使用-rdl-创建对象) 中详细介绍。 @@ -106,7 +105,9 @@ fn main() { `rdl` 是 Ribir Declarative Language 的缩写, `rdl!` 宏的目的就是帮助你以声明式的方式来创建对象。 -> 注意:`rdl!` 并不关注类型,只在语法层面做处理,所以并不是只有 widget 才可以用它。 +> 注意 +> +> `rdl!` 并不关注类型,只在语法层面做处理,所以并不是只有 widget 才可以用它。 ### 声明式创建对象 @@ -128,7 +129,6 @@ pub struct Counter { fn use_rdl(ctx!(): &BuildCtx) { let _ = rdl!{ Counter { } }; } -; ``` 上面的例子中,`Counter` 继承了 `Declare`, 并标记 `count` 默认值为 `1`。 所以在 `rdl!` 中,你可以不用给 `count` 赋值,`rdl!` 创建它时会默认赋值为 `1`。`Declare` 还有一些其它的特性,我们暂不在这里展开。 @@ -185,6 +185,8 @@ fn main() { 注意到 `rdl!{ $row { ... } }` 了吗? 它和结构体字面量语法一样,但是加上 `$` 后,它表示作为父亲使一个变量而不是类型,所以它不会新建一个 widget,而是直接使用这个变量来和孩子组合。 +> 小提示 +> > 在 Ribir 中,父子的组合并不是任意的,而是有类型限制的,父亲可以约束孩子的类型并给出组合逻辑。这确保了组合的正确性。 > > 在我们上面的例子中,`Row` 接收任意数目,任意类型的 widget,`Text` 不能接收任何孩子, 而 `FilledButton` 则更复杂一点,它允许接收一个 `Label` 作为它的文字和一个 `Svg` 作为按钮图标。 @@ -193,7 +195,7 @@ fn main() { ### 表达式创建对象 -除了通过结构体字面量创建对象以外,你还可以通过 `rdl! {...}` 包裹任意表达式来创建对象。这种方式的好处是,你可以在 `{...}` 中写任意的表达式,你没有必要单独使用这种写法,但它在嵌套组合中非常有用。比方: +除了通过结构体字面量创建对象以外,你还可以通过 `rdl! {...}` 包裹任意表达式来创建对象。这种方式的好处是,你可以在 `{...}` 中写任意代码在创建对象。这在嵌套组合中非常有用,也只在嵌套作为孩子时有必要。下面的例子展示如何在 `rdl` 中使用表达式创建对象: ```rust ignore use ribir::prelude::*; @@ -201,7 +203,7 @@ use ribir::prelude::*; let _ = fn_widget! { rdl!{ Row { rdl!{ - // 在这里你可以写任意的表达式,通过逻辑来决定要返回一个什么样的 widget + // 在这里你可以写任意的表达式,表达式的结果将作为孩子 if xxx { ... } else { @@ -226,11 +228,11 @@ fn main() { 相信你应该已经完全理解它了。 -## @ 语法糖 +## `@` 语法糖 -在组合 widget 的过程中,用到了大量的 `rdl!`,这看上去太冗长了,无法让你一眼看到重点信息。但另一方面,它又让你在与 Rust 语法交互的例子中(特别是复杂的例子)能有一个清晰的声明式结构——当你看到 `rdl!` 时,你就知道一个 widget 节点的组合或创建开始了。 +在组合 widget 的过程中,我们用到了大量的 `rdl!`。一方面,它让你在与 Rust 语法交互时(特别是复杂的例子)能有一个清晰的声明式结构——当你看到 `rdl!` 时,你就知道一个 widget 节点的组合或创建开始了;另一方面,当每一个节点都用 `rdl!` 包裹时,它又看上去太冗长了,无法让你一眼看到重点信息。 -实际上,Ribir 为 `rdl!` 提供了一个 `@` 语法糖,在实际使用的过程中,基本上用的都是 `@` 而非 `rdl!`。总共有三种情况: +好在,Ribir 为 `rdl!` 提供了一个 `@` 语法糖,在实际使用的过程中,基本上用的都是 `@` 而非 `rdl!`。总共有三种情况: - `@ Row {...}` 作为结构体字面量的语法糖,展开为 `rdl!{ Row {...} }` - `@ $row {...}` 作为变量结构体字面量的语法糖,展开为 `rdl!{ $row {...} }` @@ -253,21 +255,21 @@ fn main() { } ``` -## 状态——可被侦听和共享的数据 +## 状态——让数据变得可被侦和共享 -你虽然创建了一个计数器,但它总是显示 `0`,也不响应按钮做任何事情。在这一节中,你将会了解到如何通过状态让你的计数器工作。 +我们虽然创建了一个计数器,但它总是显示 `0`,也不响应按钮做任何事情。在这一节中,你将会了解到如何通过状态让你的计数器工作。 -状态是可被侦听和共享的数据,它可以由任意类型的数据转换而来。 +状态是一个将数据变得可被侦听和共享的包装器。 `状态 = 数据 + 可侦听 + 可共享` 一个可交互的 Ribir widget 的完整个生命周期是这样的: -1. 将 widget 的驱动源——数据,转换为状态。 -2. 对数据进行声明式映射构建出视图。 +1. 将你的数据转换为状态。 +2. 对状态进行声明式映射构建出视图。 3. 在交互过程中,通过状态来修改数据。 -4. 状态将数据的变更根据其映射关系,直接修改到视图上。 -5. 重复步骤 3,4 。 +4. 通过状态接收到数据的变更,根据映射关系点对点更新视图 +5. 重复步骤 3 和 4 。 ![状态的生命周期](../../assets/data-flows.svg) @@ -294,7 +296,7 @@ fn main() { } ``` -通过这 3 处变更,计数器的小例子全部完成了。但是在变更 2 和变更 3 中,有新的东西被引入了 —— `$` 和 `pipe!`。它们在使用 Ribir 的过程中都非常重要,让我们用两个小节来分别展开介绍。 +通过这 3 处变更,计数器的小例子全部完成了。但是在变更 2 和变更 3 中,有新的东西被引入了 —— `$` 和 `pipe!`。它们是 Ribir 中非常重要的用法,让我们用两个小节来分别展开介绍。 ## $ 语法糖 @@ -306,7 +308,7 @@ fn main() { 除了 `write` 以外, Ribir 还有一个 `silent` 写引用,通过 `silent` 写引用修改数据不会触发视图更新。 -状态的 `$` 语法糖展开为: +状态的 `$` 语法糖展开逻辑为: - `$counter.write()` 展开为 `counter.write()` - `$counter.silent()` 展开为 `counter.silent()` @@ -333,9 +335,9 @@ move |_| *$count.write() += 1 ### 语法糖展开的优先级 还记得我们在[组合-widget](#组合-widget)中也同样用到了 `$` 吗? -比如 `rdl!{ $row { ... } }` 或者 `@$row { ... }`,这可不是对状态数据的引用哦。因为 `rdl!` 赋予了它特殊的语义——通过变量声明父 widget。 +比如 `rdl!{ $row { ... } }` 或者 `@$row { ... }`,这可不是对状态数据的引用哦。因为 `rdl!` 赋予了它其它的语义——通过变量声明父 widget。 -在 Ribir 中,无论是 `@` 还是 `$`,它们首先应遵循它们所在宏的语义,其次才是它们作为语法糖的意义。当我们在一个非 Ribir 提供的宏中使用 `@` 或 `$` 时,它们就不再具有作为一个语法糖的意义了,因为外部宏很可能为它们赋予了特殊的意义,即使这个宏被使用在 `fn_widget!` 中。比如: +无论是 `@` 还是 `$`,它们首先应遵循它们所在宏的语义,其次才是一个 Ribir 语法糖。当我们在一个非 Ribir 提供的宏中使用 `@` 或 `$` 时,它们就不再是 Ribir 的语法糖,因为外部宏很可能为它们赋予了特殊的语义。比如: ```rust ignore use ribir::prelude::*; @@ -346,7 +348,6 @@ fn_widget!{ @Row { ... } } } - ``` ## `Pipe` 流 —— 保持对数据的持续响应 @@ -355,7 +356,7 @@ fn_widget!{ Ribir 提供了一个 `pipe!` 宏来辅助你快速创建 `Pipe` 流。它接收一个表达式,并监测表达式中的所有用 `$` 标记出的状态,以此来触发表达式的重算。 -比方,下面的例子中, `sum` 是一个 `a`, `b` 之和的 `Pipe` 流,每当 `a` 或 `b` 变更时,`sum` 都能向它的下游发送最新结果。 +在下面的例子中, `sum` 是一个 `a`, `b` 之和的 `Pipe` 流,每当 `a` 或 `b` 变更时,`sum` 都能向它的下游发送最新结果。 ```rust use ribir::prelude::*; @@ -366,7 +367,7 @@ let b = State::value(0); let sum = pipe!(*$a + *$b); ``` -在声明一个对象时,你可以通过一个 `Pipe` 流去初始化它的属性,这样它的属性就会持续随着这个 `Pipe` 流变更。如我们在[状态可被侦听和共享的数据)](#状态可被侦听和共享的数据)中见过的: +在声明一个对象时,你可以通过一个 `Pipe` 流去初始化它的属性,这样它的属性就会持续随着这个 `Pipe` 流变更。如我们在[状态——让数据变得可被侦和共享](#状态——让数据变得可被侦和共享)中见过的: ```rust ignore @Text { text: pipe!($count.to_string()) } @@ -374,7 +375,7 @@ let sum = pipe!(*$a + *$b); ### 动态渲染不同的 widget -到目前为止,你所有创建的 widget 的视图结构都是静态的,它们仅仅只有属性会随着数据变更,但 widget 的结构不会随着数据变更。实际上,你同样可以通过 `Pipe` 流来创建持续变化的 widget 结构。 +到目前为止,所有你创建的视图的结构都是静态的,它们仅仅只有属性会随着数据变更,但 widget 的结构不会随着数据变更。实际上,你同样可以通过 `Pipe` 流来创建持续变化的 widget 结构。 假设你有一个计数器,这个计数器不是用文字来显示数目,而是通过红色小方块来计数: @@ -560,7 +561,7 @@ fn main() { ## 内建 widget -Ribir 提供了一组内建 widget,让你可以配置基础的样式、响应事件和生命周期等等。内建 widget 和普通的 widget 的重要差别在于——在声明式创建 widget 时,你可以直接将内建 widget 的字段和方法当做是你所创建的 widget 自己的一样来来使用,Ribir 会帮你完成内建 widget 的创建和组合。 +Ribir 提供了一组内建 widget,让你可以配置基础的样式、响应事件和生命周期等等。内建 widget 和普通的 widget 的重要差别在于——在声明式创建 widget 时,你可以直接将内建 widget 的字段和方法当做是你所创建 widget 自己的一样来使用,Ribir 会帮你完成内建 widget 的创建和组合。 拿 `Margin` 举例,假设你要为一个 `Text` 设置 10 像素的空白边距,代码如下: @@ -626,59 +627,74 @@ fn main() { ### 转换和分离,将状态转换为子状态 -**转换**是将一个父状态转换为子状态,但子状态仍保留了和父状态同样的修改通知范围。因此,转换仅是缩减了数据的可见范围,除此之外,它与父状态别无二致。 +**转换**是将一个父状态转换为子状态,父子状态共享同样的数据,修改父状态等同于修改子状态,反之亦然。它仅仅是缩减了数据的可见范围,方便你只想使用和传递传递部分状态。 -**分离**是从一个父状态中分离出子状态,子状态不仅缩减了数据的可见范围,同时拥有了一个独立的修改通知范围。通过子状态修改数据,只会通知子状态的依赖;通过父状态的修改数据,会同时通知父子状态的依赖。 +**分离**是从一个父状态中分离出子状态,父子状态共享同样的数据。不同的是,通过子状态修改数据不会触发父状态的依赖视图更新,而通过父状态修改数据则会导致分离子状态失效。 -你要注意的是,不论是转换还是分离,父子状态共享的都是同一份数据。因此,它们对数据的修改会影响到彼此,但它们触发的依赖更新的范围可能不同。 +你要注意的是,不论是转换还是分离,父子状态共享的都是同一份数据。因此,它们对数据的修改会影响到彼此,但它们所推送的数据变更的范围可能不同。 仔细阅读下面的例子,会帮助你更好的理解状态的转换和分离是如何工作的: ```rust use ribir::prelude::*; - struct AppData { - count: usize, - } - - let state = State::value(AppData { count: 0 }); - let map_count = state.map_writer(|d| &d.count, |d| &mut d.count); - let split_count = state.split_writer(|d| &d.count, |d| &mut d.count); - - watch!($state.count).subscribe(|v| println!("父状态通知: {v}")); - watch!(*$map_count).subscribe(|v| println!("子状态(转换)通知: {v}")); - watch!(*$split_count).subscribe(|v| println!("子状态(分离)通知: {v}")); - - // 通过父状态修改数据, 父子状态的依赖都会被通知 - state.write().count = 1; - // 强制数据变更的通知立即发出 - AppCtx::run_until_stalled(); - // 打印内容: - // 父状态通知: 1 - // 子状态(转换)通知: 1 - // 子状态(分离)通知: 1 - - // 通过转换子状态修改数据,父子状态的依赖都会被通知 - *map_count.write() = 2; - AppCtx::run_until_stalled(); - // 打印内容: - // 父状态通知: 2 - // 子状态(转换)通知: 2 - // 子状态(分离)通知: 2 - - // 通过分离子状态修改数据,只有子状态的依赖会被通知 - *split_count.write() = 3; - AppCtx::run_until_stalled(); - // 打印内容: - // 子状态(分离)通知: 3 +struct AppData { + count: usize, +} +let state = State::value(AppData { count: 0 }); +let map_count = state.map_writer(|d| &d.count, |d| &mut d.count); +let split_count = state.split_writer(|d| &d.count, |d| &mut d.count); + +watch!($state.count).subscribe(|_| println!("父状态数据")); +watch!(*$map_count).subscribe(|_| println!("子状态(转换)数据")); +watch!(*$split_count).subscribe(|_| println!("子状态(分离)数据")); +state + .raw_modifies() + .filter(|s| s.contains(ModifyScope::FRAMEWORK)) + .subscribe(|_| println!("父状态 框架")); +map_count + .raw_modifies() + .filter(|s| s.contains(ModifyScope::FRAMEWORK)) + .subscribe(|_| println!("子状态(转换)框架")); +split_count + .raw_modifies() + .filter(|s| s.contains(ModifyScope::FRAMEWORK)) + .subscribe(|_| println!("子状态(分离)框架")); + +// 通过分离子状态修改数据,父子状态的订阅者都会被推送数据通知, +// 只有分离子状态的订阅者被推送框架通知 +*split_count.write() = 1; +// 推送是是异步的,强制推送立即发出 +AppCtx::run_until_stalled(); +// 打印内容: +// 父状态数据 +// 子状态(转换)数据 +// 子状态(分离)数据 +// 子状态(分离)框架 + +// 通过父状态修改数据, 分离状态会失效,父子状态的依赖都会被推送 +state.write().count = 3; +AppCtx::run_until_stalled(); +// 打印内容: +// 父状态数据 +// 子状态(转换)数据 +// 父状态 框架 +// 子状态(转换)框架 + +// 通过转换子状态修改数据,父子状态的依赖都会被推送 +*map_count.write() = 2; +AppCtx::run_until_stalled(); +// 打印内容: +// 父状态数据 +// 子状态(转换)数据 +// 父状态 框架 +// 子状态(转换)框架 ``` -在上面的例子中,父子状态的通知顺序是不确定的,因此你可能看到不一样的打印顺序。 - -因为 Ribir 的数据修改通知是批量发出的,所以每次数据修改都调用了 `AppCtx::run_until_stalled()` 来方便你理解,但这不应该出现在你真实的代码中。 +因为 Ribir 的数据修改通知是异步批量发出的,所以在例子中为了方便理解,我们每次数据修改都调用了 `AppCtx::run_until_stalled()` 来强制理解发送,但这不应该出现在你真实的代码中。 -如果你的状态转换或分离的读写是同一个路径,你可以使用 Ribir 提供的 `map_writer!` 和 `split_writer!` 来简化你的代码: +如果你的状态读写器转换或分离自同一个路径,你可以使用 Ribir 提供的 `map_writer!` 和 `split_writer!` 来简化你的代码: ```rust ignore // let map_count = state.map_writer(|d| &d.count, |d| &mut d.count) @@ -693,11 +709,11 @@ let split_count = split_writer!($state.count); let count_reader = state.map_reader(|d| &d.count); ``` -并没有一个 `split_reader` 提供,因为仅分离一个只读的子状态而不可修改其意义等于转换一个只读子状态。 +但 Ribir 并没有提供一个 `split_reader`,因为分离一个只读的子状态,其意义等同于转换一个只读子状态。 ### 溯源状态 -任何状态都可以通过 `origin_reader` 和 `origin_writer` 来获得当前状态的来源。根状态的源状态是自己,而子状态的源状态是它的父亲。 +任何状态都可以通过 `origin_reader` 和 `origin_writer` 来获得当前状态的来源。根状态的源状态是自己,而子状态的源状态是转换或分离出它的父状态。 ```rust @@ -721,4 +737,4 @@ let _: &Writer = split_count.origin_writer(); ## 下一步 -至此,你已经掌握了开发 Ribir 引用所需的全部语法和基础概念了。是时候到[开发一个 Todos 应用](../todos/README.md)将它们付诸实践了。 \ No newline at end of file +至此,你已经掌握了开发 Ribir 引用所需的全部语法和基础概念了。是时候到[实践: Todos 应用](../实践:Todos%20应用/非侵入式开发.md)将它们付诸实践了。 \ No newline at end of file diff --git "a/docs/zh/\347\256\200\344\273\213.md" "b/docs/zh/\347\256\200\344\273\213.md" index 9fb6ab8ab..8ef2282cc 100644 --- "a/docs/zh/\347\256\200\344\273\213.md" +++ "b/docs/zh/\347\256\200\344\273\213.md" @@ -64,9 +64,9 @@ fn_widget!{ 上面的例子展示了内建 widget 组合的方式,即使 `Text` 没有一个 `margin` 字段,但仍可以使用 `Margin::margin` 字段,并和它组合成一个新的 widget 。当一个 widget 使用了 `margin` 字段,`Margin` 才会被创建,否则不会有任何开销。 -**消化 composite widget**: 在描述数据的视图时,除了一些基础 widget,多数情况 widget 是由其它 widget 组合而成。例如一个 `Button`,它由 `Text`、`Icon` 或 `BoxDecoration` 等 widget 组合而成, `Button` 本身不是一个视图元素,这类 widget 我们称为 composite widget 。Composite widget 在视图构建时会被消化掉,它们就像一个函数一样,在构建视图的时候被调用一次,构建出最终的视图并创建好相应的更新逻辑,并不存在于最终的视图中。 +**消化 compose widget**: 在描述数据的视图时,除了一些基础 widget,多数情况 widget 是由其它 widget 组合而成。例如一个 `Button`,它由 `Text`、`Icon` 或 `BoxDecoration` 等 widget 组合而成, `Button` 本身不是一个视图元素,我们称这类 widget 为 compose widget 。Compose widget 在视图构建时会被消化掉,它们就像一个函数一样,在构建视图的时候被调用一次,构建出最终的视图并创建好相应的更新逻辑,并不存在于最终的视图中。 -**有写入源的状态才是真状态**: 和其它的声明式框架在 widget 中增加字段来控制 widget 的更新不一样。Ribir 是非侵入的,Ribir 将整个 widget 作为一个状态来控制更新。同时提供了状态分裂的能力,使视图的局部可以直接依赖部分数据的变更来更新(后续教程中具体介绍)。另一个大的差别在于——有状态和无状态的是可以互相转换的。如果一个状态没有任何写入源,它将退化成无状态的,因为不会有任何人去更新它。例如: +**没有写入源的状态会退化成数据**: 和其它的声明式框架在 widget 中增加字段来控制 widget 的更新不一样。Ribir 是非侵入的,Ribir 将整个 widget 作为一个状态来控制更新。同时提供了状态分裂的能力,使视图的局部可以直接依赖部分数据的变更来更新(后续教程中具体介绍)。另一个大的差别在于——有状态和无状态的是可以互相转换的。如果一个状态没有任何写入源,它将退化成无状态的,因为不会有任何人去更新它。例如: ```rust use ribir::prelude::*;