dart_board_core

The kernel module of dart board.

This is the heart of dart board. It's not your features, it's just a widget. One main widget, some extra ones, and Navigation/Routing interfaces.

If this was a home theater, This is your Receiver. You plug your features (Cable/VCR/DVD/Nintendo) into it, and it plugs into the TV and Speakers. It plugs everything into it and lets you switch the channel, or even do picture in picture.

Although Dart Board is not a Stereo receiver, we'll keep this analogy going.

DartBoardCore

This interface gives you access to information about the system. What features are plugged in, what inputs are available, and what channels are ready to see.

Generally speaking, if you aren't AB/Feature gating, the only thing you need it the DartBoard Widget.

void main() => runApp(DartBoard(
      features: [
        SpaceXUIFeature(),
        SpaceSceneFeature(),
        ThemeFeature(data: ThemeData.dark()),
        EntryPoint()
      ],
      initialPath: '/entry_point',
    ));

This launches DartBoard, loads your features, and goes to the /entry_point route. For standard usage, you are done.

AB Testing & Feature Flags

AB (change implementation)

DartBoardCore
  .instance
  .setFeatureImplementation(
      'YourFeatureNamespace', 'YourImplementationName')

If you have multiple features with the same namespace but different ImplementationName you can switch between them at runtime.

Feature Flag / Disabling

DartBoardCore
  .instance
  .setFeatureImplementation(
      'YourFeatureNamespace', null)

Same as the AB switching, but just pass null as your ImplementationName and the feature will be disabled.

Access with DartBoardCore.nav globally

It maintains a Stack of Path's.

You can push new paths, and optionally expand them (e.g. [/a/b/c] -> [/a, /a/b, /a/b/c])

It gives you a couple of ways to edit the stack and modify as necessary. Generally paths should be generated by registered Route's in the features, but Dynamic (runtime) paths are possible. They however can not be shared between instances and will result in a 404.


abstract class DartBoardNav {
  /// The currently active (foreground) route
  String get currentPath;

  /// Change Notifier to listen to changes in nav
  ChangeNotifier get changeNotifier;

  /// Get the current stack
  List<DartBoardPath> get stack;

  /// Push a route onto the stack
  /// expanded will push sub-paths (e.g. /a/b/c will push [/a, /a/b, /a/b/c])
  void push(String path, {bool expanded});

  /// Pop the top most route
  void pop();

  /// Pop routers until the predicate clears
  void popUntil(bool Function(DartBoardPath path) predicate);

  /// Clear all routes in the stack that match the predicate
  void clearWhere(bool Function(DartBoardPath path) predicate);

  /// Pop & Push (replace top of stack)
  /// Does not work on '/'
  void replaceTop(String path);

  /// Append to the current route (e.g. /b appended to /a = /a/b)
  void appendPath(String path);

  // Replace the Root (Entry Point)
  // Generally for Add2App
  void replaceRoot(String path);

  /// Push a route with a dynamic route name
  void pushDynamic(
      {required String dynamicPathName, required WidgetBuilder builder});
}

Route Types in DartBoard

These route types should allow you to match a wide range of URI patterns for your features.

NamedRouteDefinition -> Matches a portion of a path for a specific name, i.e. /page /details MapRoute -> Named Route that allows multiple pages (Syntactic sugar) UriRoute -> Matches everything that hits it. Can globally handle routing, or can be used with PathedRoute to provide detailed parsing of the resource. PathedRoute -> Use this for deep-linked trees. E.g. /category/details/50 it takes a List of Lists of RouteDefinitions. Each level of depth represents the tree.

How to fulfill complicated roots? NamedRouteDefinition works good for static, fixed targets. But what if you want something more advanced?

E.g. you want /store/pots/2141 to resolve.

UriRoute and PathedRoute solve those issues for you.

PathedRoute will handle directory structures. You do this with a list of lists. Each level can hold any number of matchers. If a path matches up to that level, the lowest matcher will take it.

// PsuedoCode
[
  [
    NamedRoute('/store', (ctx,settings)=>StorePage()),    
  ],
  [
    NamedRoute('/pots', (ctx,settings)=>PotsPage()),
    NamedRoute('/pans'  (ctx,settings)=>PotsPage()),
  ],
  [
    UriRoute((context, uri)=>Parse and Display)
  ]
]

This PathedRoute config would respond to many routes: [/store, /store/pots, /store/pans, /store/pots/*, /store/pans/*]

The * is the UriRoute. You can use this to manage all your Routing, or you can use it with a Pathed route to parse the information.

UriRoute will parse the resource request and let you access query params, path segments and anything else encoded in the page request.

Anonymous Routes

Sometimes you want to just push a screen right? Like you didn't register it in a feature, you want it to be dynamic for whatever reason.

void pushDynamic({required String dynamicRouteName, required WidgetBuilder builder});

is what you can use here. Give it a unique name which will be prefixed with _, e.g. /_YourDynamicRoute3285 If you see the _ that means you can not share this route. If you give it to someone else it's going to 404 for them. It's dynamically allocated for the users session.

R#outing Demonstration in the SpaceX Example

The SpaceX features are designed as demonstrations of Add2App and Navigator 2.0 usage.

  @override
  List<RouteDefinition> get routes => [
        PathedRouteDefinition([
          [
            NamedRouteDefinition(
                route: '/launches', builder: (ctx, settings) => LaunchScreen())
          ],
          [UriRoute((ctx, uri) => LaunchDataUriShim(uri: uri))]
        ]),
      ];

This matches /launches and also /launches/[ANY_ROUTE_NAME]

/launches appends the name of the mission to the URL, and you end up with something like /launches/Starlink%207

UriRoute can then pull the data from the URI and pass it to the page to load what it needs to.

Helpful Widgets (General Utilities)

RouteWidget (embedded routes)

Want to use your named routes anywhere? E.g. in a Dialog, or as a small portion of a screen?

    showDialog(
        useSafeArea: true,
        context: navigatorContext,
        barrierDismissible: true,
        builder: (ctx) => RouteWidget("/request_login"));

and pass arguments RouteWidget(itemPreviewRoute, args: {"id": id})

RouteWidget can handle that for you, enabling you to break screens up into multiple decoupled features that share a common core and state.

Convertor<In, Out>

Conversion in the widget tree

              Convertor<MinesweeperState, MineFieldViewModel>(
                  convertor: (input) => buildVm(input),
                  builder: (ctx, out) => MineField(vm: out),
                  input: state)))

Will only trigger an update if the VM changes/doesn't hit equality.

Ideal for ViewModel generation from a DataSource, to help reduce the number of builds to relevant changes.

LifecycleWidget

LifeCycleWidget(
                key: ValueKey("LocatorDecoration_${T.toString()}"),
                preInit: () => doSomethingBeforeCtx,
                
                    
                child: Builder(builder: (ctx) => child))

This widget can tap into life cycle

3 hooks

  /// Called in initState() before super.initState()
  final Function() preInit;

  /// Called after initState()  (with context)
  final Function(BuildContext context) init;

  /// Called in onDispose
  final Function(BuildContext context) dispose;

You can use this with something like a PageDecoration to start a screen time counter, or to periodically set a reminder/start/stop a service etc.

It's very useful within the context of features and setting up integrations.