lively_generator 1.0.1
lively_generator: ^1.0.1 copied to clipboard
Code generator for the lively reactive Flutter widget package.
lively_generator #
Code generator for the lively package.
This package is a dev_dependency. It is not imported directly — build_runner
discovers it automatically via build.yaml.
Setup #
# pubspec.yaml
dependencies:
lively: ^1.0.0
dev_dependencies:
lively_generator: ^1.0.0
build_runner: ^2.0.0
Add part 'your_file.g.dart'; to every source file that uses @Live() or @LiveStore(), then run:
dart run build_runner build
# or watch mode:
dart run build_runner watch
AI Agent Usage Guide #
This section is written for AI agents (Copilot, Claude, Cursor, etc.) generating or assisting with
lively-based Flutter code. Read it fully before producing any code. The rules are strict and order-dependent.
@Live() — Reactive Widgets #
Minimal example
// counter_page.dart
part 'counter_page.g.dart';
@Live()
class CounterPage extends _$CounterPage {
int count = 0;
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () => count++, // triggers rebuild automatically
child: Text('$count'),
);
}
}
Usage in widget tree:
CounterPageWidget() // generated name = class name + "Widget"
Naming rule
| User writes | Generated widget name |
|---|---|
class FooPage extends _$FooPage |
FooPageWidget |
class MyScreen extends _$MyScreen |
MyScreenWidget |
The user class always extends _$ClassName (leading _$). The generated file provides that abstract base.
Field classification — @Live() #
First-match-wins order. Apply top to bottom.
| Priority | Field declaration | Behavior |
|---|---|---|
| 1 | late final T x (no init, T not CN, not disposable) |
Widget constructor param |
| 2 | late final T x (no init, T extends ChangeNotifier) |
Widget param + CN listener + didUpdateWidget |
| 3 | late final T x = expr (T extends ChangeNotifier) |
CN listener only — no param, no setter |
| 4 | final T x = expr (not late) |
Non-reactive constant — no codegen |
| 5 | Disposable type with initializer | Auto-disposed in dispose() |
| 6 | ChangeNotifier subclass (mutable field, has init) | Listener wired; setter re-wires on replacement |
| 7 | Plain user-defined class (mutable) | _Live<ClassName> proxy generated — deep reactivity |
| 8 | List<T> |
Backed by LiveList<T> |
| 9 | Set<T> |
Backed by LiveSet<T> |
| 10 | Map<K, V> |
Backed by LiveMap<K, V> |
| 11 | Everything else mutable | Reactive scalar — setter calls _scheduleRebuild() |
Disposable types (checked before ChangeNotifier):
TextEditingController→.dispose()AnimationController→.dispose()FocusNode→.dispose()StreamController→.close()- Other known disposables →
.dispose()or.close()or.cancel()depending on type
Widget constructor params — late final fields #
late final without an initializer = widget parameter. Params are set before initState() runs.
@Live()
class ProfilePage extends _$ProfilePage {
late final String title; // required param (non-nullable)
late final int? subtitle; // optional param (nullable → defaults to null)
int count = 0;
@override
void initState() {
super.initState();
// title and subtitle are already set here — safe to read
}
}
Rules:
- Non-nullable
late final T x→requirednamed param - Nullable
late final T? x→ optional named param (norequired) late final T x = expr(has initializer) → NOT a param; treated as a non-reactive constant
Reactive scalars #
Every mutable non-final field becomes reactive. All setters call _scheduleRebuild() unconditionally.
Batching: Multiple assignments in one sync block schedule exactly one rebuild via Future.microtask().
name = 'Jane'; // schedules microtask
age = 30; // microtask already scheduled — skipped
// one rebuild fires after the sync block
@LiveStore() — Reactive Stores #
Naming convention — CRITICAL
The user writes the class with a leading underscore. The generator strips it to produce the public name.
| User writes | Generated public class |
|---|---|
class _UserStore extends _$UserStore |
UserStore |
class _AppStore extends _$AppStore |
AppStore |
Field classification — @LiveStore()
Same first-match-wins order as @Live(), with these differences:
| Field | @Live() | @LiveStore() |
|---|---|---|
late final T x (no init, not CN) |
Widget param | Constructor param |
late final T x (no init, T extends CN) |
Widget param + listener + didUpdateWidget |
Constructor param + listener (no didUpdateWidget) |
| Everything else | Same | Same |
Owned vs. borrowed @LiveStore fields
| Declaration | Ownership | dispose() behavior |
|---|---|---|
CartStore cart = CartStore() |
Owned (has inline init) | removeListener + cart.dispose() |
late final SettingsStore s (param) |
Borrowed | removeListener only |
late final SettingsStore s = GetIt.instance.get() |
Borrowed | removeListener only |
ChangeNotifier integration — three patterns #
Pattern 1 — owned mutable field:
UserModel user = UserModel();
// addListener in initState; setter re-wires; removeListener in dispose (NOT .dispose())
Pattern 2 — borrowed param (late final, no init):
late final UserModel store;
// widget constructor param; addListener in initState; didUpdateWidget re-wires; removeListener in dispose
Pattern 3 — service-located (late final with init):
late final UserModel store = GetIt.instance.get<UserModel>();
// addListener in initState; removeListener in dispose; no param, no setter
Proxy objects — deep reactivity #
When a mutable field's type is a plain user-defined class, the generator produces a _Live<ClassName> subclass.
Proxy constraints — when proxy is NOT generated:
- Class is
finalorsealed - Class has no no-arg (or all-optional) constructor
- Private fields (
_field) on nested classes - Cycles (
A → B → A) — second encounter treated as a leaf
When proxy cannot be generated, the field falls back to plain reactive scalar.
Common mistakes to avoid #
-
Wrong base class.
@Live()→extends _$ClassName.@LiveStore()→ user class has a_prefix:class _StoreName extends _$StoreName. -
Missing
partdirective. Every file using these annotations needspart 'filename.g.dart';at the top. -
Using
finalwhen you want a param.final String title = 'x'is a non-reactive constant. Uselate final String title;(no initializer) for a param. -
Expecting deep reactivity without proxy support. If the nested class is
final,sealed, has no accessible default constructor, or uses private fields — only reference replacement triggers rebuilds. -
Using identity-based objects as Map keys.
Map<Car, String>whereCardoesn't override==/hashCodewill break after key wrapping. -
Calling
.dispose()on a borrowed ChangeNotifier.livelydoes NOT call.dispose()on borrowed CNs. Only owned CNs (with inline initializer) are auto-disposed. -
Expecting
notify()outside the class.notify()is a protected escape hatch — call it from inside the user class only.
Quick decision table for field declarations #
| I want... | Write this |
|---|---|
| A required widget param | late final String title; |
| An optional widget param | late final String? title; |
| A reactive field | String name = 'John'; |
| A non-reactive constant | final String appTitle = 'App'; |
| An auto-disposed controller | TextEditingController ctrl = TextEditingController(); |
| A reactive list | List<String> items = []; |
| A reactive map | Map<String, User> users = {}; |
| Borrowed ChangeNotifier param | late final MyStore store; |
| Owned ChangeNotifier | MyStore store = MyStore(); |
| Service-located store | late final MyStore store = GetIt.instance.get(); |
| Force a rebuild manually | Call notify() inside the class |
For full documentation, examples, and framework comparisons see the lively README.