DEV Community

Solomon Nsumei
Solomon Nsumei

Posted on • Edited on

7

Easy State Management with ValueNotifiers and Inherited Widgets in Flutter

Introduction

The flutter ecosystem has grown rapidly in a few years, seeing it grow from just a mobile development framework to support both web and desktop development.

Building mobile apps using flutter, there are tons of state management libraries or packages to choose from which can be a beginner's nightmare.

In this tutorials we will be managing our own app state with only InheritedWidgets, ValueNotifiers and ValueListenableBuilders.

What we will be building

We will be implementing a favorites list from a list of fruits.

At the end of this tutorial you should

  • have a good understanding of InheritedWidgets, ValueNotifiers and ValueListenableBuilder and how they can be used to manage state in your flutter applications.
  • be able to use them to manage you own application state without needing StatefulWidgets or external state management libraries or packages.

So let's dive right in...

Creating your flutter project

  • Create a new flutter project. I will not go into details of creating a flutter project since it is pretty straightforward.

Here is a look of our boilerplate after cleanup

import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
debugShowCheckedModeBanner: false,
home: MyWidget()
);
}
}
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Wish List'),
),
body: Center(
child: Text('Hello, World!'),
),
);
}
}
view raw main1.dart hosted with ❤ by GitHub

Now lets take a look at the ValueNotifier with the ValueListenableBuilder and how they can help us manage state in our flutter apps.

Implementing a Simple ValueNotifier

ValueNotifier class is a subclass of ChangeNotifier which manages the state of a single value and notifies listeners when the value changes. It works with the ValueListenableBuilder widget which automatically listens to changes on the valueListenable and rebuilds enclosed widgets accordingly.

import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
debugShowCheckedModeBanner: false,
home: MyWidget()
);
}
}
class MyWidget extends StatelessWidget {
final ValueNotifier<int> counter = ValueNotifier<int>(0);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Wish List'),
),
body: Center(
child: Text('${counter.value}'),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
counter.value += 1;
print(counter.value);
}
)
);
}
}
view raw main2.dart hosted with ❤ by GitHub

If you run the code above you discover that the counter value changes but the app does not display the changes. This is where the ValueListenableBuilder comes in.

ValueListenableBuilder is a widget whose content is always synced with a ValueListenable. The ValueListenableBuilder requires two arguments, a ValueListenable, which is a ValueNotifier instance, and a builder, which is a callback that rebuilds the widget based on the changes to the value being listened to. Implementing that in our code will look like

import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
debugShowCheckedModeBanner: false,
home: MyWidget()
);
}
}
class MyWidget extends StatelessWidget {
final ValueNotifier<int> counter = ValueNotifier<int>(0);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Wish List'),
),
body: Center(
child: ValueListenableBuilder(
valueListenable: counter,
builder: (context, value, _) {
return Text('${counter.value}');
}
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
counter.value += 1;
print(counter.value);
}
)
);
}
}
view raw main3.dart hosted with ❤ by GitHub

Now that you know what a valueNotifier and valueListenableBuilder are and how to use them, let's get started on our project.

Implementing a Custom ValueNotifier

The above example showed a simple use case of the ValueNotifier, for more complex and custom implementation, one will need to extend the ValueNotifier class.
Now, lets create our custom ValueNotifier class

// Add the value notifier class with constructor
class FavoriteNotifier extends ValueNotifier<List<String>> {
FavoriteNotifier(List<String> value) : super(value);
}

Because we are interested in a list of favorites, we made our FavoriteNotifier class inherit from ValueNotifier> and implemented the constructor.

Next we add methods to add and remove from the favorites list.

// Add the value notifier class with constructor
class FavoriteNotifier extends ValueNotifier<List<String>> {
FavoriteNotifier(List<String> value) : super(value);
// Add or remove favorites depending on their status
void toggleFavorite(String item) {
if (!value.contains(item)) {
value.add(item);
} else {
value.remove(item);
}
notifyListeners();
}
// Clear the list
void clearFavorites() {
value.clear();
notifyListeners();
}
}

