state_lifecycle_observer
A Flutter package to solve state reuse problems using an Observer pattern inspired by Android's LifecycleObserver and LifecycleOwner.
Features
- LifecycleObserver: A base class for creating reusable state observers.
- LifecycleOwnerMixin: A mixin to manage the lifecycle of observers within a
State. - Built-in Observers: Classified into Base, Widget, and Anim categories to cover common scenarios.
Usage
- Create a
StatefulWidgetand mixinLifecycleOwnerMixin. - Instantiate observers in
initState. They automatically register themselves. - Call
super.build(context)in yourbuildmethod.
class MyLogo extends StatefulWidget {
final Duration speed;
const MyLogo({super.key, required this.speed});
@override
State<MyLogo> createState() => _MyLogoState();
}
class _MyLogoState extends State<MyLogo>
with TickerProviderStateMixin, LifecycleOwnerMixin<MyLogo> {
// LOGIC REUSE: Pass 'this' as the first argument.
// The observer automatically registers itself to the mixin.
late final _animObserver = AnimControllerObserver(
this,
duration: () => widget.speed,
);
late final _scrollObserver = ScrollControllerObserver(
this,
initialScrollOffset: 100.0,
);
@override
void initState() {
super.initState();
_animObserver.target.repeat(reverse: true);
}
@override
Widget build(BuildContext context) {
// Notify observers about build
super.build(context);
return SingleChildScrollView(
controller: _scrollObserver.target,
child: ScaleTransition(
scale: _animObserver.target,
child: const FlutterLogo()
),
);
}
}
Using Callbacks
For simple use cases where you don't need a full observer, you can use addLifecycleCallback:
class _MyWidgetState extends State<MyWidget> with LifecycleOwnerMixin {
@override
void initState() {
super.initState();
addLifecycleCallback(
onInitState: () {
debugPrint('Widget initialized');
},
onDidUpdateWidget: () {
debugPrint('Widget updated');
},
onBuild: (context) {
debugPrint('Widget building');
},
onDispose: () {
debugPrint('Widget disposed');
},
);
}
@override
Widget build(BuildContext context) {
super.build(context);
return Container();
}
}
Built-in Observers
The library provides three main categories of built-in observers: Base, Widget, and Anim.
1. Base Observers (observer/base.dart)
General-purpose observers for data and async operations.
ListenableObserver: Listens to anyListenable(e.g.,ValueNotifier,ChangeNotifier) and rebuilds the widget when notified.FutureObserver<T>: Manages aFuture, exposing the current state as anAsyncSnapshot.StreamObserver<T>: Manages aStreamsubscription, creating anAsyncSnapshotand handling active/done states.
2. Widget Observers (observer/widget.dart)
Observers that simplify the creation, disposal, and management of common Flutter controllers.
ScrollControllerObserver: ManagesScrollController.PageControllerObserver: ManagesPageController.TabControllerObserver: ManagesTabController. RequiresTickerProvider.TextEditingControllerObserver: ManagesTextEditingController.FocusNodeObserver: ManagesFocusNode.
3. Anim Observers (observer/anim.dart)
Observers for animation-related classes.
AnimControllerObserver: ManagesAnimationController. Automatically syncsdurationandreverseDurationfrom the widget configuration.AnimationObserver<T>: Listens to anAnimation<T>object and rebuilds the widget when the value changes.
Custom Observer
You can easily create your own observers by extending LifecycleObserver<V>.
Example: A UserDataObserver that fetches data.
import 'package:flutter/material.dart';
import 'package:state_lifecycle_observer/state_lifecycle_observer.dart';
class Data {
final String id;
final String info;
Data(this.id, this.info);
}
// LifecycleObserver<V> where V is ValueNotifier<Data?>
class UserDataObserver extends LifecycleObserver<ValueNotifier<Data?>> {
// Mechanism to retrieve the latest param from the widget
final String Function() getUserId;
// Internal state to track changes
late String _currentUserId;
UserDataObserver(
super.state, {
required this.getUserId,
});
// 1. Create the target (called in constructor and when key changes)
@override
ValueNotifier<Data?> buildTarget() {
_currentUserId = getUserId();
final notifier = ValueNotifier<Data?>(null);
_fetchData(_currentUserId, notifier); // Start fetch
return notifier;
}
// 2. Handle widget updates (if key doesn't change)
@override
void onDidUpdateWidget() {
super.onDidUpdateWidget();
// Check if the dependency (userId) has changed without triggering a full rebuild (if key wasn't used)
final newUserId = getUserId();
if (newUserId != _currentUserId) {
debugPrint('UserId changed from $_currentUserId to $newUserId');
_currentUserId = newUserId;
_fetchData(_currentUserId, target);
}
}
@override
void onBuild(BuildContext context) {
debugPrint('Building with user: $_currentUserId');
}
// 3. Cleanup
@override
void onDisposeTarget(ValueNotifier<Data?> target) {
target.dispose();
}
void _fetchData(String id, ValueNotifier<Data?> notifier) async {
// Simulate network request
await Future.delayed(const Duration(milliseconds: 500));
// Simple check to avoid race conditions if observer was disposed/recreated
if (_currentUserId == id) {
notifier.value = Data(id, 'Info for $id');
}
}
}
Using key to Recreate Targets
The key parameter functions similarly to React's useEffect dependencies or Flutter's Key.
When the value returned by the key callback changes, the observer will:
- Dispose the current
target(callsonDisposeTarget). - Re-create the
target(callsbuildTarget).
This is useful when your Controller depends on a specific property (e.g. userId) and needs to be fully reset when that property changes.
_observer = MyObserver(
this,
// When 'userId' changes, the old target is disposed and a new one is built.
key: () => widget.userId,
);
Note: Using
keyis not strictly necessary to recreate the target. You can create a new Observer instance.
Composable Observers (Nested Observers)
Like React Hooks, LifecycleObserver supports composability — an Observer can create and manage other Observers internally. This enables powerful reuse patterns where complex behaviors are built from simpler, composable building blocks.
How It Works
When an Observer creates child Observers within its lifecycle methods (e.g., onInitState), the child Observers automatically register with the top-level State via Dart's Zone mechanism. This means you don't need to pass the State reference through every level of nesting.
Example: Composing Observers
/// A high-level observer composed of multiple lower-level observers.
class UserProfileObserver extends LifecycleObserver<void> {
late final TextEditingControllerObserver _nameController;
late final FutureObserver<UserData> _dataFetcher;
UserProfileObserver(super.state, {required String Function() userId});
@override
void onInitState() {
super.onInitState();
// Child observers automatically register with the top-level State.
// No need to pass `state` — they use Zone lookup.
_nameController = TextEditingControllerObserver(state);
_dataFetcher = FutureObserver(
state,
future: () => fetchUserData(userId()),
);
}
@override
void buildTarget() {}
}
// Usage: just create the high-level observer
class _MyPageState extends State<MyPage> with LifecycleOwnerMixin {
late final _profileObserver = UserProfileObserver(this, userId: () => widget.userId);
@override
Widget build(BuildContext context) {
super.build(context);
// All nested observers are managed automatically
return ...;
}
}
Comparison with Hooks / flutter_hooks
| Feature | state_lifecycle_observer | flutter_hooks / React Hooks |
|---|---|---|
| Paradigm | OOP (Classes) | Functional (Hooks) |
| Base Class | Standard StatefulWidget |
HookWidget |
| Lifecycle | Explicit (buildTarget, onDispose) |
Implicit (useEffect) |
| Learning Curve | Low (Standard Flutter) | Moderate (Rules of Hooks) |
| Magic | Low (Mixin + List) | High (Element logic) |
| Conditional Logic | Supported anywhere | Only allowed in build |
| Compose | Create Observers in Custom Observer | Call hooks inside custom hooks |
| Registration | Automatic via Zone | Automatic via fiber context |
| Nesting Depth | Unlimited | Unlimited |
| Lifecycle Sync | All nested Observers follow parent lifecycle | All hooks follow component lifecycle |
This composability makes LifecycleObserver as flexible as hooks for building reusable, modular state logic.