Skip to content

widget_listener_api

GIfatahTH edited this page Oct 2, 2021 · 17 revisions

Table of Contents

Option of State subscription and Reactive Builders

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!

ReactiveStatelessWidget

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

OnBuilder

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

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

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.

App internationalization and theme swishing

 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(),
     );
   }
 }

Plugins initialization

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();
  }
}

App lifecycle

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();
  }
}

futureBuilder

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

future

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 an ArgumentError

onWaiting

The callback is to be invoked if the future is waiting. If set to null, the onData will be called instead.

onError

The callback is to be invoked if the future ends with an error. If set to null, the onData will be called instead.

onData

The callback is to be invoked if the future ends with data.

dispose

The callback is to be involved when the widget is removed from the widget tree.

sideEffects

Used for side effects. It takes a SideEffects object.

streamBuilder

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
 }
)
Clone this wiki locally