Flutter 状态管理解惑(2)InheritedWidget详解

302 阅读6分钟

理论

关于状态提升的概念

当一个页面上有多个子组件时,每个子组件都是独立封装的Widget,此时,子组件之间是平行的关系,所以各自的状态都是自己管理,他们之间彼此操作就相对困难。此时,我们只需要将多个子组件所需要相互影响的状态参数,提取到他们的父widget中,由父容器统一管理。这也就是 "状态提升".

继承式组件的概念

在实际开发中,状态会被提升到很高层次,而使用状态的地方可能是很深的子组件,此时,如果一个一个逐级传参,那么代码会写的很复杂,widget的构造函数会很多形参。Flutter给出的解决方案是, InheritedWidget,也就是继承式组件。

其实我们在不经意间已经使用了继承式组件,当我们写这样的代码时:

Theme.of(context).backgroundColor

或者:

MediaQuery.of(context).size.width

进入查看源码,其实内部都用到了InheritedWidget。两者的源代码分别是:

MediaQuery.of

static MediaQueryData of(BuildContext context) {
  assert(context != null);
  assert(debugCheckHasMediaQuery(context));
  return context.dependOnInheritedWidgetOfExactType<MediaQuery>()!.data;
}

Theme.of

static ThemeData of(BuildContext context) {
  final _InheritedTheme? inheritedTheme = context.dependOnInheritedWidgetOfExactType<_InheritedTheme>();
  final MaterialLocalizations? localizations = Localizations.of<MaterialLocalizations>(context, MaterialLocalizations);
  final ScriptCategory category = localizations?.scriptCategory ?? ScriptCategory.englishLike;
  final ThemeData theme = inheritedTheme?.theme.data ?? _kFallbackTheme;
  return ThemeData.localize(theme, theme.typography.geometryThemeFor(category));
}

代码实验室

实践是检验真理的唯一标准。下面将用几个实际案例来分析 InheritedWidget 的用法和原理。

基本用法

反面例子

先来一个反例,当我们需要从app根部就定义一个颜色变量,并且要在很深的子组件中使用颜色值时,我们的代码可能是这样:

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp(
    color: Colors.black,
  ));
}

class MyApp extends StatelessWidget {
  Color color;

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page', color: color),
    );
  }
}

class MyHomePage extends StatefulWidget {
  Color color;

  final String title;

  MyHomePage({super.key, required this.title, required this.color});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Foo(
              color: widget.color,
            ),
          ],
        ),
      ),
    );
  }
}

class Foo extends StatefulWidget {
  final Color color;

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

  @override
  State<StatefulWidget> createState() {
    return FooState();
  }
}

class FooState extends State<Foo> {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: widget.color,
      width: 100,
      height: 100,
    );
  }
}

显然,一个变量从树根传到叶子节点,中间层层传参,不仅代码难看,而且难以维护。

改良版

我们使用InheritedWidget来进行优化:

import 'package:flutter/material.dart';

void main() {
  runApp(MyColor(
    color1: Colors.greenAccent,
    child: 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(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  final String title;

  const MyHomePage({super.key, required this.title});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: const <Widget>[
            Foo(),
          ],
        ),
      ),
    );
  }
}

class Foo extends StatefulWidget {
  const Foo({super.key});

  @override
  State<StatefulWidget> createState() {
    return FooState();
  }
}

class FooState extends State<Foo> {
  @override
  Widget build(BuildContext context) {
    MyColor? myColor = context.dependOnInheritedWidgetOfExactType<MyColor>();

    return Container(
      color: myColor?.color1 ?? Colors.green,
      width: 100,
      height: 100,
    );
  }
}

class MyColor extends InheritedWidget {
  final Color color1;

  const MyColor({super.key, required super.child, required this.color1});

  @override
  bool updateShouldNotify(covariant InheritedWidget oldWidget) {
    return true;
  }
}

上面的代码中:

  1. 我们删掉了多次传递的color参数,
  2. 重新定义了一个 MyColor类,继承了InheritedWidget类,在其中定义了我们希望往下传递的属性 Color color1
  3. 在runApp根部用MyColor把MyApp包裹起来了,并且定义了color1的颜色值
  4. 在要使用color1的地方,编写代码:MyColor? myColor = context.dependOnInheritedWidgetOfExactType<MyColor>(); 获取MyColor对象,并且将其中的 color1的值使用到我们的Container中。

InheritedWidget的简单用法就是如下,但是其中有几点要注意:

  1. context.dependOnInheritedWidgetOfExactType<MyColor>() 的作用是,从当前节点往上查找,直到找到最近的一个MyColor对象为止。也就是说,如果我们在这颗widget树上定义了多个MyColor,那么只会使用其中最接近当前节点的一个(就近原则),如果一直查找到根节点都没有找到,那就会返回null。
  2. 我们在定义 MyColor extends InheritedWidget 这个类 的时候,必须重写一个函数 updateShouldNotify. 这个函数的作用是,在 MyColor 发生变化时,是否需要通知到使用到 MyColor中任意属性的 Widget。也就是说,就像上面直接return true时,只要发生变化,就会通知到 Foo去更新状态。

细说updateShouldNotify

它的作用很简单,但是实际使用的时候有一些情况会混淆概念。比如说,以下案例:

import 'package:flutter/material.dart';

void main() {
  runApp(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(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  final String title;

  const MyHomePage({super.key, required this.title});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  Color _color = Colors.blueAccent;

  @override
  Widget build(BuildContext context) {
    return MyColor(
      color1: _color,
      child: Scaffold(
        appBar: AppBar(title: Text(widget.title)),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Foo(),
              const SizedBox(height: 20),
              ElevatedButton(
                  onPressed: () {
                    setState(() {
                      _color = Colors.deepOrangeAccent;
                    });
                  },
                  child: const Text("改变MyColor"))
            ],
          ),
        ),
      ),
    );
  }
}

