Flutter and the Provider Pattern
Whether you're developing a native or hybrid application, managing state is a challenge that needs to be tackled and effectively controlled. In Flutter, there are several ways to manage the global state of an application, and in this post, we will discuss Provider, a popular library that helps us with this task.
Example Application
To see how Provider works, let's create a simple application that illustrates it. This app displays a screen with an AppBar and its title, a Text widget in the center, and two floating buttons in the bottom right corner. These buttons will function to change the value of the text shown in both the AppBar title and the Text in the app's body. We'll ensure that the text value is managed globally.
Github Repository: Flutter Provider Pattern
Let's get to work!
First, we'll install the library: provider: ^5.0.0 (At the time I wrote the post, this was the version).
You can find it
here
.
Within `lib`, we create a folder named `widgets`, and inside it, a file named `floating_actions.dart` (lib/widgets/floating_actions.dart) to create the widget with the two floating buttons:
import 'package:flutter/material.dart';
class FloatingAction extends StatelessWidget {
@override
Widget build(BuildContext context) {
(context);
return Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
FloatingActionButton(
child: Icon(Icons.wifi),
backgroundColor: Colors.orange,
onPressed: () {}),
SizedBox(
height: 10.0,
),
FloatingActionButton(
child: Icon(Icons.bluetooth),
backgroundColor: Colors.blue,
onPressed: () {})
],
);
}
}
Note that the onPressed is currently without any action; we will add its functionality later on.
Now, inside lib/widgets, we will create another file named body.dart (**lib/widgets/body.dart**) to create the widget with the centered text.
import 'package:flutter/material.dart';
class Body extends StatelessWidget {
@override
Widget build(BuildContext context) {
(context);
return Center(
child: Text("",
style: TextStyle(fontSize: 20),
),
);
}
}
The text displayed for now is an empty string. We will create another widget inside lib, called home **
import 'package:flutter/material.dart';
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Center(child: Text("")),
),
body: Body(),
floatingActionButton: FloatingAction(),
);
}
}
Here we are rendering a Scaffold, with an AppBar and its empty title for now, also rendering the Body and FloatingAction widgets created earlier. No Provider yet, we are just building the graphical interface of the app, let's move on to modifying the main.dart:
import 'package:flutter/material.dart';
import 'package:provider_pattern/home.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Provider',
home: Home()
);
}
}
With this, we are all set to start building our Provider. Let's do the following: Inside lib, create another directory called provider, and within it, create a file named signal_mode.dart (lib/provider/signal_mode.dart).
import 'package:flutter/material.dart';
class SignalMode extends ChangeNotifier {
String _signalMode = 'Wifi mode';
String get signalMode => _signalMode;
set signalMode(String mode) {
this._signalMode = mode;
notifyListeners();
}
}
Done, this is our Provider. It's the class responsible for centralizing the value of the private variable `_signalMode`, which is of type String and represents the text displayed both in the AppBar title and in the center of the screen.
Notice the first detail: it extends the `ChangeNotifier` class, responsible for exposing notification events.
Let's see what we have: a `get` method to retrieve the value of `_signalMode` and a `set` method to modify it. Within this method, we observe a call to `notifyListeners()`, which notifies all listening widgets of the change, thereby updating their state.
It's time to encapsulate those widgets that will listen to changes. Since we want to have control over the state at all levels of the application, we will do this in the main file.
Keep in mind that it's not always necessary to wrap it at the highest level of the application if you want to share specific state for a certain level.
If we modify our main file, it would look like this:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:provider_pattern/home.dart';
import 'package:provider_pattern/provider/signal_mode.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => SignalMode(),
child: MaterialApp(
title: 'Provider',
home: Home()),
);
}
}
This way, the entire app is set up to listen to changes from the SignalMode class. From now on, we can integrate various widgets into our app. Let's modify the FloatingAction class to connect it with our Provider, as follows:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:provider_pattern/provider/signal_mode.dart';
class FloatingAction extends StatelessWidget {
@override
Widget build(BuildContext context) {
final signal = Provider.of<SignalMode>(context);
return Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
FloatingActionButton(
child: Icon(Icons.wifi),
backgroundColor: Colors.orange,
onPressed: () {
signal.signalMode = "Wifi mode";
}),
SizedBox(
height: 10.0,
),
FloatingActionButton(
child: Icon(Icons.bluetooth),
backgroundColor: Colors.blue,
onPressed: () {
signal.signalMode = "Bluetooth mode";
})
],
);
}
}
Note the following line here:
final signal = Provider.of<SignalMode>(context);
This way, we obtain an instance of our Provider specifying that it is of type SignalMode.
Another point to note here is in the onPressed of the buttons, each one makes a call modifying the value of signalMode
onPressed: () {
signal.signalMode = "Wifi mode";
})
onPressed: () {
signal.signalMode = "Bluetooth mode";
})
This way, we can modify the global state. Now, we just need to display the values. Let's see how:
import 'package:flutter/material.dart';
import 'package:provider_pattern/provider/signal_mode.dart';
import 'package:provider_pattern/widgets/body.dart';
import 'package:provider_pattern/widgets/floating_actions.dart';
import 'package:provider/provider.dart';
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
final signal = Provider.of<SignalMode>(context);
return Scaffold(
appBar: AppBar(
title: Center(child: Text(signal.signalMode)),
),
body: Body(),
floatingActionButton: FloatingAction(),
);
}
}
In the `Home` class, we similarly obtain the instance of the Provider and read the `signalHome` property.
We do the same in the `Body` class:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:provider_pattern/provider/signal_mode.dart';
class Body extends StatelessWidget {
@override
Widget build(BuildContext context) {
final signal = Provider.of<SignalMode>(context);
return Center(
child: Text(
signal.signalMode,
style: TextStyle(fontSize: 20),
),
);
}
}
We have completed our example, but I'll leave you with one last consideration regarding the use of multiple providers. If we want to integrate multiple providers, we would need to modify our main file again as follows:
void main() => runApp(AppState());
class AppState extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => SignalMode())
],
child: MyApp(),
);
}
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Provider',
home: Home()
);
}
}
Note here how `MultiProvider` accepts an array of Providers:
providers: [
ChangeNotifierProvider(create: (_) => SignalMode())
],
Here ends this post, see you next time!!