Application Architecture
Explains the Rid application architecture.
UI separated from App Logic
Each application created with Rid is divided into two major parts.
The UI implemented in Flutter/Dart concerns itself only with rendering Widgets and user interaction. It delegates to Rust for all application logic.
All application state is held by Rust. Application logic mutating that state is implemented in Rust.
Rust State and Logic Implementation
A rid application has one main model, the Store, which holds the application state. This state is not modified from the UI. Instead the UI sends messages to Rust in order to relay user interaction. Rust then modifies the state of the application and responds with a Reply in order to communicate that the message has been handled. At that point the UI can query the state of the Store and update itself.
All logic needed to derive the new state from the previous one as a result of a user interaction is implemented in Rust. Flutter only consumes this state and when needed transforms it slightly and only locally in order to make it presentable via a Widget.
Flutter should never modify the global application state directly.
Store, Message and Reply
The Store struct of your application holds all the application's state. The Store can reference other model structs.
Rid assumes that all Messages may be handled asynchronously and thus will never make the
assumption that the state of the Store was modified in response to it after the update
method completes.
Instead it uses a Request/Reply mechanism to allow signaling that the state of the Store was completely updated in response to a Message.
The Message is defined via an enum. It is used to send messages to the model and should be
the only means of mutating it. The variants of the message enum can have associated data
which is used to pass a message payload from Dart (see Msg::Add(u32)
below).
The Message enum is associated with a Reply enum which is used to respond to messages after they are handled.
Example
#[rid::store]
pub struct Store {
count: u32,
}
impl rid::RidStore<Msg> for Store {
fn create() -> Self {
Self { count: 0 }
}
fn update(&mut self, req_id: u64, msg: Msg) {
match msg {
Msg::Inc => {
self.count += 1;
rid::post(Reply::Increased(req_id));
}
Msg::Add(n) => {
self.count += n;
rid::post(Reply::Added(req_id, n.to_string()));
}
}
}
}
#[rid::message(Reply)]
#[derive(Debug, Clone)]
pub enum Msg {
Inc,
Add(u32),
}
#[rid::reply]
#[derive(Clone)]
pub enum Reply {
Increased(u64),
Added(u64, String),
}
As you can see the #[rid::message(Reply)]
attribute defines the type of the Reply used to
respond to Messages.
👉 read more about rid::message and rid::reply
Accessing State and Sending Messages
As mentioned, all state is held by Rust. It is exposed to Flutter via a Getter based API. State is only transfered once accessed in order to improve performance. To make things easier the recommended API converts all data to Dart instances to avoid memory races and access issues.
TODO: link separate document of higher level API details
For cases where more control is required and the performance suffers, i.e. when sending huge
lists of items, a lower level API is provided as well. As an example when using that API an item of a
Vec<u8>
is only passed once it is accessed by indexing into the collection. However the
developer is now responsible to properly lock the Store to ensure that this vector wasn't
mutated in the meantime.
TODO: link separate document of raw API details
In response to user interaction like a button click we send messages to Rust in order to cause the Store to be updated.
Using the recommended higher level API to interact with the above Rust Store from Flutter we could do the following.
class _MyHomePageState extends State<MyHomePage> {
final store = Store.instance;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('You have counted to:'),
Text('${store.count}'),
],
),
),
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () => {
store.msgAdd(10).then((_) => setState(() {}));
}),
child: Icon(Icons.add),
),
FloatingActionButton(
onPressed: () => {
store.msgInc(10).then((_) => setState(() {}));
}),
child: Icon(Icons.add),
),
],
),
);
}
}
Rid's Raw Api
TODO: This is a quick summary of the raw rid API. It will be moved into its own doc shortly and a doc for the recommended higher level rid API will be provided alongside it.
How Data is Passed
- primitives like
u8
,i32
and C-styleenum
s are copied and passed by value - strings like
String
,&str
are passed by reference, but Rid immediately releases them after converting into a Dart String - structs are passed as pointers by reference and expose Getters to access their fields
- collections like
Vec
are passed by reference and Rid exposes an Iterable interface to provide access to each item
Accessing and Iterating Collections
Rid wraps the retrieved pointer of a collection in an API that exposes a convenient iterable interface as well as an indexing operator.
Have a look at the below list of todos
defined on the model
.
#[rid::model]
#[derive(Debug, rid::Debug)]
pub struct Todo {
title: String
}
#[rid::model]
#[rid::structs(Todo)]
pub struct Model {
todos: Vec<Todo>,
}
Note: that we #[derive(Debug, rid::Debug)]
for the Todo
in order to call it from Flutter
via todo.debug([pretty])
, see rid::debug.
Those todos
can be used on the Flutter end like any Iterable.
queryTodos(Pointer<Model> model) {
final todos = model.todos;
final total = todos.length;
print("Total Todos: $total");
print("\nTodos:");
for (final todo in matchingTodos.iter()) {
print(" ${todo.debug()}");
}
final firstTodo = todos[0];
final todoTitles = todos.iter().map((todo) => todo.title);
final todoSummedIds =
todos.iter().map((todo) => todo.id).reduce((acc, id) => acc + id);
final todosUrgent =
todos.iter().where((todo) => todo.title.contains('urgent'));
}
See also: #rid::structs