class Foo extends StatefulWidget {
  const Foo({super.key});

  @override
  State<StatefulWidget> createState() {
    return FooState();
  }
}

class FooState extends State<Foo> {
  @override
  Widget build(BuildContext context) {
    MyColor? myColor = context.dependOnInheritedWidgetOfExactType<MyColor>();

    return Container(
      color: myColor?.color1 ?? Colors.green,
      width: 100,
      height: 100,
    );
  }
}

class MyColor extends InheritedWidget {
  final Color color1;

  const MyColor({super.key, required super.child, required this.color1});

  @override
  bool updateShouldNotify(covariant InheritedWidget oldWidget) {
    return true;
  }
}

我们用一个按钮改变 MyColor中的颜色值,并且setState刷新, 我们可以看到方块的颜色值实际上发生了变化,但是这个并不属于 MyColor 中 updateShouldNotify return true发挥的作用。而是 我们在setState的时候,整个Foo都发生了重建。不信的话,我们把 return true改成 false,结果也是一样。

而我们想要color1发生变化,并且用 updateShouldNotify 来控制这种变化是否生效,正确的做法是: 给 Foo()前面加上const,让 setState时,这个Foo不参与重绘。

正确案例:

import 'package:flutter/material.dart';

void main() {
  runApp(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(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  final String title;

  const MyHomePage({super.key, required this.title});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  Color _color = Colors.blueAccent;

  @override
  Widget build(BuildContext context) {
    return MyColor(
      color1: _color,
      child: Scaffold(
        appBar: AppBar(title: Text(widget.title)),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              const Foo(),
              const SizedBox(height: 20),
              ElevatedButton(
                  onPressed: () {
                    setState(() {
                      _color = Colors.deepOrangeAccent;
                    });
                  },
                  child: const Text("改变MyColor"))
            ],
          ),
        ),
      ),
    );
  }
}

class Foo extends StatefulWidget {
  const Foo({super.key});

  @override
  State<StatefulWidget> createState() {
    return FooState();
  }
}

class FooState extends State<Foo> {

  @override
  void didChangeDependencies() {
     super.didChangeDependencies();
     debugPrint("didChangeDependencies");
  }

  @override
  Widget build(BuildContext context) {
    MyColor? myColor = context.dependOnInheritedWidgetOfExactType<MyColor>();

    return Container(
      color: myColor?.color1 ?? Colors.green,
      width: 100,
      height: 100,
    );
  }
}

class MyColor extends InheritedWidget {
  final Color color1;

  const MyColor({super.key, required super.child, required this.color1});

  @override
  bool updateShouldNotify(covariant MyColor oldWidget) {
    debugPrint('updateShouldNotify  ${oldWidget.color1} -> $color1');
    return oldWidget.color1 != color1;
  }
}

在updateShouldNotify中,只有在原始颜色值和新颜色值不同时,才允许通知Foo。

另外提及一个小细节:

FooState中有一个 didChangeDependencies 函数,它的作用是,当直接或者间接父级组件发生变化时,就算 Foo在创建时使用的时 const,这个 didChangeDependencies 也会被执行。注意,必须有变化,才会执行 didChangeDependencies ,而是否有变化,控制权在 MyColor 的 updateShouldNotify 的返回值上。

最终优化版

上面的基本上已经是完整写法,但是在代码可读性上还有待优化。

能够优化的部分是dependOnInheritedWidgetOfExactType这个函数,在使用时过长,我们可以参考 Theme.of()这种写法来获取MyColor对象。 优化后的结果如下:

import 'package:flutter/material.dart';

void main() {
  runApp(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(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  final String title;

  const MyHomePage({super.key, required this.title});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  Color _color = Colors.blueAccent;

  @override
  Widget build(BuildContext context) {
    return MyColor(
      color1: _color,
      child: Scaffold(
        appBar: AppBar(title: Text(widget.title)),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              const Foo(),
              const SizedBox(height: 20),
              ElevatedButton(
                  onPressed: () {
                    setState(() {
                      _color = Colors.deepOrangeAccent;
                    });
                  },
                  child: const Text("改变MyColor")),
              const SizedBox(height: 20),
              ElevatedButton(
                  onPressed: () {
                    setState(() {
                      _color = Colors.brown;
                    });
                  },
                  child: const Text("改变MyColor2"))
            ],
          ),
        ),
      ),
    );
  }
}

class Foo extends StatefulWidget {
  const Foo({super.key});

  @override
  State<StatefulWidget> createState() {
    return FooState();
  }
}

class FooState extends State<Foo> {
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    debugPrint("didChangeDependencies");
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      color: MyColor.maybeOf(context)?.color1 ?? Colors.black,
      width: 100,
      height: 100,
    );
  }
}

class MyColor extends InheritedWidget {
  final Color color1;

  const MyColor({super.key, required super.child, required this.color1});

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

  @override
  bool updateShouldNotify(covariant MyColor oldWidget) {
    debugPrint('updateShouldNotify  ${oldWidget.color1} -> $color1');
    return oldWidget.color1 != color1;
  }
}

拓展

InheritedWidget 还有一个 InheritedModel 子类,属于是高级版的 InheritedWidget。它在updateShouldNotify 控制刷新的基础上,还可以细分刷新的具体场景,针对多个属性变化定制刷新策略,这个有空再写。