Unit

Unit is a key component in this pattern. In contrast to Model, Unit is MUTABLE. All units are structured in a tree-layout hierarchy and form a tree of units in the app. It is very similar to the concept of the widget tree in Flutter.

Following the structure, all units obey communication rules:

  • A child unit notifies only its direct parent Unit regarding any state changes. It is called "report".

  • A parent unit is the only one who can update its children units' states. It is called "command".

  • Units at the same level, like under the same parent unit, cannot communicate with each other or depend on each other by any means.

  • In some special cases, a unit's state can be observed by multiple parent units at the same time. In those cases, each attribute can be observed by only one parent.

Overview

A unit consists of 3 main components:

  • Unit: Very similar to the Widget class of UI. It is the configuration to build a unit.

  • State: The state object of a unit. It contains all attributes of the unit.

  • Script: The object responsible for handling Paper received to update the State accordingly. Every state changing process will occur in Script, not in State.

/// sample.dart

import 'package:paper/paper.dart';

part 'sample_script.dart';

class Sample extends Unit<SamplePaper> {
  Sample(super.agent, {super.listener});

  
  Script<SamplePaper, SampleState> createScript() => SampleScript();

  
  UnitState<SamplePaper, Sample> createState() => SampleState();
}

class SampleState extends UnitState<SamplePaper, Sample> {

  late final child = UnitAgent<SampleChildPaper>();

  
  Set<Agent>? register() => {
        child.log((a) => SampleChild(a, listener: sampleChildListener)),
      };

  
  void initState(Sample unit) {
    super.initState(unit);

    criteria = CriteriaUnit(
      selectedCategory: 'all',
      selectedCurrency: 'usd',
      selectedTimeFrame: '24h',
    );
  }

  
  void dispose() {
    // Dispose the state here
    super.dispose();
  }

  PaperListener<SampleChildPaper, SamplePaper> get sampleChildListener => reporter(
        (r) => r
            .on<SampleChildFirstPaper>((p) => SampleFirstPaper())
            ?.on<SampleChildSecondPaper>((p) => SampleFirstPaper())
      );
}
/// sample_script.dart

part of 'sample.dart';

class SampleScript extends Script<SamplePaper, SampleState> {

  
  void map() => on<SampleFirstPaper>(onFirstPaper)
        ?.on<SampleSecondPaper>(onSecondPaper);

  void onFirstPaper(
    SampleFirstPaper p,
    SampleState s,
    SourceVerifier ifFrom,
  ) {
    // Handle SampleFirstPaper
  }

  void onSecondPaper(
    SampleSecondPaper p,
    SampleState s,
    SourceVerifier ifFrom,
  ) {
    // Handle SampleSecondPaper
  }
}
/// sample_paper.dart

part of 'sample.dart';

class SamplePaper extends Paper {}

class SampleFirstPaper extends Paper {}

class SampleSecondPaper extends Paper {}

There is something familiar, right? We have Unit and Widget, UnitState and State. And the way to initialize a unit is very similar to how we initialize a widget. The only difference is that we have an extra object - Script to handle the incoming Paper and update the State accordingly.

Unit

  • UnitAgent<P> agent: the agent that the unit will be attached to.

  • UnitState<P, Unit<P>> createState(): override this to create a state object for the unit.

  • Script<P, UnitState<P, Unit<P>>> createScript(): override this to create a script object for the unit.

UnitState

  • Future<void> initState(U unit): override this to initialize the state of the unit. It is called very first after the state is constructed. The method has an input of U unit, which is the unit used to construct the state. It is very like the getter widget of State in Widget. We don't set up a similar getter because we believe that every configuration should be set up in one place for better maintenance.

  • Set<Agent>? register(): override this to register the children units. It is called after initState.

  • void dispose(): override this to dispose the state of the unit. It is called when the unit is disposed.

  • report(Paper p, {dynamic from}): Reports a paper to the unit. It is called when the unit receives a paper. The paper will be processed by the script of the unit. If from is defined, the injected object will be cached in the script and be accessible via SourceVerifier in the input of handlers.

  • UnitWidgetState get widgetState: used for injecting as parent to child Unit Widget via UnitWidget.parent.

