Tutorial
Managing State in 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.
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
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.
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,
)
]),
),
)),
);
}
}
Our account page will simply display whatever account information, which doesn’t exist yet.
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.
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.
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()),
]),
),
I recommend creating a snippet, since you’ll probably be accessing provider a lot. Here’s what the snippet would look like in VSCode:
"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.
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.
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.
FlatButton(
onPressed: () {
formKey.currentState.save();
Provider.of<Data>(context).updateAccount(data);
formKey.currentState.reset();
},
)
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.