0

Flutter’s InheritedWidgets: Getting Started | Kodeco

#Flutters #InheritedWidgets #Started #Kodeco

In Flutter, you’ve likely come across third-party state management packages like Provider or Bloc. You’ve undoubtedly interacted with core utilities like Theme, Navigator, or even MediaQuery. All these tools have something in common: They’re powered by InheritedWidget, a foundational widget that propagates state down the widget tree. In this tutorial, you’ll harness the same power to complete the Weather++ app. And by the end, you’ll be able to answer the following questions:

  • What is InheritedWidget and how does it work?
  • How do you use InheritedWidget?
Note: This tutorial assumes you have prior knowledge of Dart and the Flutter framework. If you’re unfamiliar with Flutter, please see Getting Started with Flutter.

Getting Started

Download the project by clicking the Download materials button at the top or bottom of this tutorial. Unzip the project, and you’ll find two folders: starter and final. The final directory is the completed project, and the starter directory is the starter project where you’ll work from now on. Open the starter project with the latest version of Android Studio or Visual Studio Code, and you’ll find a similar project structure:

Project structure of starter project

The folders outlined in red are specific to this project, while the others are Flutter boilerplates.

  • assets/secrets.json: JSON file for storing keys like the API key; you’ll use this key to fetch the weather data in later steps.
  • lib/location: Contains Dart files for handling user location. You’ll create more files in this directory later.
  • lib/weather: Deals with weather-specific Dart files like widgets and data classes.
  • lib/constants.dart: Container for app-wide constants.
  • lib/home.dart: Contains widgets you see the first time you launch the app.
  • lib/main.dart: Initializes the app and wraps the HomeWidget in a MaterialApp.
  • lib/secrets.dart: Houses the logic that loads the secrets in assets/secrets.json.

Now, open pubspec.yaml and click the Pub get tab that appears in your IDE. Run the project to see this on your target emulator or device:

Empty starter project

This is the basic structure of the app. The white boxes are widget placeholders; you’ll replace them with real widgets as you progress with this tutorial.

Overview of Inherited Widgets

You’ll start this section by delving into the concept of InheritedWidget, understanding its definition, significance, and functionality. Later, you’ll explore how it differs from StatefulWidget and StatelessWidget.

What Is an InheritedWidget?

InheritedWidget is a foundational widget in the Flutter framework that enables efficient propagation of state down the widget tree. It uses the build context to share state and rebuilds dependent widgets when this state changes. Without InheritedWidget, if you were to share ThemeData — like light and dark mode — between a parent and a child widget, it would look like this:


class ParentWidget extends StatelessWidget {
  final ThemeData theme;

  const ParentWidget({Key? key, required this.theme}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ChildWidget(
      theme: theme,
    );
  }
}

class ChildWidget extends StatelessWidget {
  final ThemeData theme;

  const ChildWidget({Key? key, required this.theme}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return IconButton(
        onPressed: () {
          // TODO: Implement functionality to change the app theme.
        },
        icon: Icon(theme.brightness == Brightness.dark
            ? Icons.nightlight
            : Icons.sunny));
  }
}

With this approach, ThemeData would need to be passed manually to every widget that needs to access it. Not only does this introduce tight coupling, but widgets also won’t be rebuilt automatically when the theme changes, leading to potential inconsistencies in the UI. Utilizing the power of InheritedWidget, this is how child widgets will access the latest ThemeData:


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

  @override
  Widget build(BuildContext context) {
    return IconButton(
        onPressed: () {
          // TODO: Implement functionality to change the app theme.
        },
        icon: Icon(Theme.of(context).brightness == Brightness.dark
            ? Icons.nightlight
            : Icons.sunny));
  }
}

You don’t need to pass the ThemeData to this widget, and when the theme changes, it’ll automatically be rebuilt.

Differences Between StatelessWidget, StatefulWidget, and InheritedWidget

A StatelessWidget is immutable, meaning once you set its properties, it cannot be changed. It’s useful for static content that doesn’t need to be mutated by the widget itself. On the other hand, a StatefulWidget can be mutable, as it maintains a separate state object(s) that can be altered over the widget’s lifecycle. This makes it suitable for dynamic content that might change over time. In contrast, an InheritedWidget is a special kind of widget designed to efficiently propagate information down the widget tree. Instead of passing data manually to each widget, descendants of an InheritedWidget can access the data it holds directly, making it a powerful tool for state propagation and management.

State Propagation and State Mananagment

To exemplify and understand InheritedWidget, you’ll work on the Weather++ app, the project you downloaded in the preceding steps. Using InheritedWidget, you’ll build a location picker whose selection state will be readable and writable from any widget in the build tree. The app uses the selected location to fetch and display the current and future weather forecasts.

State Propagation with InheritedLocation

The first step to using an InheritedWidget is to subclass it. So, in the starter project, create the file inherited_location.dart inside the location package, and add the code below:


import 'package:flutter/widgets.dart';
import 'location_data.dart';

class InheritedLocation extends InheritedWidget {
  final LocationData? location;

  const InheritedLocation(
      {Key? key, required Widget child, required this.location})
      : super(key: key, child: child);
}

InheritedLocation is an InheritedWidget, and it’ll rebuild all dependent widgets when location changes. LocationData is a data container that holds the latitude, longitude, and name of a specific location. The first two properties are used to fetch the weather data from OpenWeather, a widely used service for accessing real-time weather data. The name, on the other hand, will be displayed in the location picker widget.

But how will InheritedLocation know when to rebuild? This is where updateShouldNotify() comes in. Override it below the constructor as shown below:


@override
bool updateShouldNotify(InheritedLocation oldWidget) {
  return oldWidget.location != location ||
      oldWidget.location?.name.isEmpty == true;
}

It will rebuild dependent widgets when location changes or if its name is empty. More on the empty condition later.

So how does InheritedLocation know its dependencies? context.dependOnInheritedWidgetOfExactType() is the answer. Calling it using the context of the dependent widget does two things. First, the framework registers the calling widget as a dependent of InheritedLocation. Secondly, it returns the reference to the instance of InheritedLocation.

For brevity, wrap that code in a little helper function called of() in the InheritedLocation class, below updateShouldNotify():


static InheritedLocation of(BuildContext context) {
  final result =
      context.dependOnInheritedWidgetOfExactType<InheritedLocation>();
  assert(result != null, 'No InheritedLocation found in context');
  return result!;
}

This way, you’ll use InheritedLocation.of(context) like Theme, Provider, and other widgets powered by InheritedWidget.