[Flutter - part 3] Passing Data Efficiently Across Widgets

[Flutter - part 3] Passing Data Efficiently Across Widgets

As our Flutter projects grow, passing and accessing data down the widget tree can quickly become cumbersome.

Let us take a simple example.

Imagine you have a Parent Widget that has a color property which a deeply nested Child Widget (E) needs to access.

Normally, you would have to pass this value through the constructors and build methods, of all intermediate widgets.

ParentWidget → WidgetB → WidgetD → WidgetE

WidgetB(color: color);
WidgetD(color: color);
WidgetE(color: color);

This approach is known as prop-drilling, and it leads to tight coupling between widgets. Every intermediate immediate becomes responsible for passing data it does not even use, and also, changes at the top may not tripper proper rebuilds at the bottom.

Since this is an issue, Flutter has provided us with Inherited Widget.

Inherited Widget allows us to easily propagate information down the widget tree.

Examples where it is used internally by Flutter widget is Theme, MediaQuery, and Navigator.

Example.

Let us create a simple project, using the following command:

flutter create PROJECT_name

In the main.dart, The following widget are defined

  1. Parent Widget
  2. Child WidgetA, WidgetB, WidgetC, WidgetD, and WidgetE

Here, we will be passing the property color down to E from the parent Widget. [Bad Example] - Without InheritedWidget

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: ParentWidget(color: Color(0xFF6EA2FF)),
    );
  }
}

class ParentWidget extends StatelessWidget {
  final Color color;
  const ParentWidget({super.key, required this.color});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          WidgetA(),
          WidgetB(color: color),
        ],
      ),
    );
  }
}

class WidgetA extends StatelessWidget {
  const WidgetA({super.key});

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

class WidgetB extends StatelessWidget {
  final Color color;
  const WidgetB({super.key, required this.color});

  @override
  Widget build(BuildContext context) {
    return WidgetD(color: color);
  }
}

class WidgetC extends StatelessWidget {
  const WidgetC({super.key});

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

class WidgetD extends StatelessWidget {
  final Color color;

  const WidgetD({super.key, required this.color});

  @override
  Widget build(BuildContext context) {
    return WidgetE(color: color);
  }
}

class WidgetE extends StatelessWidget {
  final Color color;

  const WidgetE({super.key, required this.color});

  @override
  Widget build(BuildContext context) {
    return Container(
      color: color,
      width: 200,
      height: 200,
      child: Text("Test"),
    );
  }
}

We are prop-drilling the color from parent to widget B, to D, then E, and each of these widget are now dependent of one another, i.e, tightly coupled, as you can see.

To solve this issue, we will be using Inherited Widget.

According to Flutter, Inherited widgets, when referenced in this way, will cause the consumer to rebuild when the inherited widget itself changes state.

Now, how does it work under the hood?

Inherited Widget, has a method called of which calls dependOnInheritedWidgetOfExactType in order to get the property from the build context.

Otherwise, we would have to call the context.inheritFromWidgetOfExactType for our descendants widget to gain access to color property in its build method. This is actually asking Flutter to go up the tree, starting from the build context, and to look for a widget that matches that type. And the of static method in the InheritedWidget simplifies that for us.

Good Example:

TestColorScope is created and placed at the top of the true so that all of its data is accessible to its descendants. Color is added as a field of TestColorScope.

class TestColorScope extends InheritedWidget {
  const TestColorScope({super.key, required this.color, required super.child});

  final Color color;

  static TestColorScope? maybeOf(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<TestColorScope>();
  }

  static TestColorScope of(BuildContext context) {
    final TestColorScope? result = maybeOf(context);
    assert(result != null, 'No Color found in context');
    return result!;
  }

  @override
  bool updateShouldNotify(TestColorScope oldWidget) => color != oldWidget.color;
}

TestColorScope have 2 methods, of and maybeOf which is called to check if a widget exists.

TestColorScope Widget, is wrapped with the Parent Widget so all descendants can access its colors.

class ParentWidget extends StatelessWidget {
  final Color color;
  ParentWidget({super.key, required this.color});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: TestColorScope(
        color: color,
        child: Column(children: [WidgetA(), WidgetB()]),
      ),
    );
  }
}

Widget E can now directly access the inherited color in its build method by calling TestColorScope.of(context).

class WidgetE extends StatelessWidget {
  const WidgetE({super.key});

  @override
  Widget build(BuildContext context) {
    final inheritedColor = TestColorScope.of(context);

    return Container(
      color: inheritedColor.color,
      width: 200,
      height: 200,
      child: Text("Test"),
    );
  }
}

Full Code

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: ParentWidget(color: Color(0xFF6EA2FF)),
    );
  }
}

class ParentWidget extends StatelessWidget {
  final Color color;
  ParentWidget({super.key, required this.color});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: TestColorScope(
        color: color,
        child: Column(children: [WidgetA(), WidgetB()]),
      ),
    );
  }
}

class WidgetA extends StatelessWidget {
  const WidgetA({super.key});

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

class WidgetB extends StatelessWidget {
  const WidgetB({super.key});

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

class WidgetC extends StatelessWidget {
  const WidgetC({super.key});

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

class WidgetD extends StatelessWidget {
  const WidgetD({super.key});

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

class WidgetE extends StatelessWidget {
  const WidgetE({super.key});

  @override
  Widget build(BuildContext context) {
    final inheritedColor = TestColorScope.of(context);

    return Container(
      color: inheritedColor.color,
      width: 200,
      height: 200,
      child: Text("Test"),
    );
  }
}

class TestColorScope extends InheritedWidget {
  const TestColorScope({super.key, required this.color, required super.child});

  final Color color;

  static TestColorScope? maybeOf(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<TestColorScope>();
  }

  static TestColorScope of(BuildContext context) {
    final TestColorScope? result = maybeOf(context);
    assert(result != null, 'No Color found in context');
    return result!;
  }

  @override
  bool updateShouldNotify(TestColorScope oldWidget) => color != oldWidget.color;
}

And that’s it! We’ve managed to decouple our widgets and make the data flow much cleaner with InheritedWidget.


References

InheritedWidget class - widgets library - Dart API
API docs for the InheritedWidget class from the widgets library, for the Dart programming language.

About Me

I am Zaahra, a Google Women Techmakers Ambassador who enjoy mentoring people and writing about technical contents that might help people in their developer journey. I also enjoy building stuffs to solve real life problems.

To reach me:

LinkedIn: https://www.linkedin.com/in/faatimah-iz-zaahra-m-0670881a1/

X (previously Twitter): _fz3hra

GitHub: https://github.com/fz3hra

Cheers,

Umme Faatimah-Iz-Zaahra Mujore | Google Women TechMakers Ambassador | Software Engineer