Flutter笔记

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来获取。步骤有两步:

  1. 给目标StatefulWidget添加GlobalKey

    //定义一个globalKey, 由于GlobalKey要保持全局唯一性,我们使用静态变量存储

    static GlobalKey<ScaffoldState> _globalKey= new GlobalKey();

    ...

    Scaffold(

        key: _globalKey , //设置key

        ... 

    )

  2. 通过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和绘制。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值