zenui 0.1.0
zenui: ^0.1.0 copied to clipboard
Fine-grained reactive UI for Flutter — aspect-level rebuilds powered by ZenSignals.
ZenUI #
Fine-grained reactive UI for Flutter.
ZenUI is the reactive UI layer of ZenSuite: Flutter widgets and context that expose one signal-backed computed per UI concern, so your tree rebuilds at the granularity you choose — not at the granularity InheritedWidget gives you for free.
Built on ZenSignals (SignalBuilder, ComputedNotifier, batch). Today the package ships ReactiveMediaQuery for aspect-level MediaQueryData; more fine-grained UI primitives will follow the same model.
Table of contents #
- Fine-grained reactive UI
- Components
- Getting started
- ReactiveMediaQuery
- API reference
- Example
- ZenSuite integration
Fine-grained reactive UI #
| Layer | Granularity | Example |
|---|---|---|
| Widget tree | Often whole subtrees | MediaQuery.of(context) when any field changes |
| Flutter aspect APIs | One MediaQueryData field |
MediaQuery.paddingOf(context) — still InheritedWidget rebuilds |
| ZenUI | One computed per aspect (and derivations you define) | ReactiveMediaQuery.paddingOf(context)(); or computed((_) => padding().bottom) for a single edge |
ZenUI’s contract:
- Push, not pull — subscribe to
ComputedNotifiers; let ZenSignals decide what re-runs. - Aspect-first — each public field of layout/system UI gets its own reactive channel.
- Interop — keep standard Flutter widgets working (
MediaQuery, Material, Cupertino) while opt-in code goes fine-grained.
| Typical Flutter UI | ZenUI | |
|---|---|---|
| Rebuild unit | Element / InheritedWidget |
Signal dependency |
MediaQuery read |
paddingOf(context) → EdgeInsets |
paddingOf(context)() → EdgeInsets, tracks only padding |
| Custom precision | Manual Listenable / setState |
createComputed on top of aspect computeds |
| Overrides | Nested MediaQuery |
ReactiveMediaQueryOverride + root merge |
Components #
Fine-grained reactive UI primitives in this package:
| Component | Description |
|---|---|
ReactiveMediaQuery |
Root widget: observes the FlutterView, syncs each MediaQueryData aspect into signals, and inserts a standard MediaQuery for interoperability. |
ReactiveMediaQueryOverride |
Composable overrides above or below the root — ancestor scopes merge at resolution; descendant scopes create a nested fine-grained subtree. |
ReactiveMediaQueryData |
Read-only bag of aspect-specific ComputedNotifiers (size, padding, viewInsets, …). |
ReactiveMediaQueryScope |
InheritedWidget locator for ReactiveMediaQueryData (notifications come from signals, not updateShouldNotify). |
More components will be added here as the library grows; each follows the same fine-grained reactive pattern.
Getting started #
Add the dependency:
dependencies:
zenui: ^0.0.1
zensignals: ^1.0.0
Import ZenUI (and ZenSignals for SignalBuilder / createComputed):
import 'package:zenui/zenui.dart';
import 'package:zensignals/zensignals.dart';
Wrap the subtree that should share fine-grained layout metrics:
runApp(
ReactiveMediaQuery(
child: MaterialApp(
home: MyHomePage(),
),
),
);
ReactiveMediaQuery #
The first ZenUI primitive: fine-grained MediaQuery for Flutter.
Setup #
Place ReactiveMediaQuery near the root of the widget tree that should share view metrics. It:
- Resolves the current
FlutterView(or uses an explicitview). - Builds
MediaQueryData.fromView, merging ancestorMediaQueryplatform data when appropriate. - Applies any
ReactiveMediaQueryOverrideancestor scopes, then optionaldataOverrideson the root. - Publishes
ReactiveMediaQueryScope+ a standardMediaQueryfor descendants.
ReactiveMediaQuery(
dataOverrides: (data) => data.copyWith(
padding: const EdgeInsets.only(bottom: 34),
),
child: MaterialApp(home: HomePage()),
)
| Parameter | Purpose |
|---|---|
view |
Explicit view; defaults to View.maybeOf or the implicit view. |
ignoreParentData |
When true, ignores platform fields from an ancestor MediaQuery (mirrors fromView behavior). |
dataOverrides |
MediaQueryData Function(MediaQueryData) applied on the root after view + ancestor overrides. |
Reading values #
Use the static *Of(context) helpers. Each returns a ComputedNotifier<T> — read with call syntax inside reactive UI (SignalBuilder):
SignalBuilder(
forceRebuild: false,
builder: (context) {
final width = ReactiveMediaQuery.widthOf(context)();
final padding = ReactiveMediaQuery.paddingOf(context)();
return Text('${width.toStringAsFixed(0)} · bottom ${padding.bottom}');
},
);
| Syntax | Fine-grained? |
|---|---|
ReactiveMediaQuery.paddingOf(context)() |
Yes — padding aspect only |
ReactiveMediaQuery.of(context).padding() |
Yes — same computed |
.value outside a reactive read |
No — snapshot (see ZenSignals) |
maybeOf / maybePaddingOf / … return null when no scope is present.
Fine-grained subscriptions #
The padding aspect still rebuilds when any edge changes. For finer precision, derive a nested computed:
class _BottomInsetState extends State<BottomBar>
with ReactiveNotifierMixin<BottomBar> {
late final _bottom = createComputed(
(_) => ReactiveMediaQuery.paddingOf(context)().bottom,
listen: false,
);
@override
Widget build(BuildContext context) {
return SignalBuilder(
forceRebuild: false,
builder: (_) => Padding(
padding: EdgeInsets.only(bottom: _bottom()),
child: const TextField(),
),
);
}
}
padding.top noise no longer rebuilds this subtree; padding.bottom changes still do. The same pattern applies to viewInsets, size / width / height, devicePixelRatio, and other aspects.
Overrides #
ReactiveMediaQueryOverride keeps overrides composable without giving up fine-grained reactivity.
Above the root — ancestor scope merged at resolution (nearest wins on conflicts):
ReactiveMediaQueryOverride(
dataOverrides: (d) => d.copyWith(size: const Size(390, 844)),
child: ReactiveMediaQuery(child: app),
)
Below the root — nested fine-grained scope for a subtree only:
ReactiveMediaQuery(
child: ReactiveMediaQueryOverride(
dataOverrides: (d) => d.copyWith(
padding: const EdgeInsets.only(bottom: 80),
),
child: bottomBar,
),
)
Root dataOverrides run after all ancestor override scopes.
API reference #
ReactiveMediaQuery #
| Member | Description |
|---|---|
ReactiveMediaQuery(...) |
Root observer; fine-grained aspect computeds. |
ReactiveMediaQuery.of(context) |
Nearest ReactiveMediaQueryData (throws if missing). |
ReactiveMediaQuery.maybeOf(context) |
Same, or null. |
sizeOf, widthOf, heightOf, paddingOf, viewInsetsOf, viewPaddingOf, … |
Per-aspect ComputedNotifiers (each has maybe*). |
ReactiveMediaQueryOverride #
| Member | Description |
|---|---|
ReactiveMediaQueryOverride({ dataOverrides, child }) |
Ancestor or descendant overrides (see Overrides). |
ReactiveMediaQueryData #
One ComputedNotifier per MediaQueryData field. Batched updates when resolved data changes.
ReactiveMediaQueryDataOverrides #
typedef ReactiveMediaQueryDataOverrides =
MediaQueryData Function(MediaQueryData data);
Example #
Runnable demo of fine-grained vs broad MediaQuery reads: example/.
cd packages/zenui/example && flutter run
See example/README.md for panel descriptions.
ZenSuite integration #
| Package | Role |
|---|---|
| ZenSignals | Fine-grained reactive state |
| ZenUI (this package) | Fine-grained reactive UI |
| ZenBus | High-performance event bus |
| ZenQuery | Async data fetching and mutations |
ZenSignals owns signals and computeds; ZenUI applies that model to Flutter layout and system UI:
ReactiveMediaQuery(
child: SignalBuilder(
builder: (context) {
final bottom = ReactiveMediaQuery.paddingOf(context)().bottom;
return Padding(
padding: EdgeInsets.only(bottom: bottom),
child: child,
);
},
),
);
License #
MIT © Bui Dai Duong