What is Flutter Provider?

Provider is a Flutter library used for DI and State Management. Provider was originally created by the community and soon became the preferred method for state management, in Google’s 2019 Flutter Meetup they urged developers to use Provider instead of the state management tool they built. Today, Provider is still provided by the community but also backed by Google’s Flutter team.

Summary

In this guide, we will walk through simple examples that demonstrate three (3) of the most common types of provider:

  • ChangeNotifierProvider
  • FutureProvider
  • StreamProvider

Since a Flutter app is composed of a hierarchy of widgets, it’s reasonable to assume that we need to take a hierarchical approach to state management as well. With Provider, we will be taking a “data down” approach. This means that the data will be injected above the widget (higher in the hierarchy) that needs to access it. Note: If the provider runs expensive logic, consider it’s placement carefully so that performance is not lost when the logic recalculates.

In all of the examples, we will use the Provider.of<T>(context, listen: false) syntax (listen is optional) to access our state. Provider.of<T>(context) takes your current context and looks up the widget tree for the nearest instance of T, that is that state your widget will receive.

Provider.of<int>(context); will return the nearest int that is being provided in the widget tree above.

Now, take a look at the app we will create during this guide.

Demo of ChangeNotifierProvider, StreamProvider, FutureProvider

Adding Provider to Flutter

Simply add the Flutter Provider library as a dependency in your Pubspec.yaml. You can scope the library to a specific version; the latest version at the time of writing this guide is ^3.1.0. If you want to always get the latest version, simply omit the version number and Flutter will ‘get’ the latest automatically: Be sure to lock this down to a version before PROD.

# Flutter Provider_Demo Dependencies
dependencies:
  flutter:
    sdk: flutter

  provider:
  cupertino_icons: ^0.1.2

Using the ChangeNotifierProvider

This type of provider is an implementation of the Observer design pattern, which may be a review if you’re familiar with Design Patterns and the GoF. The general concept is that we define a class which will notify any listening classes when a change takes place. You can think of it like playing a game of BINGO!, the announcer constantly notifies the players of the new number that was drawn and the players take some action; the announcer is the provider class and the players are the listeners.

For this example, we will base the functionality off of the “Counter App” that Flutter includes when creating a new project. When the app starts we will see a screen with a “+” button and a label which increases each time we press the button.

class DataProvider extends ChangeNotifier {
  int _count = 0;
  int get count => _count;

  DataProvider() {}

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

Notice that this class extends ChangeNotifier, this gives our DataProvider class access to the notifyListeners() method. You can call this at any time, each time it will notify all listeners of the current state – in this case, “count”.

In order to use our new DataProvider class, we need to inject it into our widget tree. We do this by adding a ChangeNotifierProvider into the widget tree. Because we will be adding multiple providers by the end of this example we will add our ChangeNotifierProvider inside of the MultiProvider widget, this widget takes an array of providers to inject and a child widget.

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MultiProvider(
        providers: [
          ChangeNotifierProvider(builder: (_) => DataProvider()),
        ],
        child: DefaultTabController(
            length: 1,
            child: DefaultTabController(
              length: 1,
              child: Scaffold(
                appBar: AppBar(
                  title: Text("Provider Demo"),
                  bottom: TabBar(
                    tabs: [
                      Tab(icon: Icon(Icons.add)),
                    ],
                  ),
                ),
                body: TabBarView(
                  children: [
                    MyCountPage(),
                  ],
                ),
              ),
            )
        )
      )
    );
  }
}

Now, you can see that inside of the ChangeNotifierProvider we are passing a new instance of our DataProvider class along with the BuildContext. At this time the constructor for DataProvider is called.

In order to access the ‘count’ from our DataProvider we must grab an instance of DataProvider in our widget and access the ‘count’. We access the same DataProvider instance in this widget when we want to increment the value of ‘count’. This automatically triggers a change notification and the text is updated accordingly. 

class MyCountPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    DataProvider _data = Provider.of<DataProvider>(context);
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('ChangeNotifierProvider Example'),
            SizedBox(height: 150),
            Text(
              '${_data.count}',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _data.incrementCount(),
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

Using the FutureProvider

This provider is used to provide data into the widget tree that is asynchronous. Future is a pattern for async development that is available in the concurrency libraries of most modern software languages. In short, a Future will schedule some task and then release the current thread back to work until the original task is completed. Then, the Future resolves with a value. This concept is similar to Observables and Promises. Instead of “this is a value of Type T” you would say “eventually, this will have a value of Type T”.

For this example, we will be reading a list of User objects from a file and displaying them in a color-coded scrollable ListView<User>. The asynchronous part of this process is the opening and reading of the file. Normally, reading a file synchronously is an expensive operation, but, when done asynchronously the rest of the app can continue running and the widget will be updated when the file read finishes.

First, we need to create a UserProvider class that implements the logic for reading our file and deserializing the data into User objects. To learn more about using JSON data with Flutter check out our guide; serializing and deserializing JSON in Flutter.

class UserProvider {
  final String _dataPath = "assets/data/users.json";
  List<User> users;

  Future<List<User>> loadUserData( ) async {
    var dataString = await loadAsset();
    Map<String, dynamic> jsonUserData = jsonDecode(dataString);
    users = UserList.fromJson(jsonUserData['users']).users;
    print('done loading user!' + jsonEncode(users));
    return users;
  }

  Future<String> loadAsset() async {
    return await Future.delayed(Duration(seconds: 10), () async {
      return await rootBundle.loadString(_dataPath);
    });
  }
}

To illustrate the delay of loading a larger file, we are adding an artificial delay of 10 seconds before attempting to read the file. Meanwhile, we will show a spinner.

Next, we add our FutureProvider into the same MultiProvider that was used with our ChangeNotifierProvider. In this case, we want to inject a Future into the FutureProvider. Since our constructor doesn’t return a future, we need to call a method from our FutureProvider that does return a Future; loadUserData().

...

    MultiProvider(
        providers: [
          ChangeNotifierProvider(builder: (_) => DataProvider()),
          FutureProvider(builder: (_) => UserProvider().loadUserData()),
          StreamProvider(builder: (_) => EventProvider().intStream(), initialData: 0)
        ],
        child:

...

// List of Users Widget
class MyUserPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var _users = Provider.of<List<User>>(context);
    return Column(
      children: <Widget>[
        Padding(
          padding: EdgeInsets.all(10.0),
          child: Text(
              'FutureProvider Example, users loaded from a File'
          ),
        ),
        Expanded(
          child: _users == null ? Container(child: CupertinoActivityIndicator(radius: 50.0)) :
          ListView.builder(
              itemCount: _users.length,
              itemBuilder: (context, index){
                return Container(
                    height: 50,
                    color: Colors.grey[(index*200) % 400],
                    child: Center(
                        child: Text(
                            '${_users[index].firstName} ${_users[index].lastName} | ${_users[index].website}'
                        )
                    )
                );
              }
          )
        )
      ],
    );
  }
}

Using the StreamProvider

This provider is designed to allow widgets to access state which occurs as part of a stream. Streams are asynchronous, but, unlike a Future<T>, a stream does not resolve once a value is returned. Instead, a stream may continue to provide values until the stream is closed. 

In our example, we are creating a stream that produces an Event every second. Each second we will increment a text widget using the current value of ‘_count’ in the EventProvider. Note: streams do not require a set interval.

Let’s take a look at our EventProvider. This class is very straightforward.

class EventProvider {
  Stream<int> intStream() {
    Duration interval = Duration(seconds: 2);
    int _count = 0;
    return Stream<int>.periodic(interval, (int _count) => _count++);
  }
}

Now, let’s add our EventProvider into our MultiProvider, the same way we configured our FutureProvider. When we register the provider we call the intStream() method as it returns a Stream<int> and the StreamProvider requires that the value being provided is a valid stream.

...

    MultiProvider(
        providers: [
          ChangeNotifierProvider(builder: (_) => DataProvider()),
          FutureProvider(builder: (_) => UserProvider().loadUserData()),
          StreamProvider(builder: (_) => EventProvider().intStream(), initialData: 0)
        ],
        child:

...

Let’s see how this is working in our MyEventPage widget. Just like our first example, with ChangeNotifierProvider we use Provider.of<T>(context) to get the latest instance of T in our current context; in this case, T is an int, not a stream.

class MyEventPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var _value = Provider.of<int>(context);
    return Container(
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('StreamProvider Example'),
            SizedBox(height: 150),
            Text('${_value.toString()}',
                style: Theme.of(context).textTheme.display1
            )
          ],
        )
      )
    );
  }
}

Since the StreamProvider is providing a stream of int we can simply listen for the state change of the latest int and then update our Text widget.

Full Code Sample

// Main app and Pages for Tab Layout
void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MultiProvider(
        providers: [
          ChangeNotifierProvider(builder: (_) => DataProvider()),
          FutureProvider(builder: (_) => UserProvider().loadUserData()),
          StreamProvider(builder: (_) => EventProvider().intStream(), initialData: 0)
        ],
        child: DefaultTabController(
            length: 3,
            child: DefaultTabController(
              length: 3,
              child: Scaffold(
                appBar: AppBar(
                  title: Text("Provider Demo"),
                  bottom: TabBar(
                    tabs: [
                      Tab(icon: Icon(Icons.add)),
                      Tab(icon: Icon(Icons.person)),
                      Tab(icon: Icon(Icons.message)),
                    ],
                  ),
                ),
                body: TabBarView(
                  children: [
                    MyCountPage(),
                    MyUserPage(),
                    MyEventPage(),
                  ],
                ),
              ),
            )
        )
      )
    );
  }
}

// Event page (counting)
class MyEventPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var _value = Provider.of<int>(context);
    return Container(
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('StreamProvider Example'),
            SizedBox(height: 150),
            Text('${_value.toString()}',
                style: Theme.of(context).textTheme.display1
            )
          ],
        )
      )
    );
  }
}

// User List Page
class MyUserPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var _users = Provider.of<List<User>>(context);
    return Column(
      children: <Widget>[
        Padding(
          padding: EdgeInsets.all(10.0),
          child: Text(
              'FutureProvider Example, users loaded from a File'
          ),
        ),
        Expanded(
          child: _users == null ? Container(child: CupertinoActivityIndicator(radius: 50.0)) :
          ListView.builder(
              itemCount: _users.length,
              itemBuilder: (context, index){
                return Container(
                    height: 50,
                    color: Colors.grey[(index*200) % 400],
                    child: Center(
                        child: Text(
                            '${_users[index].firstName} ${_users[index].lastName} | ${_users[index].website}'
                        )
                    )
                );
              }
          )
        )
      ],
    );

  }
}

// Counter Page
class MyCountPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    DataProvider _data = Provider.of<DataProvider>(context);
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('ChangeNotifierProvider Example'),
            SizedBox(height: 150),
            Text(
              '${_data.count}',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _data.incrementCount(),
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

// EventProvider (Stream)
class EventProvider {
  Stream<int> intStream() {
    Duration interval = Duration(seconds: 2);
    int _count = 0;
    return Stream<int>.periodic(interval, (int _count) => _count++);
  }
}

// UserProvider (Future)
class UserProvider {
  final String _dataPath = "assets/data/users.json";
  List<User> users;

  Future<List<User>> loadUserData( ) async {
    var dataString = await loadAsset();
    Map<String, dynamic> jsonUserData = jsonDecode(dataString);
    users = UserList.fromJson(jsonUserData['users']).users;
    print('done loading user!' + jsonEncode(users));
    return users;
  }

  Future<String> loadAsset() async {
    return await Future.delayed(Duration(seconds: 10), () async {
      return await rootBundle.loadString(_dataPath);
    });
  }
}

// DataProvider (ChangeNotifier)
class DataProvider extends ChangeNotifier {
  int _count = 0;
  int get count => _count;

  DataProvider() {}

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

// User Model
class User {
  final String firstName, lastName, website;
  const User(this.firstName, this.lastName, this.website);

  User.fromJson(Map<String, dynamic> json):
    this.firstName = json['first_name'],
    this.lastName = json['last_name'],
    this.website = json['website'];

  Map<String, dynamic> toJson() => {
    "first_name": this.firstName,
    "last_name": this.lastName,
    "website": this.website
  };
}

// User List Model
class UserList {
  final List<User> users;
  UserList(this.users);

  UserList.fromJson(List<dynamic> usersJson) :
      users = usersJson.map((user) => User.fromJson(user)).toList();
}

JSON File with User Data

{
  "users": [
    {
      "first_name": "Douglas",
      "last_name": "Tober",
      "website": "Codetober.com"
    },
    {
      "first_name": "Brad",
      "last_name": "Traversy",
      "website": "traversymedia.com"
    },
    {
      "first_name": "Bucky",
      "last_name": "Roberts",
      "website": "lhventures.us"
    },
    {
      "first_name": "Doug",
      "last_name": "Tober",
      "website": "j5technology.com"
    }
  ]
}