This package offers a set of mixin types that makes the binding of data that is stored within GetIt
really easy.
When I write of binding, I mean a mechanism that will automatically rebuild a widget that if data it depends on changes
Several users asked for support of data binding for GetIt like provider
offers. At the same time I have to admit I got really intrigued by flutter_hooks
from Remi Rousselet, so I started to think about how to create something similar for GetIt
. I'm very thankful for Remi's work. I took more than one inspiration from his code
As I want to keep GetIt
free of Flutter dependencies I choose to write a separate package with mixins to achive this goal.
To be clear you can achieve the same using different Flutter Builders but it will make your Flutter code less readable and you will have more to type.
For this readme I expect that you know how to work with GetIt
Lets create some model class that we want to access with the mixins:
class Model extends ChangeNotifier {
String _country;
set country(String val) {
_country = val;
notifyListeners();
}
String get country => _country;
String _emailAddress;
set emailAddress(String val) {
_emailAddress = val;
notifyListeners();
}
String get emailAddress => _emailAddress;
final ValueNotifier<String> name;
final Model nestedModel;
Stream<String> userNameUpdates;
Future get initializationReady;
}
Now we will explore how to access the different properties by using the get_it_mixin
When you add the GetItMixin
to your StatelessWidget
you get a lot of new functions that you can use inside the Widget the easiest one is get()
and getX()
which will access data from GetIt
as if you would to GetIt.I<Type>()
class TestStateLessWidget extends StatelessWidget with GetItMixin {
@override
Widget build(BuildContext context) {
final email = get<Model>().emailAddress;
return Column(
children: [
Text(email),
Text(getX((Model x) => x.country, instanceName: 'secondModell')),
],
);
}
}
As you can see get()
is used exactly like using GetIt
directly with all its parameters. getX()
does the same but offers a selector function that has to return the final value from the referenced object. Most of the time you probably will only use get()
, but the selector function can be used to do any data processing that might me needed before you can use the value.
get() and getX() can be called multiple times inside a Widget and also outside the build()
function.
The following functions will return a value and rebuild the widget every-time this data inside GetIt changes.
Imagine you have an object inside GetIt
registered that implements ValueListenableBuilder<String>
named currentUserName
and we want the above widget to rebuild every-time it's value changes.
We could do this adding a ValueListenableBuilder
:
class TestStateLessWidget1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: [
ValueListenableBuilder<String>(
valueListenable: GetIt.I<ValueListenable<String>>(instanceName: 'currentUserName'),
builder: (context, val,_) {
return Text(val);
}
),
],
);
}
}
With the mixin we can now write this:
class TestStateLessWidget1 extends StatelessWidget with GetItMixin {
@override
Widget build(BuildContext context) {
final currentUser =
watch<ValueListenable<String>, String>(instanceName: 'currentUserName');
return Column(
children: [
Text(currentUser)
],
);
}
}
Unfortunately we have to provide a second generic parameter because Dart can't infer the type of the return value.
watch
can not only observe Valuelistenables
inside GetIt, you also can pass any Valuelistenable as target
parameter. For instance a ValueListenable
that was passed as parameter to the Widget.
Luckily we will see with the following functions there is a way to help the compiler.
Important: These functions can only be called inside the build()
function and you can only watch any objects only once. The functions must be called on every build
, in the same order, and cannot be called conditionally otherwise the mixin gets confused
You can't use any of the watch functions (you can use get
and getX
though) inside an Builder
because a Builder gets its own context and looses the connection to the mixin. If you want to use a watch function inside a Builder
, wrap the content of the Builder in another Widget that uses the mixin too.
But you shouldn't need any Builders if you use the mixin.
In a real app it's way more probable that your business object wont be the ValueListenable
itself but it will have some properties that might be ValueListenables
like the name
property of our Model
class. To react to changes to of such properties you can use watchX()
:
class TestStateLessWidget1 extends StatelessWidget with GetItMixin {
@override
Widget build(BuildContext context) {
final name = watchX((Model x) => x.name);
/// if the valueListenable is nested deeper in your object
final innerName = watchX((Model x) => x.nestedModel.name);
return Column(
children: [
Text(name),
Text(innerName),
],
);
}
}
This widget will rebuild whenever one of the watched ValueListenables
changes.
You might be wondering why I did not pass the type Model
as generic Parameter to watchX()
. The reason it that the signature of it looks like this:
R watchX<T, R>(
ValueListenable<R> Function(T x) select, {
String instanceName,
}) =>
which means you would have to pass two generic types, not only T
but also R
. If you pass T
inside the select
function the compiler is able to infer R
.
Another popular pattern is that a business object implements Listenable
like ChangeNotifier
and it will notify its listeners whenever one of its properties changes. As we want to only rebuild a Widget when a value that it needs is updated watchOnly()
lets you define which property you want tp observe and it will only trigger the rebuild if it really changes.
watchXonly()
does the same but for nested Listenables
class TestStateLessWidget1 extends StatelessWidget with GetItMixin {
@override
Widget build(BuildContext context) {
final country = watchOnly((Model x) => x.country);
/// if the watched property is nested deeper in you object
final innerEmail = watchXOnly((Model x) => x.nestedModel,(Model o)=>o.emailAddress);
return Column(
children: [
Text(country),
Text(innerEamil),
],
);
}
}
This Widget will rebuild when either country
of the Model
object or emailAddress
of the nested Model
changes. If you update emailAddress
of Model
it won't update although it too calls notifyListeners
If you want to get an update whenever Model triggers notifyListener
you can achieve this by using this selector method:
final model = watchOnly((Model x) => x);
In case you want to update your widget as soon as a Stream in your Model emits a new value or as soon as a Future
completes you can use watchStream
and watchFuture
. The nice thing is that you don't have to care to cancel subscriptions, the mixin takes care of that. So instead of using a StreamBuilder
you can just do:
class TestStateLessWidget1 extends StatelessWidget with GetItMixin {
@override
Widget build(BuildContext context) {
final currentUser = watchStream((Model x) => x.userNameUpdates, '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),
],
);
}
}
These functions can handle if the selector function returns different Streams and Futures on following build
calls. In this case the old subscription is cancelled and the new Stream
subscribed. Check he API docs for more details.
Maybe you don't need a value updated but want to show a Snackbar as soon as a Stream
emits a value or a ValueListenable
updates a value or a Future
. If you wanted to do this without this mix_in you would need a StatefulWidget
where you subscribe to a Stream
in iniState
and dispose your subscription in the dispose
function of the State
.
With this mixin you can register handlers for Streams
, ValueListenables
and Futures, and the mixin will dispose everything for you as soon as the widget gets destroyed.
class TestStateLessWidget1 extends StatelessWidget with GetItMixin {
@override
Widget build(BuildContext context) {
/// Registers a handler for a valueListenable
registerHandler((Model x) => x.name, (context,name,_)
=> showNameDialog(context,name));
registerStreamHandler((Model x) => x.userNameUpdates, (context,name,_)
=> showNameDialog(context,name));
registerFutureHandler((Model x) => x.initializationReady, (context,__,_)
=> Navigator.of(context).push(....));
return Column(
children: [
//...whatever widgets needed
],
);
}
}
For instance you could register a handler for thrownExceptions
of a flutter_command
while you use watch()
to get the values.
In the example above you see that the handler function has a third parameter that we ignored. Your handler gets a dispose function passed there that a handler could use to kill a registration from within itself.
If you already used the synchronization functions from GetIt you know both of this functions (otherwise check them out in the GetIt readme). The mixin variant returns the actual status as bool
value and trigger a rebuild when this status changes. Additionally you can register handlers that are called when the status is true
.
class TestStateLessWidget1 extends StatelessWidget with GetItMixin {
@override
Widget build(BuildContext context) {
final isReady = allReady();
if (isReady) {
return MyMainPageContent();
} else {
return CircularProgressIndicator();
}
}
or with the handler:
class TestStateLessWidget1 extends StatelessWidget with GetItMixin {
@override
Widget build(BuildContext context) {
allReady(
onReady: (context) =>
Navigator.of(context).pushReplacement(MainPageRoute()));
return CircularProgressIndicator();
}
}
isReady<T>()
can be used in the same way to react on the status of a single asynchronous singleton.
With pushScope()
you can push one scope that will be popped when the Widget/State is destroyed.
You can pass an init
function that will be called immediately after the scope was pushed and an optional dispose
function that is called directly before the scope is popped.
void pushScope({void Function(GetIt getIt) init, void Function() dispose});
As it is possible that objects registered in a higher GetIt-Scope can shadow objects of the same registration type in a lower scope it is important to ensure that the UI can update its references to the newly active object (the one last registered). The get_it_mixin detects such changes and updates them on the next rebuild but if you want to ensure that this happens immediately you can put a call to
/// Will triger a rebuild of the Widget if any new GetIt-Scope is pushed or popped
/// This function will return `true` if the change was a push otherwise `false`
/// If no change has happend the return value will be null
bool? rebuildOnScopeChanges();
in the build()
method of your root widget.
All the functions above are available for StatefulWidgets
too. However with this mixin the need for StatefulWidgets
will drastically decline.
In case you need one and also want to use the comfort of this you have to use two different mixins.
class TestStatefulWidget extends StatefulWidget with GetItStatefulWidgetMixin {
@override
_TestStatefulWidgetState createState() => _TestStatefulWidgetState();
}
class _TestStatefulWidgetState extends State<TestStatefulWidget> with GetItStateMixin {
@override
Widget build(BuildContext context) {
final currentUser = watchX((Model x) => x.name,);
return Column(
children: [
Text(currentUser),
],
);
}
}
Unfortunately we need two mixins in this case otherwise the automatic updating could not be realised.