Flutter漫说:组件生命周期、State状态管理及局部重绘的实现(Inherit)
目录
生命周期
State改变时组件如何刷新
InheritedWidget
InheritedModel
InheritedNotifier
Notifier
生命周期
flutter的生命周期其实有两种:StatefulWidget和StatelessWidget。
这两个是flutter的两个基本组件,名称已经很好表明了这两个组件的功能:有状态和无状态。
(1)StatelessWidget
StatelessWidget是无状态组件,它的生命周期非常简单,只有一个build,如下:
class WidgetA extends StatelessWidget {@overrideWidget build(BuildContext context) {return ...;}
}
对于StatelessWidget来说只渲染一次,之后它就不再有任何改变。
由于无状态组件在执行过程中只有一个 build 阶段,在执行期间只会执行一个 build 函数,没有其他生命周期函数,因此在执行速度和效率方面比有状态组件更好。所以在设计组件时,要考虑业务情况,尽量使用无状态组件。
(2)StatefulWidget
StatelessWidget是有状态组件,我们讨论的生命周期也基本指它的周期,如图:
包含以下几个阶段:
createState
该函数为 StatefulWidget 中创建 State 的方法,当 StatefulWidget 被调用时会立即执行 createState 。
initState
该函数为 State 初始化调用,因此可以在此期间执行 State 各变量的初始赋值,同时也可以在此期间与服务端交互,获取服务端数据后调用 setState 来设置 State。
didChangeDependencies
该函数是在该组件依赖的 State 发生变化时,这里说的 State 为全局 State ,例如语言或者主题等,类似于前端 Redux 存储的 State 。
build
主要是返回需要渲染的 Widget ,由于 build 会被调用多次,因此在该函数中只能做返回 Widget 相关逻辑,避免因为执行多次导致状态异常,注意这里的性能问题。reassemble
主要是提供开发阶段使用,在 debug 模式下,每次热重载都会调用该函数,因此在 debug 阶段可以在此期间增加一些 debug 代码,来检查代码问题。
didUpdateWidget
该函数主要是在组件重新构建,比如说热重载,父组件发生 build 的情况下,子组件该方法才会被调用,其次该方法调用之后一定会再调用本组件中的 build 方法。
deactivate
在组件被移除节点后会被调用,如果该组件被移除节点,然后未被插入到其他节点时,则会继续调用 dispose 永久移除。
dispose
永久移除组件,并释放组件资源。
在StatelessWidget中,只要我们调用setState,就会执行重绘,也就是说重新执行build函数,这样就可以改变ui。
State改变时组件如何刷新
先来看看下方的代码:
class MyHomePage extends StatefulWidget {@override_MyHomePageState createState() => _MyHomePageState();
}class _MyHomePageState extends State<MyHomePage> {int _counter = 0;void _incrementCounter() {setState(() {_counter++;});}@overrideWidget build(BuildContext context) {return Scaffold(body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: [WidgetA(_counter),WidgetB(),WidgetC(_incrementCounter)],),),);}
}class WidgetA extends StatelessWidget {final int counter;WidgetA(this.counter);@overrideWidget build(BuildContext context) {return Center(child: Text(counter.toString()),);}
}class WidgetB extends StatelessWidget {@overrideWidget build(BuildContext context) {return Text('I am a widget that will not be rebuilt.');}
}class WidgetC extends StatelessWidget {final void Function() incrementCounter;WidgetC(this.incrementCounter);@overrideWidget build(BuildContext context) {return RaisedButton(onPressed: () {incrementCounter();},child: Icon(Icons.add),);}
}
我们有三个Widget,一个负责显示count,一个按钮改变count,一个则是静态显示文字,通过这三个Widget来对比比较页面的刷新逻辑。
上面代码中,三个Widget是在_MyHomePageState的build中创建的,执行后点击按钮可以发现三个Widget都刷新了。
在Flutter Performance面板上选中Track Widget Rebuilds即可看到
虽然三个Widget都是无状态的StatelessWidget,但是因为_MyHomePageState的State改变时会重新执行build函数,所以三个Widget会重新创建,这也是为什么WidgetA虽然是无状态的StatelessWidget却依然可以动态改变的原因。
所以:无状态的StatelessWidget并不是不能动态改变,只是在其内部无法通过State改变,但是其父Widget的State改变时可以改变其构造参数使其改变。实际上确实不能改变,因为是一个新的实例。
下面我们将三个组件提前创建,可以在_MyHomePageState的构造函数中创建,修改后代码如下:
class _MyHomePageState extends State<MyHomePage> {int _counter = 0;List<Widget> children;_MyHomePageState(){children = [WidgetA(_counter),WidgetB(),WidgetC(_incrementCounter)];}void _incrementCounter() {setState(() {_counter++;});}@overrideWidget build(BuildContext context) {return Scaffold(body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: children,),),);}
}
再次执行,发现点击没有任何效果,Flutter Performance上可以看到没有Widget刷新(这里指三个Widget,当然Scaffold还是刷新了)。
这是因为组件都提前创建了,所以执行build时没有重新创建三个Widget,所以WidgetA显示的内容并没有改变,因为它的counter没有重新传入。
所以,不需要动态改变的组件可以提前创建,build时直接使用即可,而需要动态改变的组件实时创建。
这样就可以实现局部刷新了么?我们继续改动代码如下:
class _MyHomePageState extends State<MyHomePage> {int _counter = 0;Widget b = WidgetB();Widget c ;_MyHomePageState(){c = WidgetC(_incrementCounter);}void _incrementCounter() {setState(() {_counter++;});}@overrideWidget build(BuildContext context) {return Scaffold(body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: [WidgetA(_counter),b,c],),),);}
}
我们只将WidgetB和WidgetC重新创建,而WidgetA则在build中创建。执行后,点击按钮WidgetA的内容改变了,查看Flutter Performance可以看到只有WidgetA刷新了,WidgetB和WidgetC没有刷新。
所以:通过提前创建静态组件build时直接使用,而build时直接创建动态Widget 这种方式可以实现局部刷新。
注意:
只要setState,_MyHomePageState就会刷新,所以WidgetA就会跟着刷新,即使count没有改变。比如上面代码中将setState中的_count++代码注释掉,再点击按钮虽然内容没有改变,但是WidgetA依然刷新。
这种情况可以通过InheritedWidget来进行优化。
InheritedWidget
InheritedWidget的作用什么?网上有人说是数据共享,有人说是用于局部刷新。我们看官方的描述:
Base class for widgets that efficiently propagate information down the tree.
可以看到它的作用是Widget树从上到下有效的传递消息,所以很多人理解为数据共享,但是注意这个“有效的”,这个才是它的关键,而这个有效的其实就是解决上面提到的问题。
那么它怎么使用?
先创建一个继承至InheritedWidget的类:
class MyInheriteWidget extends InheritedWidget{final int count;MyInheriteWidget({@required this.count, Widget child}) : super(child: child);static MyInheriteWidget of(BuildContext context){return context.dependOnInheritedWidgetOfExactType<MyInheriteWidget>();}@overridebool updateShouldNotify(MyInheriteWidget oldWidget) {return oldWidget.count != count;}
}
这里将count传入。重点注意要实现updateShouldNotify函数,通过名字可以知道这个函数决定InheritedWidget的Child Widget是否需要刷新,这里我们判断如果与之前改变了才刷新。这样就解决了上面提到的问题。
然后还要实现一个static的of方法,用于Child Widget中获取这个InheritedWidget,这样就可以访问它的count属性了,这就是消息传递,即所谓的数据共享(因为InheritedWidget的child可以是一个layout,里面有多个widget,这些widget都可以使用这个InheritedWidget中的数据)。
然后我们改造一下WidgetA:
class WidgetA extends StatelessWidget {@overrideWidget build(BuildContext context) {final MyInheriteWidget myInheriteWidget = MyInheriteWidget.of(context);return Center(child: Text(myInheriteWidget.count.toString()),);}
}
这次不用在构造函数中传递count了,直接通过of获取MyInheriteWidget,使用它的count即可。
最后修改_MyHomePageState:
class _MyHomePageState extends State<MyHomePage> {int _counter = 0;Widget a = WidgetA();Widget b = WidgetB();Widget c ;_MyHomePageState(){c = WidgetC(_incrementCounter);}void _incrementCounter() {setState(() {_counter++;});}@overrideWidget build(BuildContext context) {return Scaffold(body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: [MyInheriteWidget(count: _counter,child: a,),b,c],),),);}
}
注意,这里用MyInheriteWidget包装一下WidgetA,而且WidgetA必须提前创建,如果在build中创建则每次MyInheriteWidget刷新都会跟着刷新,这样updateShouldNotify函数的效果就无法达到。
执行,点击按钮,可以发现只有WidgetA刷新了(当然MyInheriteWidget也刷新了)。如果注释掉setState中的_count++代码,再执行并点击发现虽然MyInheriteWidget刷新了,但是WidgetA并不刷新,因为MyInheriteWidget的count并未改变。
下面我们改动一下代码,将WidgetB和C都放入MyInheriteWidget会怎样?
@overrideWidget build(BuildContext context) {return Scaffold(body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: [MyInheriteWidget(count: _counter,child: Column(children: [a,b,c],),),],),),);}
}
MyInheriteWidget的child是一个Column,将a、b、c都放在这下面。执行会发现依然是WidgetA刷新,B和C都不刷新。这是因为在B和C中没有执行MyInheriteWidget的of函数,就没有执行dependOnInheritedWidgetOfExactType,这样其实就没构成依赖,MyInheriteWidget就不会通知它们。
如果我们修改WidgetC,在build函数中添加一行MyInheriteWidget.of(context);那么虽然没有任何使用,依然能会跟着刷新,因为建立了依赖关系就会被通知。
InheritedWidget会解决多余的刷新问题,比如在一个页面中有多个属性,同样有多个Widget来使用这些属性,但是并不是每个Widget都使用所有属性。如果用最普通的实现方式,那么每次setState(无论改变哪个属性)都需要刷新这些Widget。但是如果我们用多个InheritedWidget来为这些Widget分类,使用相同属性的用同一个InheritedWidget来包装,并实现updateShouldNotify,这样当改变其中一个属性时,只有该属性相关的InheritedWidget才会刷新它的child,这样就提高了性能。
InheritedModel
InheritedModel是继承至InheritedWidget的,扩充了它的功能,所以它的功能更加强大。具体提现在哪里呢?
通过上面我们知道,InheritedWidget可以通过判断它的data是否变化来决定是否刷新child,但是实际情况下这个data可以是多个变量或者一个复杂的对象,而child也不是单一widget,而是一系列widget组合。比如展示一本书,数据可能有书名、序列号、日期等等,但是每个数据可能单独变化,如果用InheritedWidget,就需要每种数据需要一个InheritedWidget类,然后将使用该数据的widget包装,这样才能包装改变某个数据时其他widget不刷新。
但是这样的问题就是widget层级更加复杂混乱,InheritedModel就可以解决这个问题。InheritedModel最大的功能就是根据不同数据的变化刷新不同的widget。下面来看看如何实现。
首先创建一个InheritedModel:
class MyInheriteModel extends InheritedModel<String>{final int count1;final int count2;MyInheriteModel({@required this.count1, @required this.count2, Widget child}) : super(child: child);static MyInheriteModel of(BuildContext context, String aspect){return InheritedModel.inheritFrom(context, aspect: aspect);}@overridebool updateShouldNotify(MyInheriteModel oldWidget) {return count1 != oldWidget.count1 || count2 != oldWidget.count2;}@overridebool updateShouldNotifyDependent(MyInheriteModel oldWidget, Set<String> dependencies) {return (count1 != oldWidget.count1 && dependencies.contains("count1")) ||(count2 != oldWidget.count2 && dependencies.contains("count2"));}
}
这里我们传入两个count,除了实现updateShouldNotify方法,还需要实现updateShouldNotifyDependent方法。这个函数就是关键,可以看到我们判断某个数据是否变化后还判断了dependencies中是否包含一个关键词:
count1 != oldWidget.count1 && dependencies.contains("count1")
这个关键词是什么?从哪里来?后面会提到,这里先有个印象。
然后同样需要实现一个static的of函数来获取这个InheritedModel,不同的是这里获取的代码变化了:
InheritedModel.inheritFrom(context, aspect: aspect);
这里的aspect就是后面用到的关键字,而inheritFrom会将这个关键字放入dependencies,以便updateShouldNotifyDependent来使用。后面会详细解释这个aspect完整作用。
然后我们改造WidgetA:
class WidgetA extends StatelessWidget {@overrideWidget build(BuildContext context) {final MyInheriteModel myInheriteModel = MyInheriteModel.of(context, "count1");return Center(child: Text(myInheriteModel.count1.toString()),);}
}
可以看到,这里定义了aspect。
然后因为有两个count,所以我们再新增两个Widget来处理count2:
class WidgetD extends StatelessWidget {@overrideWidget build(BuildContext context) {final MyInheriteModel myInheriteModel = MyInheriteModel.of(context, "count2");return Center(child: Text(myInheriteModel.count2.toString()),);}
}class WidgetE extends StatelessWidget {final void Function() incrementCounter;WidgetE(this.incrementCounter);@overrideWidget build(BuildContext context) {return RaisedButton(onPressed: () {incrementCounter();},child: Icon(Icons.add),);}
}
这里可以看到WidgetD的aspect与WidgetA是不同的。
最后修改_MyHomePageState:
class _MyHomePageState extends State<MyHomePage> {int _counter = 0;int _counter2 = 0;Widget a = Row(children: [WidgetA(),WidgetD()],);Widget b = WidgetB();Widget c ;Widget e ;_MyHomePageState(){c = WidgetC(_incrementCounter);e = WidgetE(_incrementCounter2);}void _incrementCounter() {setState(() {_counter++;});}void _incrementCounter2() {setState(() {_counter2++;});}@overrideWidget build(BuildContext context) {return Scaffold(body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: [MyInheriteModel(count1: _counter,count2: _counter2,child: a,),b,c,e],),),);}
}
WidgetD和E是处理count2的,A和C则是处理count。而MyInheriteModel的child不是单一Widget,而是一个Row,包含WidgetD和A。
执行代码,可以发现点击WidgetC的时候,只有WidgetA刷新了(当然MyInheriteModel也刷新);而点击WidgetD的时候,只有WidgetE刷新了。这样我们就实现了MyInheriteModel中的局部刷新。
其实原理很简单,aspect就相当于一个标记,当我们通过InheritedModel.inheritFrom(context, aspect: aspect);获取MyInheriteModel时,实际上将本Widget依赖到MyInheriteModel,并且将这个Widget标记。这时候如果data改变,遍历它的所有依赖时,会通过每个依赖的Widget获取它对应的标记集dependencies,然后触发updateShouldNotifyDependent判断该Widget是否刷新。
所以在InheritedModel(其实是InheritedElement)中存在一个map,记录了每个依赖的Widget对应的dependencies,所以一个Widget可以有多个标记,因为dependencies是一个Set,这样就可以响应多个数据的变化(比如多个数据组成一个String作为文本显示)。
上面其实可以用两个InheritedWidget也可以实现,但是布局越复杂,就需要越多的InheritedWidget,维护起来也费时费力。
所以可以看到InheritedModel使用更灵活,功能更强大,更适合复杂的数据和布局使用,并且通过细分细化每一个刷新区域,使得每次刷新都只更新最小区域,极大的提高了性能。
InheritedNotifier
InheritedNotifier同样继承至InheritedWidget,它是一个给Listenable的子类的专用工具,它的构造函数中要传入一个Listenable(这是一个接口,不再是之前的各种数据data),比如动画(如AnimationController),然后其依赖的组件则根据Listenable进行更新。
首先还是先创建一个InheritedNotifier:
class MyInheriteNotifier extends InheritedNotifier<AnimationController>{MyInheriteNotifier({Key key,AnimationController notifier,Widget child,}) : super(key: key, notifier: notifier, child: child);static double of(BuildContext context){return context.dependOnInheritedWidgetOfExactType<MyInheriteNotifier>().notifier.value;}
}
这里提供的of函数则直接返回AnimationController的value即可。
然后创建一个Widget:
class Spinner extends StatelessWidget {@overrideWidget build(BuildContext context) {return Transform.rotate(angle: MyInheriteNotifier.of(context) * 2 * pi,child: Text("who!!"),);}
}
内容会根据AnimationController进行旋转。
修改WidgetA:
class WidgetA extends StatelessWidget {@overrideWidget build(BuildContext context) {return Center(child: Text("WidgetA"),);}
}
然后修改_MyHomePageState:
class _MyHomePageState extends State<MyHomePage6> with SingleTickerProviderStateMixin {AnimationController _controller;@overridevoid initState() {super.initState();_controller = AnimationController(vsync: this,duration: Duration(seconds: 10),)..repeat();}@overrideWidget build(BuildContext context) {return Scaffold(body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: [WidgetA(),MyInheriteNotifier(notifier: _controller,child: Spinner()),],),),);}
}
运行会看到Text在不停的旋转,当然如果有其他Widget可以看到并不跟着刷新。
总之InheritedNotifier是一个更细化的工具,聚焦到一个具体场景中,使用起来也更方便。
Notifier
最后再简单介绍一下Notifier,考虑一个需求:页面A是列表页,而页面B是详情页,两个页面都有点赞操作和显示点赞数量,需要在一个页面点赞后两个页面的数据同时刷新。这种情况下就可以使用flutter提供另外一种方式——Notifier。
Notifier其实就是订阅模式的实现,主要包含ChangeNotifier和ValueNotifier,使用起来也非常简单。通过addListener和removeListener进行订阅和取消订阅(参数是无参无返回值的function),当数据改变时调用notifyListeners();通知即可。
ValueNotifier是更简单的ChangeNotifier,只有一个数据value,可以直接进行set和get,set时自动执行notifyListeners(),所以适合单数据的简单场景。
当时注意Notifier只是共享数据并通知变化,并不实现刷新,所以还要配合其他一并实现。比如上面的InheritedNotifier(因为Notifier都继承Listenable接口,所以两个可以很简单的配合使用),或者第三方库Provider(web开发的习惯)等等。
源码
关注公众号:BennuCTech,发送“Inherite1”获取源码。
Flutter漫说:组件生命周期、State状态管理及局部重绘的实现(Inherit)相关推荐
- React 重温之 组件生命周期
生命周期 任何事物都不会凭空产生,也不会无故消亡.一个事物从产生到消亡经理的各个阶段,我们称之为 生命周期. 具体到我们的前端组件上来,一个组件的生命周期可以大体分为创建.更新.销毁这个三个阶段. 本 ...
- Ext js 2.0 Overview(3) 组件生命周期
Component Life Cycle(组件生命周期) In general, the Component architecture in 2.0 will "just work.&quo ...
- day4 vue 学习笔记 组件 生命周期 数据共享 数组常用方法
系列文章目录 day1学习vue2笔记 vue指令 day2 学习vue2 笔记 过滤器 侦听器 计算属性 axios day3 vue2 学习笔记 vue组件 day4 vue 学习笔记 组件 生命 ...
- React心得之降龙十八掌:第三式-见龙在田( 组件生命周期详解)
引言 (乾卦九二)<彖>曰:"'见龙在田',德施普也.""见龙在田,利见大人." 在傅佩荣<自我的觉醒>中这样说道,见龙在田:龙出现在地 ...
- Android开发之旅:组件生命周期(二)
引言 应用程序组件有一个生命周期--一开始Android实例化他们响应意图,直到结束实例被销毁.在这期间,他们有时候处于激活状态,有时候处于非激活状态:对于活动,对用户有时候可见,有时候不可见.组件生 ...
- 组件生命周期管理和通信方案
随着移动互联网的快速发展,项目的迭代速度越来越快,需求改变越来越频繁,传统开发方式的工程所面临的一些,如代码耦合严重.维护效率低.开发不够敏捷等问题就凸现了出来.于是越来越多的公司开始推行" ...
- 学习:组件生命周期(1)
引言 应用程序组件有一个生命 周期--一开始Android实例化他们响应意图,直到结束实例被销毁.在这期间,他们有时候处于激活状态,有时候处于非激活状态:对于活动,对用户有时 候可见,有时候不可见.组 ...
- java不同进程的相互唤醒_Java线程生命周期与状态切换
前提 最近有点懒散,没什么比较有深度的产出.刚好想重新研读一下JUC线程池的源码实现,在此之前先深入了解一下Java中的线程实现,包括线程的生命周期.状态切换以及线程的上下文切换等等.编写本文的时候, ...
- Taro+react开发(45)taro中组件生命周期
组件生命周期# 每一个组件都有几个你可以重写以让代码在处理环节的特定时期运行的"生命周期方法".方法中带有前缀 will 的在特定环节之前被调用,而带有前缀 did 的方法则会在特 ...
最新文章
- usaco party lamps
- java的reflection
- bash脚本一条命令直接发送http请求
- java dateutils_Java DateUtils java时间工具类 kaki的博客
- CentOS7.2中安装rabbitmq
- Extjs中三种不同的数据提交方式
- 零基础学python-5.6 数字位操作与其它工具
- 【单片机仿真】(五)寻址方式 — 立即寻址与寄存器间接寻址
- 机械制造技术类毕业论文文献都有哪些?
- navigate实现页面跳转及传参
- 本博客博文介绍和索引【花谢悦神】
- 聊聊手机之--小米6
- 今天才知道我们的遭遇不是个例,是不是真有四十大盗呢?
- Telegram、Telethon
- vue 模拟随机变速的动态打字特效【支持多行文本】(含css实现闪烁光标,js动态改变setInterval定时器的时间间隔)
- 微信公众号里打开链接下载APP
- 三十四、Zabbix-触发器、动作及邮件报警
- system pause
- zbb20180930 Postman 使用方法详解
- C++语法——详解智能指针的概念、实现原理、缺陷