Skip to main content

Observers

Observer Widget

Observer({required Widget Function(BuildContext context) builder})

One of the most visual reactions in the app is the UI. The Observer widget (which is part of the pub package), provides a granular observer of the observables used in its builder function. Whenever these observables change, Observer rebuilds and renders.

Immediate context for builder

The key thing to note is that the builder function will only track the observables in its immediate execution context. If an observable

is being used in a nested function, it will not be tracked. Make sure to dereference (a.k.a. read) an observable in the immediate execution context. If no observables are found when running the builder function, it will warn you on the console.

This is one of the most common gotchas when using MobX. Just because you have nested functions inside a builder, which are reading

an observable, it does not actually track the observable. It can appear deceiving but the fact is, the observable was not in the immediate execution context. Be watchful for this scenario, especially when your Observer-wrapped Widgets are not updating properly.

Below is the Counter example in its entirety.

import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';

part 'counter.g.dart';

class Counter = CounterBase with _$Counter;

abstract class CounterBase with Store {
@observable
int value = 0;

@action
void increment() {
value++;
}
}

class CounterExample extends StatefulWidget {
const CounterExample({Key key}) : super(key: key);

@override
_CounterExampleState createState() => _CounterExampleState();
}

class _CounterExampleState extends State<CounterExample> {
final _counter = Counter();

@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text('Counter'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Observer(
builder: (_) => Text(
'${_counter.value}',
style: const TextStyle(fontSize: 20),
)),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _counter.increment,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}

ReactionBuilder widget

If you ever ran into a need for running a reaction when a Widget loads, you most likely created a StatefulWidget and put your reaction in the initState().

This forces you to make a wrapper widget, whose sole purpose is to run the reaction and dispose it as part of the dispose() method. Rather that doing that ceremony everytime, there is a simpler way of handling this with the ReactionBuilder.

ReactionBuilder({required ReactionDisposer Function(BuildContext context) builder, required Widget child})

Here is a snippet from the ConnectivityExample:


class ConnectivityExample extends StatelessWidget {
const ConnectivityExample(this.store, {Key? key}) : super(key: key);

final ConnectivityStore store;

@override
Widget build(BuildContext context) => ScaffoldMessenger(
child: ReactionBuilder(
builder: (context) {
return reaction((_) => store.connectivityStream.value, (result) {
final messenger = ScaffoldMessenger.of(context);

messenger.showSnackBar(SnackBar(
content: Text(result == ConnectivityResult.none
? 'You\'re offline'
: 'You\'re online')));
}, delay: 4000);
},
child: Scaffold(
appBar: AppBar(
title: const Text('Settings'),
),
body: const Padding(
padding: EdgeInsets.all(16),
child: Text(
'Turn your connection on/off for approximately 4 seconds to see the app respond to changes in your connection status.'),
),
)),
);
}

Notice the use of the ReactionBuilder that sets up the reaction in the builder() method.

This simplifies the outer widget and keeps it as a StatelessWidget. Without the ReactionBuilder, you would have to create a wrapper StatefulWidget, prepare the reaction in the initState() and then dispose it in the dispose() of the widget.

Use the ReactionBuilder to keep your UI tree as a set of StatelessWidgets.