result_notifier 0.4.1 copy "result_notifier: ^0.4.1" to clipboard
result_notifier: ^0.4.1 copied to clipboard

Pragmatic state management for Flutter, based on familiar and platform-native concepts

Result Notifier #

Pragmatic and magic-free state management for Flutter - simply lagom.

result_notifier.png

Result Notifier is a simple and modest package for state management, based on familiar and platform-native concepts, rather than introducing new abstractions and mental models. In fact, the package really is little more than a few additions to ValueNotifier and ChangeNotifier (ok, perhaps slightly more than a few, but nothing crazy). As the name of this package alludes to, one of the most important additions is the concept of a Result type, which can represent either some Data, an Error or a Loading state.

style: lint

The essence of Result Notifier #

There are basically four concepts that make ResultNotifier different from ValueNotifier and ChangeNotifier:

  • It holds a Result value and provides methods for accessing and mutating the value. The value (result) can be in one of three different states:
    • Data: The result contains some concrete data.
    • Error: Represents an error, along with the previous data, if any.
    • Loading: Represents a loading/reloading state, along with the previous data, if any.
  • It's refreshable - by providing a synchronous or asynchronous "fetcher" function, you can specify how the data should be refreshed when needed (or stale).
  • It's cacheable - by setting a cacheDuration, you can specify how long the data should be cached before it's considered stale.
  • It's composable - you can easily combine the data of multiple ResultNotifiers (and even other ValueListenables) using CombineLatestNotifier or for instance apply an effect using EffectNotifier.

Getting Started #

Simply add the dependency and start writing some notifiers!

A simple start #

The simplest form of notifier only holds a value, much like a ValueNotifier. But with ResultNotifier, the value is wrapped in a Result type, which can represent either some data, an error, or a loading state.


final notifier = ResultNotifier<String>(data: 'Hello...');
notifier.toLoading(); // Convenience method to set the value to Loading, keeping the previous data.
print(notifier.data); // Prints 'Hello...'
notifier.value = Data('Hello Flutter!');
// or:
notifier.data = 'Hello Flutter!';

Fetching async data (e.g. from an API) #

Often you'll want to do something a little more elaborate, like fetching data from an API. In this case, you can FutureNotifier (or ResultNotifier.future), which is a ResultNotifier that uses a "fetcher" function that returns a Future.

final notifier = ResultNotifier<String>.future(
  (_) async {
    final response = await http.get(Uri.parse('https://www.boredapi.com/api/activity/'));
    final json = jsonDecode(response.body) as Map<String, dynamic>;
    return json['activity'] as String;
  },
);

Observing (listening, watching...) changes #

ValueListenableBuilder-based observation #

Since ResultNotifier implements ValueListenable, you can simply use ValueListenableBuilder to observe changes in a Widget. However, this package also provides a class called ResultBuilder, which makes it even easier to handle the different states of the result.

ResultBuilder<String>(
  notifier,
  onLoading: (context, data) => const CircularProgressIndicator(),
  onError: (context, error, stackTrace, data) => Text('Error: $error'),
  onData: (context, data) => Text(data),
),

"Hooks-style" inline observation #

If you're into the "hooks-style" of building widgets, you can use the watch (available via extension methods on ResultNotifier, ValueNotifier, ChangeNotifier etc.) method to observe changes on Listenables. While superficially similar to flutter_hooks, this implementation doesn't suffer from some of the complexities of that architectural style though, mainly because of a narrower scope and the fact that it relies on already existing ResultNotifier (or Listenable) instances.

Watcher(builder: (context) {
  final result = notifier.watch(context);
  return Text(result.when(
    data: (data) => data,
    loading: (data) => 'Loading - $data',
    error: (error, _, data) => 'Error - $data - $error',
  ));
});

This type of observation is very lightweight, especially if using the stateless Watcher or WatcherWidget (there is also a stateful implementation). Observation is facilitated through the use of the WatcherRef interface, which is implemented by the BuildContext (WatcherContext) passed to the builder in Watcher and build method in WatcherWidget etc. Although the watch method is provided by WatcherRef, it's often more convenient to use the extension method on ResultNotifier (and Listenable etc.).

