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:

Basic count and countPlusOne capsules
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 awaits 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.

Using The CapsuleReader
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 CapsuleHandles are also capsules. This can lead to some pretty interesting patterns, a (somewhat useless) example of which is below.

Generic capsules example
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:

  1. Conditionally using capsules
  2. Inline capsules

When Do You Need to Reduce Rebuilds?

Consider the following capsule.

Capsule with excessive rebuilds
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.

Limiting extra rebuilds
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.

Inline capsule example with .map()
@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.

Manual inline capsule example
@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.