Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve state management docs #456

Merged
merged 27 commits into from
Sep 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
eaff4d1
Add tokio macros in code snippets
temeddix Sep 29, 2024
f5aa762
Add example actor code
temeddix Sep 29, 2024
753aa7a
Add doc comments in example actor module
temeddix Sep 29, 2024
fcfe7cb
Improve actor code readability
temeddix Sep 29, 2024
b7ee03b
Shorten actor names
temeddix Sep 29, 2024
d2d0cd9
Update the actor code snippet in the docs
temeddix Sep 29, 2024
e89e059
Remove unneeded doc sentences
temeddix Sep 29, 2024
c521287
Update a code snippet to make it consistent
temeddix Sep 29, 2024
a81185e
Improve code snippet readability
temeddix Sep 29, 2024
0305c24
Make the comments more consistent
temeddix Sep 29, 2024
5e7b392
Update a comment for clarity
temeddix Sep 29, 2024
a3d3d0b
Add a link to the example code
temeddix Sep 29, 2024
4045161
Rewrite the counter code with an actor
temeddix Sep 29, 2024
5c0e436
Shorten the example actor code
temeddix Sep 29, 2024
349d6f5
Organize code
temeddix Sep 29, 2024
26b87a6
Format comments
temeddix Sep 29, 2024
23f4e1e
Oragnize comments in the first template code
temeddix Sep 29, 2024
e6c93cd
Add comments in the template code
temeddix Sep 29, 2024
efdc112
Fix the error on the web
temeddix Sep 29, 2024
568187b
Rewrite a comment
temeddix Sep 29, 2024
55e6b6e
Rewrite a comment
temeddix Sep 29, 2024
d291557
Add a trailing comma in the template
temeddix Sep 29, 2024
ae571ba
Organize `common` module in example code
temeddix Sep 29, 2024
214453f
Add `anyhow` to the example code
temeddix Sep 29, 2024
377eb22
Add a link to the example code in the docs
temeddix Sep 29, 2024
3a04b86
Organize error code
temeddix Sep 29, 2024
a9ff342
Fix an import alias
temeddix Sep 29, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion documentation/docs/applying-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ Various comments are written in the actual code to help you understand the whole

If you already have a Rust crate that you want to use here, just put it inside `./native` and set it as a dependency of the `hub` crate.

Now by heading over to `./native/hub/src/lib.rs`, you can start writing Rust!
Now, by heading over to `./native/hub/src/lib.rs`, you can start writing Rust!

