watch_it

Creator: coderz1093

Last updated:

0 purchases

watch_it Image
watch_it Images

Languages

Categories

Add to Cart

Description:

watch it

❤️ Sponsor
watch_it #
A simple state management solution powered by get_it.

This package is the successor of the get_it_mixin, here you can find what's new


We now have a support discord server https://discord.gg/ZHYHYCM38h

This package offers a set of functions to watch data registered with GetIt. Widgets that watch data will rebuild automatically whenever that data changes.
Supported data types that can be watched are Listenable / ChangeNotifier, ValueListenable / ValueNotifier, Stream and Future. On top of that there are several other powerful functions to use in StatelessWidgets that normally would require a StatefulWidget.
ChangeNotifier based example:
// Create a ChangeNotifier based model
class UserModel extends ChangeNotifier {
get name => _name;
String _name = '';
set name(String value){
_name = value;
notifyListeners();
}
...
}

// Register it
di.registerSingleton<UserModel>(UserModel());

// Watch it
class UserNameText extends WatchingWidget {
@override
Widget build(BuildContext context) {
final userName = watchPropertyValue((UserModel m) => m.name);
return Text(userName);
}
}
copied to clipboard
Whenever the name property changes the watchPropertyValue function will trigger a rebuild and return the latest value of name.
Accessing GetIt #
WatchIt exports the default instance of get_it as a global variable di (dependency injection) which lets
you access it from anywhere in your app. To access any get_it registered
object you only have to type di<MyType>() instead of GetIt.I<MyType>().
If you prefer to use GetIt.I or you have your own global variable that's fine too as they all
will use the same instance of GetIt.
If you want to use a different instance of get_it you can pass it to
the functions of this library as an optional parameter.
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());
copied to clipboard
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);
...
}
}
copied to clipboard
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 WatchingStatefulWidgetMixin 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);
...
}
}
copied to clipboard
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);
}
}
copied to clipboard
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));
...
}
}
copied to clipboard
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);
}
}
);
...
}
}

copied to clipboard
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.
Rules #
There are some important rules to follow in order to avoid bugs with the watch or register* methods:

watch methods must be called within build()

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 Lifting the magic curtain
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);
copied to clipboard
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;
copied to clipboard
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);
}
copied to clipboard
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}) {
copied to clipboard
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);
}
}
copied to clipboard
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});
copied to clipboard
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);
copied to clipboard
Could be an example. Or even more expressive and concise:
final userName = watchPropertyValue((UserManager m) => m.userName);
copied to clipboard
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);
copied to clipboard
Watching `ValueListenable / ValueNotifier #
R watchValue<T extends Object, R>(ValueListenable<R> Function(T) selectProperty,
{String? instanceName, GetIt? getIt}) {
copied to clipboard
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);
}
copied to clipboard
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);
copied to clipboard
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);
}
copied to clipboard
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),
],
);
}
}
copied to clipboard
Please check the API docs for details.
isReady #
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();
}
}
copied to clipboard
callOnce() and onDispose() #
If you want to execute a function only on the first built (even in in a StatelessWidget), you can use the callOnce function anywhere in your build function. It has an optional dispose handler which will be called when the widget is disposed.
To dispose anything when the widget is disposed you can use call onDispose anywhere in your build function
Check out the GetIt docs for more information on the isReady and allReady functionality:
https://pub.dev/packages/get_it
Pushing a new GetIt Scope #
With pushScope() you can push a scope when a Widget/State is mounted, and automatically drop it when the Widget/State is destroyed. You can pass an optional init or dispose function.
void pushScope({void Function(GetIt getIt) init, void Function() dispose});
copied to clipboard
The newly created Scope gets a unique name so that it is ensured the right Scope is dropped even if you push or drop manually other Scopes.
The WatchingWidgets #
Some people don't like mixins so WatchIt offers two Widgets that can be used instead.

WatchingWidget - can be used instead of StatelessWidget
WatchingStatefulWidget - instead of StatefulWidget

Lifting the magic curtain #
*It's not necessary to understand the following chapter to use WatchIt successfully.
You might be wondering how on earth is this possible, that you can watch multiple objects at the same time without passing some identifier to any of the watch functions. The reality might feel a bit like a hack but the advantages that you get from it justify it absolutely.
When applying the WatchItMixin to a Widget you add a handler into the build mechanism of Flutter that makes sure that before the build function is called a _watchItState object that contains a reference to the Element of this widget plus a list of WatchEntrys is assigned to a private global variable. Over this global variable the watch* functions can access the Element to trigger a rebuild.
With each watch* function call a new WatchEntry is added to that list and a counter is incremented.
When a rebuild is triggered the counter is reset and incremented again with each watch* call so that it can access the data it stored during the last build.
Now it should be clear why the watch* functions always have to happen in the same order and no conditionals are allowed that would change the order between two builds because then the relation between watch* call and its WatchEntry would be messed up.
If you think that all sounds very familiar to you then probably because the exact same mechanism is used by flutter_hooks or React Hooks.
Find out more! #
To learn more about GetIt, watch the presentation: GetIt in action By Thomas Burkhart, in there the predecessor of this package called ´get_it_mixin´ is described but the video should still be helpful for the GetIt part.
What's different from the get_it_mixin #
Two main reasons lead me to replace the get_it_mixin package with watch_it

The name `get_it_mixin seemed not to catch with people and only a fraction of my get_it users used it.
The API naming wasn't as intuitive as I thought when I first wrote them.

These are the main differences:

Widgets now can be const!
a reduced API with more intuitive naming.The old package had too many functions which were only slight variations of each other. You can easily achieve the same functionality with the functions of this package.
no get/getX functions anymore because you can just use the included global get_it instance di<T>.
only one mixin for all Widgets. You only need to apply it to the widget and no mixin for States as now all watch* functions are global functions.

Please let me know if you miss anything

License

For personal and professional use. You cannot resell or redistribute these repositories in their original state.

Customer Reviews

There are no reviews.