Skip to content

Instantly share code, notes, and snippets.

@Tomic-Riedel
Created April 15, 2023 18:04
Show Gist options
  • Save Tomic-Riedel/d1595cb4b3e49a5128c87454c1e484d3 to your computer and use it in GitHub Desktop.
Save Tomic-Riedel/d1595cb4b3e49a5128c87454c1e484d3 to your computer and use it in GitHub Desktop.

Flutter Provider explained - The complete crash course

Introduction

Provider is an excellent package for state management in your Flutter apps. It can sometimes be a bit confusing for beginners, but don't worry! In this article, I will guide you through the process of understanding and using Provider in your projects. Let's dive in!

Note: You can view the whole source code used in this article here.

Why should I learn Provider?

You might be wondering why you should learn Provider when there are other state management solutions available. The reason is simple: Provider is easy to learn, efficient, and widely adopted by the Flutter community. It helps you manage the state of your app in a clean and organized way, which in turn makes your code more maintainable and scalable.

Installation

Before we begin, let's add the Provider package to our Flutter app. Run the command flutter pub add provider. This will add Provider to your pubspec.yaml file.

Next, let's import the Provider package by adding the following line at the top of your main.dart file:

import 'package:provider/provider.dart';

How Provider works

Provider works by exposing an object (usually a model or a view model) to the widget tree. This object can be accessed by any descendant widget that listens to it. When the object changes, the listening widgets will rebuild, allowing your UI to update accordingly.

To use Provider, you need to wrap your widget tree with a ChangeNotifierProvider. This will ensure that the object is accessible to all its descendants. Then, you can create a class that extends ChangeNotifier and notify listeners when the state changes.

A simple counter example

Let's start with a basic example: a counter app that increases or decreases a number using Provider.

First, create a class called Counter that extends ChangeNotifier:

class Counter extends ChangeNotifier {
  int _count = 0;

  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }

  void decrement() {
    _count--;
    notifyListeners();
  }
}

In this class, we have a private variable _count that stores the current count. We also have two methods, increment and decrement, which modify the count and notify listeners.

Now, let's create the UI:

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => Counter(),
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Provider Example')),
        body: Center(child: CountDisplay()),
        floatingActionButton: Column(
          mainAxisAlignment: MainAxisAlignment.end,
          children: [
            FloatingActionButton(
              onPressed: () {
                Provider.of<Counter>(context, listen: false).increment();
              },
              child: Icon(Icons.add),
            ),
            SizedBox(height: 10),
            FloatingActionButton(
              onPressed: () {
                Provider.of<Counter>(context, listen: false).decrement();
              },
              child: Icon(Icons.remove),
            ),
          ],
        ),
      ),
    );
  }
}

class CountDisplay extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counter = Provider.of<Counter>(context);
    return Text('Count: ${counter.count}', style: TextStyle(fontSize: 24));
  }
}

In our main function, we wrap the MyApp widget with a ChangeNotifierProvider, which provides an instance of the Counter class to the widget tree. Inside the MyApp widget, we have a Scaffold with a CountDisplay widget at the center and two FloatingActionButton widgets for incrementing and decrementing the count.

In the CountDisplay widget, we use Provider.of<Counter>(context) to access the Counter instance. This will ensure that the widget rebuilds whenever the count changes. We then display the current count using a Text widget.

In the two FloatingActionButton widgets, we use the increment and decrement methods from the Counter class to modify the count. Notice that we pass listen: false when accessing the Counter instance in these buttons. This is because we don't want the buttons to rebuild when the count changes.

A more complex example: To-Do List app

Now that you understand the basics of Provider, let's move on to a more complex example: a To-Do List app. In this app, we'll have a list of tasks that can be marked as complete or incomplete.

First, create a Task class to represent a single task:

class Task {
  final String title;
  bool isComplete;

  Task({required this.title, this.isComplete = false});
}

Next, create a TaskList class that extends ChangeNotifier:

class TaskList extends ChangeNotifier {
  List<Task> _tasks = [];

  List<Task> get tasks => _tasks;

  void addTask(Task task) {
    _tasks.add(task);
    notifyListeners();
  }

  void toggleTask(Task task) {
    task.isComplete = !task.isComplete;
    notifyListeners();
  }

  void removeTask(Task task) {
    _tasks.remove(task);
    notifyListeners();
  }
}

This class maintains a list of tasks and provides methods to add, toggle, and remove tasks. Don't forget to call notifyListeners() whenever the list changes.

Now, let's create the UI for our To-Do List app:

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => TaskList(),
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('To-Do List')),
        body: TaskListView(),
        floatingActionButton: FloatingActionButton(
          onPressed: () async {
            final taskTitle = await showDialog<String>(
              context: context,
              builder: (context) => AddTaskDialog(),
            );
            if (taskTitle != null) {
              Provider.of<TaskList>(context, listen: false).addTask(Task(title: taskTitle));
            }
          },
          child: Icon(Icons.add),
        ),
      ),
    );
  }
}

class TaskListView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final taskList = Provider.of<TaskList>(context);
    return ListView.builder(
      itemCount: taskList.tasks.length,
      itemBuilder: (context, index) {
        final task = taskList.tasks[index];
        return ListTile(
          title: Text(task.title),
          leading: Checkbox(
            value: task.isComplete,
            onChanged: (newValue) {
              taskList.toggleTask(task);
            },
          ),
          trailing: IconButton(
            icon: Icon(Icons.delete),
            onPressed: () {
              taskList.removeTask(task);
            },
          ),
        );
      },
    );
  }
}

