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?
Retrieving Location From the Platform
You’ll use the method channel to get a one-off location each time the app opens. The linked tutorial already provides a detailed explanation of the method channel, so it’s glossed over in this tutorial.
Getting Current Location on iOS
Open Runner.xcworkspace in the starter project’s ios folder with Xcode. Then, open AppDelegate.swift inside the Runner directory.
On iOS, CoreLocation
contains the classes and protocols needed to retrieve the location from the OS. So, add import CoreLocation
below the import
statement for Flutter
.
Next, make AppDelegate
adopt CLLocationManagerDelegate
, like so:
@objc class AppDelegate: FlutterAppDelegate, CLLocationManagerDelegate {
This is so you can implement the methods required to get the current location.
Next, add these three properties to the application()
above, like so:
private let channelName = "com.kodeco.weather_plus_plus" // 1
private var locManager: CLLocationManager! // 2
private var flutterResult: FlutterResult! // 3
Here’s an explanation of the code above:
-
channelName
is the unique name for the method channel. - You’ll use
locManager
to request the current location. - You’ll use
flutterResult
to inform Flutter when you get the location.
Now, inside application()
, below the GeneratedPluginRegistrant
line, set up the method channel, like so:
let controller = window?.rootViewController as! FlutterViewController
let channel = FlutterMethodChannel(name: channelName, binaryMessenger: controller.binaryMessenger)
channel.setMethodCallHandler({
[weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
self?.flutterResult = result
switch call.method {
case "getLocation":
self?.getLocation()
default:
result(FlutterMethodNotImplemented)
}
})
Basically, this is saying that when the Flutter side calls "getLocation"
on the method channel, execute the getLocation()
in this class.
The next steps are to implement the getLocation()
, set up locManager
, and request the location within it. Add the following code below the application
function, as shown below:
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
...
...
}
private func getLocation() {
locManager = CLLocationManager()
locManager.delegate = self
locManager.desiredAccuracy = kCLLocationAccuracyBest
locManager.requestWhenInUseAuthorization()
locManager.requestLocation()
}
Next, implement didUpdateLocations
below the getLocation
:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
let location = locations[0] as CLLocation
let latLng = ["lat": location.coordinate.latitude, "lng": location.coordinate.longitude]
flutterResult(latLng)
}
In the code above, when you get a new location, you serialize its longitude and latitude to a Dictionary
(equivalent to Dart’s Map) and send it to Flutter.
In line with Murphy’s law, which states that all things that can go wrong will go wrong, it’s crucial to handle potential errors in your code. Handle the location error by implementing didFailWithError
below the previous function:
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
let fError = FlutterError(code: "LOCATION_ERROR", message: "Failed to get location", details: error)
flutterResult(fError)
}
Like the serialized location data, this sends the error to Flutter.
The final step is to declare your intent to use the user’s location in info.plist. Open the file and add this declaration to it after the last key-value pair inside the dict
node:
<key>NSLocationWhenInUseUsageDescription</key>
<string>Needs access to location to display weather information.</string>
Build and run on iOS, and you’ll see this:
There are no visual differences between this and the previous screenshot, but you’ll notice that the MissingPluginException
is no longer being thrown.
Getting Current Location on Android
You’ll repeat the above steps, but for Android. So start by opening the android directory in the starter project with Android Studio.
Open the MainActivity
, and add these import
statements:
import io.flutter.plugin.common.MethodChannel
import io.flutter.embedding.engine.FlutterEngine
Then, declare these variables inside the MainActivity
class:
private val channelName = "com.kodeco.weather_plus_plus"
private var flutterResult: MethodChannel.Result? = null
Below these variables, override configureFlutterEngine()
, and set up the method channel within it:
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
val messenger = flutterEngine.dartExecutor.binaryMessenger
val channel = MethodChannel(messenger, channelName)
channel.setMethodCallHandler { call, result ->
this.flutterResult = result
when (call.method) {
"getLocation" -> getLocation()
else -> result.notImplemented()
}
}
}
Executing getLocation on the method channel from Flutter calls the getLocation()
in this MainActivity
.
So, declare this getLocation()
after configureFlutterEngine
, and leave it empty for now:
private fun getLocation() {
}
Unlike iOS, you’ll have to check and handle the location permission yourself. Therefore, import the RequestPermission
class, as shown below:
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
Then, write the code below inside MainActvity
after getLocation()
:
private val permissionLauncher =
registerForActivityResult(RequestPermission()) { granted: Boolean ->
if (granted) {
getLocation()
} else {
flutterResult?.error("LOCATION_ERROR", "Failed to get location permission", "")
}
}
Here, you’re calling getLocation()
if the user grants permission. Otherwise, you inform Flutter of the rejection.
In getLocation()
, you’ll confirm that the app has location permission; otherwise, you’ll request the permission with permissionLauncher
.
First, add these import
statements:
import android.Manifest.permission
import androidx.core.content.ContextCompat
import android.content.pm.PackageManager
import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.Priority
Then, update the getLocation
, as shown below:
private fun getLocation() {
val permissions = listOf(permission.ACCESS_FINE_LOCATION, permission.ACCESS_COARSE_LOCATION)
if (permissions.any {
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
}) {
permissionLauncher.launch(permission.ACCESS_FINE_LOCATION)
return
}
}
Below this logic, request the current location, as shown below:
private fun getLocation() {
...
val locationClient = LocationServices.getFusedLocationProviderClient(this)
locationClient.getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, null)
.addOnCompleteListener {
if (it.isSuccessful) {
val latLng =
hashMapOf(Pair("lat", it.result.latitude), Pair("lng", it.result.longitude))
flutterResult?.success(latLng)
} else {
flutterResult?.error("LOCATION_ERROR", "Failed to get location", it.exception)
}
}
}
Here, you’re requesting the current location from LocationServices
and notifying Flutter of a successful or an error response.
You’ll also need to declare your intent to use the user’s location on Android. Add these declarations to the app’s manifest file:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
Now, the manifest file will look like:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
...
Now, build and run the app. Again, there’ll be no visual differences, but you’ll notice that the MissingPluginException
won’t be thrown.
Next, you’ll fix the issues with the OpenWeather API and subsequently display the current weather data.
Displaying Current Weather Data
The app will populate the UI with weather data from OpenWeather using the weather conditions and forecast endpoints. Head over to the official documentation to get a free API key. Note that it takes about three hours or more for OpenWeather to activate the API key. Once you have the key, open the secrets.json file in the assets directory, and paste the key between the double quotes after openWeatherApiKey
, like so:
{
"openWeatherApiKey": "PASTE YOUR KEY HERE"
}
Every time the user picks a new location, InheritedLocation
triggers the didChangeDependencies()
in the widgets it depends on. Hence, this is where you’ll fetch the weather details for a new location. So, open home.dart file, and add an override for didChangeDependencies()
below build()
in _HomeWidgetState
:
@override
void didChangeDependencies() {
super.didChangeDependencies();
}
Leave it empty for now. The WeatherService
encapsulates the logic for fetching the weather data for a given location. To move things along, we’ve already implemented it for you, but you can take a look at the source code.
Now, add these import
statements to home.dart:
import 'weather/weather_service.dart';
import 'location/location_provider.dart';
import 'location/inherited_location.dart'
Then, declare the method that fetches the weather with WeatherService
:
void fetchCurrentWeather(LocationData? loc) async {
if (loc == null || lastLocation == loc) return; // 1
lastLocation = loc;
currentWeather = WeatherService.instance().getCurrent(loc);
final result = await currentWeather!;
if (context.mounted && loc.name.isEmpty) { // 2
LocationProvider.of(context)?.
updateLocation(loc.copyWith(name: result.name));
}
}
The code above:
- Ensures there’s an actual location change before requesting the data.
- Updates the location name if it doesn’t have a name. Recall from an earlier step that locations with no name are those coming from the method channel.
Next, call fetchCurrentWeather()
in didChangeDependencies()
, and pass the current location to it:
@override
void didChangeDependencies() {
fetchCurrentWeather(InheritedLocation.of(context).location);
...
}
Run the app, and it should display the weather for the current location:
Hooray!
Tap the location picker, select another location, and the UI will display the weather data for the new location.
By the way, did you see the placeholder widget flash before it showed the progress indicator? You no longer need the placeholder. To remove it, open home.dart, and remove this condition in the FutureBuilder
of _HomeWidgetState
:
if (sps.connectionState == ConnectionState.none) {
return const CurrentWeatherPlaceholderWidget();
}
Then, remove the import for 'weather/current_weather_placeholder.dart'
, and delete that file. Rerun, and the experience should be smoother now.