Flutter’s InheritedWidgets: Getting Started

Learn how to implement InheritedWidgets into your Flutter apps! In this tutorial, see how InheritedWidgets can be used to manage state with a weather app. By Wilberforce Uwadiegwu.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

State Management With LocationProvider

InheritedLocation is responsible for propagating the location state, but it’s immutable, so how do you update the location property? This is where the state management widget comes in. You’ll wrap InheritedLocation in LocationProvider, a StatefulWidget that will be responsible for managing the state of the location data.

Start by creating a location_provider.dart file in the location package. Then, create a corresponding widget inside it:

import 'package:flutter/material.dart';

class LocationProvider extends StatefulWidget {
  const LocationProvider({super.key});

  @override
  State<LocationProvider> createState() => LocationProviderState();
}

class LocationProviderState extends State<LocationProvider> {
  @override
  Widget build(BuildContext context) {
    return const Placeholder();
  }
}

Next, ensure that the LocationProvider accepts a child. Later, you’ll return this LocationProvider from the build() of MyApp (in main.dart) and pass the MaterialApp as this child property. For now, declare a static of() that returns LocationProviderState.

class LocationProvider extends StatefulWidget {
  final Widget child;

  const LocationProvider({super.key, required this.child});

  static LocationProviderState? of(BuildContext context) {
    return context.findAncestorStateOfType<LocationProviderState>();
  }

  ...
}

Why the static of()? Same reason for the same function in InheritedLocation — brevity! In the next step, you’ll add a method to update the location data in LocationProviderState. Hence, of() is just a simple and unified way to get a reference to this class so you can mutate its state.

Next, add these import statements:

import 'inherited_location.dart';
import 'location_data.dart';

Then, add a property of type LocationData to LocationProviderState and update its build() to return InheritedLocation:

LocationData? _location;

@override
Widget build(BuildContext context) {
  return InheritedLocation(
    location: _location,
    child: widget.child,
  );
}

Notice that _location is private; this is so you have InheritedLocation as the single source of truth on the latest location state.

Next, define updateLocation() below build():

void updateLocation(LocationData newLocation) {
  setState(() => _location = newLocation);
}

When a user chooses a new location, you’ll invoke this method. Consequently, all dependent widgets will be rebuilt, and the weather data for the new location will be fetched.

The final step for LocationProviderState is to initialize _location with the user’s current location using method channel.

Note: Method channel is a Flutter service used to call native platform-specific code. You can learn more about it in Platform-Specific Code With Flutter Method Channel: Getting Started.

In the coming steps, you’ll implement the iOS and Android part of the channel. For now, add these import statements:

import '../constants.dart';
import 'package:flutter/services.dart';

Then, add _getLocation() to LocationProviderState below updateLocation(), like so:

void _getLocation() async {
  try {
    final loc = await kMethodChannel
      .invokeMethod<Map<Object?, Object?>>('getLocation');
    updateLocation(LocationData.fromMap(loc!));
  } on PlatformException catch (e) {
    throw Exception('Failed to get location: ${e.message}');
  }
}

On the platform side, the location will be serialized to a Map before passing it over to Flutter. Hence, _getLocation() gets the location from the platform, deserializes it to LocationData, and calls updateLocation() with the data. kMethodChannel is a constant of MethodChannel in the constants.dart file in the root lib package.

So, where should you call _getLocation() to fetch the user location? It should be when LocationProviderState is initialized, so in the initState():

@override
void initState() {
  _getLocation();
  super.initState();
}

There will be no visual changes, but build and run to ensure that there are no compile-time errors.

Building the Location Picker

So far, you’re propagating the location state with InheritedLocation and managing the same state with LocationProvider. Now, you’ll wrap it all together in a widget that allows the user to pick their current location from a list augmented by a set of hardcoded ones.

So, create the location_picker.dart file in the location package, and add the code as shown below:

import 'location_data.dart';

final _locations = {
  const LocationData(
    lat: 51.509865,
    lng: -0.118092,
    name: 'London, UK',
  ),
  const LocationData(
    lat: -35.282001,
    lng: 149.128998,
    name: 'Canberra, Australia',
  ),
  const LocationData(
    lat: 35.652832,
    lng: 139.839478,
    name: 'Tokyo, Japan',
  ),
};

Next, import the materials package, like so:

import 'package:flutter/material.dart';

Then, create an empty StatefulWidget called LocationPicker below the import statement:

class LocationPicker extends StatefulWidget {
  const LocationPicker({super.key});

  @override
  State<LocationPicker> createState() => _LocationPickerState();
}

class _LocationPickerState extends State<LocationPicker> {
  @override
  Widget build(BuildContext context) {
    return const Placeholder();
  }
}

