dart_board_core 0.9.15+1 dart_board_core: ^0.9.15+1 copied to clipboard
Feature Framework for Flutter. Provides a kernel that allows feature encapsulation, isolation and integration.
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.
Navigation #
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.