Although the use of watch is pretty straightforward, there are a few things to keep in mind:

  • The watch method can only be invoked within the build method of a WatcherWidget, or a Widget that mixes in WatcherMixin or StatefulWatcherMixin.
  • Disposal is handled automatically whenever you stop calling watch in the build method, or when the Widget is removed from the tree (i.e. disposed).
  • Conditional logic in the build method works with watch, just remember the point above - i.e. the Widget will not be rebuilt for Listenables that weren't watched in the last call to build.

One additional benefit of this style of observation is that it makes it easier to watch for changes in multiple Listenables or ResultNotifiers, without resorting to a lot of nested ValueListenableBuilders or ResultBuilders. For example:

Watcher(builder: (context) {
  final result1 = notifier1.watch(context);
  final result2 = notifier2.watch(context);
  final combined = [result1, result2].combine((data) => '${data[0]} and ${data[1]}');
  return Text(combined.when(
    data: (data) => data,
    loading: (data) => 'Loading - $data',
    error: (error, _, data) => 'Error - $data - $error',
  ));
});

Going deeper #

Cache expiration #

When using remote data, it's common to cache the data for some period of time and refresh it when it's stale. This can be accomplished by setting the cacheDuration to an appropriate value. But remember that caching is one of the roots of all evil, so don't enable it unless you're sure you really need it, and only when you're finished implementing the core functionality of your app.

Effects #

You can also use effects (see EffectNotifier), to build more complex chains of notifiers:

final notifier = ResultNotifier<String>(data: 'Þetta er frábært!');
final effect = notifier.effect((_, input) => input.toUpperCase());
effect.onData(print);
notifier.data = 'Þetta er frábært!'; // Prints: "ÞETTA ER FRÁBÆRT!"

See also the effects example for a more complete demonstration of these concepts.

Inline effects #

One advantage of using the hooks-style approach to observing changes is that you can easily do things like combining the results of multiple notifiers inline (without having to create a new notifier). This kind of effects is supported both through extension methods on Iterable as well on a set of Record definitions (see for instance ResultTuple).

Widget build(BuildContext context) {
  final activity = activityRepository.watch();
  final counter = experiencePoints.watch();

  /// Here we combine the data from the two notifiers, using the `combine` (or `combineData`) extension method defined
  /// in `ResultTuple` (there is also `ResultTriple` etc). 
  final resultFromRecord = (activity, counter).combine((a, b) => '$a - total experience points: $b');

  /// You can also use similar functionality exposed as extension methods on Iterable. 
  final resultFromList = [activity, counter].combine((data) => '${data[0]} - count: ${data[1]}');

  /// Or if you just want the data: 
  final resultData = (activity, counter).combineData((a, b) => '$a - count: $b');
}

ResultStore - a key-based store for notifiers #

To create an auto-disposable store of notifiers, each associated with a key - see ResultStore. ResultStore can be useful to for instance support pagination, where each page is represented by a key and a unique notifier.

ResourceProvider- Providing notifiers to a subtree of widgets #

A ResourceProvider can be used to handle the lifecycle (i.e. creation and disposal) of a notifier (or arbitrary resource), and provide it to a subtree of widgets.

Using regular ValueNotifiers / ValueListenabless #

This package adds the extension method toResultListenable to ValueListenable, which transform it into a ResultListenable that can be used in for instance effects, such as CombineLatestNotifier.

Examples #

You can find a more complete example here, and additional examples in the examples directory in the repository.

More examples #

For an even more real-worldish example, check out this fork of Andrea Bizzotto's TMDB Movie App, which uses Result Notifier instead of Riverpod.

When to use it - and when not to #

Result Notifier is probably most suitable for cases when your state management needs are in the ballpark of "low to moderate", or as we say in Sweden: lagom. If you need more advanced state management, you might want to reach for something more elaborate. But then again, maybe not - as in most cases, this very much depends on your general application architecture and modularization. And remember - excessive use of state management may also be a sign of a flawed architecture or over-engineering.

Things left to do... #

The usual stuff, more tests and more docs 😅.

8
likes
0
pub points
24%
popularity

Publisher

verified publisherleafnode.se

Pragmatic state management for Flutter, based on familiar and platform-native concepts

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter, meta

More

Packages that depend on result_notifier