-
Notifications
You must be signed in to change notification settings - Fork 56
widget_listener_api
- ReactiveStatelessWidget and OnReactive
- OnBuilder
- Child
- TopStatelessWidget
- futureBuilder
- streamBuilder
There are two ways to for get your widget rebuilds by state:
Widget Builders | Style | Link |
---|---|---|
ReactiveStatelessWidget OnReactive
|
👩🏻💻 By default | Finish him! |
OnBuilder |
👨🏻🚒 Strictly to target widget | Get Over Here! |
The simplest way to listen to an injected state and rebuild a part of the widget tree, is to use ReactiveStatelessWidget
instead of StatelessWidget
.
This is the default flutter counter app written to use ReactiveStatelessWidget
.
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
//We use ReactiveStatelessWidget
class MyHomePage extends ReactiveStatelessWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
static final _counter = 0.inj();
static void _incrementCounter() {
_counter.state++;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'${_counter.state}',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
// Hooks you can override to trigger side effects.
@override
void didMountWidget() {
/// Called when the widget is first inserted in the widget tree
}
@override
void didUnmountWidget() {
/// Called when the widget is removed from the widget tree
}
@override
void didNotifyWidget(SnapState<dynamic> snap) {
/// Called when the widget is notified to rebuild, it exposes the SnapState of
/// the state that emits the notification
}
@override
bool shouldRebuildWidget(
SnapState<dynamic> oldSnap, SnapState<dynamic> currentSnap) {
/// Condition on when to rebuild the widget, it exposes the old and the new
/// SnapState of the the state that emits the notification.
return true;
}
@override
void didAddObserverForDebug(List<InjectedBaseState> observers) {
/// Called in debug mode only when an state is added to the list of listeners
}
}
Any injected state when consumed, will look up the widget tree for the nearest ReactiveStatelessWidget
and register it to rebuild when the state changes.
Notice in the above example, that the hole Scaffold will rebuild. To limit the rebuild to the Text widget only we can simply use OnReactive
widget.
// We use StatelessWidget
class MyHomePage extends StatelessWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
static final _counter = 0.inj();
void _incrementCounter() {
_counter.state++;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
// We use OnReactive to limit the rebuild to this Text Widget
OnReactive(
() => Text(
'${_counter.state}',
style: Theme.of(context).textTheme.headline4,
),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
See more details on OnReactive api
With ObBuilder
you can explicitly listen to one or many injected state.
Here is the same example written using OnBuilder
.
class MyHomePage extends StatelessWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
static final _counter = 0.inj();
static void _incrementCounter() {
_counter.state++;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
// Use of OnBuilder
OnBuilder(
listenTo: _counter,
builder: () => Text(
'${_counter.state}',
style: Theme.of(context).textTheme.headline4,
),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
See more details on OnBuilder api
Child widget is used in combination with other listener widgets to control the part of the sub-widget tree to rebuild.
Child(
(child) => On(
() => Column(
children: [
Text('model.state'), // 👍🏻 Only this part will rebuild
child, // This part will not rebuild
],
),
).listenTo(model),
child: WidgetNotToRebuild(),
);
TopStatelessWidget
is used instead of StatelessWidget
on top of the MaterialApp
widget.
TopStatelessWidget
is useful in many cases:
- App internationalization using [InjectedI18N].
- App theme management using [InjectedTheme].
- Plugins initialization
- App life cycle tracking.
void main() {
runApp(MyApp());
}
final themeRM = RM.injectTheme(
...
);
final i18nRM = RM.injectedI18N(
...
);
class MyApp extends TopStatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
//
theme: themeRM.lightTheme, //light theme
darkTheme: themeRM.darkTheme, //dark theme
themeMode: themeRM.themeMode, //theme mode
//
locale: i18nRM.locale,
localeResolutionCallback: i18nRM.localeResolutionCallback,
localizationsDelegates: i18nRM.localizationsDelegates,
home: HomePage(),
);
}
}
In Flutter it is common to initialize plugins inside the main method:
void main()async{
WidgetsFlutterBinding.ensureInitialized();
await initializeFirstPlugin();
await initializeSecondPlugin();
runApp(MyApp());
}
If you want to initialize plugins and display splash screen while waiting for them to initialize and display an error screen if any of them fails to initialize or request for permission with the ability to retry the initialization you can use TopStatelessWidget
:
class MyApp extends TopStatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
List<Future<void>>? ensureInitialization() {
return [
initializeFirstPlugin(),
initializeSecondPlugin(),
];
}
@override
Widget? splashScreen() {
return Material(
child: Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
),
);
}
@override
Widget? errorScreen(error, void Function() refresh) {
return ElevatedButton.icon(
onPressed: () => refresh(),
icon: Icon(Icons.refresh),
label: Text('Retry again'),
);
}
@override
Widget build(BuildContext context) {
return MyHomePage();
}
}
To invoke side effects depending on the app life cycle, use didChangeAppLifecycleState
hook
class MyApp extends TopStatelessWidget {
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
print(state);
}
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container();
}
}
Widget futureBuilder<F>({
Future<F>? Function(T?, Future<T>)? future,
required Widget Function()? onWaiting,
required Widget Function(dynamic)? onError,
required Widget Function(F) onData,
void Function()? dispose,
SideEffects<F>? sideEffects,
Key? key
}
)
futureBuilder
listens to a future from the injected state and exposes callbacks to handle waiting, error, data state status.
It is important to notice that it listens to a future from the injected state and not to the injected state. Let's see the difference with this example:
final model = RM.injectFuture(()async=> ...);
// 1. First widget
// listen using futureBuilder
// listen to the future of the state
model.futureBuilder(
onWaiting: ()=> ..,
onError: (error)=> ..
onData: (data)=>...
)
// 2. Second widget
// listen using listenTo
// listen to the state
OnBuilder.all(
listenTo: model,
onWaiting: ()=> ..,
onError: (err, refresh)=> ..
onData: (data)=>...
)
Both ways of listening using futureBuilder
(first widget) or OnBuilder
(second widget) give the same result (Displaying a waiting widget the data or error widget).
The difference is when the state of the model is mutated the second widget will rebuild but the first widget (futureBuilder
) will not rebuild.
For example, if we call the refresh
method:
model.refresh();
// The first widget (futureBuilder) will not be affected
// But the second widget (listen) will display the waiting widget (e.g. Indicator)
// then a data or error widget
In futureBuilder
, onWaiting an onError
are required but can be null. If any of the is set to null, the onData
will be called instead.
model.futureBuilder(
onWaiting: ()=> ...,
onError: null, // Here on error is null, will take onData instead.
onData: (data)=> ...,
)
// When the future ends will error, the `onData` will be invoked
The future parameter is optional.
- If the future is not given and the state is injected using
RM.injectFuture
, that the future will be the injected future. - If the future is not given and the state is not injected using
RM.injectFuture
it will throw anArgumentError
The callback is to be invoked if the future is waiting. If set to null, the onData
will be called instead.
The callback is to be invoked if the future ends with an error. If set to null, the onData
will be called instead.
The callback is to be invoked if the future ends with data.
The callback is to be involved when the widget is removed from the widget tree.
Used for side effects. It takes a SideEffects
object.
Widget streamBuilder<S>({
required Stream<S>? Function(T?, StreamSubscription<dynamic>?) stream,
required Widget Function()? onWaiting,
required Widget Function(dynamic)? onError,
required Widget Function(S) onData,
Widget Function(S)? onDone,
void Function()? dispose,
SideEffects<S>? sideEffects,
Key? key
}
)