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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Flutter’s InheritedWidgets: Getting Started
30 mins
- Getting Started
- Overview of Inherited Widgets
- What Is an InheritedWidget?
- Differences Between StatelessWidget, StatefulWidget, and InheritedWidget
- State Propagation and State Mananagment
- State Propagation with InheritedLocation
- State Management With LocationProvider
- Building the Location Picker
- Integrating the Location Picker
- Retrieving Location From the Platform
- Getting Current Location on iOS
- Getting Current Location on Android
- Displaying Current Weather Data
- Displaying Future Weather Data
- Where to Go From Here?
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.
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:
- Gets the latest location from the InheritedWidget.
- 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.
-
_locations
is a set ofLocationData
, and since it’s a set, equal items are replaced instead of appended like in a list. But what makes twoLocationData
equal? Open the source ofLocationData
, 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. Thename
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:
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:
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. :)