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 3 of 4 of this article. Click here to view the first page.

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

Note: You can skip this part if you’re not testing on iOS.

Open Runner.xcworkspace in the starter project’s ios folder with Xcode. Then, open AppDelegate.swift inside the Runner directory.

Xcode showing AppDelegate.swif

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:

  1. channelName is the unique name for the method channel.
  2. You’ll use locManager to request the current location.
  3. 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:


Screenshot of the starter project on iOS after adding the method change;

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

Note: You can skip this part if you’re not testing 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:

  1. Ensures there’s an actual location change before requesting the data.
  2. 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:

Current weather shown in app

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.