class AddTaskDialog extends StatefulWidget {
  @override
  _AddTaskDialogState createState() => _AddTaskDialogState();
}

class _
_AddTaskDialogState createState() => _AddTaskDialogState();
}

class _AddTaskDialogState extends State<AddTaskDialog> {
  TextEditingController _controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      title: Text('Add Task'),
      content: TextField(
        controller: _controller,
        decoration: InputDecoration(labelText: 'Task title'),
        autofocus: true,
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.of(context).pop(),
          child: Text('Cancel'),
        ),
        TextButton(
          onPressed: () {
            if (_controller.text.isNotEmpty) {
              Navigator.of(context).pop(_controller.text);
            }
          },
          child: Text('Add'),
        ),
      ],
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

In the main function, we wrap our MyApp widget with a ChangeNotifierProvider that provides an instance of the TaskList class to the widget tree. Inside the MyApp widget, we have a Scaffold with a TaskListView widget and a FloatingActionButton for adding tasks.

In the TaskListView widget, we use Provider.of<TaskList>(context) to access the TaskList instance and display the tasks using a ListView.builder. We also use the toggleTask and removeTask methods to update the tasks' state and remove them from the list.

The AddTaskDialog widget is a simple dialog that prompts the user to enter a task title. Once the user adds a task, the title is passed back to the MyApp widget, which then adds the new task to the TaskList.

Advanced Provider Usage: FutureProvider, StreamProvider, Provider, and MultiProvider

In this chapter, we will delve deeper into the Provider package and explore its advanced features, including FutureProvider, StreamProvider, Provider, and MultiProvider. These tools will help you better manage the state of your Flutter applications and deal with asynchronous data more effectively.

FutureProvider

FutureProvider is a useful widget for providing a value that is obtained through a Future. It will automatically handle the loading state while waiting for the Future to complete and update the UI accordingly.

Let's create an example where we fetch a list of random numbers from a server:

class NumberList {
  Future<List<int>> fetchNumbers() async {
    await Future.delayed(Duration(seconds: 2));
    return List.generate(10, (index) => Random().nextInt(100));
  }
}

To use FutureProvider, wrap your widget tree with it and provide an instance of NumberList:

FutureProvider<List<int>>(
  create: (context) => NumberList().fetchNumbers(),
  child: MyApp(),
),

Inside the MyApp widget, you can access the Future value with a Consumer widget:

Consumer<List<int>>(
  builder: (context, numbers, child) {
    if (numbers == null) {
      return CircularProgressIndicator();
    } else {
      return ListView.builder(
        itemCount: numbers.length,
        itemBuilder: (context, index) => ListTile(title: Text('${numbers[index]}')),
      );
    }
  },
),

StreamProvider

StreamProvider is similar to FutureProvider, but it works with Stream objects instead of Future. This is especially useful when dealing with real-time data updates. Let's modify the previous example to use a Stream that emits random numbers:

class NumberStream {
  Stream<int> randomNumbers() async* {
    while (true) {
      await Future.delayed(Duration(seconds: 1));
      yield Random().nextInt(100);
    }
  }
}

Now, replace FutureProvider with StreamProvider:

StreamProvider<List<int>>(
  create: (context) => NumberStream().randomNumbers(),
  initialData: [],
  child: MyApp(),
),

And update the MyApp widget to consume the stream:

Consumer<int>(
  builder: (context, number, child) {
    return ListTile(title: Text('$number'));
  },
),

Provider

Provider is a more generic implementation that allows you to provide any value without handling Future or Stream. This can be useful when you want to provide simple values, like configurations or other constant data.

For example, providing a theme configuration:

class ThemeConfig {
  final String fontFamily;
  final Color primaryColor;

  ThemeConfig({required this.fontFamily, required this.primaryColor});
}

final themeConfig = ThemeConfig(fontFamily: 'Roboto', primaryColor: Colors.blue);

Provider<ThemeConfig>(
  create: (context) => themeConfig,
  child: MyApp(),
),

In the MyApp widget, you can access the ThemeConfig instance:

final themeConfig = Provider.of<ThemeConfig>(context);

MultiProvider

When your app requires multiple providers, it can be cumbersome to nest them manually. MultiProvider is a convenient widget that allows you to provide multiple providers in a more concise manner.

Assuming we want to provide both the NumberList and ThemeConfig from the previous examples, we can use MultiProvider:

MultiProvider(
  providers: [
    FutureProvider<List<int>>(
      create: (context) => NumberList().fetchNumbers(),
    ),
    Provider<ThemeConfig>(
      create: (context) => themeConfig,
    ),
  ],
  child: MyApp(),
),

Now, both providers are available within the MyApp widget tree, and you can use them as needed:

Consumer2<List<int>, ThemeConfig>(
  builder: (context, numbers, themeConfig, child) {
    if (numbers == null) {
      return CircularProgressIndicator();
    } else {
      return ListView.builder(
        itemCount: numbers.length,
        itemBuilder: (context, index) => ListTile(
          title: Text(
            '${numbers[index]}',
            style: TextStyle(fontFamily: themeConfig.fontFamily),
          ),
        ),
      );
    }
  },
),

Conclusion

In this article, you've learned the basics of state management using the Provider package in Flutter. We've covered how to create a simple counter app and a more complex To-Do List app. Provider is an excellent choice for managing the state of your Flutter apps, as it's easy to learn, efficient, and widely adopted by the community.

If you want to learn more about Provider and other state management solutions, check out the official Flutter documentation.

If you enjoyed this tutorial and found it helpful, please give it some claps! And don't forget to follow me for more quality content on Flutter and other topics.}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment