Tutorial

Managing State in Flutter

Flutter

While this tutorial has content that we believe is of great benefit to our community, we have not yet tested or edited it to ensure you have an error-free learning experience. It's on our list, and we're working on it! You can help us out by using the "report an issue" button at the bottom of the tutorial.

Most non-trivial apps will have some sort of state change going on and over time managing that complexity becomes increasingly difficult. Flutter apps are no different, but luckily for us, the Provider package is a perfect solution for most of our state management needs.

Prerequisites

We’re going to be passing data between 3 different screens, to keep things brief I will be assuming that you are already familiar with basic navigation and routes. If you need a quick refresher, you can check out my intro here. We’ll also be setting up a basic form without any validation, so a little knowledge on working with form state is a plus.

Installation

First, we’re going to need to add the Provider package to our pubspec.yaml file, I really recommend using the Pubspec Assist extension if you’re using VSCode. You can check out the full package here.

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  provider: ^3.1.0

Problem

Imagine you want to build an app that customizes some of its screens with some of the user’s data, like their name. The normal methods for passing down data between screens would quickly become a tangled mess of callbacks, unused data, and unnecessarily rebuilt widgets. With a front-end library like React this is a common problem called prop drilling.

If we wanted to pass data up from any of those widgets then you need to further bloat every intermediate widget with more unused callbacks. For most small features, this may make them almost not worth the effort.

Luckily for us, the Provider package allows us to store our data in a higher up widget, like wherever we initialize our MaterialApp, then access and change it directly from sub-widgets, regardless of nesting and without rebuilding everything in between.

Setup

We’re just going to need 2 screens, our router, and a navbar. We’re just setting up a page to display our account data and another to update it with the state itself being stored, changed, and passed down from our router.

* screens đź“‚
  * account.dart 
  * settings.dart
* main.dart 
* navbar.dart
main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import './screens/account.dart';
import './screens/settings.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Provider Demo',
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: AccountScreen(), routes: {
      'account_screen': (context) => AccountScreen(),
      'settings_screen': (context) => SettingsScreen(),
    });
  }
}

We’re creating our form state, setting a map to store our inputs, and adding a submit button that we’ll use later.

settings.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../main.dart';
import '../navbar.dart';

class SettingsScreen extends StatelessWidget {
  static const String id = 'settings_screen';

  final formKey = GlobalKey<FormState>();

  Map data = {'name': String, 'email': String, 'age': int};

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      bottomNavigationBar: Navbar(),
      appBar: AppBar(title: Text('Change Account Details')),
      body: Center(
        child: Container(
        padding: EdgeInsets.symmetric(vertical: 20, horizontal: 30),
        child: Form(
          key: formKey,
          child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: <Widget>[
                TextFormField(
                  decoration: InputDecoration(labelText: 'Name'),
                  onSaved: (input) => data['name'] = input,
                ),
                TextFormField(
                  decoration: InputDecoration(labelText: 'Email'),
                  onSaved: (input) => data['email'] = input,
                ),
                TextFormField(
                  decoration: InputDecoration(labelText: 'Age'),
                  onSaved: (input) => data['age'] = input,
                ),
                FlatButton(
                  onPressed: () => formKey.currentState.save(),
                  child: Text('Submit'),
                  color: Colors.blue,
                  textColor: Colors.white,
                )
              ]),
        ),
      )),
    );
  }
}

Settings Page

Our account page will simply display whatever account information, which doesn’t exist yet.

account.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../main.dart';
import '../navbar.dart';

class AccountScreen extends StatelessWidget {
  static const String id = 'account_screen';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      bottomNavigationBar: Navbar(),
      appBar: AppBar(
        title: Text('Account Details'),
      ),
      body: Center(
        child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text('Name: '),
              Text('Email: '),
              Text('Age: '),
            ]),
      ),
    );
  }
}

Provider

Setting up a Provider in incredibly easy, we just need to wrap our MaterialApp in a Provider with the type of our data, which in our case is a map. Finally, we need to set create to then use our context and data. Just like that, our data map is now available in every other screen and widget, assuming you import main.dart and the provider package.

main.dart
class _MyHomePageState extends State<MyHomePage> {
  Map data = {
    'name': 'Frank Abignale',
    'email': 'someEmail@alligatorio',
    'age': 47
  };

  @override
  Widget build(BuildContext context) {
    return Provider<Map>(
      create: (context) => data,
      child: MaterialApp(home: AccountScreen(), routes: {
        'account_screen': (context) => AccountScreen(),
        'settings_screen': (context) => SettingsScreen(),
      }),
    );
  }
}

Everything we passed to our provider creator is now available on Provider.of<Map>(context). Note that the type you pass in must match the type of data our Provider is expecting.

account.dart
body: Center(
  child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
      Text('Name: ' + Provider.of<Map>(context).data['name'].toString()),
      Text('Email: ' + Provider.of<Map>(context).data['email'].toString()),
      Text('Age: ' + Provider.of<Map>(context).data['age'].toString()),
    ]),
  ),

Account Screen

I recommend creating a snippet, since you’ll probably be accessing provider a lot. Here’s what the snippet would look like in VSCode:

dart.json
"Provider": {
  "prefix": "provider",
  "body": [
    "Provider.of<$1>(context).$2"
  ]
}

Change Notifier

Using Provider this way seems very top down, what if we want to pass data up and alter our map? The Provider alone isn’t enough for that. First, we need to break down our data into its own class that extends ChangeNotifier. Provider won’t work with that, so we need to change it to a ChangeNotifierProvider and pass in an instance of our Data class instead.

Now we’re passing down a whole class and not just a single variable, this means that we can start creating methods that can manipulate our data, which will also be available to everything that accesses Provider.

After we change any of our global data we want to use notifyListeners, which will rebuild every widget that depends on it.

main.dart
class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<Data>(
      create: (context) => Data(),
      child: MaterialApp(home: AccountScreen(), routes: {
        'account_screen': (context) => AccountScreen(),
        'settings_screen': (context) => SettingsScreen(),
      }),
    );
  }
}

class Data extends ChangeNotifier {
  Map data = {
    'name': 'Frank Abignale',
    'email': 'someEmail@alligatorio',
    'age': 47
  };

  void updateAccount(input) {
    data = input;
    notifyListeners();
  }
}

Since we changed our Provider type, we need to update our calls to it.

account.dart
Widget>[
  Text('Name: ' + Provider.of<Data>(context).data['name'].toString()),
  Text('Email: ' + Provider.of<Data>(context).data['email'].toString()),
  Text('Age: ' + Provider.of<Data>(context).data['age'].toString()),
]),

To pass data up, we just need to access the provider just like before and use our method that was passed down in the Data class.

setting.dart
 FlatButton(
  onPressed: () {
    formKey.currentState.save();
    Provider.of<Data>(context).updateAccount(data);
    formKey.currentState.reset();
  },
)

screenshot: Finished Example App

Conclusion

Once you get proficient with Provider it will save you an enormous amount of time and frustration. As always, if you had any trouble reproducing this app check out the repo here.

Creative Commons License