get_it_hooks

This package offers a set of hooks for Remi Rousselet's (flutter_hooks)https://pub.dev/packages/flutter_hooks package, 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

These hooks offer the exact same functionality as get_it_mixin. You may ask why then also hooks? The reason is that flutter_hooks and get_it_mixin try both to override the createElement() function of the widget which means you could not use the mixin together with any hooks and vice versa.

Getting started

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

No we will explore how to access the different properties by using the get_it_hooks. To make this work you have to add flutter_hooks to your dependencies alongside get_it_hooks.

Reading Data

As all hooks get_it_hooks start with use.... The easiest ones are useGet() and useGetX() which will access data from GetIt as if you would to GetIt.I<Type>()

class TestStateLessWidget extends HookWidget{

  @override
  Widget build(BuildContext context) {
    final email = get<Model>().emailAddress;
    return Column(
      children: [
        Text(email),
        Text(useGetX((Model x) => x.country, instanceName: 'secondModell')),
      ],
    );
  }
}

As you can see useGet() is used exactly like using GetIt directly with all its parameters. useGetX() 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 useGet(), but the selector function can be used to do any data processing that might me needed before you can use the value.

useGet() and useGetX() can be called multiple times inside a Widget and also outside the build() function. Because they are no real hooks but just convenience functions.

Watching Data

The following functions will return a value and rebuild the widget every-time this data inside GetIt changes. Important: All following functions can only be called inside the build() function. Also all of these function have to be called always and in the same order on every build meaning they can't be called conditionally otherwise the hooks gets confused

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: 'currentUser'),
          builder: (context, val,_) {
            return Text(val);
          }
        ),
      ],
    );
  }
}

With the hooks we can now write this:

class TestStateLessWidget1 extends HookWidget{
  @override
  Widget build(BuildContext context) {
    final currentUser = 
       useWatch<ValueListenable<String>, String>(instanceName: 'currentUser');

    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. Luckily we will see with the following functions there is a way to help the compiler.

useWatchX

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 useWatchX():

class TestStateLessWidget1 extends HookWidget{
  @override
  Widget build(BuildContext context) {
    final name = useWatchX((Model x) => x.name);
    /// if the valueListenable is nested deeper in you object
    final innerName = useWatchX((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 useWatchX(). The reason it that the signature of it looks like this:

R useWatchX<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.

useWatchOnly & useWatchXonly

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 useWatchOnly() lets you define which property you want to observe and it will only trigger the rebuild if it really changes. useWatchXonly() does the same but for nested Listenables

class TestStateLessWidget1 extends HookWidget{
  @override
  Widget build(BuildContext context) {
    final country = useWatchOnly((Model x) => x.country);
    /// if the watched property is nested deeper in you object
    final innerEmail = useWatchXOnly((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 = useWatchOnly((Model x) => x);

Streams and Futures

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 useWatchStream and useWatchFuture. The nice thing is that you don't have to care to cancel subscriptions, hooks takes care of that. So instead of using a StreamBuilder you can just do:

class TestStateLessWidget1 extends HookWidget{
  @override
  Widget build(BuildContext context) {
    final currentUser = useWatchStream((Model x) => x.userNameUpdates, 'NoUser');
    final ready =
        useWatchFuture((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.

Event handlers

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 hooks you can register handlers for Streams, ValueListenables and Futures, and hooks 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
    useRegisterHandler((Model x) => x.name, (context,name,_) 
        => showNameDialog(context,name));
        
    useRegisterStreamHandler((Model x) => x.userNameUpdates, (context,name,_) 
        => showNameDialog(context,name));

    useRegisterFutureHandler((Model x) => x.initializationReady, (context,__,_) 
        => Navigator.of(contex).push(....));
    return Column(
      children: [
        //...whatever widgets needed 
      ],
    );
  }
}

For instance you could register a handler for thrownExceptions of a flutter_command while you use useWatch() 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.

useAllReady() & useIsReady()

If you already used the synchronization functions from GetIt you know both of this functions (otherwise check them out in the GetIt readme). The hook variant returns the actual status as bool value and triggers a rebuild when this status changes. Additionally you can register handlers that are called when the status is true.

class TestStateLessWidget1 extends HooksWidget{
  @override
  Widget build(BuildContext context) {
    final isReady = useAllReady();

    if (isReady) {
      return MyMainPageContent();
    } else {
      return CircularProgressIndicator();
    }
}

or with the handler:

class TestStateLessWidget1 extends Widget{
  @override
  Widget build(BuildContext context) {
    useAllReady(
      onReady: (context) =>
          Navigator.of(context).pushReplacement(MainPageRoute()));

  return CircularProgressIndicator();
  }
}

useIsReady<T>() can be used in the same way to react on the status of a single asynchronous singleton.

Pushing a new GetIt Scope

With usePushScope() 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 usePushScope({void Function(GetIt getIt) init, void Function() dispose});

StatefulWidgets

Instead of a StatefulWidget you have to use a StatefulHookWidget. Otherwise there are no differences to HookWidget.

Libraries

get_it_hooks