Widgets that make it easy to use ValueNotifier and helps to implement state management patterns in Flutter. Heavily inspired by package:flutter_bloc.
Lets take a look at how to use ValueNotifierProvider
to provide a CounterNotifier
to a CounterPage
and react to value changes with ValueNotifierBuilder
.
class CounterNotifier extends ValueNotifier<int> {
CounterNotifier() : super(0);
void increment() => value = value + 1;
void decrement() => value = value - 1;
}
void main() => runApp(CounterApp());
class CounterApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: ValueNotifierProvider(
create: (_) => CounterNotifier(),
child: CounterPage(),
),
);
}
}
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Counter')),
body: ValueNotifierBuilder<CounterNotifier, int>(
builder: (_, count) => Center(child: Text('$count')),
),
floatingActionButton: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () => context.read<CounterNotifier>().increment(),
),
const SizedBox(height: 4),
FloatingActionButton(
child: const Icon(Icons.remove),
onPressed: () => context.read<CounterNotifier>().decrement(),
),
],
),
);
}
}
At this point we have successfully separated our presentational layer from our business logic layer. Notice that the CounterPage
widget knows nothing about what happens when a user taps the buttons. The widget simply notifies the CounterNotifier
that the user has pressed either the increment or decrement button.
ValueNotifierProvider is a Flutter widget which provides a notifier to its children via ValueNotifierProvider.of<T>(context)
. It is used as a dependency injection (DI) widget so that a single instance of a notifier can be provided to multiple widgets within a subtree.
In most cases, ValueNotifierProvider
should be used to create new notifiers which will be made available to the rest of the subtree. In this case, since ValueNotifierProvider
is responsible for creating the notifier, it will automatically handle closing it.
ValueNotifierProvider(
create: (_) => NotifierA(),
child: ChildA(),
);
By default, ValueNotifierProvider will create the notifier lazily, meaning create
will get executed when the notifier is looked up via ValueNotifierProvider.of<NotifierA>(context)
.
To override this behavior and force create
to be run immediately, lazy
can be set to false
.
ValueNotifierProvider(
lazy: false,
create: (_) => NotifierA(),
child: ChildA(),
);
In some cases, ValueNotifierProvider
can be used to provide an existing notifier to a new portion of the widget tree. This will be most commonly used when an existing notifier
needs to be made available to a new route. In this case, ValueNotifierProvider
will not automatically close the notifier since it did not create it.
ValueNotifierProvider.value(
value: ValueNotifierProvider.of<NotifierA>(context),
child: ScreenA(),
);
then from either ChildA
, or ScreenA
we can retrieve NotifierA
with:
// with extensions
context.read<NotifierA>();
// without extensions
ValueNotifierProvider.of<NotifierA>(context);
The above snippets result in a one time lookup and the widget will not be notified of changes. To retrieve the instance and subscribe to subsequent value changes use:
// with extensions
context.watch<NotifierA>();
// without extensions
ValueNotifierProvider.of<NotifierA>(context, listen: true);
In addition, context.select
can be used to retrieve part of a value and react to changes only when the selected part changes.
final isPositive = context.select<CounterNotifier>((notifier) => notifier.value >= 0);
The snippet above will only rebuild if the value of the CounterNotifier
changes from positive to negative or vice versa and is functionally identical to using a ValueNotifierSelector
.
MultiValueNotifierProvider is a Flutter widget that merges multiple ValueNotifierProvider
widgets into one.
MultiValueNotifierProvider
improves the readability and eliminates the need to nest multiple ValueNotifierProviders
.
By using MultiValueNotifierProvider
we can go from:
ValueNotifierProvider<NotifierA>(
create: (_) => NotifierA(),
child: ValueNotifierProvider<NotifierB>(
create: (_) => NotifierB(),
child: ValueNotifierProvider<NotifierC>(
create: (_) => NotifierC(),
child: ChildA(),
)
)
)
to:
MultiValueNotifierProvider(
providers: [
ValueNotifierProvider<NotifierA>(
create: (_) => NotifierA(),
),
ValueNotifierProvider<NotifierB>(
create: (_) => NotifierB(),
),
ValueNotifierProvider<NotifierC>(
create: (_) => NotifierC(),
),
],
child: ChildA(),
)
ValueNotifierBuilder is a Flutter widget which requires a ValueNotifier
and a builder
function. ValueNotifierBuilder
handles building the widget in response to new values. ValueNotifierBuilder
is very similar to ValueListenableBuilder
but has a more simple API to reduce the amount of boilerplate code needed. The builder
function will potentially be called many times and should be a pure function that returns a widget in response to the value.
See ValueNotifierListener
if you want to "do" anything in response to value changes such as navigation, showing a dialog, etc...
If the notifier
parameter is omitted, ValueNotifierBuilder
will automatically perform a lookup using ValueNotifierProvider
and the current BuildContext
.
ValueNotifierBuilder<NotifierA, NotifierAState>(
builder: (_, value) {
// return widget here based on NotifierA's value
}
)
Only specify the notifier if you wish to provide a ValueNotifier that will be scoped to a single widget and isn't accessible via a parent ValueNotifierProvider
and the current BuildContext
.
ValueNotifierBuilder<NotifierA, NotifierAState>(
notifier: notifier, // provide the local ValueNotifier instance
builder: (_, value) {
// return widget here based on NotifierA's value
}
)
For fine-grained control over when the builder
function is called an optional buildWhen
can be provided. buildWhen
takes the previous ValueNotifier value and current ValueNotifier value and returns a boolean. If buildWhen
returns true, builder
will be called with value
and the widget will rebuild. If buildWhen
returns false, builder
will not be called with value
and no rebuild will occur.
ValueNotifierBuilder<NotifierA, NotifierAState>(
buildWhen: (previousValue, value) {
// return true/false to determine whether or not
// to rebuild the widget with value
},
builder: (_, value) {
// return widget here based on NotifierA's value
}
)
ValueNotifierSelector is a Flutter widget which is analogous to ValueNotifierBuilder
but allows developers to filter updates by selecting a new value based on the current notifier value. Unnecessary builds are prevented if the selected value does not change. The selected value must be immutable in order for ValueNotifierSelector
to accurately determine whether builder
should be called again.
If the notifier
parameter is omitted, ValueNotifierSelector
will automatically perform a lookup using ValueNotifierProvider
and the current BuildContext
.
ValueNotifierSelector<NotifierA, NotifierAState, SelectedState>(
selector: (value) {
// return selected value based on the provided value.
},
builder: (_, value) {
// return widget here based on the selected value.
},
)
ValueNotifierListener is a Flutter widget which takes a ValueNotifierWidgetListener
and an optional notifier
and invokes the listener
in response to value changes in the notifier. It should be used for functionality that needs to occur once per value change such as navigation, showing a SnackBar
, showing a Dialog
, etc...
listener
is only called once for each value change (NOT including the initial value) unlike builder
in ValueNotifierBuilder
and is a void
function.
If the notifier parameter is omitted, ValueNotifierListener
will automatically perform a lookup using ValueNotifierProvider
and the current BuildContext
.
ValueNotifierListener<NotifierA, NotifierAState>(
listener: (context, value) {
// do stuff here based on NotifierA's value
},
child: Container(),
)
Only specify the notifier if you wish to provide a notifier that is otherwise not accessible via ValueNotifierProvider
and the current BuildContext
.
ValueNotifierListener<NotifierA, NotifierAState>(
notifier: notifier,
listener: (context, value) {
// do stuff here based on NotifierA's value
}
)
For fine-grained control over when the listener
function is called an optional listenWhen
can be provided. listenWhen
takes the previous notifier value and current notifier value and returns a boolean. If listenWhen
returns true, listener
will be called with value
. If listenWhen
returns false, listener
will not be called with value
.
ValueNotifierListener<NotifierA, NotifierAState>(
listenWhen: (previousValue, value) {
// return true/false to determine whether or not
// to call listener with value
},
listener: (context, value) {
// do stuff here based on NotifierA's value
},
child: Container(),
)
MultiValueNotifierListener is a Flutter widget that merges multiple ValueNotifierListener
widgets into one.
MultiValueNotifierListener
improves the readability and eliminates the need to nest multiple ValueNotifierListeners
.
By using MultiValueNotifierListener
we can go from:
ValueNotifierListener<NotifierA, NotifierAState>(
listener: (context, value) {},
child: ValueNotifierListener<NotifierB, NotifierBState>(
listener: (context, value) {},
child: ValueNotifierListener<NotifierC, NotifierCState>(
listener: (context, value) {},
child: ChildA(),
),
),
)
to:
MultiValueNotifierListener(
listeners: [
ValueNotifierListener<NotifierA, NotifierAState>(
listener: (context, value) {},
),
ValueNotifierListener<NotifierB, NotifierBState>(
listener: (context, value) {},
),
ValueNotifierListener<NotifierC, NotifierCState>(
listener: (context, value) {},
),
],
child: ChildA(),
)
ValueNotifierConsumer exposes a builder
and listener
in order react to new values. ValueNotifierConsumer
is analogous to a nested ValueNotifierListener
and ValueNotifierBuilder
but reduces the amount of boilerplate needed. ValueNotifierConsumer
should only be used when it is necessary to both rebuild UI and execute other reactions to value changes in the notifier
. ValueNotifierConsumer
takes a required ValueNotifierWidgetBuilder
and ValueNotifierWidgetListener
and an optional notifier
, ValueNotifierBuilderCondition
, and ValueNotifierListenerCondition
.
If the notifier
parameter is omitted, ValueNotifierConsumer
will automatically perform a lookup using
ValueNotifierProvider
and the current BuildContext
.
ValueNotifierConsumer<NotifierA, NotifierAState>(
listener: (context, value) {
// do stuff here based on NotifierA's value
},
builder: (_, value) {
// return widget here based on NotifierA's value
}
)
An optional listenWhen
and buildWhen
can be implemented for more granular control over when listener
and builder
are called. The listenWhen
and buildWhen
will be invoked on each notifier
value
change. They each take the previous value
and current value
and must return a bool
which determines whether or not the builder
and/or listener
function will be invoked. The previous value
will be initialized to the value
of the notifier
when the ValueNotifierConsumer
is initialized. listenWhen
and buildWhen
are optional and if they aren't implemented, they will default to true
.
ValueNotifierConsumer<NotifierA, NotifierAState>(
listenWhen: (previous, current) {
// return true/false to determine whether or not
// to invoke listener with value
},
listener: (context, value) {
// do stuff here based on NotifierA's value
},
buildWhen: (previous, current) {
// return true/false to determine whether or not
// to rebuild the widget with value
},
builder: (_, value) {
// return widget here based on NotifierA's value
}
)
DependencyProvider is a Flutter widget which provides a dependency to its children via DependencyProvider.of<T>(context)
. It is used as a dependency injection (DI) widget so that a single instance of a dependency can be provided to multiple widgets within a subtree. ValueNotifierProvider
should be used to provide notifier whereas DependencyProvider
should only be used for dependencies.
DependencyProvider(
create: (_) => DependencyA(),
child: ChildA(),
);
then from ChildA
we can retrieve the Dependency
instance with:
// with extensions
context.read<DependencyA>();
// without extensions
DependencyProvider.of<DependencyA>(context)
MultiDependencyProvider is a Flutter widget that merges multiple DependencyProvider
widgets into one.
MultiDependencyProvider
improves the readability and eliminates the need to nest multiple DependencyProvider
.
By using MultiDependencyProvider
we can go from:
DependencyProvider<DependencyA>(
create: (_) => DependencyA(),
child: DependencyProvider<DependencyB>(
create: (_) => DependencyB(),
child: DependencyProvider<DependencyC>(
create: (_) => DependencyC(),
child: ChildA(),
)
)
)
to:
MultiDependencyProvider(
providers: [
DependencyProvider<DependencyA>(
create: (_) => DependencyA(),
),
DependencyProvider<DependencyB>(
create: (_) => DependencyB(),
),
DependencyProvider<DependencyC>(
create: (_) => DependencyC(),
),
],
child: ChildA(),
)
- Dart 2: >=2.19.0
- Flutter 3: >=3.0.0
Thanks to @felangel, bloc library contributors and Icons8 by Electricity icon.