flagnetic_flutter 0.1.0
flagnetic_flutter: ^0.1.0 copied to clipboard
Flutter widgets for Flagnetic feature flags and experiments. A scope, a flag builder, listenables, the app lifecycle, a store on every platform and a Dev Tools screen.
flagnetic_flutter #
Flutter widgets for Flagnetic feature flags and experiments, served by a self-hosted Flagnetic instance: a scope holding the client, a builder widget rebuilding when a flag changes, listenables for a flag and for the fallback chain's stage, a lifecycle observer, a store on every platform and a Dev Tools screen for local overrides. The client itself lives in flagnetic, which this package re-exports, so an app imports one package.
flutter pub add flagnetic_flutter
Requires Flutter 3.35 or newer, on iOS, Android, desktop and the web. Supported versions follow Flutter's stable releases of about the last twelve months; a version that leaves upstream support is dropped in a minor release.
A kill switch reaches an app in the foreground within 5 seconds at the 99th percentile, a backgrounded app when it resumes, and an app built with ClientOptions(sse: false) within 5 minutes and at every resume; the flagnetic README has the details and the offline test mode.
import 'package:flagnetic_flutter/flagnetic_flutter.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final store = FlutterStore();
final client = Flagnetic.client(
'fn_cli_…',
baseUrl: Uri.parse('https://flags.example.com'),
context: FlagneticContext(
// The project declares its unit types; the SDK invents none.
units: {'anonymous': await anonymousUnitId(store, 'anonymous')},
),
options: ClientOptions(store: store, appVersion: '1.42.0'),
);
runApp(FlagneticScope(client: client, child: const MyApp()));
}
FlagBuilder<bool>(
flagKey: 'checkout',
defaultValue: false,
builder: (context, showCheckout, detail) =>
showCheckout ? const Checkout() : const Basket(),
)
What it adds #
FlagneticScopeholds the client for the widgets below and, unlessobserveLifecycleis off, attaches aFlagneticLifecycleObserver. It does not own the client: construct it beforerunAppand close it when the app does.FlagneticScope.of(context)returns the client and throws aFlutterErrorwhen there is no scope above;maybeOfreturns null instead.FlagBuilder<T>builds from a flag's value and rebuilds when it changes, reading the client of the nearest scope unless one is passed. The type ofdefaultValuenames the flag type asked for:boolis boolean,Stringis string,numis number, anything else is JSON. The first frame already has a value, the store's last good values or the built-in default, so a screen never waits on the network.FlagListenable<T>andStageListenableareValueListenables over the client's change and stage listeners.FlagListenable.detailsays why the current value was served. Calldisposewhen they are no longer used;FlagBuilderdoes it for you.FlagneticLifecycleObservercallsclient.foreground()when the app resumes andclient.background()when it is paused, hidden or detached;inactiveis a transient interruption (a call, the app switcher) and changes nothing. In the foreground the client holds the ruleset stream, so a kill switch reaches an open app within seconds; in the background it holds no connection, and resuming refetches unless the last attempt is less than 10 seconds old. On the web the client already follows the page's visibility by itself, and the observer's calls change nothing more.FlutterStoreis the defaultFlagneticStore. On a device, each unit set's last good values, a whole evaluate response, go to their own file under the application support directory, written beside it and renamed so a crash never leaves half a document; the index of unit sets, the event queue and the anonymous unit IDs go to shared preferences. On the web every entry lives in shared preferences. A cold start offline serves the last good values, and events recorded offline are sent on a later start. A store that throws never breaks a flag check: the client logsstore_failedand serves its built-in defaults.
Dev Tools #
Testers can override flags on their device in a debug or profile build:
final client = Flagnetic.client(
'fn_cli_…',
baseUrl: Uri.parse('https://flags.example.com'),
context: flagneticContext,
options: ClientOptions(store: store, allowOverrides: !kReleaseMode),
);
final devTools = FlagneticDevTools(client: client, store: store);
Navigator.push(context, MaterialPageRoute(
builder: (_) => FlagneticDevToolsScreen(devTools: devTools),
));
FlagneticDevToolsedits the client's local overrides and persists them in the store under the SDK key's own entry, so a tester's overrides survive a restart.hasOverridesis aValueListenable<bool>for a badge. In a release buildsetandremoveare refused, loggingoverride_refusedwith causerelease_build,cleardoes nothing, and a persisted override is never restored, whateverallowOverridessays: an override cannot ship. An environment marked protected refuses overrides too.FlagneticDevToolsScreenlists every flag the client holds with its value, reason and stage. A boolean flag toggles inline; every other type opens a dialog parsed by the flag's type; an overridden flag shows a button removing its override, and the app bar clears them all.
Checks #
flutter pub get
dart format --output=none --set-exit-if-changed .
flutter analyze --fatal-infos
flutter test
flutter test --platform chrome
In this repository pubspec_overrides.yaml resolves flagnetic from ../dart, so a change to both packages is tested together; the published package requires the released flagnetic.