Tutorial

How To Managing State in Flutter with Provider

Flutter

Introduction

State management involves tracking state changes throughout an application.

The provider package is one solution for state management needs.

In this article, you will learn how to apply provider to a sample Flutter application to manage state for user account information.

Prerequisites

To complete this tutorial, you will need:

  • To download and install Flutter.
  • To download and install Android Studio or Visual Studio Code.
  • It is recommended to install plugins for your code editor:
    • Flutter and Dart plugins installed for Android Studio.
    • Flutter extension installed for Visual Studio Code.
  • Familiarity with navigation and routes will be beneficial, but not required.
  • Familiarity with form state will also be beneficial, but not required.

This tutorial was verified with Flutter v2.0.6, Android SDK v31.0.2, and Android Studio v4.1.

Understanding the Problem

Consider a situation where 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 widget that is higher up, like wherever we initialize our MaterialApp, then access and change it directly from sub-widgets, regardless of nesting and without rebuilding everything in between.

Step 1 — Setting Up the Project

Once you have your environment set up for Flutter, you can run the following to create a new application:

  • flutter create flutter_provider_example

Navigate to the new project directory:

  • cd flutter_provider_example

Using flutter create will produce a demo application that will display the number of times a button is clicked.

Step 2 — Adding the provider Plugin

Next up, we’ll need to add the provider plugin within our pubspec.yaml:

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter

  provider: ^3.1.0

Then, save the changes to your file.

Note: If you are using VS Code, you may want to consider using the Pubspec Assist extension for quickly adding dependencies.

We can now go ahead and run this on the iOS or Android simulator or device of your choice.

Step 3 — Scaffolding the Project

We’re going to need 2 screens, our router, and a navbar. We’re 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.

Open main.dart in your code editor and modify the following lines of code:

lib/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(),
    });
  }
}

Create a navbar.dart file and open it with your code editor:

lib/navbar.dart
import 'package:flutter/material.dart';
import './screens/account.dart';
import './screens/settings.dart';

class Navbar extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.blue,
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: <Widget>[
          TextButton(
            onPressed: () =>
              Navigator.pushReplacementNamed(context, AccountScreen.id),
            child: Icon(Icons.account_circle, color: Colors.white)
          ),
          TextButton(
            onPressed: () =>
              Navigator.pushReplacementNamed(context, SettingsScreen.id),
            child: Icon(Icons.settings, color: Colors.white)
          ),
        ],
      ),
    );
  }
}

In the lib directory, create a new screens subdirectory:

  • mkdir lib/screens

In this subdirectory, create a settings.dart file. This will be used for creating our form state, setting a map to store our inputs, and adding a submit button that we’ll use later:

lib/screens/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>();

  final 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,
                ),
                TextButton(
                  onPressed: () => formKey.currentState.save(),
                  child: Text('Submit'),
                  style: TextButton.styleFrom(
                    primary: Colors.white,
                    backgroundColor: Colors.blue,
                  ),
                )
              ]
            ),
          ),
        ),
      ),
    );
  }
}

Also in this subdirectory, create an account.dart file. This will be used to display account information:

lib/screens/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: '),
          ],
        ),
      ),
    );
  }
}

Compile your code and have it run in an emulator:

Screenshot of the application running on a mobile device. It displays the Change Account Details Page with fields for name, email, and age.

At this point, you have an application with an Account screen and a Settings screen.

Step 4 — Using Provider

Setting up a provider will require wrapping our MaterialApp in a Provider with the type of our data.

Revisit main.dart and open it in your code editor. For this tutorial, the data type is a Map. Finally, we need to set create to then use our context and data:

lib/main.dart
// ...

class _MyHomePageState extends State<MyHomePage> {
  Map data = {
    'name': 'Sammy Shark',
    'email': 'example@example.com',
    'age': 42
  };

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

The data map is now available in every other screen and widget that main.dart calls and imports the provider package.

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.

Note: If you are using VS Code, you may want to consider using snippets since you’ll probably be accessing provider a lot:

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

Revisit account.dart and open it in your code editor. Add the following lines of code:

lib/screens/account.dart
// ...

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

// ...

Compile your code and have it run in an emulator:

Screenshot of the application running on a mobile device. It displays the Account Details for an example user - their name, email, age.

At this point, you have an application with hardcoded user data that is displayed on the Account screen.

Step 5 — Using ChangeNotifier

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.

Revisit main.dart and open it in your code editor:

lib/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': 'Sammy Shark',
    'email': 'example@example.com',
    'age': 42
  };

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

Since we changed our Provider type, we need to update our calls to it. Revisit account.dart and open it in your code editor:

lib/screens/account.dart
// ...

body: Center(
  child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <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 will need to access the Provider use our method that was passed down in the Data class. Revisit settings.dart and open it in your code editor:

lib/screens/settings.dart
TextButton(
  onPressed: () {
    formKey.currentState.save();
    Provider.of<Data>(context, listen: false).updateAccount(data);
    formKey.currentState.reset();
  },
)

Compile your code and have it run in an emulator:

Animated gif of the application running on a mobile device. It displays someone using the form on the Settings screen to update user information. Then they navigate to the Account screen and the new user information is displayed.

At this point, you have an application that supports updating user information on the Settings screen and displaying the changes on the Account screen.

Conclusion

In this article, you learned how to apply provider to a sample Flutter application to manage state for user account information.

If you’d like to learn more about Flutter, check out our Flutter topic page for exercises and programming projects.

Creative Commons License