概述
本篇,将从UI元素操作演进的过程,慢慢过渡到Flutter框架的UI刷新方式,最后通过手写Flutter Provider的方式,解决关于Flutter状态管理的疑惑。
UI元素操作方式的演进
android:
findViewById(R.id.text).setText("xxxx")
javascript:
getElementById("elementName").text = ""
以上是安卓和 js下的UI元素操作方式,这属于是常规UI框架上传统的UI操作方式。相当于是 通过拿到元素对象之后,再去对其中的某些属性进行修改。类似“遥控器”,要操作某个元素,必须先拿到它的遥控器,也可以称之为“句柄”。
在安卓的MVC开发模式中经常见到很多的findViewById, 后来慢慢出现了ButterKnife, ViewBinding这些概念,不过都没有脱离遥控器模式的范畴,后来的, DataBinding 视图和元素实现了双向绑定,形成了 MVVM的开发模式,不再是遥控器模式,而是直接通过操作数据本身,就能影响到视图,而通过操作视图也能影响到数据,数据和视图实现了一体化。
而同样是谷歌的作品,Flutter在开发模式上进行了框架式的预定义,即 MVVM (改变数据之后setState)的方式 成为主流 ,在某些特殊的场景下,依然可以使用 遥控器(定义key,绑定到widget,然后 key.currentState?.xxxxxx();)的方式 。 只不过此时,MVVM的思维已经彻底成为主流。
SetState的原理
这是一段AndroidStudio自动生成的简单的计数器dart代码:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@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 {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.title)),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('You have pushed the button this many times:'),
Text('$_counter')
])),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
));
}
}
运行起来如下:
当 我们需要修改 计数器的数字 时,我们的正确操作方式应该是对_count变量进行更改,然后调用 setState :
setState(() {
_counter++;
});
之后,_MyHomePageState 的 build 则会重新执行。
容易忽略的是,在dart语言中,new 关键字被省略(但并不是不存在),其实每一个widget名称前头都有一个 new, 如果将上方的_MyHomePageState类补全new之后:
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(title: Text(widget.title)),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('You have pushed the button this many times:'),
new Text('$_counter')
])),
floatingActionButton: new FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
));
}
}
setState的原理,其实就是 重新执行build函数,将视图树结构重新创建。
(可能有人会有疑问,setState之后,并不是所有的widget都实际上去重建了,某些实际上不需要重建的 widget实际上它对应的element还是在重用,并没有造成太多的性能浪费,这里给出解释:确实Element重用了,但是这个只是 flutter框架在底层对代码的性能做出的兜底策略,让开发者即使写出烂代码,也能让app拥有不错的性能,而我们作为开发者,必须有写出优秀代码的追求!)
可是这样就导致一个问题,我一个 小小widget的数据变化,居然要引起 整棵 widget树的全部重建,这不合理,很大的资源浪费!
我改变的只是 Text('$_counter')这个widget,但是,我们可以从androidStudio右侧的FlutterPerformance中看到,其他的所有widget都在参与重建。
以上有多个widget都参与重绘了52次,而 我们理想中的操作,仅仅是 其中某一个Text被重绘52次而已。
如何解决setState造成的性能浪费?
const 关键字
上图中,icon并没有参与重绘, 观察代码:const Icon(Icons.add)它的创建方式是 const,而其他widget都是 new。
这两者的区别就是,const修饰的 widget会作为常量进行处理,也就是说,在整个widget树进行刷新的时候,它不参与销毁重建,而是进入缓存池,下次构建widget树时还是用同一个对象。
但是const有一定的局限性,被const修饰的 widget,不能接收外部传参。这种情况在我们必须传参构建一个widget的情况下就不推荐。
将 widget的创建过程不放在build内
这样确实可以绕过整棵树的刷新过程,比如:
final w = new Text('$_counter'),然后在 build内部直接用 w.
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
late final w = new Text('$_counter')
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(title: Text(widget.title)),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('You have pushed the button this many times:'),
w // 外部定义的text
])),
floatingActionButton: new FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
));
}
}
这样能达成目的,但是这样这个组件就永久不能更新了,因为它永远不会参与 widget树的重绘。
它相当于绕过了 Flutte框架对于 setState 做出的性能兜底,历史倒退了属于是。
而且如果我们在创建 它时需要使用context,也无法使用 build函数自带的context上下文。
Widget拆分
比如上面的 new Text('$_counter')这个组件,如果我们只希望它在自身范围内进行setState重绘,那么,我们可以将它独立成为一个 StatefulWidget,让它自己刷新自己,但是这样又会引起一个问题,右下角的操作+按钮, 与 它处于两个不同的widget中,那么这就涉及到 多个widget之间交流状态的过程。
下一章节细说。
如何使得不同的组件之间能够交流状态
子widget使用外部变量
这个是最简单的,比如我们定义一个Foo的Widget:
class Foo extends StatelessWidget {
final String content;
const Foo.name({Key? key, required this.content}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(content),
const FlutterLogo(size: 50, textColor: Colors.amber)
],
);
}
}
而父组件中只需要:const Foo(content: "外部传入的内容") 就能将内容传递进来。
子widget改变外部变量
比如我们想要在Foo中增加一个按钮,让它可以改变传入的值。此时,我们可以传递一个函数到Foo中,而这个函数的内容就是setState,如下:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@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 {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void add(int v) {
setState(() {
_counter += v;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('$_counter', style: Theme.of(context).textTheme.headline4),
Foo(content: "外部传入的内容", changeValueFunc: add)
])));
}
}
class Foo extends StatelessWidget {
final String content;
final void Function(int) changeValueFunc;
const Foo({Key? key, required this.content, required this.changeValueFunc})
: super(key: key);
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(content),
ElevatedButton(
onPressed: () => changeValueFunc(1), child: const Text("+1")),
ElevatedButton(
onPressed: () => changeValueFunc(2), child: const Text("+2")),
const FlutterLogo(size: 50, textColor: Colors.amber)
],
);
}
}
当按下内部的+按钮时,整棵widget树都将重建,每次重建都使用了最新的count值,UI也随之刷新。
外部组件改变子widget的状态
由于这次的子组件是一个有状态的,所以这次要使用 StatefulWidget 来定义Foo。要实现这个效果,我们要让子组件提供一个控制器给外部。
非常明显的案例就是,TextField这个组件,它给外界提供了一个 TextEditingController的控制器,可以由外部控制文本内容。
我们要实现的也是这种效果。
比如下方是一个简单的组件封装,可以自主改变logo:
class Foo extends StatefulWidget {
const Foo({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() {
return FooState();
}
}
class FooState extends State<Foo> {
double _logoSizeOffset = 20;
@override
Widget build(BuildContext context) {
return Container(
color: Colors.amber,
child: Column(
children: [
FlutterLogo(
size: _logoSizeOffset,
),
Slider(
value: _logoSizeOffset,
max: 100,
min: 20,
onChanged: (v) {
_logoSizeOffset = v;
setState(() {});
})
],
),
);
}
}
效果如下:
现在不只是需要它可以自己改变logo大小,还需要由外界控制logo大小,实现方式如下:
-
新建一个 ChangeNotify 实现类,实际上他就是一个控制器
这里有一些要素要注意:
-
必须继承 ChangeNotifier,它是 Listenable 的其中一个实现,作用是实现值的监听,当值有变化时,通知所有的监听者
-
在内部定义 Foo需要的所有变量,数量不限,可以很多个。
-
定义操作函数,比如
changeSize, 其中必须改变 变量的值,并且调用notifyListeners()去通知监听者。
class LogoSizeController extends ChangeNotifier { double logoSizeOffset = 20; LogoSizeController({required this.logoSizeOffset}); void changeSize(double size) { logoSizeOffset = size; notifyListeners(); } } -
-
将控制器放置到 Foo中,注意,现在logoSize的控制权不再是Foo内部的某个double变量,而是 LogoSizeController控制器对象。
-
Foo接收一个控制器对象
-
FooState内部使用控制器中的变量值
-
需要根据数据变化而重绘刷新的位置,则需要用
AnimatedBuilder(或者新版本里面的 ListenableBuilder)包裹,并设置 控制器对象,还要 定义builder函数,注意:这个builder所返回的 Widget树结构都会随着 控制器数据的改变而重绘。所以,没必要重绘的位置,不要包裹进来。
class Foo extends StatefulWidget { final LogoSizeController logoSizeController; const Foo({Key? key, required this.logoSizeController}) : super(key: key); @override State<StatefulWidget> createState() { return FooState(); } } class FooState extends State<Foo> { @override Widget build(BuildContext context) { return AnimatedBuilder( animation: widget.logoSizeController, builder: (BuildContext context, Widget? child) { return Container( color: Colors.amber, child: Column( children: [ FlutterLogo( size: widget.logoSizeController.logoSizeOffset, ), Slider( value: widget.logoSizeController.logoSizeOffset, max: 100, min: 20, onChanged: (v) { widget.logoSizeController.changeSize(v); }) ], ), ); }, ); } } -
-
使用Foo对象的地方,则需要创建控制器,并且设置给Foo,而只要拥有这个控制器对象,我们可以在任何位置(当前Widget,当前Widget的其他子组件)改变Foo的状态。
class MyHomePage extends StatefulWidget { const MyHomePage({Key? key, required this.title}) : super(key: key); final String title; @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { LogoSizeController logoSizeController = LogoSizeController(logoSizeOffset: 30); @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ ElevatedButton( onPressed: () { logoSizeController.changeSize(100); }, child: const Text("logoSize Max")), ElevatedButton( onPressed: () { logoSizeController.changeSize(20); }, child: const Text("logoSize Min")), Foo( logoSizeController: logoSizeController, ), ], ), ), ); } }
ChangeNotify的冷知识
本节详解上一篇提到的ChangeNotify类的基本用法。本节介绍两个小细节:
ValueNotifier
-
对于 单个值改变的监听,就像上面的logoSize,其实有更简单的写法 : ValueNotifier, 带泛型,需要传入初始值用法如下:
var fontSize = ValueNotifier<double>(20); // 定义控制器使用控制器时,则必须多调一个.value才能拿到值:
widget.cmmController.logoSize.value原理也十分简单,就是对我们常用场景的官方封装, 提供get方法以获取值,提供set方法改变值(排除值相同的情况),并通知到监听者。
class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> { ValueNotifier(this._value); @override T get value => _value; T _value; set value(T newValue) { if (_value == newValue) return; _value = newValue; notifyListeners(); } @override String toString() => '${describeIdentity(this)}($value)'; }
Listenable.merge
Listenable提供了merge方法,如果一个组件需要两个或者更多ValueNotifier中的值,则可以用这个merge将他们共同监听,只要有一个发生变化,这个组件都会刷新。
用法如下:
class FooState extends State<Foo> {
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: Listenable.merge([
widget.cmmController.logoSize,
widget.cmmController.fontSize,
]),
builder: (BuildContext context, Widget? child) {
return Container(
//...
);
},
);
}
}
完整代码如下
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final CommController _commController = CommController();
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
onPressed: () {
_commController.logoSize.value = 100;
},
child: const Text("logoSize Max")),
ElevatedButton(
onPressed: () {
_commController.logoSize.value = 20;
},
child: const Text("logoSize Min")),
ElevatedButton(
onPressed: () {
_commController.fontSize.value += 10;
},
child: const Text("fontSize+10")),
ElevatedButton(
onPressed: () {
if (_commController.fontSize.value > 10) {
_commController.fontSize.value -= 10;
}
},
child: const Text("fontSize-10")),
Foo(cmmController: _commController),
],
),
),
);
}
}
class CommController {
var fontSize = ValueNotifier<double>(20);
var logoSize = ValueNotifier<double>(20);
}
class Foo extends StatefulWidget {
final CommController cmmController;
const Foo({Key? key, required this.cmmController}) : super(key: key);
@override
State<StatefulWidget> createState() {
return FooState();
}
}
class FooState extends State<Foo> {
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: Listenable.merge([
widget.cmmController.logoSize,
widget.cmmController.fontSize,
]),
builder: (BuildContext context, Widget? child) {
return Container(
color: Colors.amber,
child: Column(
children: [
Text(
"文字尺寸",
style: TextStyle(fontSize: widget.cmmController.fontSize.value),
),
FlutterLogo(
size: widget.cmmController.logoSize.value,
),
Slider(
value: widget.cmmController.logoSize.value,
max: 100,
min: 20,
onChanged: (v) {
widget.cmmController.logoSize.value = v;
})
],
),
);
},
);
}
}
InheritedWidget 继承式组件
下篇继续。