问题描述:
发展壮大离不开广大客户长期以来的信赖与支持,我们将始终秉承“诚信为本、服务至上”的服务理念,坚持“二合一”的优良服务模式,真诚服务每家企业,认真做好每个细节,不断完善自我,成就企业,实现共赢。行业涉及房屋鉴定等,在网站建设公司、网络营销推广、WAP手机网站、VI设计、软件开发等项目上具有丰富的设计经验。
在Flutter开发的过程中,当我们获取到新的数据或者数据发生变化,需要去执行setState进行页面刷新的时候,经常会出现不必要的子节点Widget也进行了build,但实际上我们是不想让它再次build,出现这些问题的典型情况是在使用FutureBuilder的时候,例如:
在上面这个示例中,如果再次调用Build方法,则会触发httpCall()的方法。
那么怎样才能避免不必要的部件构建呢?
分析:
在Flutter中,Build方法的设计方式是pure/without side effects,书面意思是无副作用的/纯粹的,简单点理解我们可以将其含义看作不会对外部的方法或者变量产生影响的。这是因为许多外部因素能够触发新的小部件的构建,例如这些情况:
但是,这也意味着Build方法可以不去触发httpCall()的方法或者不修改任何状态。
解决
回归问题,当前我们面临的问题是Build方法造成了副作用,也就是造成了无关的Build调用麻烦。
所以,只要我们使Build方法保持纯粹/无副作用,这样就算多少次调用它,也不会对其他Widget的Build方法产生影响。
在上面的示例中,我们将Widget转换为StatefulWidget,然后提取httpCall()到initState中,这样问题就解决了
另外,还可以使一个Widget能够在不强迫其子部件也构建的情况下进行重新构建。
在Widget的实例保持不变时;Flutter会有意识的不去重建子部件。这意味着我们可以缓存Widget树的某些部分,以防止不必要的重新构建。
最简单的方法是使用const修饰构造函数:
由于const的修饰,即使调用了数百次build,DecoratedBox的实例也将保持不变。
或者你可以这样使用以达到相同的结果:
在这个例子中,当StreamBuilder收到新值的通知时,即使StreamBuilder的Column进行了重构,subtree也不会进行重构。这是因为由于闭包,MyWidget的实例没有改变。
这种模式在动画中经常使用。典型的是使用AnimatedBuilder和所有的*Transition时,例如AlignTransition。
我们还可以将subtree存储到类的一个字段中,但是并不推荐你这样做,因为它会破坏Flutter的热重载。
状态可变的 widget 。
通过其类的定义能够看到 StatefulWidget 配置 StatefulElement 。
State 是 StatefulWidget 的内部逻辑与状态,由 StatefulWidget 的 createState 创建。
StatefulWidget 实例本身是不可变的, 但是 StatefulWidget 将其可变的状态,存储在与之关联的 State 对象中。
不管什么时候,只要在树中 mount 一个新的 StatefulElement ,必然需要注入一个 StatefulWidget ,注入一个 StatefulWidget 时, framework 都会调用一次 createState 方法。
其实,在 StatefulElement 构造的时候,就会调用 createState ,创建 _state 对象,( _state 是 StatefulElement 的变量)并且在 StatefulElement 的初始化方法中为 _state 关联当前的 StatefulElement 和用以配置 StatefulElement 的 StatefulWidget 。
StatefulElement 初始化方法如下:
这意味着如果 StatefulWidget 被插入到树中的多个位置,则会有多个 State 对象分别与它们关联。
关于此类的定义如下:
描述: 重写此方法以执行初始化。
场景: 如果 State 的 build 方法依赖于本身可以改变状态的对象时。(例如 ChangeNotifier 或 Stream ,或者可以订阅并接收通知的其他对象)正确的方式是:
注意点: 此方法中不能使用 BuildContext.dependOnInheritedWidgetOfExactType 。但是此方法被调用后会立即调用 didChangeDependencies ,在 didChangeDependencies 可以使用 BuildContext.dependOnInheritedWidgetOfExactType 。
调用时机: StatefulElement ,首次插入树中时会调用此方法,在 build 方法调用之前调用。
描述: StatefulElement 通过此方法返回的 widget 并通过调用 updateChild 来更新自己。
调用时机: framework 调用此方法的几个不同的场景如下:
描述: StatefulElement 存在,并且符合 Widget.canUpdate 的情况下对 StatefulWidget 进行更新。
调用时机: 不论何时只要 StatefulElement 的配置 widget 改变的时候就会调用。
注意: didUpdateWidget 方法最终会调用 build 方法,因此在此方法中调用 setState 是多余的。如果重写此方法,请确保调用 super.didUpdateWidget(oldWidget) 。
调用时机: 当此 State 对象的依赖项( InheritedWidget )更改时调用。
描述: 用于开发阶段 hot reload 。
调用时机: hot reload 时调用,调用后 build 方法也将被调用。无需在此方法中做任何操作。
调用时机: 当 StatefulElement 从树中移除的时候会调用。
调用时机: 当 StatefulElement 从树中 unmount 的时候会调用。
StatefulWidget 用以配置 StatefulElement ,但在这两者之间的 State 承接了 StatefulElement 的生命周期,而 StatefulWidget 仅仅只是连接了 State 与 StatefulElement 的不可变的实例,因此 StatefulWidget 的生命周期,依赖于 StatefulElement ,而 State 却是其最简单直接的体现形式。
为了能更好的理解 StatefulWidget 的生命周期,我画了一张关于 State 、 StatefulElement 、 Component 、 Element 的关系图。
Flutter的数据存储分为三类
Preference相当于iOS的NSUserDefaults,其实也是按plist的方式存储的
step1:添加依赖
step2:pub get
step3:导入头文件
在path_provider中有三个获取文件路径的方法:
- getTemporaryDirectory()
://获取应用缓存目录,等同iOS的NSTemporaryDirectory()和Android的getCacheDir() 方法。
- getApplicationDocumentsDirectory():
//获取应用文件目录类似于iOS的NSDocumentDirectory和Android上的 AppData目录。
step1:添加依赖
step2:pub get
step3:导入头文件
Dart的 IO 库包含了文件读写的相关类,它属于 Dart 语法标准的一部分,所以通过 Dart IO 库,无论是 Dart VM 下的脚本还是 Flutter,都是通过 Dart IO 库来操作文件的,不过和 Dart VM 相比,Flutter 有一个重要差异是文件系统路径不同,这是因为Dart VM 是运行在 PC 或服务器操作系统下,而 Flutter 是运行在移动操作系统中,他们的文件系统会有一些差异。
Android 和 iOS 的应用存储目录不同, PathProvider 插件提供了一种平台透明的方式来访问设备文件系统上的常用位置。该类当前支持访问两个文件系统位置:
File代表一个整体的文件,他有三个构造函数,分别是:
文件读取本身有两种形式,一种是文本,一种是二进制。
2.2.1 读取文本内容
如果是文本文件,File提供了readAsString、readAsLines、readAsStringSync、readAsLinesSync方法,读取文本内容
readAsString 一次性读取所有文本
readAsLines 一行行的读取文本
结果返回的是一个List,list中表示文件每行的内容
readAsStringSync、readAsLinesSync同步读取文本
2.2.2 读取二进制内容
如果文件是二进制,那么可以使用readAsBytes或者同步的方法readAsBytesSync:
dart中表示二进制有一个专门的类型叫做Uint8List,他实际上表示的是一个int的List。
上面提到的读取方式,都是一次性读取整个文件,缺点就是如果文件太大的话,可能造成内存空间的压力。
所以File为我们提供了另外一种读取文件的方法,流的形式来读取文件.
示例
dart提供了open和openSync两个方法来进行随机文件读写:
写入和文件读取一样,可以一次性写入或者获得一个写入句柄,然后再写入。
一次性写入的方法有四种,分别对应字符串和二进制
句柄形式可以调用openWrite方法,返回一个IOSink对象,然后通过这个对象进行写入:
默认情况下写入是会覆盖整个文件的,但是可以通过下面的方式来更改写入模式:
虽然dart中所有的异常都是运行时异常,但是和java一样,要想手动处理文件读写中的异常,则可以使用try,catch:
我们还是以计数器为例,实现在应用退出重启后可以恢复点击次数。 这里,我们使用文件来保存数据:
1.引入PathProvider插件;在pubspec.yaml文件中添加如下声明:
执行 flutter pub get
2.实现如下
参考:
有时候我们不希望某个页面每次打开时都重新加载,比如就我们之前的Tabbar结构的页面,每当我们在切换Tab的时候都会执行 void initState() ,这就意味着页面每次都会重新渲染,之所以这样就是因为我们的 State 状态没有保存,如下图所示:
[没有状态保存效果图]
给当前 State 类添加一个扩展(这里就用扩展这个词吧,其实类似于iOS下的 Category ),一个系统的扩展类 AutomaticKeepAliveClientMixin ,并重写 wantKeepAlive 方法,让一个普通的 State 类,具有保存状态的能力。
在Dart语法中通过使用 with 关键字来添加扩展:
bool get wantKeepAlive = true; 之后,当前 State 就具备保存能力了,也就意味着重复切换Tab后, void initState() 就不会重复执行了(由原来的 viewWillAppear() 变成了 viewDidLoad() )。
按照上面方式修改后,发现切换Tab后 void initState() 依然重复执行了,这是为什么呐?这里我们看下我们之前 root_page.dart 里面是如何配置我们的tabbar结构的:
这里我们是通过一个 _viewControllers 的List,把4个子页面放在了里面,全局有一个 _currentIndex ,当 onTap 回调后后,更新 _currentIndex 的值,执行 setState () 后, body 对应的 widget 页面发生改变。而问题也就出在这里,当 body 部分发生改变时,根据Flutter的底层渲染逻辑,这里会移除掉之前的 Widget ,并重新创建新的 Widget ,我们之前在 _viewControllers 放的子页面,并不像iOS下是一个实例对象,存在就直接拿来使用。在Flutter 中 setState () 后界面会被重新绘制,而 body 部分只知道我要渲染一个什么样的 widget ,而该类型的 widget 每次都是会重新创建,这也就意味着我们在Tab切换时,每次都是重新创建,所以每次都执行了 initState() 。
显然我们现在的方式是不合理的,那在Flutter中如何管理这样的子页面,而避免重复渲染呐?
这就要用到一个新的部件了: PageView() ,内部的2个关键属性:
子页面切换通过 _controller.jumpToPage(index); 来实现。
这样子页面也就不会重新创建渲染了,我们的状态保存也就能正常实现了。
学习是一个循序渐进的过程,我们总是在踩坑中不断的前行,把坑填平了也就意味着我们在这个新的东西面前立了足,就可能进行更多为什么的探索了。
provider 是flutter 中的状态管理 开源库;
存储的数据对象 必须extends ChangeNotifier;下层widget 通过 Provider.of(context) 函数 获取model对象 ,并且可以建立依赖关系;当数据对象发生变化时,依赖的widget 会重新build,像不像InheritedWidget Provider 没错 下层widget就是 封装了InheritedWidget
主要 通过 Provider.ofT(context) 函数,来获取;
推荐使用 Provider.of而不是 Consumer,因为 listen默认为true,也就是说 默认 依赖于 持有数据model的widget 对应的element;
数据类 可继承的 ChangeNotifier,本身和privider框架 没有关系;
ChangeNotifier 是 flutter框架 提供的工具类, 用来实现一对多的订阅通知功能。