Notice the use of the notifyListeners method in each method we implemented. Without it the listeners will not get notified when the value changes or is updated.

There's a problem, how do we pass our custom valueNotifier down our widget tree to maintain the same instance and not multiple instances? Enter InheritedWidgets, but before then lets update our UI code.

class MyWidget extends StatelessWidget {
final List<String> fruits = ['Mango', 'Pear', 'Cashew',
'Grape', 'Guava', 'Coconut', 'Orange'];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Favorite Fruits'),
actions: [
Icon(Icons.favorite),
FavoriteCount(),
]
),
body: ListView.separated(
itemCount: fruits.length,
separatorBuilder: (context, _) => Divider(),
itemBuilder: (context, index) {
final fruit = fruits[index];
return ListTile(
leading: Icon(Icons.local_florist),
title: Text(fruit),
onTap: () {},
);
}
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.clear),
onPressed: () {
}
)
);
}
}
// Favorites count widget
class FavoriteCount extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Text("0"),
),
);
}
}
view raw mywidget.dart hosted with ❤ by GitHub

We have completed our UI and included a FavoriteCount widget to show how many items are in our favorites list, as well as a list of fruits to our app page.

Running the app now will display a list but clicking on any of the items will do nothing.

Wrapping it all with the Inherited Widget

InheritedWidgets allow us to receive data from widget ancestors without having to pass it down a widget tree.

We can create our own Inherited widget by extending the InheritedWidget class, adding the data we want to share, and creating an of method to call in our child widgets whenever we want to access the data.

Let's implement that in our code.

// Add our favorite State
class FavoriteState extends InheritedWidget{
final FavoriteNotifier favoriteNotifier;
FavoriteState({required this.favoriteNotifier, required Widget child})
: super(child: child);
static FavoriteState of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType()!;
}
@override
bool updateShouldNotify(FavoriteState oldWidget) {
return oldWidget.favoriteNotifier.value != favoriteNotifier.value;
}
}

Now lets wrap our MaterialsApp with the FavoriteState so the data it holds will be available in the child widgets also.

class MyApp extends StatelessWidget {
final List<String> favoriteList = [];
@override
Widget build(BuildContext context) {
return FavoriteState(
favoriteNotifier: FavoriteNotifier(favoriteList),
child: MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
debugShowCheckedModeBanner: false,
home: MyWidget()
),
);
}
}
view raw myapp.dart hosted with ❤ by GitHub

Finally we will be wrapping the children we need to receive updates from our FavoriteNotifier with ValueListenableBuilder and implement calls to update the UI accordingly.

class MyWidget extends StatelessWidget {
final List<String> fruits = ['Mango', 'Pear', 'Cashew',
'Grape', 'Guava', 'Coconut', 'Orange'];
@override
Widget build(BuildContext context) {
final favoriteNotifier = FavoriteState.of(context).favoriteNotifier;
return Scaffold(
appBar: AppBar(
title: Text('Favorite Fruits'),
actions: [
Icon(Icons.favorite),
FavoriteCount(),
]
),
body: ListView.separated(
itemCount: fruits.length,
separatorBuilder: (context, _) => Divider(),
itemBuilder: (context, index) {
final fruit = fruits[index];
return ValueListenableBuilder(
valueListenable: favoriteNotifier,
builder: (context, List<String> value, _) {
return ListTile(
leading: Icon(Icons.local_florist, color: value.contains(fruit)
? Colors.orange
: Colors.grey[600]),
title: Text(fruit),
onTap: () {
favoriteNotifier.toggleFavorite(fruit);
},
);
}
);
}
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.clear),
onPressed: () {
favoriteNotifier.clearFavorites();
}
),
);
}
}
view raw mywidget2.dart hosted with ❤ by GitHub

Running the app now will show the fruits in the favorites list icons as gold, while the others as grey, also the FAB button clears the list.

Now let's update the FavoriteCount widget to display the count of fruits in our favorite collection.

class FavoriteCount extends StatelessWidget {
@override
Widget build(BuildContext context) {
final favoriteNotifier = FavoriteState.of(context).favoriteNotifier;
return Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: ValueListenableBuilder(
valueListenable: favoriteNotifier,
builder: (context, List<String> value, _) {
return Text("${value.length}");
},
),
),
);
}
}

