理论
关于状态提升的概念
当一个页面上有多个子组件时,每个子组件都是独立封装的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;
}
}
上面的代码中:
- 我们删掉了多次传递的color参数,
- 重新定义了一个 MyColor类,继承了InheritedWidget类,在其中定义了我们希望往下传递的属性
Color color1
- 在runApp根部用MyColor把MyApp包裹起来了,并且定义了color1的颜色值
- 在要使用color1的地方,编写代码:
MyColor? myColor = context.dependOnInheritedWidgetOfExactType<MyColor>();
获取MyColor对象,并且将其中的 color1的值使用到我们的Container中。
InheritedWidget
的简单用法就是如下,但是其中有几点要注意:
context.dependOnInheritedWidgetOfExactType<MyColor>()
的作用是,从当前节点往上查找,直到找到最近的一个MyColor对象为止。也就是说,如果我们在这颗widget树上定义了多个MyColor,那么只会使用其中最接近当前节点的一个(就近原则),如果一直查找到根节点都没有找到,那就会返回null。- 我们在定义 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 控制刷新的基础上,还可以细分刷新的具体场景,针对多个属性变化定制刷新策略,这个有空再写。