/ intermediate

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.

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.

  @override
  bool operator ==(dynamic other) {
    if (other is! Position) return false;
    return hashCode == other.hashCode;
  }

  @override
  int get hashCode => _hashCode;

  @override
  String toString() {
    return 'Position($latitude, $longitude, $accuracy, $altitude, $speed, $speedAccuracy)';
  }

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.

  1. For unit testing, we need to be able to mock the Location object from the location package.
  2. We need to actually create the Inherited Widget to share the data we are storing.
  3. 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

@visibleForTesting
typedef Location LocationFactory();

@visibleForTesting
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._({
    @required this.currentLocation,
    this.lastLocation,
    this.error,
    Key key,
    Widget child,
  }) : super(key: key, child: child);

  static LocationContext of(BuildContext context) {
    return context.inheritFromWidgetOfExactType(LocationContext);
  }

  @override
  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);

  @override
  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.

  @override
  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 Futures. 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 awaiting, 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.

  @override
  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.

  @override
  Widget build(BuildContext context) {
    return LocationContext._(
      lastLocation: _lastLocation,
      currentLocation: _currentLocation,
      error: _error,
      child: widget.child,
    );
  }

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 {
  @override
  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 {
  @override
  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!

Brian Armstrong

A web application developer with 15+ years programming experience. Flutter evangelist, React.js enthusiast.

Read More