Sharing Location using Inherited Widget
Sometimes you need to use a location from multiple routes/widgets within your app. Instead of having to set up and manage the location plugin in all these various places, we can instead set it up once and share that data between many widgets using an Inherited Widget.
Outdated
The version of the plugin discussed here (1.0.3+1) is outdated and only supports an old version of the location package. The latest version (1.1.0) is in GitHub and supports the latest version of the location package. This code is still a useful example, and you can view the changes required for the update on this commit. It was a fairly minor change.
Download
Visit the GitHub repository to clone the source for this application.
This code was tested with Flutter 0.7.5, Dart 2.1.0-dev.3.0
You can also use this in your projects by importing from pub: location_context
Setup
We're going to build this project as a package so that it can be easily portable between many different projects.
In Android Studio menu, use File -> New -> New Flutter Project..., then select "Flutter Package". Set the project name to "location_context", leave all the other defaults, and click Finish.
From the command line, flutter create -t package location_context
The Code
Step 1: Import dependencies
Our package is going to use a few other packages.
Add the following lines to our pubspec.yaml
under "dependencies:
"
location: ^1.3.4
quiver: "^2.0.0+1"
The location
plugin is used to interface with the GPS hardware and get the device's actual location data to share.
The quiver
package is used for its helper method hashObjects
that we are going to use to create the hashCode for our Position
class. (Note: flutter also uses this package internally, so we need to make sure to keep the version in sync. A warning is generated if the dependencies collide.)
After we have added these dependencies to our pubspec.yaml
file, don't forget to run flutter packages get
to actually download those dependencies for use.
Step 2: Create the library root
In your editor of choice, open up lib/location_context.dart
. This is our entry point for the package, so we're going to set it up. You'll have some errors until we create the other two files we are going to use for the library, just ignore them for now.
Replace the class Calculator
(this should be the default class created in the file) with the following code. Leave the library
line at the top of the file untouched.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:location/location.dart';
import 'package:quiver/core.dart';
part 'src/position.dart';
part 'src/context.dart';
Because this is the entry point for our package, and because we're using part
to import our other files, all of our imports go in this file. Even though they aren't used in this file, they will be used in the part
files.
Using part
allows us to split a library into multiple Dart files. Since they're all part of the same library, private members are accessible across all the given files. We care because Position
has a private constructor that we want LocationContext
to be able to use. We'll create these classes shortly.
Step 3: Our Position Class
We are now going to create lib/src/position.dart
. This file will hold the class that we will use to carry around our position data. The location
plugin returns a map holding the data. We will use that map, in the future, to initialize these Position
objects. I find using a class to be more programmer friendly than using a map because an IDE can auto complete the properties. Also, we can get compile-time errors if we try to access a property that doesn't exist (like a typo) instead of getting a runtime error.
Create lib/src/position.dart
and start adding code.
Step 3.1: Create the class outline
part of location_context; // Let it know we're in the library
class Position {
final double latitude;
final double longitude;
final double accuracy;
final double altitude;
final double speed;
final double speedAccuracy; // Always 0 on ios
final int _hashCode;
Position({
this.latitude,
this.longitude,
this.accuracy,
this.altitude,
this.speed,
this.speedAccuracy,
}) : _hashCode = hashObjects(
[latitude, longitude, accuracy, altitude, speed, speedAccuracy]);
}
All these properties are items that the location
plugin returns for us to use. We are also creating a constructor that lets us define each of these properties manually.
Since all the properties are final, we will also calculate our _hashCode
here using Quiver's hashObjects
method, and store the value for later use. This way we don't have to calculate it each time we want it because it will never change.
Step 3.2: Construct from a map
We also want to be able to just simply construct this object from the map that is returned by location
. Because we are expecting specific keys in the map to be defined, we are going to make this a private constructor, so that only our library files can access it.
Add the following code in the Position
class just below the default constructor.
Position._fromMap(Map<String, double> data)
: this(
latitude: data['latitude'],
longitude: data['longitude'],
accuracy: data['accuracy'],
altitude: data['altitude'],
speed: data['speed'],
speedAccuracy: data['speed_accuracy'],
);
For this constructor, we simply initialize the object using the map as the source of truth using the defined keys.
Step 3.3: Finishing Touches
Other things we need for this class are the ability to compare it to other objects (with both equality and hashes), and we want a nice pretty string representation for debugging (and testing) purposes.
Add the following code inside the Position
class just below the _fromMap
constructor.
operator ==(dynamic other) {
if (other is! Position) return false;
return hashCode == other.hashCode;
}
int get hashCode => _hashCode;
String toString() {
return 'Position($latitude, $longitude, $accuracy, $altitude, $speed, $speedAccuracy)';
}
bool
Firstly, we overriding our equality operator to do an accurate comparison. We compare the hashCode of this
vs other
to see if the values they contain are the same. If they have the same hashCode, then they contain identical data.
Next, we return our calculated _hashCode
for the hashing function.
Finally, we create a nice little toString
method that will give us a human readable value for this object if we output it or cast it to a string.
Step 4: Create our Location Context
We are now going to create lib/src/context.dart
that will contain our Inherited Widget to do our magic. We have a few things to consider as we create this widget so that it will work as expected.
- For unit testing, we need to be able to mock the Location object from the location package.
- We need to actually create the Inherited Widget to share the data we are storing.
- As per the example in the location package, we need to be able to update and store the value being shared.
We will accomplish step 1 using a form of dependency injection.
We will accomplish step 2 using an Inherited Widget that is created from inside our LocationContextWrapper
's State, with our Wrapper passing the location data as properties.
We will accomplish step 3 using a Stateful Widget named _LocationContextWrapper
to update and store the value.
Create lib/src/context.dart
and start adding code.
Step 4.1: Location Dependency Injection
Our first step is to allow the Location object we will be using to be mocked. We are going to do this by using a form of dependency injection. Add the following code to the top of the new file.
part of location_context; // Let it know we're in the library
typedef Location LocationFactory();
void mockLocation(LocationFactory mock) {
_createLocation = mock;
}
LocationFactory _createLocation = () => Location();
First, we define the type (Location LocationFactory()
) for the "factory" method that we will be using for our "dependency injection". It is simply a method that takes no arguments and returns an instance of Location.
Second, we define our method that lets us override our factory method so that we can inject a different dependency. mockLocation
receives a callback as its argument, then assigns that callback to override the default factory method we define in the next step.
Last, but not least, we define the factory method we will call to return an instance of our Location
dependency. By default _createLocation
will return a stock instance of Location
. Our Widgets will use this method to get the instance of Location
that they should be using. Since we can override this method using mockLocation
it allows our widgets to get a mocked version when they are under test.
We mark the two parts that do the mocking as @visibleForTesting
to let developers know that these constructs are "private" and that they shouldn't be accessing them in their code, even though they are visible outside the package.
Step 4.2: Create the Inherited Widget
The basics of the Inherited Widget are pretty straight forward. We pass it information, and that information becomes accessible to any widget below it in the widget tree hierarchy. We are going to share three values: lastLocation
, currentLocation
, and error
.
lastLocation
and currentLocation
are pretty self explanatory. This gives our app the ability to calculate a directional vector if that's what we need. The error
is just a string field that contains the last error message we may have encountered.
This is just the start of the class, we will be making some modifications to it in later sections.
class LocationContext extends InheritedWidget {
final Position currentLocation;
final Position lastLocation;
final String error;
LocationContext._({
this.currentLocation,
this.lastLocation,
this.error,
Key key,
Widget child,
}) : super(key: key, child: child);
static LocationContext of(BuildContext context) {
return context.inheritFromWidgetOfExactType(LocationContext);
}
bool updateShouldNotify(LocationContext oldWidget) {
return currentLocation != oldWidget.currentLocation ||
lastLocation != oldWidget.lastLocation ||
error != oldWidget.error;
}
}
This is fairly straight forward. We extend InheritedWidget
, define our three values, and initialize them in the constructor.
First, note that we only have a private constructor: LocationContext._
. This creates a named constructor for the class, but because it is prefixed with (only) _
it is private. This means that this class can only be initialized from inside this package. This is important because this inherited widget is, quite literally, useless without the widgets we will create in the next step. We need to make sure it is always initialized correctly, so we'll create a helper method a little later for this.
The next method of
is where some of our "magic" happens. This method allows for any child widget to call LocationContext.of(context)
and be returned an instance of this widget, allowing them to have access to its public members. If this doesn't quite make sense, you'll see how it is used in the example.
Lastly, updateShouldNotify
is used to tell the framework if it should notify widgets below this in the hierarchy tree that an update has occurred so that they can rebuild themselves, if necessary. This widget notifies of an update if any of its properties have changed.
Step 4.3: Create the Stateful Widget
Our stateful widget will interface with the Location to store and update our information and pass it to the Inherited Widget we just created in the last step. To keep things simpler for people consuming this package, this widget is going to be package private. After we've created this widget, as mentioned in the last step, we'll create the method to properly initialize everything.
Add the following code to the bottom of lib/src/context.dart
class _LocationContextWrapper extends StatefulWidget {
final Widget child;
_LocationContextWrapper({Key key, this.child}) : super(key: key);
State<StatefulWidget> createState() => _LocationContextWrapperState();
}
class _LocationContextWrapperState extends State<_LocationContextWrapper> {
}
We have a small StatefulWidget
that contains a member widget named child
. This child
is the widget that will be wrapped by the LocationContext
to receive the location updates. We also then create the skeleton of the actual State
class.
Let's start fleshing out the state by adding in our member variables. Add the following inside of _LocationContextWrapperState
// initialize our Location through our injector
final Location _location = _createLocation();
String _error;
Position _currentLocation;
Position _lastLocation;
StreamSubscription<Map<String, double>> _locationChangedSubscription;
First, we use our "injector" from Step 4.1 to initialize our Location object _location
. This is what makes our unit tests work.
You'll notice some similarities between our next three variables and what our LocationContext
stores. That's because these (_error
, _currentLocation
, and _lastLocation
) are the values we are managing locally and passing to LocationContext
.
Lastly, _locationChangedSubscription
will be used to listen for location updates from our Location
object. Note that it is returning a Map
, so this value will need to be converted to a Position
instance before it is usable by our inherited widget.
Next, we're going to initialize our state. Continue adding code just below _locationChangedSubscription
.
void initState() {
super.initState();
_locationChangedSubscription =
_location.onLocationChanged().listen((Map<String, double> result) {
final Position nextLocation = Position._fromMap(result);
setState(() {
_error = null;
_lastLocation = _currentLocation;
_currentLocation = nextLocation;
});
});
initLocation();
}
We're really doing two things in this method. First, we're subscribing to the _location.onLocationChanged
stream to listen for location updates. When we receive an update, we create a new Position
instance from the Map, then we update our state by clearing the _error
, updating _lastLocation
, and setting _currentLocation
to the location we just received.
The second thing we do is call a method (initLocation
) to do our initial load of the location data. We're calling a method to do this so that we can use async
/await
to keep the code more linear, but we could also just use Future
s. Add the method below:
void initLocation() async {
try {
final Map<String, double> result = await _location.getLocation();
setState(() {
_error = null;
_lastLocation = Position._fromMap(result);
_currentLocation = _lastLocation;
});
} on PlatformException catch (e) {
setState(() {
if (e.code == 'PERMISSION_DENIED') {
_error = 'Location Permission Denied';
} else if (e.code == 'PERMISSION_DENIED_NEVER_ASK') {
_error =
'Location Permission Denied. Please open App Settings and enabled Location Permissions';
}
});
}
}
We await
the current location the device is reporting, then update our state like we did before. Since this is our first state, we're going to set _lastLocation
and _currentLocation
to the same value.
If an error is encountered while we're await
ing, then we catch it and update the state to set the _error
as needed.
Next, we need to make sure we're properly unsubscribing from the Stream. We're going to handle this in dispose
so it is called when the Widget is destroyed.
void dispose() {
_locationChangedSubscription?.cancel();
super.dispose();
}
We're using ?.
here for null safety, just in case something went wrong and we were never able to initialize the subscription. Mostly just being overly cautious.
Last, but not least, we're going to actually build something using this information. This is where we actually use the Inherited Widget that we're so concerned about.
build(BuildContext context) {
return LocationContext._(
lastLocation: _lastLocation,
currentLocation: _currentLocation,
error: _error,
child: widget.child,
);
}
Widget
Simply, we just build the Inherited Widget and pass it the data we have gathered on the State, as well as the child that we want it to wrap. This means whenever we get a new location and update our state, we're going to pass that new information to the LocationContext
which will, in turn, make it accessible to any widgets farther down the tree that want it.
Step 4.4: Wrap it Up
As we discussed at the beginning of Section 4.3, we're missing something crucial: a way to actually use _LocationContextWrapper
because it is package private, so we need to add a method to help us use it. We're going to add a new static
method to LocationContext
(our only outside-accessible member of this package) to do this.
Add the following above the of
method in LocationContext
.
static Widget around(Widget child, {Key key}) {
return _LocationContextWrapper(child: child, key: key);
}
And, that's it. This little method provides our interface for making all the magic happen. We've now built everything we need to share our location.
Step 5: Usage Example
Now that we've done all the work to create our Inherited Widget, create a new flutter app and replace the contents of lib/main.dart
with the following code. An explanation of what is going on can be found in the comments.
import 'package:flutter/material.dart';
// This is the package we just created.
// You can use a relative path to the location_context.dart file as well
import 'package:location_context/location_context.dart';
// To get this to work, you need to register for an API key to google maps. They're free.
const MAPS_API_KEY = '';
void main() => runApp(LocationContextExampleApp());
class LocationContextExampleApp extends StatelessWidget {
Widget build(BuildContext context) {
// BEHOLD! THE MAGIC!
// We're just going to wrap the entire app in our Inherited Widget.
// This way, every widget in the app will have access to the data.
return LocationContext.around(
// We're wrapping it around a normal MaterialApp.
// We can continue as normal, and it will just work.
MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MapViewPage(),
),
);
}
}
class MapViewPage extends StatelessWidget {
Widget build(BuildContext context) {
// Here's how we access the data from the InheritedWidget.
// Using our context, it's able to navigate up the Widget hierarchy until it finds the InheritedWidget
// Once it is found, it returns it and we can access the data inside
final LocationContext loc = LocationContext.of(context);
final Size size = MediaQuery.of(context).size;
final List<Widget> children = List();
// If we had an error fetching our location, display it in red
if (loc.error != null) {
children.add(Center(
child: Text('Error ${loc.error}', style: TextStyle(color: Colors.red)),
));
} else {
// Otherwise, load a static image of a map centered on the current location. Make sure to add a marker so we know where it is.
final Position pos = loc.currentLocation;
if (pos != null) {
Uri uri = Uri.https('maps.googleapis.com', 'maps/api/staticmap', {
'center': '${pos.latitude},${pos.longitude}',
'zoom': '18',
'size': '${size.width.floor()}x${size.height.floor()}',
'key': MAPS_API_KEY,
'markers': 'color:blue|size:small|${pos.latitude},${pos.longitude}',
});
children.addAll(<Widget>[
// Stretch the google map image
Expanded(
child: Image.network(uri.toString()),
),
// Add some text to tell us what our actual coordinates are
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(child: Center(child: Text('Latitude: ${pos.latitude}'))),
Expanded(child: Center(child: Text('Longitude: ${pos.longitude}'))),
],
),
]);
} else {
// There was no error, but also no currentLocation
children.add(Center(child: Text('Location Not Found')));
}
}
return Scaffold(
appBar: AppBar(
title: Text('Location Context Example'),
),
body: Column(
children: children,
),
);
}
}
In Conclusion
You've now built an Inherited Widget that shares Location data to all the widgets in your app that want it.
Thanks for reading! Happy inheriting!