Example code for guidance can be found [here](https://github.com/cunarist/rinf/tree/main/flutter_package/example).

!!! info

Expand Down
1 change: 1 addition & 0 deletions documentation/docs/frequently-asked-questions.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ pub async fn respond() {
```

```rust title="native/hub/src/lib.rs"
#[tokio::main]
async fn main() {
tokio::spawn(sample_functions::respond());
}
Expand Down
54 changes: 30 additions & 24 deletions documentation/docs/state-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,45 +8,47 @@ Rinf performs best when the application logic is written entirely in Rust, with

The actor model is highly recommended for managing asynchronous state in Rust. By encapsulating state and behavior within actor structs, which maintain ownership and handle their own async tasks, the actor model provides a scalable and modular way to manage complex state interactions.

1. **Encapsulation**: Actors encapsulate state and behavior, allowing for modular and maintainable code.
2. **Concurrency**: Each actor operates independently, making it easier to handle concurrent tasks without manual synchronization.
3. **Scalability**: Actors are well-suited for scalable systems where tasks and state management need to be handled in parallel.

Several crates on `crates.io` provide building blocks for implementing the actor model in Rust. Although Rinf uses `tokio` by default, you can choose any async Rust runtime that fits your needs. Consider exploring these crates to find one that aligns with your requirements.

Here’s a basic example using the [`actix`](https://github.com/actix/actix) crate, a popular choice for the actor model:
Here’s a basic example using the [`messages`](https://crates.io/crates/messages) crate, which is a flexible and runtime-agnostic actor library that works nicely with Rinf.

```rust title="native/hub/src/lib.rs"
use actix::prelude::*;
use messages::prelude::*;

rinf::write_interface!()
rinf::write_interface!();

// this is our Message
// we have to define the response type (rtype)
#[derive(Message)]
#[rtype(usize)]
// Represents a message to calculate the sum of two numbers.
struct Sum(usize, usize);

// Actor definition
// Actor definition that will hold state in real apps.
struct Calculator;

impl Actor for Calculator {
type Context = Context<Self>;
}
// Implement `Actor` trait for `Calculator`.
impl Actor for Calculator {}

// now we need to implement `Handler` on `Calculator` for the `Sum` message.
// Implement `Handler` for `Calculator` to handle `Sum` messages.
#[async_trait]
impl Handler<Sum> for Calculator {
type Result = usize; // <- Message response type

fn handle(&mut self, msg: Sum, _ctx: &mut Context<Self>) -> Self::Result {
type Result = usize;
async fn handle(&mut self, msg: Sum, _: &Context<Self>) -> Self::Result {
msg.0 + msg.1
}
}

#[actix::main] // <- starts the system and block until future resolves
// Implement the start method for `Calculator`.
impl Calculator {
pub fn start() -> Address<Self> {
let context = Context::new();
let actor = Self {};
let addr = context.address();
tokio::spawn(context.run(actor));
addr
}
}

// Main function to start the business logic.
#[tokio::main]
async fn main() {
let addr = Calculator.start();
let res = addr.send(Sum(10, 5)).await; // <- send message and get future for result
let mut addr = Calculator::start();
let res = addr.send(Sum(10, 5)).await;

match res {
Ok(result) => println!("SUM: {}", result),
Expand All @@ -55,6 +57,10 @@ async fn main() {
}
```

Several crates on `crates.io` provide building blocks for implementing the actor model in Rust. Consider exploring these crates to find one that aligns with your requirements.

Please refer to the [example code](https://github.com/cunarist/rinf/tree/main/flutter_package/example) for detailed usage.

## 🧱 Static Variables

Generally, it's advisable to avoid static variables due to their characteristics, which can lead to issues such as difficulties in testing and managing lifetimes. If you must use static variables, you can declare them as shown below, ensuring they span the entire duration of the app.
Expand Down
3 changes: 3 additions & 0 deletions documentation/docs/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ pub async fn calculate_precious_data() {
```rust title="native/hub/src/lib.rs"
mod tutorial_functions;

#[tokio::main]
async fn main() {
tokio::spawn(tutorial_functions::calculate_precious_data());
}
Expand Down Expand Up @@ -138,6 +139,7 @@ pub async fn stream_amazing_number() {
```rust title="native/hub/src/lib.rs"
mod tutorial_functions;

#[tokio::main]
async fn main() {
tokio::spawn(tutorial_functions::stream_amazing_number());
}
Expand Down Expand Up @@ -226,6 +228,7 @@ pub async fn tell_treasure() {
```rust title="native/hub/src/lib.rs"
mod tutorial_functions;

#[tokio::main]
async fn main() {
tokio::spawn(tutorial_functions::tell_treasure());
}
Expand Down
2 changes: 2 additions & 0 deletions flutter_package/example/native/hub/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@ tokio_with_wasm = { version = "0.7.1", features = [
"macros",
] }
wasm-bindgen = "0.2.93"
messages = "0.3.1"
anyhow = "1.0.89"
sample_crate = { path = "../sample_crate" }
77 changes: 77 additions & 0 deletions flutter_package/example/native/hub/src/actors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//! The actor model is highly recommended for state management,
//! as it provides modularity and scalability.
//! This module demonstrates how to use actors
//! within the async system in Rust.
//! To build a solid app, do not communicate by sharing memory;
//! instead, share memory by communicating.

use crate::common::*;
use crate::messages::*;
use messages::prelude::*;
use rinf::debug_print;

// The letter type for communicating with an actor.
pub struct ClickedLetter;

// The actor that holds the counter state and handles messages.
pub struct CountingActor {
// The counter number.
count: i32,
}

// Implementing the `Actor` trait for `CountingActor`.
// This defines `CountingActor` as an actor in the async system.
impl Actor for CountingActor {}

impl CountingActor {
pub fn new(counting_addr: Address<Self>) -> Self {
spawn(Self::listen_to_button_click(counting_addr));
CountingActor { count: 0 }
}

async fn listen_to_button_click(mut counting_addr: Address<Self>) {
// Spawn an asynchronous task to listen for
// button click signals from Dart.
let receiver = SampleNumberInput::get_dart_signal_receiver();
// Continuously listen for signals.
while let Some(dart_signal) = receiver.recv().await {
let letter = dart_signal.message.letter;
debug_print!("{letter}");
// Send a letter to the counting actor.
let _ = counting_addr.send(ClickedLetter).await;
}
}
}

#[async_trait]
impl Handler<ClickedLetter> for CountingActor {
type Result = ();
// Handles messages received by the actor.
async fn handle(&mut self, _msg: ClickedLetter, _context: &Context<Self>) {
// Increase the counter number.
let new_number = self.count + 7;
self.count = new_number;

// The send method is generated from a marked Protobuf message.
SampleNumberOutput {
current_number: new_number,
dummy_one: 11,
dummy_two: None,
dummy_three: vec![22, 33, 44, 55],
}
.send_signal_to_dart();
}
}

// Creates and spawns the actors in the async system.
pub async fn create_actors() -> Result<()> {
// Create actor contexts.
let counting_context = Context::new();
let counting_addr = counting_context.address();

// Spawn actors.
let actor = CountingActor::new(counting_addr);
spawn(counting_context.run(actor));

Ok(())
}
21 changes: 12 additions & 9 deletions flutter_package/example/native/hub/src/common.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use std::error::Error;
// `tokio_with_wasm` enables `tokio` code
// to run directly on the web.
pub use tokio_with_wasm::alias as tokio;

/// This `Result` type alias allows handling any error type
/// that implements the `Error` trait.
/// In practice, it is recommended to use custom solutions
/// or crates like `anyhow` dedicated to error handling.
/// Building an app differs from writing a library, as apps
/// may encounter numerous error situations, which is why
/// a single, flexible error type is needed.
pub type Result<T> = std::result::Result<T, Box<dyn Error + Send + Sync>>;
/// This `Result` type alias unifies the error type.
/// Building an app differs from writing a library,
/// as app may encounter numerous error situations.
/// Therefore, a single, flexible error type is recommended.
pub type Result<T> = anyhow::Result<T>;

/// Because spawn functions are used very often,
/// we make them accessible from everywhere.
pub use tokio::task::{spawn, spawn_blocking};
10 changes: 6 additions & 4 deletions flutter_package/example/native/hub/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
//! This `hub` crate is the
//! entry point of the Rust logic.

mod actors;
mod common;
mod messages;
mod sample_functions;

use common::*;
use tokio_with_wasm::alias as tokio;

rinf::write_interface!();

// You can go with any async runtime, not just tokio's.
// You can go with any async library, not just `tokio`.
#[tokio::main(flavor = "current_thread")]
async fn main() {
// Spawn concurrent tasks.
// Always use non-blocking async functions like `tokio::fs::File::open`.
// If you must use blocking code, use `tokio::task::spawn_blocking`
// or the equivalent provided by your async library.
tokio::spawn(sample_functions::tell_numbers());
tokio::spawn(sample_functions::stream_fractal());
tokio::spawn(sample_functions::run_debug_tests());
spawn(sample_functions::stream_fractal());
spawn(sample_functions::run_debug_tests());
spawn(actors::create_actors());

// Keep the main function running until Dart shutdown.
rinf::dart_shutdown().await;
Expand Down
63 changes: 17 additions & 46 deletions flutter_package/example/native/hub/src/sample_functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

use crate::common::*;
use crate::messages::*;
use crate::tokio;
use rinf::debug_print;
use std::time::Duration;

Expand All @@ -12,42 +11,14 @@ const IS_DEBUG_MODE: bool = true;
#[cfg(not(debug_assertions))]
const IS_DEBUG_MODE: bool = false;

// Business logic for the counter widget.
pub async fn tell_numbers() {
let mut vector = Vec::new();

// Stream getter is generated from a marked Protobuf message.
let receiver = SampleNumberInput::get_dart_signal_receiver();
while let Some(dart_signal) = receiver.recv().await {
// Extract values from the message received from Dart.
// This message is a type that's declared in its Protobuf file.
let number_input = dart_signal.message;
let letter = number_input.letter;
debug_print!("{letter}");

// Perform a simple calculation.
vector.push(true);
let current_number = (vector.len() as i32) * 7;

// The send method is generated from a marked Protobuf message.
SampleNumberOutput {
current_number,
dummy_one: number_input.dummy_one,
dummy_two: number_input.dummy_two,
dummy_three: number_input.dummy_three,
}
.send_signal_to_dart();
}
}

// Business logic for the fractal image.
pub async fn stream_fractal() {
let mut current_scale: f64 = 1.0;

let (sender, mut receiver) = tokio::sync::mpsc::channel(5);

// Send frame join handles in order.
tokio::spawn(async move {
spawn(async move {
loop {
// Wait for 40 milliseconds on each frame
tokio::time::sleep(Duration::from_millis(40)).await;
Expand All @@ -62,15 +33,15 @@ pub async fn stream_fractal() {

// Calculate the fractal image
// parallelly in a separate thread pool.
let join_handle = tokio::task::spawn_blocking(move || {
let join_handle = spawn_blocking(move || {
sample_crate::draw_fractal_image(current_scale)
});
let _ = sender.send(join_handle).await;
}
});

// Receive frame join handles in order.
tokio::spawn(async move {
spawn(async move {
loop {
let join_handle = match receiver.recv().await {
Some(inner) => inner,
Expand All @@ -95,18 +66,6 @@ pub async fn stream_fractal() {
});
}

// A dummy function that uses sample messages to eliminate warnings.
#[allow(dead_code)]
async fn use_messages() {
let _ = SampleInput::get_dart_signal_receiver();
SampleOutput {
kind: 3,
oneof_input: Some(sample_output::OneofInput::Age(25)),
}
.send_signal_to_dart();
let _ = DeeperDummy {};
}

// Business logic for testing various crates.
pub async fn run_debug_tests() -> Result<()> {
if !IS_DEBUG_MODE {
Expand Down Expand Up @@ -170,7 +129,7 @@ pub async fn run_debug_tests() -> Result<()> {
let mut join_handles = Vec::new();
let chunk_size = 10_i32.pow(6);
for level in 0..10 {
let join_handle = tokio::task::spawn_blocking(move || {
let join_handle = spawn_blocking(move || {
let mut prime_count = 0;
let count_from = level * chunk_size + 1;
let count_to = (level + 1) * chunk_size;
Expand Down Expand Up @@ -208,7 +167,7 @@ pub async fn run_debug_tests() -> Result<()> {

debug_print!("Debug tests completed!");

tokio::spawn(async {
spawn(async {
// Panic in a separate task
// to avoid memory leak on the web.
// On the web (`wasm32-unknown-unknown`),
Expand All @@ -219,3 +178,15 @@ pub async fn run_debug_tests() -> Result<()> {

Ok(())
}

// A dummy function that uses sample messages to eliminate warnings.
#[allow(dead_code)]
async fn use_messages() {
let _ = SampleInput::get_dart_signal_receiver();
SampleOutput {
kind: 3,
oneof_input: Some(sample_output::OneofInput::Age(25)),
}
.send_signal_to_dart();
let _ = DeeperDummy {};
}
Loading