Kinda Code
Home/Flutter/Flutter: ValueListenableBuilder Example

Flutter: ValueListenableBuilder Example

Last updated: June 05, 2023

This article walks you through a complete example of using ValueListenableBuilder, ValueNotifier, and ValueListenable to manage state in a multi-page (or multi-screen) Flutter application.

A Quick Note

The ValueListenableBuilder widget uses a builder callback to rebuild whenever a ValueListenable object triggers its notifications, providing the builder with the value of the object:

ValueListenableBuilder({
  Key? key, 
  required ValueListenable<T> valueListenable, 
  required ValueWidgetBuilder<T> builder, 
  Widget? child
})

A ValueListenable is an interface that exposes a value and can be implemented by a ValueNotifier. The value property of the ValueNotifier is the current value stored in the notifier.

These words may be confusing. For more clarity, check the complete example below.

App Preview

The app we are going to build is a task app. It contains 2 pages (screens): HomePage and ArchivePage:

  • HomePage displays uncompleted tasks. This one also has a floating action button that can be used to add a new task. Next to each task, there will be a checkbox used to mark the task as completed. You can press the “View Completed Button” to navigate to the ArchivePage.
  • ArchivePage displays completed tasks. Next to each task, there will be an icon button used to bring that task to the “uncompleted” state.

A demo is worth more than a thousand words:

The Code

Create a new Flutter project and add 2 new files: home_screen.dart and archive_screen.dart. Here’s the directory structure:

.
├── archive_page.dart
├── home_page.dart
└── main.dart

1. Remove all of the default code in main.dart and add the following:

// main.dart
import 'package:flutter/material.dart';

import './home_page.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        // Remove the debug banner
        debugShowCheckedModeBanner: false,
        title: 'Kindacode.com',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: const HomePage());
  }
}

2. The final code for HomePage:

// home_page.dart
import 'package:flutter/material.dart';

import './archive_page.dart';

// This screen only displays un-completed tasks
class HomePage extends StatelessWidget {
  // Using "static" so that we can easily access it from other screens
  static final ValueNotifier<List<Map<String, dynamic>>> tasksNotifier =
      ValueNotifier([]);

  const HomePage({Key? key}) : super(key: key);

  // This function will be triggered when the floating button is pressed
  // Add new task
  void _addNewTask() {
    final List<Map<String, dynamic>> tasks = [...tasksNotifier.value];
    tasks.add({
      "id": DateTime.now().toString(),
      "title": "Task ${DateTime.now()}",
      "isDone": false
    });
    tasksNotifier.value = tasks;
  }

  // This function will be triggered when the checkbox next to a task is tapped
  // Finish a task
  // Change a task from "uncompleted" to "completed"
  void _finishTask(String updatedTaskId) {
    final List<Map<String, dynamic>> tasks = [...tasksNotifier.value];

    final int index = tasks.indexWhere((task) => task['id'] == updatedTaskId);
    tasks[index]['isDone'] = true;

    tasksNotifier.value = tasks;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Kindacode.com'),
      ),
      body: ValueListenableBuilder<List<Map<String, dynamic>>>(
        valueListenable: HomePage.tasksNotifier,
        builder: (_, tasks, __) {
          final uncompletedTasks =
              tasks.where((task) => task['isDone'] == false).toList();

          return Padding(
            padding: const EdgeInsets.all(20),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                ElevatedButton(
                    onPressed: () {
                      Navigator.of(context).push(MaterialPageRoute(
                        builder: (BuildContext context) => const ArchivePage(),
                      ));
                    },
                    child: const Text('View Completed Tasks')),
                const SizedBox(
                  height: 20,
                ),
                Text(
                  'You have ${uncompletedTasks.length} uncompleted tasks',
                  style: const TextStyle(fontSize: 18),
                ),
                const SizedBox(
                  height: 10,
                ),
                Expanded(
                  child: ListView.builder(
                    itemCount: uncompletedTasks.length,
                    itemBuilder: (_, index) => Card(
                        margin: const EdgeInsets.symmetric(vertical: 15),
                        elevation: 5,
                        color: Colors.amberAccent,
                        child: ListTile(
                          title: Text(uncompletedTasks[index]['title']),
                          trailing: IconButton(
                            icon: const Icon(Icons.check_box_outline_blank),
                            onPressed: () =>
                                _finishTask(uncompletedTasks[index]['id']),
                          ),
                        )),
                  ),
                ),
              ],
            ),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _addNewTask,
        child: const Icon(Icons.add),
      ),
    );
  }
}

3. The final code for ArchivePage:

// archive_page.dart
import 'package:flutter/material.dart';

import './home_page.dart';

// This screen only display completed tasks
class ArchivePage extends StatelessWidget {
  const ArchivePage({Key? key}) : super(key: key);

  // Change a task from "completed" to "uncompleted"
  void _uncheckTask(String updatedTaskId) {
    final List<Map<String, dynamic>> tasks = [...HomePage.tasksNotifier.value];

    final int index = tasks.indexWhere((task) => task['id'] == updatedTaskId);
    tasks[index]['isDone'] = false;

    HomePage.tasksNotifier.value = tasks;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Archive Screen'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(20),
        child: ValueListenableBuilder<List<Map<String, dynamic>>>(
          valueListenable: HomePage.tasksNotifier,
          builder: (_, tasks, __) {
            final completedTasks =
                tasks.where((task) => task['isDone'] == true).toList();
            return ListView.builder(
              itemCount: completedTasks.length,
              itemBuilder: (_, index) => Card(
                  margin: const EdgeInsets.symmetric(vertical: 15),
                  elevation: 5,
                  color: Colors.pinkAccent,
                  child: ListTile(
                    title: Text(completedTasks[index]['title']),
                    trailing: IconButton(
                      icon: const Icon(Icons.check_box),
                      onPressed: () =>
                          _uncheckTask(completedTasks[index]['id']),
                    ),
                  )),
            );
          },
        ),
      ),
    );
  }
}

Now run your project and play around with it to see what will happen.

Conclusion

We’ve gone over an end-to-end example of implementing the ValueListenableBuilder widget in Flutter. In the future, if you have more complex use cases and want to have more available solutions to choose from, you can browse the following articles:

You can also check out our Flutter category page, or Dart category page for the latest tutorials and examples.