dbrij_ship_rfw
Server driven Flutter screens for Dbrij Ship. Screens described by a blob that rides in an asset pack, staged and rolled back on the same release rail as everything else.
Why a compiled app may do this
A Remote Flutter Widgets blob can only compose widgets the app already registered: it names them and arranges them, the way HTML names elements a browser already implements. It cannot introduce a widget, or a function, or a line of Dart. So it is declarative data, not downloaded code, which is the distinction both app stores draw.
The practical consequence is the useful part: layout, order, copy, which fields a form asks for, whether a promotion appears, all change after the app is in the store. What still needs a store release is anything your registered widgets cannot already do, which is why the widget catalogue you register is the real design decision here, not the blobs.
Install
dependencies:
dbrij_ship: ^0.1.0
dbrij_ship_rfw: ^0.1.0
Use
import 'package:dbrij_ship_rfw/dbrij_ship_rfw.dart';
final ui = ShipRemoteUi(
assets: assets, // your ShipAssets
widgets: createMaterialWidgets(), // plus your own, see below
);
await assets.restore(); // the pack already on disk
await ui.loadAll(['home', 'checkout']);
ui.publishShipValues(ship); // flags and config, where a blob can read them
ShipRemoteWidget(
ui: ui,
name: 'checkout',
// REQUIRED, and the most important line in this file.
fallback: (context) => const LocalCheckoutScreen(),
onEvent: (name, args) {
if (name == 'checkout.begin') startCheckout(args);
},
)
The fallback is not a degraded mode
A remote screen is absent on first launch, absent offline before any pack has landed, absent under the kill switch, and absent the moment you roll a pack back. An app whose checkout only exists on the server is an app that stops selling when the server has an opinion.
So fallback is required. The local screen is the app; the remote one is an override.
Events go to your code, always
A blob names an event and passes arguments. What that does is yours, in Dart, in the store build. Remote data decides what to show and asks for things; local code decides what happens. That boundary is the safety model, and it is also what keeps you inside the store rules: nothing downloaded ever executes.
Your own widgets
Register a library and blobs can use it:
ShipRemoteUi(
assets: assets,
widgets: LocalWidgetLibrary({
'ProductCard': (context, source) => ProductCard(
title: source.v<String>(['title']) ?? '',
priceMinor: source.v<int>(['priceMinor']) ?? 0,
),
}),
);
// in the blob
import core.widgets;
widget root = app.ProductCard(title: "Coffee", priceMinor: 250000);
A blob referencing a widget you did not register renders RFW's own error box rather than crashing. Ship a blob to your staging channel first: that is what channels are for.
Building a pack
A pack is a zip, so zip works. This tool exists for the one part it cannot do: compiling
.rfwtxt into the binary format a screen ships as, using the Flutter team's own encoder.
It means the thing you review in a pull request is text.
dart run dbrij_ship_rfw:pack ui_pack -o build/pack.zip
dbrij ship release build/pack.zip --app shp_app_id --rollout 10
Everything that is not .rfwtxt is copied in untouched. A blob that does not parse fails
the build rather than failing on somebody's phone during a rollout, and anything that looks
executable is refused: a pack carries data, and a .so in one means somebody has
misunderstood what it is for.
Full guide
Libraries
- dbrij_ship_rfw
- Server driven Flutter screens for Dbrij Ship.