finally, if you run the app now, you notice that both the icon color and the favorite count is updated when we tap on any of the fruit.

Here is the full code:

import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final List<String> favoriteList = [];
@override
Widget build(BuildContext context) {
return FavoriteState(
favoriteNotifier: FavoriteNotifier(favoriteList),
child: MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
debugShowCheckedModeBanner: false,
home: MyWidget()
),
);
}
}
class MyWidget extends StatelessWidget {
final List<String> fruits = ['Mango', 'Pear', 'Cashew',
'Grape', 'Guava', 'Coconut', 'Orange'];
@override
Widget build(BuildContext context) {
final favoriteNotifier = FavoriteState.of(context).favoriteNotifier;
return Scaffold(
appBar: AppBar(
title: Text('Favorite Fruits'),
actions: [
Icon(Icons.favorite),
FavoriteCount(),
]
),
body: ListView.separated(
itemCount: fruits.length,
separatorBuilder: (context, _) => Divider(),
itemBuilder: (context, index) {
final fruit = fruits[index];
return ValueListenableBuilder(
valueListenable: favoriteNotifier,
builder: (context, List<String> value, _) {
return ListTile(
leading: Icon(Icons.local_florist, color: value.contains(fruit)
? Colors.orange
: Colors.grey[600]),
title: Text(fruit),
onTap: () {
favoriteNotifier.toggleFavorite(fruit);
},
);
}
);
}
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.clear),
onPressed: () {
favoriteNotifier.clearFavorites();
}
),
);
}
}
class FavoriteCount extends StatelessWidget {
@override
Widget build(BuildContext context) {
final favoriteNotifier = FavoriteState.of(context).favoriteNotifier;
return Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: ValueListenableBuilder(
valueListenable: favoriteNotifier,
builder: (context, List<String> value, _) {
return Text("${value.length}");
},
),
),
);
}
}
// Add the value notifier class with constructor
class FavoriteNotifier extends ValueNotifier<List<String>> {
FavoriteNotifier(List<String> value) : super(value);
// Add or remove favorites depending on their status
void toggleFavorite(String item) {
if (!value.contains(item)) {
value.add(item);
} else {
value.remove(item);
}
notifyListeners();
}
// Clear the list
void clearFavorites() {
value.clear();
notifyListeners();
}
}
// Add our favorite State
class FavoriteState extends InheritedWidget{
final FavoriteNotifier favoriteNotifier;
FavoriteState({required this.favoriteNotifier, required Widget child})
: super(child: child);
static FavoriteState of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType()!;
}
@override
bool updateShouldNotify(FavoriteState oldWidget) {
return oldWidget.favoriteNotifier.value != favoriteNotifier.value;
}
}
view raw main.dart hosted with ❤ by GitHub

You can run the final code on dartpad by clicking this link
Code Editor

Conclusion

We simply constructed our own application state with flutter's built-in widgets, without the need for external libraries or rebuilding the entire widget tree with setState in a StatefulWidget.

You can explore more on your own and post your comments. If you have any question, please post it on the comments section and I'll be glad to answer. Happy coding!

Sentry blog image

The Visual Studio App Center’s retiring

But sadly….you’re not. See how to make the switch to Sentry for all your crash reporting needs.

Read more

Top comments (2)

Collapse
 
dartmitai profile image
Dmitriy Bezencev

I didn't understand anything from the article, why do we need valuenotifier if we could do it all on inherited

Collapse
 
solnsumei profile image
Solomon Nsumei

By inherited, do you mean inheritedWidget or other subclasses of inheritedWidget? Without the value notifier in this context, there is no way your widgets will be notified to update. There are other ways to implement with InheritedNotifier too. You can try out other implementations, create an article, we can learn from it.

Billboard image

Create up to 10 Postgres Databases on Neon's free plan.

If you're starting a new project, Neon has got your databases covered. No credit cards. No trials. No getting in your way.

Try Neon for Free →

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay