StatefulWidget和StatelessWidget两点不同:
1、StatefulWidget可以拥有状态,这些状态在widget生命周期中是可以变的,而StatelessWidget是不可变的
2、StatefulWidget至少由两个类组成:
一个StatefulWidget类
一个State类,StatefulWidget本身是不变的,但是State类中持有的状态在Widget生命周期中可能会发生变化。
setState方法的作用是通知Flutter框架,有状态发生了变化,Flutter框架收到通知后,会执行build方法来根据新的状态重新构建界面。
为什么要把build方法放在State中,而不是放在StatefulWidget中?
1、状态访问不便,
2、继承StatefulWidget不便
路由Route通常指的是页面Page,Route在android中通常指的是Activity,在iOS中指的是ViewController。
MaterialPageRoute是Material组件库的一个Widget,他可以针对不同平台,实现与平台页面切换动画风格一致的路由切换动画:
1、android,当打开新页面时,新的页面会从屏幕底部滑动到屏幕顶部;当关闭页面时,当前页面会从屏幕顶部滑动到屏幕底部后消失,同时上一个页面会显示到屏幕上。
2、iOS,当打开页面时,新的页面会从屏幕右侧边缘一致滑动到屏幕左边,直到新页面全部显示到屏幕上,而上一个页面则会从当前屏幕滑动到屏幕左侧而消失;当页面关闭时,正好相反,当前页面会从屏幕右侧划出,同时上一个页面会从屏幕左侧滑入。
如果想自定义路由切换动画,可以自己继承PageRoute来实现。
Navigator类
pubspec.yaml 管理包
name。 应用或包名称
description。 应用或包描述/简介
version。 应用或包的版本号
dependencies。 应用或包依赖的其它包或插件
dev_dependencies。 开发环境依赖的工具包(而不是flutter应用本身依赖的包)
flutter。 flutter相关的配置选项
资源管理 assets
Dart在单线程中是以消息循环机制来运行的,其中包含两个任务队列,一个“微任务队列”microtask queue,另一个叫做“事件队列”event queue。微任务队列的执行优先级高于事件队列。在事件循环中,当某个任务发生异常并没有被捕获时,程序并不会退出,而直接导致的结果是当前任务的后续代码就不会执行了,也就是说一个任务的异常是不会影响其它任务执行的。
Widget与Element
在flutter中,Widget的功能是“描述一个UI元素的配置数据”,它就是说,Widget其实并不是表示最终绘制在设备屏幕上的显示元素,而只是显示元素的一个配置数据。实际上,flutter中真正代表屏幕上显示元素的类是element,也就是说Widget只是描述element的一个配置,一个widget可以对应多个element,这是因为同一个widget对象可以被添加到UI树的不同部分,而真正渲染时,UI树的每一个element节点都会对应一个widget对象。
State
一个StatefulWidget类会对应一个State类,State表示与其对应的StatefulWidget要维护的状态,State中保存的状态信息可以:
1、在widget build时可以被同步读取
2、在widget生命周期中可以被改变,当State被改变时,可以手动调用其setState()方法通知flutter framework状态发生改变,flutter framework在收到消息后,会重新调用其build方法重新构建widget树,从而达到更新UI的目的。
State中有两个常用属性:
1、widget 他表示与该State实例关联的widget实例,由flutter framework 动态设置。这种关联并非永久的,因为在应用生命周期中,UI树上的某一个节点的widget实例在重新构建时可能会变化,但State实例只会在第一次插入到树中时被创建,当在重新构建时,如果widget被修改了,flutter framework会动态设置State.widget为新的widget实例。
2、context 它是BuildContext类的一个实例,表示构建widget的上下文,它是操作widget在树中位置的一个句柄,它包含了一些查找、遍历当前Widget树的一些方法。每一个widget都有一个自己的context对象。
生命周期:
1、initState。当widget第一次插入到widget树时会被调用,只会调用一次。
2、didChangeDependences。当State对象的依赖发生变化时会被调用。
3、build。
调用时机:a、initState()之后
b、didUpdateWidget()之后
c、setState()之后
d、didChangeDependences()之后
e、在State对象从树中一个位置移除后(会调用deactivate)又重新插入到树的其它位置之后。
4、didUpdateWidget。在widget重新构建时,flutter framework会调用widget.canUpdate来检测widget树中同一位置的新旧节点,然后决定是否需要更新,如果返回true则会调用此回调。widget.canUpdate会在新旧widget的key和runtimeType同时相等时返回true,也就是说在新旧widget的key和runtimeType同时相等时didUpdateWidget就会被调用。
状态管理
1、widget管理自己的state
2、父widget管理子widget状态
3、混合管理(父widget和子widget都管理状态)
选择:
1、如果状态是用户数据,如复选框的选中状态/滑块位置,则该状态最好由父widget管理
2、如果状态是有关界面外观效果的,例如颜色、动画,那么状态最好由widget本身来管理
3、如果某一个状态是不同widget共享的则最好由它们共同的父widget管理
布局Widget
flutter中根据widget是否需要包含子节点将widget分为3类,分别对应3种element。
LeafRenderObjectWidget
SingleChildRenderObjectWidget
MultiChildRenderObjectWidget
Flutter中的很多Widget是直接继承自StatelessWidget或StatefulWidget,然后在build()方法中构建真正的RenderObjectWidget。如Text,它其实是继承自StatelessWidget,然后在build()方法中通过RichText来构建子树,而RichText才是继承自LeafRenderObjectWidget。其实StatelessWidget和StatefulWidget就是两个用于组合Widget的基类,它们本身并不会关联最终的渲染对象(RenderObjectWidget)。
线性布局Row、Column
弹性布局Flex Expanded(类似weight布局属性)
流式布局Wrap、Flow
Flow主要用于一些需要自定义布局策略或性能要求较高(如动画)的场景。有如下优点:
1、性能好:Flow是一个对child尺寸以及位置调整非常高效的控件,Flow用转换矩阵在对child进行位置调整的时候进行了优化:在Flow定位过后,如果child的尺寸或者位置发生了变化,在FlowDelegate中的paintChildren()方法中调用context.paintChild进行重绘,而context.paintChild在重绘时使用了转换矩阵,并没有实际调整Widget位置
2、灵活:由于我们需要自己实现FlowDelegate的paintChild()方法,所以我们需要自己设计计算每一个widget的位置,因此,可以自定义布局策略。
缺点:
1、使用复杂
2、不能自适应子Widget大小,必须通过指定父容器大小或实现TestFlowDelegate的getSize返回固定大小
容器Widget
容器类widget和布局类widget都作用于其子widget,不同的是:
1、布局类widget一般都需要接收一个widget数组(children),它们直接或者间接继承自(或包含)MultiChildRenderObjectWidget;而容器类widget一般只需要接收一个子widget(child),它们直接或间接继承自(或包含)SingleChildRenderObjectWidget。
2、布局类Widget是按照一定的排列方式来对其子widget进行排列;而容器类widget一般只是包装其子widget,对其添加一些修饰(填充或背景色等)、变换(旋转或剪裁等)、或限制(大小等)。
Padding
布局限制类容器ConstrainedBox、SizedBox,两者都是通过RenderConstrainedBox来渲染的,SizedBox是ConstrainedBox的一个定制。如果有多个ConstrainedBox限制时,对于minWidth、minHeight来说,取父子中相应数值较大的;对于maxWidth、maxHeight,取父子中相应数值较小的。
装饰容器DecoratedBox
变换Transform。 Transform的变换是在应用的绘制阶段,而不是在布局(layout)阶段。所以无论对子widget应用何种变化,其占用空间的大小和在屏幕上的位置都是固定不变的,因为这些是在布局阶段就确定的。
RotatedBox。 RotatedBox和Transform.rotate功能相似,它们都可以对子widget进行旋转变换,但有一点不同:RotatedBox的变换是在layout阶段,会影响在子widget的位置和大小。
Container。它本身不对应具体的RenderObject。它是上述等widget的一个组合widget,所以只需要通过一个Container可以实现同时需要装饰、变换、限制的场景。
通过Scaffold.of(context)可以获取父级最近的Scaffold Widget的State对象。flutter还有一种通用的获取StatefulWidget对象State的方法:通过GlobalKey来获取。步骤有两步:
-
给目标StatefulWidget添加GlobalKey
//定义一个globalKey, 由于GlobalKey要保持全局唯一性,我们使用静态变量存储
static GlobalKey<ScaffoldState> _globalKey= new GlobalKey();
...
Scaffold(
key: _globalKey , //设置key
...
)
-
通过GlobalKey来获取State对象
_globalKey.currentState…
TabBar和TabBarView联动
可滚动Widget
Scrollbar Scrollbar是一个Material风格的滚动指示器(滚动条),如果要给可滚动Widget添加滚动条,只需将Scrollbar作为可滚动widget的父widget即可。
SingleChildScrollView 类似于android中的ScrollView
GridView
CustomScrollView CustomScrollView是可以使用sliver来自定义滚动模型(效果)的widget。他可以包含多个滚动widget,将多个滚动widget在整个页面内实现统一的滑动效果。sliver通常指具有特定滚动效果的可滑动块,可滚动widget,如ListView、GridView等都有对应的sliver实现如SliverList、SliverGrid等。对于大多数Sliver来说,它们和可滚动Widget最主要的区别是Sliver不会包含Scrollable Widget,也就是说Sliver本身不包含滚动交互模型,正因为如此,CustomScrollView才可以将多个Sliver“粘”在一起,这些Sliver共用CustomScrollView的Scrollable,最终实现统一的滑动效果。CustomScrollView的子widget必须都是Sliver。
滚动位置恢复:PageStorage是一个用于保存页面(路由)相关数据的Widget,它并不会影响子树的UI外观。PageStorage是一个功能型widget,它拥有一个存储桶(bucket),子树中的widget可以通过指定不同的PageStorageKey来存储各自的数据或状态。每次滚动结束,Scrollable Widget都会将滚动位置offset存储到PageStorage中,当Scrollable Widget重新创建时再恢复。如果ScrollController.keepScrollOffset为false,则滚动位置将不会被存储,Scrollable Widget重新创建时会使用ScrollController.initialScrollOffset;ScrollController.keepScrollOffset为true时,Scrollable Widget在第一次创建时,会滚到initialScrollOffset处,因为这时还没有存储过滚动位置。在接下来的滚动中就会存储、恢复滚动位置,而initialScrollOffset会被忽略。
当一个路由中包含多个Scrollable Widget时,如果在进行一些跳转或切换操作之后,滚动位置不能正确恢复,这时可以通过显示指定PageStorageKey来分别跟踪不同Scrollable Widget的位置。不同的PageStorgeKey,需要不同的值,这样才可以区分保存的滚动位置。注意:一个路由中包含多个Scrollable Widget时,如果要分别跟踪他们的滚动位置,并非一定就得给他们分别提供PageStorageKey。这是因为Scrollable本身是一个StatefulWidget,它的状态中也会保存当前滚动位置。所以,只要Scrollable Widget本身没有被从树上detach掉,那么其state就不会销毁(dispose),滚动位置就不会丢失。只有当widget发生结构变化,导致Scrollable Widget的State销毁或者重新构建时才会丢失状态,这种情况就需要显示指定PageStorageKey,通过PageStorage来存储滚动位置,一个典型的场景是在使用TabBarView时,在Tab发生切换时,Tab页中的Scrollable Widget的State就会销毁,这是如果想恢复滚动位置就需要指定PageStorageKey。
ScrollController控制原理:
当ScrollController和Scrollable Widget关联时,Scrollable Widget首先会调用ScrollController的createScrollPosition()方法来创建一个ScrollPosition来存储滚动位置信息,接着,Scrollable Widget会调用attach()方法,将创建的ScrollPosition添加到ScrollController的positions属性中,这一步称为“注册位置”,只有注册后animateTo()和jumpTo()才可以被调用。当Scrollable Widget销毁时,会调用ScrollController的detach()方法,将其ScrollPosition对象从ScrollController的positions属性中移除,这一步称为“注销位置”,注销后animateTo()和jumpTo()将不能再被调用。需要注意的是,ScrollController的animateTo()和jumpTo()内部会调用所有ScrollPosition的animateTo()和jumpTo(),以实现所有和该ScrollController关联的Scrollable Widget都滚动到指定位置。
滚动监听
widget树中子widget可以通过发送通知(Notification)与父(包括祖先)widget通信。父widget可以通过NotificationListener来监听自己关注的通知,这种通信方式称为“事件冒泡”,Scrollable widget在滚动时会发送ScrollNotification类型的通知,ScrollBar正是通过监听滚动通知来实现的。通过NotificationListener监听滚动事件和通过ScrollController有两个主要的不同:
1、通过NotificationListener可以在从Scrollable widget到widget树根之间任意位置都能监听。而ScrollController只能和具体的Scrollable widget关联后才可以。
2、收到滚动事件后获得的信息不同:NotificationListener在收到滚动事件时,通知中会携带当前滚动位置和ViewPort的一些信息,而ScrollController只能获取当前滚动位置。
功能型Widget
导航返回拦截WillPopScope
数据共享InheritedWidget
主题Theme
事件处理与通知
flutter中的手势系统有两个独立的层。第一层为原始指针(pointer)事件,它描述了屏幕上指针(例如触摸、鼠标、触控笔)的位置和移动。第二层为手势,描述由一个或多个指针移动组成的语义动作,如拖动、缩放、双击等。
当指针按下时,flutter会对应用程序执行命中测试(hit test),以确定指针与屏幕接触的位置存在哪些widget,指针按下事件(以及该指针的后续事件)然后被分发到由命中测试发现的最内部的widget,然后从那里开始,事件会在widget树中向上冒泡,这些事件会从最内部的widget被分发到widget根的路径上的所有widget,也是“事件冒泡”。
手势识别GestureDetector
GestureDetector内部是使用一个或多个GestureRecognizer来识别各种手势的,而GestureRecognizer的作用就是通过Listener来将原始指针事件转换为语义手势,GestureDetector直接可以接收一个子widget。GestureRecognizer是一个抽象类,一种手势的识别器对应一个GestureRecognizer的子类。使用GestureRecognizer后一定要调用其dispose()方法来释放资源(主要是取消内部的计时器)。
手势冲突只是手势级别的,而手势是对原始指针的语义化的识别,所以在遇到复杂的场景冲突时,都可以通过Listener直接识别原始指针事件来解决冲突。
事件总线
Dart中实现单例模式的标准做法是使用static变量+工厂构造函数的方式。
动画
Animation
Curve
AnimationController
Ticker
Tween
AnimatedWidget
AnimatedBuilder AnimatedWidget可以从动画中分离出widget,而动画的渲染过程(即设置宽高)仍然在AnimatedWidget中,AnimatedBuilder把渲染逻辑分离出来。将外部引用child传递给AnimatedBuilder后AnimatedBuilder再将其传递给匿名构造器,然后将该对象用作其子对象,最终的结果是AnimatedBuilder返回的对象插入到widget树中。这样做的三个好处:
1、不用显式地去添加帧监听器,然后再调用setState()了,这个好处和AnimatedWidget是一样的
2、动画构建的范围缩小了,如果没有builder,setState()将会在父widget的上下文调用,这将导致父widget的build方法重新调用,而有了builder之后,只会导致动画widget的build重新调用,在复杂布局下性能会提高
3、通过AnimatedBuilder可以封装常见的过渡效果来复用动画。
FadeTransition
ScaleTransition
SizeTransition
FractionalTransition
AnimationStatus
自定义路由切换动画 1、PageRouteBuilder 2、直接继承PageRoute
默认:MaterialPageRoute、CupertinoPageRoute
Hero动画
Hero指的是可以在路由(页面)之间切换时,有一个共享的widget可以在新旧路由间切换,由于共享的widget在新旧路由页面上的位置、外观可能有所差异,所以在路由切换时会逐渐过渡,这样就产生一个Hero动画。实现Hero动画只需要用Hero Widget将要共享的widget包装起来,并提供一个相同的tag即可,framework知道新旧路由页中共享元素的位置和大小,根据这两个端点,在动画执行过程中自动求出过渡时的差值。
交错动画
复杂动画需要由一个动画序列或重叠的动画组成。需要注意:
1、要创建交错动画,需要使用多个动画对象
2、一个AnimationController控制所有动画
3、给每一个动画对象指定间隔(interval)
自定义Widget
三种方式:
1、组合现有widget
2、自绘(CustomPaint Canvas) 如果CustomPaint有子节点,为了避免子节点不必要的重绘并提高性能,通常情况下都会将子节点包裹在RepaintBoundary widget中,这样会在绘制时创建一个新的的绘制层(Layer),其子widget将在新的layer上绘制,而父widget将在原来layer上绘制,也就是说RepaintBoundary子widget的绘制将独立于父widget的绘制,RepaintBoundary会隔离其子节点和CustomPaint本身的绘制边界。
3、实现RenderObject RenderObject也是通过Canvas来绘制的,不同点在于:CustomPaint只是为了方便开发者封装的一个代理类,它直接继承自SingleChildRenderObjectWidget,通过RenderCustomPaint的paint方法将Canvas和画笔Painter连接起来实现最终绘制。
绘制是比较昂贵的操作,所以在实现自定义组件时应该考虑到性能开销,两条建议:
1、尽可能利用好shouldRepaint返回值;在UI树重新build时,控件在绘制前都会先调用该方法以确定是否有必要重绘;假如我们绘制的UI不依赖外部的状态,那么就应该始终返回false,因为外部状态改变导致重新build时不影响UI外观。如果绘制依赖外部状态,那么就应该在shouldRepaint中判断依赖的状态是否改变,如果已改变则应该返回true来重绘,反之返回false不重绘。
2、绘制尽可能多的分层。
文件操作与网络请求
PathProvider插件
Dio 网络库
web_socket_channel 库
Json转Model json_serializable 库
flutter中没有像java开发中的Gson/Jackson一样的序列化库,因为这样的库需要使用运行时反射,这在flutter中是禁用的。运行时反射会干扰Dart的tree shaking,使用tree shaking,可以在release版中“去除”未使用的代码,这可以显著优化应用程序的大小。由于反射会默认应用到所有代码,因此tree shaking会很难工作,因为在启用反射时很难知道哪些代码未被使用,因此冗余代码很难剥离,所以flutter禁用了Dart的反射功能。
element与buildContext
组件最终的layout、渲染都是通过RenderObject来完成的,从创建到渲染的大体流程是:根据widget生成element,然后创建相应的RenderObject并关联到element.renderObject属性上,最后再通过RenderObject来完成布局排列和组合。最终所有的element的renderObject构成一棵“渲染树”。
element生命周期:
1、framework调用widget.createElement创建一个element实例,记为element
2、framework调用element.mount(parentElement,newSlot),mount方法中首先调用element所对应widget的createRenderObject方法创建与element相关联的renderObject对象,然后调用element.attachRenderObject方法将element.renderObject添加到渲染树中插槽指定的位置(这一步不是必须的,一般发生在element树结构发生变化时才需要重新attach)。插入到渲染树后的element就处于“active”状态,处于“active”状态后就可以显示在屏幕上了。
3、当element父widget的配置数据改变时,为了进行element复用,framework在决定重新创建element前会先尝试复用相同位置旧的element:调用element对应widget的canUpdate方法,如果返回true,则复用旧element,旧的element会使用新的widget配置数据更新,反之则会创建一个新的elment,不会复用。widget.canUpdate主要判断newWidget与oldWidget的runtimeType和key是否同时相等,如果同时相等就返回true,否则就返回false。根据这个原理,当我们需要强制更新一个widget时,可以通过指定不同的key来禁止复用。
4、当有祖先element决定移除element时(如widget树结构发生了变化,导致element对应的widget被移除),这时该祖先element就会调用deactivateChild方法移除它,移除后element.renderObject也会被从渲染树中移除,然后framework会调用element.deactivate方法,这时element状态变为“inactive”状态。
5、“inactive”状态的element将不会再显示到屏幕。为了避免在一次动画执行过程中反复创建、移除某个特定element,“inactive”状态的element在当前动画最后一帧结束前都会保留,如果在动画执行结束后它还未能重新变成“active”状态,framework就会调用其unmount方法将其彻底移除,这时element的状态为defunct,它将永远不会再被插入到树中。
6、如果element要重新插入到element树的其它位置,如element或element的祖先拥有一个GlobalKey(用于全局复用元素),那么framework会先将element从现有位置移除,然后再调用其activate方法,并将其renderObject重新attach到渲染树。
BuildContext是一个抽象接口类,BuildContext就是widget对应的element可以通过context再StatelessWidget和StatefulWidget的build方法中直接访问element对象。
RenderObject RenderBox
每个element都对应一个renderObject,可以通过element.renderObject来获取。renderObject主要用来Layout和绘制。