The build() of this _LocationPickerState will return two widgets: a small progress indicator when InheritedLocation has an invalid location, and a popup selector otherwise.

Start with the progress indicator; declare it below _LocationPickerState in the same file, as shown below:

class _ProgressIndicator extends StatelessWidget {
  
  const _ProgressIndicator({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const SizedBox.square(
      dimension: 10,
      child: Center(
        child: CircularProgressIndicator(
          color: Colors.white,
          strokeWidth: 2,
        ),
      ),
    );
  }
}

Below this widget, declare a _PopupButton:

class _PopupButton extends StatelessWidget {
  final LocationData location;
  const _PopupButton({Key? key, required this.location}): super(key: key);

  @override
  Widget build(BuildContext context) {
    return const Placeholder();
  }
}

The location property is the currently selected location.

Replace the Placeholder in _PopupButton with this PopupMenuButton:

Widget build(BuildContext context) {
  return PopupMenuButton<LocationData>(
    child: Padding(
      padding: const EdgeInsets.symmetric(vertical: 7, horizontal: 10),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          const Icon(Icons.location_on_outlined),
          const SizedBox(width: 5),
          Text(
            location.name,
            style: Theme.of(context).textTheme.titleMedium?.copyWith(
              color: Colors.white,
              fontWeight: FontWeight.w500,
            ),
          ),
          const SizedBox(width: 5),
          const Icon(Icons.keyboard_arrow_down)
        ],
      ),
    ),
    onSelected: (LocationData value) {
      // TODO: Implement
    },
    itemBuilder: (BuildContext context) {
      return _locations.map((LocationData item) {
        return PopupMenuItem<LocationData>(
          value: item,
          child: Text(item.name),
        );
      }).toList();
    },
  );
}

This will display a button with a location icon, the name of the current location, and a down-facing arrow. When you tap it, it’ll display a popup list of location names. You might’ve noticed that the onSelected() callback is empty; this is where you update LocationProvider with the selected location.

So, import 'location_provider.dart', and implement onSelected(), as seen below:

onSelected: (LocationData value) {
  LocationProvider.of(context)?.updateLocation(value);
}

Integrating the Location Picker

Still in the same location_picker.dart, import 'location_picker.dart', and replace the build() of _LocationPickerState with:

Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 15),
      child: Builder(
        builder: (ctx) {
          final location = InheritedLocation.of(context).location; // 1
          if (location == null || location.name.isEmpty) { // 2
            return const _ProgressIndicator();
          }

          _locations.add(location); // 3
          return Material(
            borderRadius: BorderRadius.circular(100),
            color: Colors.transparent,
            clipBehavior: Clip.antiAlias,
            child: _PopupButton(location: location),
          );
        },
      ),
    );
  }

Here’s what this code does:

  1. Gets the latest location from the InheritedWidget.
  2. A location is valid only if it’s not null and has a non-empty name. By this logic, the location obtained from the system’s location services is invalid for display here because it doesn’t have a name. In later steps, when the network call to OpenWeather returns, you’ll update the current location with the city name from the network response.
  3. _locations is a set of LocationData, and since it’s a set, equal items are replaced instead of appended like in a list. But what makes two LocationData equal? Open the source of LocationData, and you’ll see a custom equality operator that checks only the latitude and longitude. So, if both are the same for two given locations, it means they’re equal. The name of the location isn’t being used in the comparison because it doesn’t fit this use case.

Now, open home.dart in the lib package and import 'location/location_picker.dart'. Then, in the Scaffold of _HomeWidgetState, add a title property to the AppBar, and pass const LocationPicker() to it, as shown below:

...
appBar: AppBar(
    title: const LocationPicker(),
...

Build and run, and you’ll see this:

Starter project with an error

The maroon rectangle at the top indicates an error in the LocationPicker widget that you just added to the AppBar. And if you check the console of the IDE, you’ll also see a No InheritedLocation found in context error. This is because you’re not using LocationProvider yet, and the framework can’t find it in the build tree.

To fix this, open main.dart inside the lib package, and import 'location/location_provider.dart'. Then, wrap the MaterialApp in the build() of MyApp in with LocationProvider, like so:

Widget build(BuildContext context) {
  return LocationProvider(
    child: MaterialApp(
        title: 'Weather++',
        ...
        home: const HomeWidget(),
  ),
  );
}

Now, run the app. The maroon bar is gone, and you’ll see a small progress indicator at the top-left of the app:

App after adding the location picker

But why isn’t it displaying the location yet? Well, there are two reasons. One is that you haven’t implemented the platform parts of the method channel yet. Check the console, and you’ll actually see an Unhandled Exception: MissingPluginException(No implementation found for method getLocation on channel com.kodeco.weather_plus_plus) error. The other reason is that you aren’t hitting the API yet. So, you’ll now fix both. :)