Capsules
Capsules are the most fundamental building block of ReArch. Capsules en-capsulate some data, thus their name.
Data here encompasses more than just raw data; it also includes functions. This is an important consideration, as ReArch is highly functional.
The Theory
When you start designing an application, it is often easiest to think in terms of the data you are modeling and how that data interacts with other data in your application. Consequently, when you go to create some non-trivial state, chances are high that it depends on some other form of state in your application.
Functions of State
Capsules let you think in this manner by themselves being functions of state (and more generally, side effects, but more on that later). This may sound wishy-washy, so here is a concrete example:
int countCapsule(CapsuleHandle use) => 0;
int countPlusOneCapsule(CapsuleHandle use) => use(countCapsule) + 1;
Notice here how countPlusOneCapsule
consumes countCapsule
;
i.e., countPlusOneCapsule
is a function of state, where the state is the current countCapsule
!
As you can see in the above example, capsules are nothing special;
every capsule is merely a function that consumes a CapsuleHandle
.
A capsule's power is derived from from how you use the CapsuleHandle
within the capsule.
Immutability
Capsules were designed with the intent that the data they produce is immutable.
In Rust, this is enforced via never providing a &mut
to any capsule's data.
In Dart, it is up to you to make sure you do not directly mutate a capsule's data
without using one of the side effects (again, more on side effects later).
The CapsuleHandle
So what is this magical CapsuleHandle
parameter all capsules consume?
The CapsuleHandle
is actually not magic at all;
it is just a temporary lease from the Container
(next page) to build a capsule's data,
given the state of the container.
More specifically, the CapsuleHandle
is a composition of two other types:
- The
CapsuleReader
allows the capsule to read the current data of itself and other capsules - The
SideEffectRegistrar
allows the capsule to register side effects
Do not use a CapsuleHandle
across an asynchronous gap
(i.e., used after any await
s or in a callback) in Dart!
Although you may not see issues immediately,
know that you may encounter issues when in more complex situations.
(Rust does not have this issue due to its ownership rules.)
The CapsuleReader
The CapsuleReader
provides the familiar use(...)
syntax in Dart and get(...)
in Rust.
int countCapsule(CapsuleHandle _) => 0;
int countPlusOneCapsule(CapsuleHandle use) {
return use(countCapsule) + 1;
}
The SideEffectRegistrar
The SideEffectRegistrar
provides the familiar set of use.fooBar()
side effects in Dart
and register(effect1(), effect2())
in Rust.
See the docs on side effects for more.
Generic Capsules
As a reminder, a capsule is just a function that consumes a CapsuleHandle
and returns some data.
Because of this realization, generic functions that consume CapsuleHandle
s are also capsules.
This can lead to some pretty interesting patterns, a (somewhat useless) example of which is below.
List<T> Function(T, int) repeatedItemFactory<T>(CapsuleHandle use) {
return (itemToRepeat, repetitions) => List.generate(repetitions, (_) => itemToRepeat);
}
// ...
final repeatedIntsFactory = container.read(repeatedItemFactory<int>);
final repeatedStringsFactory = container.read(repeatedItemFactory<String>);
final repeatedStrings = repeatedStringsFactory("this will be repeated 1234 times!", 1234);
Notice how the capsule above returns a function; this is a very useful pattern that will be revisited again in the pages on the factories/actions.
Reducing Builds
Reducing builds is a useful optimization that can be done in ReArch through:
- Conditionally using capsules
- Inline capsules
When Do You Need to Reduce Rebuilds?
Consider the following capsule.
FooBar expensiveOperationUsingCount(CapsuleHandle use) {
final count = use(countCapsule);
final isWatching = use(isWatchingCapsule);
return someExpensiveOperation(isWatching ? count : null);
}
This looks ok until you realize that whenever count
changes,
someExpensiveOperation
/some_expensive_operation
will be called again,
even if isWatching
/is_watching
is false!
These extra rebuilds can be removed with the following.
FooBar expensiveOperationUsingCount(CapsuleHandle use) {
if (use(isWatchingCapsule)) {
// Having this use within a branch will stop rebuilds caused by
// countCapsule when isWatching is false.
return someExpensiveOperation(use(countCapsule));
}
return someExpensiveOperation(null);
}
However, this simple conditional watching is not always enough to reduce some builds;
sometimes, someExpensiveOperation
is a Widget
rebuild in Flutter,
where the rebuild depends upon some non-encapsulable data (such as a key in a large collection).
For situations like this, you likely need inline capsules
to prevent rebuilding potentially thousands of Widgets displaying the current data in a collection
whenever just a single entry is modified.
Inline Capsules
Inline capsules are just regular capsules (which, as a reminder, are just functions), but are unique because they are closures.
While you can define inline capsules in Rust, I have not found a useful application of them yet, mostly because ReArch for Rust does not have an accompanying UI framework. Thus, the rest of this section only applies to Dart at the moment.
There are two ways to create inline capsules:
myCapsule.map()
, a convenience extension method that creates an inline capsule- Manually (but caution must be used here)!
Each solves slightly different use-cases. However, when possible, it is best to make new intermediary top-level capsules instead, and let rearch use its own optimizations to limit rebuilds. Remember, capsules are cheap, and are designed to be composed. You should only very rarely use inline capsules, and when you do, make sure they are very cheap (ideally just a constant-time look-up).
.map()
The example Flutter app utilizes .map()
to reduce some rebuilds,
so see that for a more full example.
However, here is a simple example to give you an idea of how .map()
works.
@rearchWidget
Widget myListItem({
int listIndex,
WidgetHandle use,
}) {
// With this inline capsule, we only rebuild when the data at
// myList[listIndex] changes, instead of the whole myList.
final dataAtIndex = use(
// This creates a new inline capsule that gets a particular index of myList:
myListCapsule.map((myList) => myList[listIndex]),
);
return Text('$dataAtIndex');
}
Manual Inline Capsules
Contrary to the .map()
convenience method, manual inline capsules define the closure explicitly.
This is beneficial when your inline capsule depends on >1 other capsules,
which should be rather rare.
Only use manual inline capsules when your inline capsule needs to use
multiple different capsules,
as manual inline capsules are easier to mess up and cause leaks!
Here's the previous example, but rewritten to use a manually defined inline capsule.
@rearchWidget
Widget myListItem({
int listIndex,
WidgetHandle use,
}) {
// With this inline capsule, we only rebuild when the data at
// myList[listIndex] changes, instead of the whole myList.
final dataAtIndex = use(
// This creates a new inline capsule that gets a particular index of myList:
(CapsuleReader use) => use(myListCapsule)[listIndex],
// While the following works, it is considered a bad practice
// (keep reading for an explanation):
// (use) => use(myListCapsule)[listIndex],
);
return Text('$dataAtIndex');
}
Notice above how the parameter is declared as CapsuleReader use
;
while you actually get a full CapsuleHandle
here for the inline capsule,
you must be careful to not use any side effects (use.fooBar()
) or you will cause leaks.
Thus, it is considered to be a best practice to prevent yourself from using side effects
by declaring the parameter type as CapsuleReader
instead of letting it get inferred
as a CapsuleHandle
(which is what happens if you do not explicitly specify CapsuleReader
).
You can actually use this same pattern for your regular capsules too
if you wish to declare that it cannot use side effects (i.e., is an idempotent capsule),
but that is not documented elsewhere in order to not confuse beginners.