Widget Communication in Flutter using Provider
I’ve been having some fun with Flutter lately and want to share a crude little example of how to use the Provider pattern for managing app state. What I like about the pattern is the straightforward nature with which it enables widgets to communicate with each other. Also, I like being able to tell the StatelessWidget to redraw itself rather than implementing those widgets as StatefulWidgets and saddling those UI elements with the added responsibility of tracking when to redraw.
This “app” I created for this example follows the tenant of doing one thing. If the user correctly enters an address into the appropriate address fields and then hits the Update Map button, the image above the address box should update with a map whose marker is placed at the address entered. Google offers both a geocoding service as well as a static maps service to help make this all possible.
With the geocoding service we can take in the user input and create a url which the geocoding api consumes and returns with a list of possible results. Many times it’s just one result. Part of the data that is returned with this result is the latitude and longitude pointing to the address. We can then take these coordinates and feed those into the static maps API. A valid get request returns the data needed to render an image.
The provider pattern comes into play by allowing one widget to provide data to a model while another widget is listening to the same model for changes. The widgets are connected in a widget tree through a common ancestor that is a ChangeNotifierProvider.
The ChangeNotifierProvider class is made available through the provider package. The class is able to coordinate state among multiple widgets by building instances of the ChangeNotifier class. When a model uses the ChangeNotifier mixin, it gives the model access to a method called notifyListeners(). When notifyListeners() is called, a message is sent up the widget tree to the ChangeNotifierProvider that has subscribed to messages coming from the ChangeNotifier model.
Let’s take a look at the code to see how we can actually implement this, starting with main(). We’re going to wrap our entire “app” in a ChangeNotifierProvider. This means anything below it in the widget tree can subscribe to and get notifications from the configured ChangeNotifiers. A long form of the class definition could look like ChangeNotifierProvider
main.dart
void main() async {
final key_data = await rootBundle.loadString('assets/secrets.json');
final keys = json.decode(key_data);
runApp(
ChangeNotifierProvider(
builder: (context) => Address(),
child: MyApp(keys),
),
);
}
Here we have a ChangeNotifierProvider with two required fields. The first is defining a ChangeNotifier model to build. In this case it is Address and if we look at the address class below we’ll see it uses the ChangeNotifier mixin.
address.dart
class Address with ChangeNotifier {
String _number;
String _street;
String _city;
String _state;
String _zip;
String _country;
Coordinates coordinates = Coordinates(0.0, 0.0);
// More implementation
}
Since we’ve made Address use the ChangeNotifier mixin, we can now have it notifyListeners():
Future<Coordinates> geocodeAddress(BuildContext context, String maps_key) async {
final GEOCODING_BASE_URL = 'https://maps.googleapis.com/maps/api/geocode';
final URL = GEOCODING_BASE_URL + '/json?address=' + this._number + '+' + this._street + ',' + this._city + ',' + this._state + '&key=' + maps_key;
final headers = {
"Content-Type": "application/json"
};
final response = await http.get(URL, headers:headers);
if (response.statusCode == 200){
this.coordinates = _parseBodyForCoordinates(response.body);
}
notifyListeners();
}
This is a method the Address class uses to geocode a given address and then it takes that result and sets the coordinates property. Then, it calls notifyListeners(), which sends a message to the ChangeNotifierProvider, telling it that its listeners should check the data held in the provided Address object. In this case, listeners would be able to see that the coordinates property was updated.
So how do we listen for changes? If we have a widget that cares about what’s going on with the Address model, we need to register it as a Consumer of Address notifications. Here’s some code for how to do that:
main.dart
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text("Provider Example"),
),
child: SafeArea(
child: Padding(
padding: EdgeInsets.only(top: 8.0),
child: Column(
children: <Widget>[
Consumer<Address>(
builder: (context, address, _) =>
MapWidget(address.coordinates, _keys['static_maps_key']),
),
AddressWidget(),
CupertinoButton(
child: Text("Update Map"),
onPressed: () => Provider.of<Address>(context, listen: false)
.geocodeAddress(context, _keys['geocoding_key']),
)
],
),
),
),
);
}
}
Shown above is the main widget tree as it appears in main.dart. It has a Column widget that contains three other widgets. The first item in the column is the MapWidget. This is the widget that cares about what’s going on with the Address model since it takes the coordinates of the address and requests an image from Google’s static maps api. When we declare MapWidget as a consumer of Address, we give it a way to access the instance of Address that ChangeNotifierProvider is providing. The ChangeNotifierProvider is able to coordinate the exchange of information by responding to Provider<Address>.of(context) calls made by any widget in the tree. That call basically asks for the instance of Address that ChangeNotifierProvider is providing, then the class making the call can change properties on Address.
In the above code, the Update Map button calls the provided Address instance to tell it to geocode the current Address instances’s address. So technically this will error if the user doesn’t input an address. Sadly, there are no tests to guard against this … However, if the user does add valid entries to the AddressBox widget, then it geocodes something when the button is pressed.
If we look at the AddressBox widget, we can see where the provider sets properties on the Address instance. Here’s the build method of the AddressBox widget.
address.dart
Widget build(BuildContext context){
return Column(
children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(2, 8, 2, 3),
child: Text(
"Enter address"
),
),
Row(
children: <Widget>[
_showStreetNumber(context),
_showStreetName(context),
],
),
Row(
children: <Widget>[
_showCity(context),
_showState(context),
_showZip(context)
],
),
Center(
child: _showCountry(context),
),
]
);
}
Nothing too exciting, just some methods that encapsulate other widgets. The point to realize here is that each encapsulation includes a call to the Provider to set one of the properties on the Address class. For instance, here is the method for _showStreetNumber() it sets the Address instance’s _number property.
address.dart
Widget _showStreetNumber(BuildContext context){
return Expanded(
flex: 1,
child: new CupertinoTextField(
placeholder: "No." ,
maxLines: 1,
keyboardType: TextInputType.number,
autofocus: false,
inputFormatters: [
LengthLimitingTextInputFormatter(8),
],
onChanged: (value) => Provider.of<Address>(context)._number = value,
),
);
}
Then from the geocodeAddress method, you may remember that we use the _number property when geocoding the address. If you cannot remember, here is a refresher:
final URL = GEOCODING_BASE_URL + '/json?address=' + this._number + '+' + this._street + ',' + this._city + ',' + this._state + '&key=' + maps_key;
On the Consumer side, each time the ChangeNotifierProvider notifies listeners of a change, it calls the builder for each of those listeners. The following code was shown above in the main.dart code. The builder below returns a new instance of MapWidget drawn according to the information passed to it by the updated address instance.
Consumer<Address>(builder: (context, address, _) => MapWidget(address.coordinates, _keys['static_maps_key']),
There you have it, the components are wired up. The AddressBox widget can now pass information to the MapWidget for the purpose of updating the map widget’s state. To wrap up, the ChangeNotifierProvider holds references to any registered ChangeNotifier. All widgets in the ChangeNotifierProvider tree can make a call to one of the configured ChangeNotifier objects through a call to Provider