Watch Functions
WARNING
This content is AI generated and is currently under review.
Watching Data
Where WatchIt really shines is data-binding. It comes with a set of watch methods to rebuild a widget when data changes.
Imagine you had a very simple shared model, with multiple fields, one of them being country:
class Model {
final country = ValueNotifier<String>('Canada');
...
}
di.registerSingleton<Model>(Model());You could tell your view to rebuild any time country changes with a simple call to watchValue:
class MyWidget extends StatelessWidget with WatchItMixin {
@override
Widget build(BuildContext context) {
String country = watchValue((Model x) => x.country);
...
}
}There are various watch methods, for common types of data sources, including ChangeNotifier, ValueNotifier, Stream and Future:
| API | Description |
|---|---|
watch | observes any Listenable you have access to |
watchIt | observes any Listenable registered in get_it |
watchValue | observes a ValueListenable property of an object registered in get_it |
watchPropertyValue | observes a property of a Listenable object and trigger a rebuild whenever the Listenable notifies a change and the value of the property changes |
watchStream | observes a Stream and triggers a rebuild whenever the Stream emits a new value |
watchFuture | observes a Future and triggers a rebuild whenever the Future completes |
To be able to use the functions you have either to derive your widget from WatchingWidget or WatchingStatefulWidget or use the WatchItMixin or WatchItStatefulWidgetMixin in your widget class and call the watch functions inside the their build functions.
Just call watch* to listen to the data type you need, and WatchIt will take care of cancelling bindings and subscriptions when the widget is destroyed.
The primary benefit to the watch methods is that they eliminate the need for ValueListenableBuilders, StreamBuilder etc. Each binding consumes only one line and there is no nesting. Making your code more readable and maintainable. Especially if you want to bind more than one variable.
Here we watch three ValueListenable which would normally be three builders, 12+ lines of code and several levels of indentation. With WatchIt, it's three lines:
class MyWidget extends StatelessWidget with WatchItMixin {
@override
Widget build(BuildContext context) {
bool loggedIn = watchValue((UserModel x) => x.isLoggedIn);
String userName = watchValue((UserModel x) => x.user.name);
bool darkMode = watchValue((SettingsModel x) => x.darkMode);
...
}
}This can be used to eliminate StreamBuilder and FutureBuilder from your UI as well:
class MyWidget extends StatelessWidget with WatchItMixin {
@override
Widget build(BuildContext context) {
final currentUser = watchStream((UserModel x) => x.userNameUpdates, initialValue: 'NoUser');
final ready = watchFuture((AppModel x) => x.initializationReady, initialValue: false).data;
bool appIsLoading = ready == false || currentUser.hasData == false;
if(appIsLoading) return CircularProgressIndicator();
return Text(currentUser.data);
}
}compare that to:
Widget build(BuildContext context) {
return FutureBuilder(
future: di<AppModel>().initializationReady,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
}
return StreamBuilder(
stream: di<UserModel>().userNameUpdates,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
}
return Text(snapshot.data!);
},
);
},
);
}Rules
There are some important rules to follow in order to avoid bugs with the watch or register* methods:
watchmethods must be called withinbuild()- It is good practice to define them at the top of your build method
- must be called on every build, in the same order (no conditional watching). This is similar to
flutter_hooks. - do not use them inside of a builder as it will break the mixins ability to rebuild
If you want to know more about the reasons for this rule check out How does it work?
The watch functions in detail
Watching Listenable / ChangeNotifier
watch observes any Listenable that you pass as parameter and triggers a rebuild whenever it notifies a change.
T watch<T extends Listenable>(T target);That listenable is passed directly in as a parameter which means it could be some local variable/property or also come from get_it. Like
final userName = watch(di<UserModel>()).name;given that UserManager is a Listenable (eg. ChangeNotifier).
If all of the following functions don't fit your needs you can probably use this one by manually providing the Listenable that should be observed.
Example:
class CounterModel with ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count += 1;
notifyListeners();
}
}
final counter = CounterModel();
...
Widget build(BuildContext context) {
watch(counter);
return Text(counter.count);
}Watching Listenable inside GetIt
watchIt observes any Listenable registered with the type T in get_it and triggers a rebuild whenever it notifies a change. It's basically a shortcut for watch(di<T>()). instanceName is the optional name of the instance if you registered it with a name in get_it. getIt is the optional instance of get_it to use if you don't want to use the default one. 99% of the time you won't need this.
T watchIt<T extends Listenable>({String? instanceName, GetIt? getIt}) {If we take our Listenable UserModel from above we could watch it like
class MyWidget extends StatelessWidget with WatchItMixin {
@override
Widget build(BuildContext context) {
final userName = watchIt<UserModel>().name;
return Text(userName);
}
}Watching only one property of a Listenable
If the Listenable parent object that you watch with watchIt notifies often because other properties have changed that you don't want to watch, the widget would rebuild without any need. In this case you can use watchPropertyValue
R watchPropertyValue<T extends Listenable, R>(R Function(T) selectProperty,
{T? target, String? instanceName, GetIt? getIt});It will only trigger a rebuild if the watched listenable notifies a change AND the value of the selected property has really changed.
final userName = watchPropertyValue<UserManager, String>((m) => m.userName);Could be an example. Or even more expressive and concise:
final userName = watchPropertyValue((UserManager m) => m.userName);which lets the analyzer infer the type of T and R.
If you have a local Listenable and you want to observe only a single property you can pass it as [target] and omit the generic parameter:
final userManager = UserManager();
...
// inside build()
final userName = watchPropertyValue((m) => m.userName, target: userManger);Watching ValueListenable / ValueNotifier
R watchValue<T extends Object, R>(ValueListenable<R> Function(T) selectProperty,
{String? instanceName, GetIt? getIt}) {watchValue observes a ValueListenable (e.g. a ValueNotifier) property of an object registered in get_it. It triggers a rebuild whenever the ValueListenable notifies a change and returns its current value. It's basically a shortcut for watchIt<T>().value As this is a common scenario it allows us a type safe concise way to do this.
class UserManager
{
final userName = ValueNotifier<String>('James');
}
// register it in GetIt
di.registerSingleton(UserManager);
// watch it
Widget build(BuildContext context) {
final userName = watchValue<UserManager, String>((user) => user.userName);
return Text(userName);
}is an example of how to use it. We can use the strength of generics to infer the type of the property and write it even more expressive like this:
final userName = watchValue((UserManager user) => user.userName);instanceName is the optional name of the instance if you registered it with a name in get_it. getIt is the optional instance of get_it to use if you don't want to use the default one. 99% of the time you won't need this.
Watching a local ValueListenable/ValueNotifier
You might wonder why watchValue has no target parameter. The reason is that Dart doesn't support positional optional parameters in combination with named optional parameters. This would require that you always would have to add a parameter name to the select function when using it in the most common way to watch a ValueListenable property of an object inside GetIt. As there is already another option to watch local ValueListenable by using watch I decided to drop the target property from watchValue. As all ValueListenable are also Listenable we can watch them with watch():
final counter = ValueNotifier<int>();
Widget build(BuildContext context) {
final counterValue = watch(counter).value;
return Text(counterValue);
}This will trigger a rebuild every time the counter.value changes.
Watching Streams and Futures
watchStream and watchFuture follow nearly the same pattern as the above watch functions.
class TestStateLessWidget extends WatchingWidget {
@override
Widget build(BuildContext context) {
final currentUser = watchStream((Model x) => x.userNameUpdateStream, 'NoUser');
final ready =
watchFuture((Model x) => x.initializationReady,false).data;
return Column(
children: [
if (ready != true || !currentUser.hasData) // in case of an error ready could be null
CircularProgressIndicator()
else
Text(currentUser.data),
],
);
}
}Important:
watchFutureandwatchStreamexpect that the selector function that is used to select the watched Stream/Future will return the same Stream/Future on every build (unless you really know what you are doing). So you should not call any async function that returns a new Future here because otherwise it can easily happen that your widget gets into an infinite rebuild cycle.
Please check the API docs for details.
isReady<T>() and allReady()
A common use case is to toggle a loading state when side effects are in-progress. To check whether any async registration actions inside GetIt have completed you can use allReady() and isReady<T>(). These methods return the current state of any registered async operations and a rebuild is triggered when they change. If you only want the onReady handler to be called once set callHandlerOnlyOnce==true
class MyWidget extends StatelessWidget with WatchItMixin {
@override
Widget build(BuildContext context) {
allReady(onReady: (context)
=> Navigator.of(context).pushReplacement(MainPageRoute()));
return CircularProgressIndicator();
}
}Check out the GetIt docs for more information on the isReady and allReady functionality: https://pub.dev/packages/get_it
Side Effects / Event Handlers
Instead of rebuilding, you might instead want to show a toast notification or dialog when a Stream emits a value or a ValueListenable changes. Normally you would need to use a Stateful widget to be able to subscribe and unsubscribe your handler function.
To run an action when data changes you can use the register*Handler methods:
| API | Description |
|---|---|
.registerHandler | Add an event handler for a ValueListenable |
.registerStreamHandler | Add an event handler for a Stream |
.registerFutureHandler | Add an event handler for a Future |
.registerChangeNotifierHandler | Add an event handler for a ChangeNotifier |
The registerHandler, registerStreamHandler and registerFutureHandler methods have an optional select delegate parameter that can be used to watch a specific field of an object in GetIt. The second parameter is the action which will be triggered when that field changes:
class MyWidget extends StatelessWidget with WatchItMixin {
@override
Widget build(BuildContext context) {
registerHandler(
select: (Model x) => x.name,
handler: (context, value, cancel) => showNameDialog(context, value));
...
}
}In the example above you see that the handler function receives the value that is returned from the select delegate ((Model x) => x.name), as well as a cancel function that the handler can call to cancel registration at any time.
In case of the registerChangeNotifierHandler the handler function receives the ChangeNotifier object itself as well as a cancel function that the handler can call to cancel registration at any time.
class Counter extends ChangeNotifier {
int value = 0;
void increment() {
value++;
notifyListeners();
}
}
di.registerSingleton<Counter>(Counter());
class MyWidget extends StatelessWidget with WatchItMixin {
@override
Widget build(BuildContext context) {
registerChangeNotifierHandler(
handler: (context, Counter value, cancel) {
if (value.value == 3) {
SnackBar snackbar = SnackBar(
content: Text('Value is 3'),
);
Scaffold.of(context).showSnackBar(snackbar);
}
}
);
...
}
}As with watch calls, all registerHandler calls are cleaned up when the Widget is destroyed. If you want to register a handler for a local variable all the functions offer a target parameter.
The WatchingWidgets
Some people don't like mixins so WatchIt offers two Widgets that can be used instead.
WatchingWidget- can be used instead ofStatelessWidgetWatchingStatefulWidget- instead ofStatefulWidget