class SampleState extends UnitState<SamplePaper, Sample>{
  final rootWidget = WidgetAgent<SamplePaper>();

  
  Set<Agent>? register() => {
    rootWidget.log((a) => RootWidget(widgetState, agent: a));
  };
}

UnitWidget

A UI (Widget) can also be turned into a unit and attached in the unit tree. And besides ordinary overridden methods, it requires an extra method createScript like Unit. UnitWidget is a subtype of StatefulWidget.

  • UnitWidgetState<P, UnitWidget<P>> createState(): override this to create a state object for the unit. It is overridden from createState of StatefulWidget.

  • Script<P, UnitWidgetState<P, UnitWidget<P>>> createScript(): override this to create a script object for the unit.

UnitWidgetState

UnitWidgetState extends from State so it inherits all natural attributes of State. It has an extra register method to register children Units.

  • Set<Agent>? register(): override this to register the children units.

For attaching children UnitWidget, we do the same way as normal StatefulWidget with a required parameter UnitWidget.parent.

class SampleUnitWidgetState extends UnitWidgetState<SamplePaper, SampleUnitWidget> {
  final childUnit = UnitAgent<SampleChildPaper>();
  final childWidget = WidgetAgent<ChildUnitWidgetPaper>();

  
  Set<Agent>? register() => {
        childUnit.log((a) => SampleChild(a, listener: sampleChildListener)),
      };

  
  void initState(){
    super.initState();

    criteria = CriteriaUnit(
      selectedCategory: 'all',
      selectedCurrency: 'usd',
      selectedTimeFrame: '24h',
    );
  }

  
  Widget build(BuildContext context) {
    return Container(
      child: ChildUnitWidget(
        parent: this,
        agent: childWidget,
        listener: childUnitWidgetListener,
      ),
    );
  }

  PaperListener<SampleChildPaper, SamplePaper> get sampleChildListener => reporter(
        (r) => r
            .on<SampleChildFirstPaper>((p) => SampleFirstPaper())
            ?.on<SampleChildSecondPaper>((p) => SampleFirstPaper())
      );

  PaperListener<ChildUnitWidgetPaper, SamplePaper> get childUnitWidgetListener => reporter(
        (r) => r
            .on<ChildUnitWidgetFirstPaper>((p) => SampleSecondPaper())
            ?.on<ChildUnitWidgetSecondPaper>((p) => SampleSecondPaper())
      );
}
  • report(Paper p, {dynamic from}): Reports a paper to the unit. It is called when the unit receives a paper. The paper will be processed by the script of the unit. If from is defined, the injected object will be cached in the script and be accessible via SourceVerifier in the input of handlers.

Script

  • void map(): override this to map the incoming Paper to the handler. It is called very first after the script is constructed.

  • Script<TPaper, TState>? on<P extends TPaper>([ PaperHandler<P, TState>? handler, PaperReporter<P, TState>? reporter ]): Registers a paper handler for P.

The PaperHandler is a typedef of

FutureOr<void> Function( TPaper p, TState s, SourceVerifier ifFrom )

with 3 parameters: TPaper p, TState s, and SourceVerifier ifFrom.

The p is the incoming paper accordingly, s is the state of the unit.

The noticeable parameter is ifFrom. Its type is SourceVerifier, which is the typedef of T? Function<T>(T). It is to indicate the sources of triggering the incoming paper.

void onPaper(
  SamplePaper p,
  SampleState s,
  SourceVerifier ifFrom,
) {
  // Handle SamplePaper

  ifFrom(s.child)?.doSomething();
}

If it is s.child who triggered the incoming paper, ifFrom will return null and doSomething will not be triggered. Otherwise, the s.child will be returned and execute doSomething.

If the incoming paper is from units via PaperListener, it is also accounted as sources.

Please refer to Bottom Navigation for illustration.

Initialize Root Unit

To initialize the root unit, we provide two APIs for each type of unit:

  • Unit:
void main() {
  final root = UnitAgent<RootPaper>();
  PaperFrameworkEstablisher().initializeUnit(RootUnit(agent: root));

  root.process(RootInitializing);
}
  • UnitWidget:
void main() {
  final root = WidgetAgent<RootPaper>();
  PaperFrameworkEstablisher().initializeWidgetUnit((s) => Root(s, agent: root));
  root.process(AppInitializing());
}