Flutter笔记整理[待拆分]

1、getter、setter
set、get 方法是一对用来读写对象属性的特殊方法,实例对象的每一个属性都有一个隐式的 get 方法, 而且如果为非 final 属性的话还会有一个 set 方法。

class Person{
  String _name = "li";

  //get 方法 : 置私有字段的 get 方法 , 让外界可以访问类对象的私有成员 ;
  get name{
    return _name;
  }

  //set 方法 : 置私有字段的 set 方法 , 让外界可以设置类对象的私有成员 ;
  set name(value){
    _name = value;
  }
}

 

2、extension ... on 为指定类扩展额外的方法

extension addFuncToInt on int{
    getString(){ //为int类添加一个getString方法
       return this.toString();
    }
}

3、jumpTo(double offset)、animateTo(double offset,...):这两个方法用于跳转到指定的位置,它们不同之处在于,后者在跳转时会执行一个动画,而前者不会

4、在Flutter中监听滚动相关的内容由两部分组成:ScrollControllerScrollNotification。ListView、GridView的组件控制器是ScrollController,我们可以通过它来获取视图的滚动信息,并且可以调用里面的方法来更新视图的滚动位置。

  var _controller = ScrollController();

  //监听页面的滚动
  _controller.addListener(() {
    print(_controller.offset);//滚动偏移量
  });

    return MaterialApp(
      home:ListView.builder(
        controller: _controller,
        itemExtent: 80,//高度
        itemCount: 100,
        itemBuilder:(BuildContext context, int index) {
          return GestureDetector(
              child: Text(name + index.toString()),
              onTap: (){
                //jumpTo(double offset)、animateTo(double offset,...):这两个方法用于跳转到指定的位置,它们不同之处在于,后者在跳转时会执行一个动画,而前者不会。
                _controller.animateTo(0, duration: Duration(seconds: 1), curve: Curves.ease);
                // _controller.jumpTo(0.0);
              },
          );
        },
      )

如果我们希望监听什么时候开始滚动,什么时候结束滚动,这个时候我们可以通过NotificationListener

  • NotificationListener是一个Widget,模板参数T是想监听的通知类型如果省略,则所有类型通知都会被监听,如果指定特定类型,则只有该类型的通知会被监听。
  • NotificationListener需要一个onNotification回调函数用于实现监听处理逻辑。该回调可以返回一个布尔值,代表是否阻止该事件继续向上冒泡,如果为true时,则冒泡终止,事件停止向上传播,如果不返回或者返回值为false 时,则冒泡继续。
    body2(){
      // return NotificationListener<ScrollStartNotification>();//只监听滚动开始类型的通知:ScrollStartNotification
      return NotificationListener(
          onNotification: (ScrollNotification notification){//onNotification回调函数,用于实现监听处理逻辑。
            if(notification is ScrollStartNotification){//滚动开始
                print("开始滚动");
            }else if(notification is ScrollUpdateNotification){//滚动中
              // 当前滚动的位置和总长度
              final currentPixel = notification.metrics.pixels;//当前滚动高度
              final totalPixel = notification.metrics.maxScrollExtent;//最大高度
              double progress = currentPixel / totalPixel;
              print("滚动中"+progress.toString());
            }else if(notification is ScrollEndNotification){//滚动结束
              print("结束滚动");
            }
    
            //该回调可以返回一个布尔值,代表是否阻止该事件继续向上冒泡,如果为true时,则冒泡终止,事件停止向上传播,如果不返回或者返回值为false 时,则冒泡继续。
            return true;
          },
          child: ListView.builder(
              itemBuilder: (BuildContext context,int index){return Text("value"+index.toString());},
              itemCount: 30,
              itemExtent: 55,
          )
      );
    }

5、yaml里面的的dependencies和dev_dependencies有什么区别?

  • devDependencies是只会在开发环境下依赖的模块,生产环境不会被打入包内。 作为开发阶段的一些工具包,主要用于帮助我们提高开发和测试效率,比如Flutter的自动化测试包等。
  • dependencies依赖的包不仅开发环境能使用,生产环境也能使用。作为APP的源码的一部分参与编译,生成最终的安装包

     

6、dart运行机制(消息循环机制)
  Dart的耗时操作是通过单线程+事件循环方式来处理的。一些耗时操作,比如网络请求,都是放到事件循环来执行的,里面存在一个事件队列,事件循环不断从事件队列中取出事件执行。但除了事件队列外,还存在一个微任务队列。微任务队列的优先级要高于事件队列;也就是说事件循环都是优先执行微任务队列中的任务,再执行事件队列中的任务;

那么在Flutter开发中,哪些是放在事件队列,哪些是放在微任务队列呢?

  • 所有的外部事件任务都在事件队列中,如IO、计时器、点击、以及绘制事件等;
  • 而微任务通常来源于Dart内部,并且微任务非常少。这是因为如果微任务非常多,就会造成事件队列排不上队,会阻塞任务队列的执行(比如用户点击没有反应的情况);

Future是一个异步操作,它表示一个可能在未来完成的任务。当您需要进行一些耗时的操作时,例如从服务器获取数据或者读取本地存储的文件,这些任务可能需要一些时间才能完成。如果在同步模式下执行这些操作,应用程序可能会被阻塞,直到任务完成为止。但是,使用Future,您可以将这些操作放在后台执行,而不会阻塞主线程。

import "dart:async";

main(List<String> args) {
//可以通过dart中scheduleMicrotask来创建一个微任务:
  scheduleMicrotask(() {
    print("Hello Microtask");
  });
}

Future的代码是加入到事件队列还是微任务队列呢?
Future中通常有两个函数执行体:

  • Future构造函数传入的函数体
  • then的函数体(catchError等同看待)

那么它们是加入到什么队列中的呢?

  • Future构造函数传入的函数体放在事件队列中
  • then的函数体要分成三种情况:
  1. 情况一:Future没有执行完成(有任务需要执行),那么then会直接被添加到Future的函数执行体后;事件队列
  2. 情况二:如果Future执行完后就then,该then的函数体被放到如微任务队列,当前Future执行完后执行微任务队列;
  3. 情况三:如果Future是链式调用,意味着then未执行完,下一个then不会执行;事件队列
    // future_1加入到eventqueue中,紧随其后then_1被加入到eventqueue中
    Future(() => print("future_1")).then((_) => print("then_1"));
    
    // Future没有函数执行体,then_2被加入到microtaskqueue中
    Future(() => null).then((_) => print("then_2"));
    
    // future_3、then_3_a、then_3_b依次加入到eventqueue中
    Future(() => print("future_3")).then((_) => print("then_3_a")).then((_) => print("then_3_b"));

代码执行顺序

import "dart:async";

main(List<String> args) {
  print("main start");

  Future(() => print("task1"));
    
  final future = Future(() => null);

  Future(() => print("task2")).then((_) {
    print("task3");
    scheduleMicrotask(() => print('task4'));
  }).then((_) => print("task5"));

  future.then((_) => print("task6"));
  scheduleMicrotask(() => print('task7'));

  Future(() => print('task8'))
    .then((_) => Future(() => print('task9')))
    .then((_) => print('task10'));

  print("main end");
}

代码执行的结果是:

main start
main end
task7
task1
task6
task2
task3
task5
task4
task8
task9
task10

代码分析:
1、main函数先执行,所以main start和main end先执行,没有任何问题;
2、main函数执行过程中,会将一些任务分别加入到EventQueue和MicrotaskQueue中;
3、task7通过scheduleMicrotask函数调用,所以它被最早加入到MicrotaskQueue,会被先执行;
4、然后开始执行EventQueue,task1被添加到EventQueue中被执行;
5、通过final future = Future(() => null);创建的future的then被添加到微任务中,微任务直接被优先执行,所以会执行task6;
6、一次在EventQueue中添加task2、task3、task5被执行;
7、task3的打印执行完后,调用scheduleMicrotask,那么在执行完这次的EventQueue后会执行,所以在task5后执行task4(注意:scheduleMicrotask的调用是作为task3的一部分代码,所以task4是要在task5之后执行的)
8、task8、task9、task10一次添加到EventQueue被执行;

 

7、Isolate

dart虽然是单线程的模型,但是也提供了一个线程的封装——Isolate。不同于其他平台的多线程模型,每个isolate都有自己独立的执行线程和事件循环,以及内存,所以isolate之间不存在锁竞争的问题,各个isolate之间通过消息通信。

  Dart是单线程的,但Flutter并不是。Flutter内部有很多线程同时工作,比如Flutter中就有一个Root Isolate,负责运行Flutter的代码。除此之外,还有其他线程,比如Ul Runner Thread、GPU Runner Thread、IO Runner Thread、Platform Runner Thread。

  我们一般通过Isolate.spawn就可以创建一个Isolate,该方法需要传两个参数,一个是需要放到Isolate里面执行的耗时操作,另一个则是message,用来负责不同Isolate间的通信

  Isolate.spawn( (number) {
      //下面这段耗时代码将被放到另一个线程操作,不会阻塞rootIsolate
      for(int i = 0;i<number;i++){
         sleep(Duration(seconds: 1));
      };
      print("i calculate finished");
  },cycleNum);

  因为不同Isolate是相互隔离的,所以如果想要通信的话,比如需要把某个图片放到该Isolate中处理,处理完后在返回给主Isolate。这样的话需要借助ReceivePort这个类来搭建管道从而完成双方通信,分为单向通信和双向通信,较为繁琐。
  所以Flutter提供了支持并发计算的compute函数,它内部封装了Isolate的创建和双向通信。

//耗时操作
  timeFunc(num){
    int value = 0;
    for(int i = 0;i<num;i++){
      sleep(Duration(seconds: 1));
      value += i;
    };
    return value;
  }

  //compute函数会将耗时操作timeFunc放到一个Isolate中去执行,而且也封装了双向通信,既可以将rootIsolate中的参数传递到新建的Isolate中去,可以将Isolate的返回值传递出来。
  var value = await compute(timeFunc,20);

什么场景该使用Future还是isolate
Future事件循环也可以不阻塞的情况下完成耗时操作,Isolate也可以。那么如何判断某个耗时操作该选用哪种方式呢?
* 建议尽可能地使用 Future(直接或间接地通过 async 方法),因为一旦事件循环拥有空闲时间,这些 Future 的代码就会被执行。如果繁重的处理可能需要一些时间才能完成,并且可能影响应用的性能,考虑使用 Isolate

* 另外一个可以帮助你决定使用 Future 或 Isolate 的因素是运行某些代码所需要的平均时间。
       如果一个方法需要几毫秒 => Future
  如果一个处理流程需要几百毫秒 => Isolate

* 以下是一些很好的 Isolate 选项:
  JSON 解码:解码 JSON(HttpRequest 的响应)可能需要一些时间 => 使用 compute
  加密:加密可能非常耗时 => Isolate
  图像处理:处理图像(比如:剪裁)确实需要一些时间来完成 => Isolate
  从 Web 加载图像:该场景下,为什么不将它委托给一个完全加载后返回完整图像的 Isolate?


8、内存对象管理大致分两部分,一个是iOS的引用计数销毁机制,一个是垃圾回收机制,Dart语言的内存管理和Java他们一样是垃圾回收。
Dart垃圾回收的策略可以简单概括为:"分代"GC。即垃圾回收分代为:"新生代","老生代"。Dart还专门设计了调度器(在引擎中hooks的),当检测到空闲且没有用户交互时进行GC操作。

  • 新生代主要是清理一些寿命很短的对象,比如StatelessWidget。

    新对象被分配到连续、可用的内存空间,这个区域包含两个部分:活跃区和非活跃区,新对象在创建时被分配到活跃区、一旦填充完毕,仍然活跃的对象会被移动到非活跃区,不再活跃的对象会被清理掉,然后非活跃区变成活跃区,活跃区变成非活跃区,以此循环。(注解:GC来完成上面的步骤)

    为了确定哪些Object是存活的或死亡的,GC从根对象开始检测。然后将有引用的Object(存活的)移动到非活动状态,直接所有存活的Object被移动。死亡的Object就被留下清除;

  • 新生代阶段未被回收的对象,将会由老生代收集器管理新的内存空间:mark-sweep。

    在老生代收集器的管理分为两个阶段:阶段1:遍历对象图,然后标记在使用的对象;阶段2:扫描整个内存,并且回收所有未标记的对象

 

9、Widget-Element-RenderObject

  • Widget 的主要作用是用来保存 Element 信息的(包括布局、渲染属性、事件响应等信息),本身是不可变的,Element 也是根据 Widget 里面保存的配置信息来管理渲染树,以及决定自身是 否需要执行渲染。
  • RenderObject 做为渲染树中的对象存在,主要作用是处理布局、绘制相关的事情,而绘制的内容是Widget传入的内容。
  • Element 可以理解为是其关联的 Widget 的实例, 承载了视图构建的上下文数据,是连接结构化的配置信息到完成最终渲染的桥梁。可以通过遍历Element来查看视图树,Element同时持有Widget和RenderObject对象。

  widget 树和 Element 树节点是一一对应关系,每一个 Widget 都会有其对应的 Element,但是 RenderObject 树则不然,并不是所有widget都有对应的RenderObject,只有需要渲染的 Widget 才会有对应的节点。Element 树相当于一个中间层,大管家,它对 Widget 和 RenderObject 都有引用。当 Widget 不断变化的时候,将新 Widget 拿到 Element 来进行对比,看一下和之前保留的 Widget 类型和 Key 是否相同,如果都一样,那完全没有必要重新创建 Element 和 RenderObject,只需要更新里面的一些属性即可,这样可以以最小的开销更新 RenderObject,引擎在解析 RenderObject 的时候,发现只有属性修改了,那么也可以以最小的开销来做渲染。

  关于Widget、Element、RenderObject 的总结。
  我们写好 Widget 树后,Flutter 会在遍历 Widget 树时调用 Widget 里面的 createElement 方法去生成对应节点的 Element 对象,同时 Element 里面也有了对 Widget 的引用。特别的是当 StatefulElement 创建的时候也执行 StatefulWidget 里面的 createState 方法创建 state,并且赋值给 Element 里的 _state 属性,当前 widget 也同时赋值给了 state 里的_widget。Element 创建好以后 Flutter 框架会执行 mount 方法,对于非渲染的 ComponentElement 来说 mount 主要执行 widget 里的 build 方法,而对于渲染的 RenderObjectElement 来说 mount 里面会调用 widget 里面的 createRenderObject 方法 生成 RenderObject,并赋值给 RenderObjectElement 里的相应属性。StatefulElement 执行 build 方法的时候是执行的 state 里面的 build 方法,并且将自身传入,也就是 常见的 BuildContext。
  如果 Widget 的配置数据发生了改变,那么持有该 Widget 的 Element 节点也会被标记为 dirty。在下一个周期的绘制时,Flutter 就会触发该 Element 树的更新,通过 canUpdate 方法来判断是否可以使用新的 Widget 来更新 Element 里面的配置,还是重新生成 Element。并使用最新的 Widget 数据更新自身以及关联的 RenderObject对象。布局和绘制完成后,接下来的事情交给 Skia 了。在 VSync 信号同步时直接从渲染树合成 Bitmap,然后提交给 GPU。

 

 

 


 

疑问①:为什么需要Element树,直接按照Widget树去渲染显示不行吗?
  答案是不行的,因为Widget非常不稳定,动不动就会执行Build方法,也就是这个Widget依赖的所有其他Widget都需会重新创建。如果Flutter直接解析Widget树,将其转化为RenderObject树去渲染的话,将会非常消耗性能。因此,这里就有另外一棵树 Element 树。Element 树这一层将 Widget 树的变化做了判断,可以只将真正需要修改的部分同步到真实的 RenderObject 树中去重新渲染,其他部分则不变。从而最大程度降低对真实渲染视图的修改,提高渲染效率,而不是销毁整个渲染视图树重建。

 

疑问②:Widget变化时,Element树怎么知道该Widget需不需要重新创建渲染呢?
  如果 Widget 的配置数据发生了改变,那么持有该 Widget 的 Element 节点也会被标记为 dirty。在下一个周期的绘制时,Flutter 就会触发该 Element 树的更新,通过 canUpdate 方法来判断是否可以使用新的 Widget 来更新 Element 里面的配置,还是重新生成 Element。而canUpdate 方法将新 Widget 拿到 Element 来进行对比,看一下和之前保留的 Widget 类型和 Key 是否相同,如果都一样,那完全没有必要重新创建 Element 和 RenderObject,只需要更新里面的一些属性即可,反之则需要重新生成。

static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }

疑问③为什么Widget和Element是一一对应的,但RenderObject却不是一一对应的呢?
  这是因为Widget这个抽象类里面有个抽象方法createElement(),也就意味着所有的Widget子类必须实现此方法,所以Widget 和 Element 是一一对应的。而RenderObject 和 widget 并不是一一对应的,只有继承自 RenderObjectWidget 的 widget 才有对应的 RenderObject。Widget分为两类可渲染的Widget和不可渲染的Widget,其中,可渲染的Widget又可分MultiChildRenderObjectWidget(比如RichText)和SingleChildRenderObjectWidget(比如Pading),而不可渲染的Widget分为StatelessWidget(比如Container)和StatefulWidget(比如TextField)。只有可渲染的Widget才有对应的RenderObject。

疑问④:Widget、Element、RenderObject分别做了什么工作?
  Widget只是描述了配置信息:

  • 首先,所有Widget其中都包含createElement方法,用于创建Element;
  • 定义了canUpdate 方法以供Element对象执行,从而判断Widget修改后需不需要重新创建渲染。
  • 对于RenderObjectWidget类的话,定义了createRenderObject,但不是Widget自己在调用
  • StatefulWidget类也定义了createState,同样不是Widget自己调用
  • 而对于StatelessWidget类,则定义了build()函数(StatefulWidget会通过state调用build())

  Element是真正保存树结构的对象:

  • 创建出来后会由framework调用mount()这个核心方法,将这个新创建的element挂载到 Element 树上给定的父节点的插槽下面。
  • 对于StatelessWidget的Element对象来说,直接调用了其Build方法;
  • 对于StatefulWidget的Element对象,则调用了其createState方法并赋值给 _state,同样也调用了其_state中的build方法;
  • 而对于RenderObjectElement对象来说,在mount方法中会调用widget的createRenderObject方法,生成RenderObject对象;
  • Element对widget和RenderObject都有引用,它作为中间者,管理着双方。

  RenderObject是真正渲染的对象:
  其中有markNeedsLayout performLayout markNeedsPaint paint等方法。RenderObject 在 Flutter 的展示分为四个阶段,即布局、绘制、合成和渲染。其中,布局和绘制在 RenderObject 中完成,Flutter 采用深度优先机制遍历渲染对象树,确定树中各个对象的位置和尺寸,并把它们绘制在不同的图层上。绘制完毕后,合成和渲染的工作则交给 Skia 搞定。

疑问⑤:Widget的Build方法中在什么时候调用,BuildContext参数又是什么?
  是Element
  在StatelessElement类中,我们发现调用了Widget的build方法,并且将this传入,这个this 就是element ,所以本质上BuildContext就是当前的Element;
  在StatefulElement中,build方法也是类似,只不过调用的是state的build方法,传入的是同样是this。

class StatelessElement extends ComponentElement {
  StatelessElement(StatelessWidget super.widget);

  @override
  Widget build() => (widget as StatelessWidget).build(this);
}


class StatefulElement extends ComponentElement {
  StatefulElement(StatefulWidget widget)
      : _state = widget.createState(),
        super(widget) {
    ... 省略断言 ...
      }
      return true;
    }());
    ... 省略断言 ...;
    state._element = this;
     ... 省略断言 ...
    state._widget = widget;
     ... 省略断言 ...
  }

  @override
  Widget build() => state.build(this);

}

疑问⑥:Widget 频繁更改创建是否会影响性能?复用和更新机制是什么样的?
  不会影响性能,widget 只是简单的配置信息,并不直接涉及布局渲染相关。Element 层通过执行Widget定义的canUpdate方法,判断新旧两个widget 的runtimeType 和 key 是否相同,从而决定是否可以直接更新之前的配置信息,而不必每次都重新创建新的 Element。

疑问⑦:createState 方法什么时候调用?
  创建Element 的时候。
我们来看一下StatefulElement的构造器:在StatefulElement 这个类中,构造器的初始化列表的给state 进行了赋值操作。通过widget调用createState方法之后,把state赋值给自己的_state 属性。

  StatefulElement(StatefulWidget widget)
      : _state = widget.createState(),
  state._element = this;
  ....省略代码
  _state._widget = widget;
  • 调用widget的createState()
  • 所以StatefulElement对创建出来的State是有一个引用的
  • 而_state又对widget和element有一个引用

疑问⑧:真正的渲染相关的代码在哪里执行呢
  RenderObject

 

10、Key
  在Flutter中,Key是不能重复使用的,所以Key一般用来做唯一标识,用于标识Widget的对象。组件在更新的时候,其状态的保存主要是通过判断组件的类型或者key值是否一致来决定是否需要重新创建。因此,当各组件的类型不同的时候,类型已经足够用来区分不同的组件了,此时我们可以不必使用key。但是如果同时存在多个同一类型的控件的时候,此时类型已经无法作为区分的条件了,我们就需要使用到key。
  举个简单的例子,现有一个List列表,cell上只有一个标题和随机的背景色,标题显示标题数组中对应序列号的文字内容。现在要求点击底部按钮后删掉顶部第一个cell,(即【红1 绿2 黄3】变为【绿2 黄3】)。如果我们不使用key来标记Widget的话,你会发现,结果并不是这样,结果分两种:

  • · 如果该Widget是StatelessWidget的话,操作后,第一个cell确实会被删除,但是剩余cell的背景色也会发生变动,(即【红1 绿2 黄3】变为【蓝2 紫3】)。这是因为每次删除后,都会重新执行Widget的Build方法,而Build方法中又会重新生成新的随即色
  • · 而如果该Widget是StateFullWidget的话,操作后,发现颜色不变化,但是数据向上移动了(即【红1 绿2 黄3 】变为【红2 绿3】)。这是因为每次删除刷新后,Widget的复用导致的。Element对象判断该Widget是可以复用还是重新创建就是通过组件的类型或者key值是否一致来决定。因为我们没有设置key,所以只需要判断组件的类型是否一致就行。当我们修改数据源后,Element发现widget树中第一位置新的Widget和旧的widget一致(因为类型一样),因此就直接复用了,只需要将标题1改为标题2即可;同样的道理,Element发现,Widget树中第二位置上新的Widget与旧的Widget一致,也就修改了数字进行复用。而至于第三个Widget,Element发现现在只需要两个Widget就行,所以直接将第三个Widget删掉了。

   所以,如果想达到预期效果,就需要设置Key。这样Widget在复用的时候,就需要不能只看类型了。比如我们为每个Widget设置一个ValueKey,给定的值就是当前Widget的标题。这样,当删除数据后刷新时,Element想复用Widget时,发现第一个Widget虽然类型相同,但是旧Widget的key是1,而要显示的新Widget的key是2,两个key不同,无法复用,所以会继续在同级目录下逐个查找,找到第二个Widget时发现旧Widget的key和类型与新Widget的一致,所以会被保存下来复用。相反,如果没有找到一致的,那么就会被销毁而重新创建。

  如果设置的是UniqueKey呢? 就会发现每次删除都会出现随机颜色的现象。这是因为每次刷新时都会生成一个新的Key,没办法复用,所以Element会强制刷新,那么对应的State也会重新创建。


Key的主要作用包括:

  • 识别Widget:Key可以用于在Widget树中唯一标识一个Widget,当需要查找或比较Widget时,Flutter框架会使用Key来进行操作,可以快速查找和比较Widget。
  • 复用Widget:当Flutter框架需要重建Widget树时,它会检查是否有相同的Key的Widget可以复用,而不是每次都重新创建新的Widget,以提高性能。
  • 控制重建:在某些情况下,使用Key可以控制Widget的重建。例如,当使用ListView或GridView等可滚动组件时,可以通过为每个item指定一个唯一的Key来控制哪些item需要重新构建,以提高性能。

在Flutter中,key的分类主要包括以下几种:

 

  • LocalKey

  用于在同一父Element下的Widget之间进行比较,也是diff算法的核心所在。它主要有3种分类:
         1. ValueKey:值键,使用给定的值来比较和匹配Widget,通常用于列表或集合中的项目,以确保正确的更新和操作。
    2. ObjectKey:对象键,ObjectKey判断两个Key是否相同的依据是:两个对象是否具有相同的内存地址,通常用于保持特定对象的身份和状态。
    3. UniqueKey:唯一键,不需要参数,并且每一次刷新都会生成一个新的Key,主要用于动画的刷新,或用于需要每次都要重建刷新widget的场景

  • GlobalKey:全局键,用于在整个应用程序中标识和访问特定的Widget。GlobalKey可以跨Widget层级使用,用于在不同的Widget树中查找和操作特定的Widget。比如在不同的屏幕上使用相同的Widget,但是保持相同的State,则需要使用GlobalKeys(例如在A页面将开关从关闭设置为打开,B页面的开关状态也会随之改变)。

    Global keys 是很昂贵的,如果你不需要访问BuildContext、Element、State这些的话,请尽量使用LocalKey。

用途:获取配置、状态以及组件位置尺寸等信息:使用GlobalKey可以获取特定Widget的状态对象,以便在需要时进行操作或访问。例如,可以使用GlobalKey来获取表单字段的文本输入框的当前文本值。
(1)_globalKey.currentWidget:获取当前组件的配置信息(存在widget树中)
(2)_globalKey.currentState:获取当前组件的状态信息(存在Element树中)
(3)_globalKey.currentContext:获取当前组件的大小以及位置信息。


11、StatefulWidget 的生命周期? StatefulWidget有哪些生命周期的回调呢?它们分别在什么情况下执行呢?
我们知道StatefulWidget本身由两个类组成的:StatefulWidget和State,我们分开进行分析。
  首先,执行StatefulWidget中相关的方法:
    1、执行StatefulWidget的构造函数(Constructor)来创建出StatefulWidget;
    2、执行StatefulWidget的createState方法,来创建一个维护StatefulWidget的State对象;
  其次,调用createState创建State对象时,执行State类的相关方法:
    1、执行State类的构造方法(Constructor)来创建State对象;
    2、执行initState,我们通常会在这个方法中执行一些数据初始化的操作,或者也可能会发送网络请求
    3、执行didChangeDependencies方法,这个方法在两种情况下会调用
      情况一:调用initState会调用;
      情况二:从其他对象中依赖一些数据发生改变时,比如InheritedWidget;
    4、Flutter执行build方法,来看一下我们当前的Widget需要渲染哪些Widget;
    5、当前的Widget不再使用时,会调用dispose进行销毁;
    6、手动调用setState方法,会根据最新的状态(数据)来重新调用build方法,构建对应的Widgets;
    7、执行didUpdateWidget方法是在当父Widget触发重建(rebuild)时,系统会调用didUpdateWidget方法;

 

12、Dart 当中的 「…」表示什么意思?
级联运算符(…)可以让你在同一个对象上连续调用多个对象的变量或方法。

querySelector('#confirm') // 获取对象 (Get an object).
  ..text = 'Confirm' // 使用对象的成员 (Use its members).
  ..classes.add('important')
  ..onClick.listen((e) => window.alert('Confirmed!'));

第一个方法 querySelector 返回了一个 Selector 对象,后面的级联操作符都是调用这个 Selector 对象的成员并忽略每个操作的返回值。

错误用法:

var sb = StringBuffer();
sb.write('foo')
  ..write('bar'); // 出错:void 对象中没有方法 write (Error: method 'write' isn't defined for 'void').

上述代码中的 sb.write() 方法返回的是 void,返回值为 void 的方法则不能使用级联运算符。


13、extends(继承), implements(接口实现), mixin(混入)
extends(继承),在flutter中继承是单继承。
  子类重写超类的方法要用@override
  子类调用超类的方法要用super
  子类会继承父类里面可见的属性和方法,但是不会继承构造函数
  子类能复写父类的getter 和 setter 方法
  子类可以继承父类的非私有变量
继承的局限在于:在flutter中只能单继承,灵活度不高。所以有后面的这两个implements、和mixin来弥补。

implements(接口实现)
  可多个接口实现(任何单独的都很苍白,对比才能更立体)。接口定义要实现的属性和方法的命名,具体实现需要在每一个具体的类中体现,且子类需要全部实现implements后的类的所有属性和方法。
  接口的局限在于:一个子类必须全部实现所有的属性和方法。mixin可以解决这个问题

mixin(混入),在现有类的基础上,引入一些新的变量。
  作为mixins 的类只能继承自object,不能继承其他的类。
  作为mixins 的类不能有构造函数。
  一个类可以mixins 多个mixin 类。
  mixins 不是继承,也不是接口,而是一种全新的特性。

关键字:
  with:子类混入某个类的时候使用
  on:定义基于某个类型的mixin,即限制mixin只能应用于特定类型的类,这就代表了在mixin中可以访问到该特定类的成员和方法。
  三者可以同时存在于一个类中,前后顺序是:extends>mixins>implements
  如果都使用了同一个方法的实现,那么在子类中的这个方法的有效性优先级:mixins>extend>implements, mixins和implements中如果跟了多个,那么后面的会覆盖前面的,没有冲突的,则都会保留,所以会存在后面的会修改掉前面的一部分逻辑代码,不需要直接继承,就可以直接实现覆盖,避免了更复杂的多继承关系。


14、Flutter渲染流程是什么?
  Flutter的渲染流程是将Widget转换为显示在屏幕上的像素的过程。它涉及多个阶段,包括构建、布局、绘制和合成。

  • 根据Widget 生成Element,然后创建相应的RenderObject并且关联到Element.renderObject 属性。最后再通过RenderObject 来完成布局和绘制。
  • 当需要更新页面的时候,由应用上层通知到Engine,Engine会等到下个Vsync信号到达的时候,去通知Framework上层,然后Framework会进行Animation, Build,Layout,Compositing,Paint,最后生成layer提交给Engine。Engine会把layer进行组合,生成纹理,最后通过OpenGl接口提交数据给GPU, GPU经过处理后在显示器上面显示。

【Engine:一个 C++实现的 SDK。其包含了 Skia引擎、Dart运行时、文字排版引擎等。在安卓上,系统自带了Skia,在iOS上,则需要APP打包Skia库,这会导致Flutter开发的iOS应用安装包体积更大。 Dart运行时则可以以 JIT、JIT Snapshot 或者 AOT的模式运行 Dart代码】


15、状态管理 (https://juejin.cn/post/6920852250667532301

 


  状态管理就是当某个状态发生改变的时候,告知使用该状态的状态监听者,让状态所监听的属性随知改变,从而达到联动效果。这也是响应式编程和命令式编程区别,Android、iOS是通过明确的命令式指令去控制我们的UI变化。而在响应式编程下,我们只需要描述好UI和状态之间的关系,然后专注于状态的改变就好了,框架会根据状态的变化来自动更新UI【观察者模式】。
  Flutter 状态管理是指在 Flutter 应用中有效地管理应用的数据和状态,以确保用户界面(UI)与数据之间的一致性和交互性。在复杂的应用中,数据通常会在不同的部分之间流动和变化,而状态管理的目标是帮助开发者更好地组织、更新和共享这些数据。

状态管理分为短时状态和应用状态两类:

  • 短时状态:只需要在一个独立widget中使用,Widget树中的其它部分并不需要访问这个状态,这种状态我们只需要使用StatefulWidget对应的State类自己管理即可。
  • 应用状态:开发中也有非常多的状态需要在多个部分进行共享,比如用户的登录状态信息,这种状态我们如果在Widget之间传递来、传递去,所以需要用全局状态管理的方式,来对状态进行统一的管理和应用。

Flutter中目前有哪些可以做到状态管理,有什么优缺点?
答:State、 InheritedWidget、 Notification、 Stream数据流。优缺点详见上面链接

状态管理方案有哪些?
Flutter状态管理方案目前有很多种,有官方推荐的,也有优秀的三方框架,分类如下:

  • Flutter 本身支持:

    State、 InheritedWidget、 Notification、 Stream 数据流

  • 官方推荐:

    Provider
    Redux
    BLoC/Rx
    MobX

  • 三方优秀框架:

    scoped_model
    闲鱼Fish-Redux

State自不必多说,接下来我们主要看InheritedWidget和Provider这两种方案:
  InheritedWidget组件特别适合在同一树型Widget中,抽象出公有状态,每一个子Widget或者孙Widget都可以获取该状态。当InheritedWidget数据发生变化时,可以自动更新依赖的子孙组件!
InheritedWidget的优点是较轻量,但也有以下几个缺点:
  1.每次更新都会通知所有的子Widget,无法定向通知/指向性通知,容易造成不必要的刷新
  2.不支持跨页面(route)的状态,意思是跨树,如果不在一个树中,我们无法获取
  3.数据是不可变的,必须结合StatefulWidget、ChangeNotifier或者Steam使用

InheritedWidget的使用分为以下步骤:

  • 创建

    ①将需要跨组件共享的数据保存在一个继承自InheritedWidget的widget中;
    ②创建一个包含共享数据(用于设置共享数据的数值)和child(设置要获取数据的依赖子控件)的构造函数;
    ③构建一个of的Static函数,方便子控件拿到该Widget,进而拿到共享数据;
    ④实现updateShouldNotify函数,返回一个布尔值,,决定是否对StatefulWidget子控件的didChangeDependencies方法进行回调;

  • 使用

    ⑤通过InheritedWidget子类的构造函数的参数分别设置初始化贡献数据和包裹要用到该数据的子控件;
    ⑥结合StatefulWidget等对数据修改后,重新执行构造函数更改共享数据的值;
    ⑦需要获取共享数据的子控件,调用InheritedWidget子类的of方法,通过上下文拿到该Widget,进而拿到共享数据。

class UserInfoViewModelWidget extends InheritedWidget{
  // 1.共享的数据
  final int counter;

  // 2.定义构造方法,用于初始化时设置共享数据
  UserInfoViewModelWidget({required  this.counter, Widget child}):super(child:child);

  // 3.获取组件最近的当前InheritedWidget,方便子控件通过该InheritedWidget拿到共享数据
  static UserInfoViewModelWidget? of(BuildContext context) {
    // 沿着Element树, 去找到最近的UserInfoViewModelElement, 从Element中取出Widget对象
    return context.dependOnInheritedWidgetOfExactType();
  }

  // 4.决定要不要回调子控件State中的didChangeDependencies(前提子空间得是StateFullWidget)
  // 如果返回true: 执行依赖当期的InheritedWidget的State中的didChangeDependencies
  @override
  bool updateShouldNotify(UserInfoViewModelWidget oldWidget) {
    return oldWidget.counter != counter;
  }
}

//在子组件中引用InheritedWidget。
class MyApp2 extends StatefulWidget {
  const MyApp2({super.key});

  @override
  State<StatefulWidget> createState() => _MyAppState();
}

class _MyAppState extends State{
  var _counter = 0;
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home:Scaffold(
        body:UserInfoViewModelWidget(  //因为 InheritedWidget 是从上到下进行数据共享、传递的,所以要把 InheritedWidget 作为根节点,需要共享数据的节点作为子节点。
            counter:_counter,
            child:body4() //只有子节点才能拿到共享的数据
        ),
        floatingActionButton: FloatingActionButton(
            child: Icon(Icons.navigate_next,color: Colors.white),
            onPressed: (){
              setState(() {//结合StatefulWidget重新初始化UserInfoViewModelWidget,重新设置共享数据的值
                _counter += 1;
              });
            }
        ),
      ),
    );
  }
}

class body4 extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Card(//子控件通过 of 方法使用了 InheritedWidget 中的数据,注册依赖关系
          child: Text("${UserInfoViewModelWidget.of(context)?.counter}",style: TextStyle(color: Colors.pink,fontSize: 20),)
      ),
    );
  }
}

接下来再看Provider,从名字上就很容易理解,它就是用于提供数据,无论是在单个页面还是在整个app 都有它自己的解决方案,可以很方便的管理状态。Provider的实现在内部还是利用了InheritedWidget实现的,但其使用更加简洁高效。Provider是一个观察者模式, 状态改变时要notifyListeners().

常用概念:

  • ChangeNotifier:系统提供的被观察者,需要共享的数据都需要放在这里,数据共享的类需要继承或混入ChangeNotifier。共享数据一般设置为私有,然后提供setget方法,在set方法中监听数据的改变,调用notifyListeners方法,通知所有消费者进行更新。
  • Provider:订阅者,只用于数据共享管理,提供给子孙节点使用。因为Provider是基于InheritedWidget,所以我们在使用ChangeNotifier中的数据时,我们可以通过Provider.of的方式来使用,但不太推荐这种方法,因为 Provider.of所在的Widget整个build方法都会重新构建。
  • ChangeNotifierProvider:订阅者,不仅能够提供数据供子孙节点使用,还可以在数据改变的时候通知所有消费者。 Model变化后会自动通知ChangeNotifierProvider(订阅者),ChangeNotifierProvider内部会重新构建InheritedWidget,而依赖该InheritedWidget的子孙Widget就会更新.
  • MultiProvider:多个订阅者:实际上就是通过每一个provider都实现了的 cloneWithChild方法把自己一层一层包裹起来。
  • Consumer:消费者,能够在复杂项目中,极大地缩小你的控件刷新范围。最多支持6中model
  • Selector: 消费者,强化的Consumer,支持过滤刷新

Provider种类:

  • Provider:只能提供恒定的数据,不能通知依赖它的子部件刷新
  • ListenableProvider: 提供的对象是继承了 Listenable 抽象类的子类,必须实现其 addListener / removeListener 方法,通常不需要
  • ChangeNotifierProvider: 对子节点提供一个继承/混入/实现了ChangeNotifier的类,只需要在Model中with ChangeNotifier ,然后在需要刷新状态时调用 notifyListeners 即可
  • ValueListenableProvider: 提供实现了继承/混入/实现了ValueListenable的Model,实际上是专门用于处理只有一个单一变化数据的ChangeNotifier。
  • StreamProvider: 专门用作提供(provide)一条 Single Stream。
  • FutureProvider:提供了一个 Future 给其子孙节点,并在 Future 完成时,通知依赖的子孙节点进行刷新

介绍完概念后,我们来看下一具体怎么使用:
创建:
    ①Provider虽然是Flutter官方提供的,但并还是需要我们对它引入依赖

dependencies:
    provider: ^6.1.2

    ②创建自己的ChangeNotifier来管理共享数据,可以继承自ChangeNotifier,也可以使用混入,这取决于概率是否需要继承自其它的类。
    ③将共享数据设置为私有属性,然后提供setget方法,在set方法中监听数据的改变后,调用notifyListeners方法,通知所有消费者进行更新。
使用:
    ④在应用程序的顶层插入ChangeNotifierProvider(即包裹住MyApp控件),这样方便在整个应用的任何地方可以使用自定义的ChangeNotifier,以便Consumer可以获取到数据。create参数则是返回被观察者的函数;
    ⑤在需要的位置使用共享的数据,有以下几种方式:
  * > Provider.of: 当Provider中的数据发生改变时, Provider.of所在的Widget整个build方法都会重新构建;
  * > Consumer(相对推荐): 当Provider中的数据发生改变时, 执行重新执行Consumer的builder;
  * > Selector: 1.selector方法(作用,对原有的数据进行转换) 2.shouldRebuild(作用,可选择要不要重新构建)

Ⅰ创建的过程比较简单,直接上代码:

import 'package:flutter/cupertino.dart';
import 'package:flutter_test_project/models/UserInfo.dart';

class UserInfoProvider with ChangeNotifier{
    UserInfo _uinfo = UserInfo("", 0);

    UserInfo get uinfo => _uinfo;

    set uinfo(UserInfo value) {
      _uinfo = value;
      //数据发生修改后,发送通知
      notifyListeners();
    }
}

而在使用的时候,首先需要将ChangeNotifierProvider插入顶层

void main() {
  runApp(
    ChangeNotifierProvider(
        create:(context)=>UserInfoProvider(),
        child: MaterialApp(
           home: MyAppScaffold()
        )
    )
  );
}

Ⅲ在需要的地方通过Provider或Consumer或Selector使用共享的数据
    Provider使用比较简单,直接通过of方法就能拿到数据,不存在嵌套子控件的问题;
    Consumer和Selector则需要包裹用到数据的子控件,但好处是数据更改时,只会局部刷新,而Provider因为没有嵌套,不知道哪些地方用到了数据,所以会全部刷新。而Selector相对于Consumer更重要的一点是,Selector能够控制是否需要重建刷新。所以在某些情况下,使用Selector来代替Consumer,性能会更高。

Consumer的builder方法解析:
  参数一:context,每个build方法都会有上下文,目的是知道当前树的位置
  参数二:ChangeNotifier对应的实例,也是我们在builder函数中主要使用的对象
  参数三:child,目的是进行优化,如果builder下面有一颗庞大的子树,当模型发生改变的时候,我们并不希望重新build这颗子树,那么就可以将这颗子树放到Consumer的child中,在这里直接引入即可(注意我案例中的Icon所放的位置)

Selector和Consumer对比,不同之处主要是三个关键点:
  关键点1:泛型参数是两个
    泛型参数一:我们这次要使用的Provider
    泛型参数二:转换之后的数据类型,比如我这里转换之后依然是使用CounterProvider,那么他们两个就是一样的类型
  关键点2:selector回调函数
    转换的回调函数,你希望如何进行转换
    S Function(BuildContext, A) selector
    我这里没有进行转换,所以直接将A实例返回即可
  关键点3:是否希望重新rebuild
    这里也是一个回调函数,我们可以拿到转换前后的两个实例;
    bool Function(T previous, T next);
    因为这里我不希望它重新rebuild,无论数据如何变化,所以这里我直接return false;

                Container(
                  width: 200,

                  child:
                  ③Consumer是否是最好的选择呢?并不是。比如这里,这里的操作只是修改共享数据,所以并不需要重新Builder,所以使用Selector来代替Consumer,从而选择要不要重新构建
                  Selector<UserInfoProvider, UserInfoProvider>(//这里的泛型就是参数provider的类型,明确类型方便我们访问provider的属性
                      selector: (ctx, provider) => provider,
                      shouldRebuild: (pre, next) => false,//是否希望重新rebuild
                      builder: (ctx, provider, child){

                          print("EditUserInfoPage---Consumer的builder方法");
                          return ElevatedButton(
                            child: Text("保存",style: TextStyle(fontSize: 20,color: Colors.white)),
                            onPressed: (){
                              if(_formKey.currentState!.validate()){//手动调用Form的State对象的validate方法
                                // 如果表单验证通过,则执行相关操作
                                _formKey.currentState!.save();//执行save方法,保存表单中的数据
                                print("用户名称是:$userName,年龄是:$age");

                                // 修改数据
                                provider.uinfo = UserInfo(userName, int.parse(age));

                                Navigator.pop(context);
                              }
                            },
                          );
                        }
                    ),

                   ②Consumer(相对推荐): 当Provider中的数据发生改变时, 执行重新执行Consumer的builder;
                   Consumer<UserInfoProvider>( //这里的泛型就是参数provider的类型,明确类型方便我们访问provider的属性
                       builder: (ctx, provider, child){
                  
                         print("EditUserInfoPage-- Consumer的builder方法");
                         return ElevatedButton(
                           child: Text("保存",style: TextStyle(fontSize: 20,color: Colors.white)),
                           onPressed: (){
                             if(_formKey.currentState!.validate()){//手动调用Form的State对象的validate方法
                               // 如果表单验证通过,则执行相关操作
                               _formKey.currentState!.save();//执行save方法,保存表单中的数据
                               print("用户名称是:$userName,年龄是:$age");

                               // 修改数据
                               provider.uinfo = UserInfo(userName, int.parse(age));
                               Navigator.pop(context);
                             }
                           },
                         );
                       }
                   ),


                   ①Provider.of: 当Provider中的数据发生改变时, Provider.of所在的Widget整个build方法都会重新构建;
                   ElevatedButton(
                     child: Text("保存",style: TextStyle(fontSize: 20,color: Colors.white)),
                     onPressed: (){
                       if(_formKey.currentState!.validate()){//手动调用Form的State对象的validate方法
                         // 如果表单验证通过,则执行相关操作
                         _formKey.currentState!.save();//执行save方法,保存表单中的数据
                         print("用户名称是:$userName,年龄是:$age");
                         
                         // 修改数据:
                         Provider.of<UserInfoProvider>(context,listen: false).uinfo = UserInfo(userName, int.parse(age));
                  
                         Navigator.pop(context);
                       }
                     },
                   ),
                )

  上面我们提到的都是一个Provider的情况,但实际开发中可能存在需要同时获取多个共享数据的情况。我们可以通过将这些数据都放在同一个被观察者对象(ChangeNotifier)里去实现该功能,但有时候这些数据并没有关联,如果嵌套层级过多不方便维护,扩展性也比较差。所以需要分开存放,比如一个页面需要用到用户信息和商品信息,总不能都放在一个被观察者里。这个时候就用到了MultiProvider和
Consumer2/Consumer3/Consumer4/Consumer5/Consumer6以及Selector2/Selector3/Selector4/Selector5/Selector6。

runApp(MultiProvider(
  providers: [
    ChangeNotifierProvider(create: (ctx) => CounterProvider()),
    ChangeNotifierProvider(create: (ctx) => UserProvider()),
  ],
  child: MyApp(),
));

class HYShowData03 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer2<UserProvider, CounterProvider>(
      builder: (ctx, userVM, counterVM, child) {
        return Text(
          "nickname:${userVM.user.nickname} counter:${counterVM.counter}",
          style: TextStyle(fontSize: 30),
        );
      },
    );
  }
}

 

16、事件监听、手势识别Gesture
在Flutter中,手势有两个不同的层次:
  第一层:原始指针事件(Pointer Events):描述了屏幕上由触摸板、鼠标、指示笔等触发的指针的位置和移动。一共有四种指针事件:

  • PointerDownEvent 指针在特定位置与屏幕接触
  • PointerMoveEvent 指针从屏幕的一个位置移动到另外一个位置
  • PointerUpEvent 指针与屏幕停止接触
  • PointerCancelEvent 指针因为一些特殊情况被取消
class HomeContent extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Listener(
        child: Container(
          width: 200,
          height: 200,
          color: Colors.red,
        ),
        onPointerDown: (event) => print("手指按下:$event"),
        onPointerMove: (event) => print("手指移动:$event"),
        onPointerUp: (event) => print("手指抬起:$event"),
      ),
    );
  }
}

  第二层:手势识别(Gesture Detector):这个是在原始事件上的一种封装。Gesture分层非常多的种类:

  • 点击:

    onTapDown:用户发生手指按下的操作
    onTapUp:用户发生手指抬起的操作
    onTap:用户点击事件完成
    onTapCancel:事件按下过程中被取消

  • 双击:

    onDoubleTap:快速点击了两次

  • 长按:

    onLongPress:在屏幕上保持了一段时间

  • 纵向拖拽:

    onVerticalDragStart:指针和屏幕产生接触并可能开始纵向移动;
    onVerticalDragUpdate:指针和屏幕产生接触,在纵向上发生移动并保持移动;
    onVerticalDragEnd:指针和屏幕产生接触结束;

  • 横线拖拽:

    onHorizontalDragStart:指针和屏幕产生接触并可能开始横向移动;
    onHorizontalDragUpdate:指针和屏幕产生接触,在横向上发生移动并保持移动;
    onHorizontalDragEnd:指针和屏幕产生接触结束;

  • 移动:

    onPanStart:指针和屏幕产生接触并可能开始横向移动或者纵向移动。如果设置了 onHorizontalDragStart 或者 onVerticalDragStart,该回调方法会引发崩溃;
    onPanUpdate:指针和屏幕产生接触,在横向或者纵向上发生移动并保持移动。如果设置了 onHorizontalDragUpdate 或者 onVerticalDragUpdate,该回调方法会引发崩溃。
    onPanEnd:指针先前和屏幕产生了接触,并且以特定速度移动,此后不再在屏幕接触上发生移动。如果设置了 onHorizontalDragEnd 或者 onVerticalDragEnd,该回调方法会引发崩溃。

  但有时候,我们也需要某个组件不响应事件,比如Cell上的按钮。这个时候就用到了AbsorbPointerIgnorePointer。这两个组件都可以做到隔离事件,但也有区别。IgnorePointer的child不响应事件,但是事件会传递到下一层;而AbsorbPointer 设为不响应事件时(即absorbing = true),事件会被吞噬,不会透传到下一层。

class MyApp5 extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: Stack(
            alignment: Alignment.center,
            children: [
              GestureDetector(
                child: Container(
                    color: Colors.red,
                    height: 200,
                    width: 200
                ),
                onTap: (){
                  print("点击红色区域");
                },
              ),

              GestureDetector(
                child: Container(
                    color: Colors.blue,
                    height: 100,
                    width: 100,
                ),
                onTap: (){
                  print("点击蓝色区域");
                },
              ),

              IgnorePointer(//IgnorePointer的child不响应事件,但是事件会传递到下一层;所以点击黄色区域打印的是“点击蓝色区域”
              child: GestureDetector(
                    child: Container(
                      color: Colors.yellow,
                      height: 50,
                      width: 50,
                    ),
                    onTap: () {
                      print("点击黄色区域");
                    }),
                ignoring: true,
              ),

              AbsorbPointer(//AbsorbPointer的child不响应事件,事件会被吞噬,不会透传到下一层。所以点击黑色区域没有反应
              child:GestureDetector(
                    child: Container(
                      color: Colors.black,
                      height: 30,
                      width: 30,
                    ),
                    onTap: () {
                      print("点击黑色区域");
                    }),
                absorbing: true,
              )
            ],
          ),
        ),
      ),
    );
  }
}

 

17、路由导航
  路由主要是用于页面跳转的一种方式,方便管理页面之间的跳转和互相传递数据,进行交互,通常也可被称为导航管理。Flutter 中的路由管理和原生开发类似,无论是 Android 还是 iOS,导航管理都会维护一个路由栈,路由入栈(push)操作对应打开一个新页面,路由出栈(pop)操作对应页面关闭操作,而路由管理主要是指如何来管理路由栈。
  Navigator是一个路由管理的组件,它提供了打开和退出路由页方法。Navigator通过一个栈来管理活动路由集合。我们开发中并不需要手动去创建一个Navigator,因为使用的MaterialApp、CupertinoApp、WidgetsApp它们默认是有插入Navigator的,所以直接使用即可:Navigator.of(context)

  Flutter 中给我们提供了两种配置路由跳转的方式:1、普通路由 2、命名路由。

  • 普通路由:使用比较简单,不需要注册路由表,也不需要设置初始路由。通过push方法,直接指定要跳转的页面路由的方式,而不使用路由名称。
    Future<T> push<T extendsObject>(Route<T> route)

  push方法需要传入一个路由对象,但是Route是一个抽象类,所以它是不能实例化的,我们一般使用Material组件库提供的组件MaterialPageRoute。MaterialPageRoute在不同的平台有不同的表现,对Android,打开一个页面会从屏幕底部滑动到顶部,关闭页面时从顶部滑动到底部消失;对iOS,打开一个页面会从屏幕右侧滑动到左侧,关闭页面时从左侧滑动到右侧消失。构造方法如下:

         MaterialPageRoute({
            WidgetBuilder builder, //构建路由页面的具体内容,返回值是一个widget。我们通常要实现此回调,返回新路由的实例。
            RouteSettings settings, //settings 包含路由的配置信息,如路由名称、是否初始路由(首页)。
            bool maintainState = true, //已经不可见(被上面的盖住完全看不到)的组件,是否还需要保存状态。maintainState设置为true是昂贵的,但如果为false,那当不可见时,对应的页面会被销掉,如果pop回去时重新创建的,那上个页面的状态就无法保持了。这可能也是MaterialPageRoute默认maintainState为true的原因。
            bool fullscreenDialog = false, //新的路由页面是否是一个全屏的模态对话框,在 iOS 中,如果fullscreenDialog为true,新页面将会从屏幕底部滑入(而不是水平方向)【present
         })

如果在 Android 上也想使用iOS的左右切换风格,可以使用 CupertinoPageRoute。当然,iOS平台我们也可以使用CupertinoPageRoute。

              //①通过以下方法实现跳转
              Navigator.of(context).push(MaterialPageRoute(builder: (context) {
                  return const SearchPage(); //直接返回要跳转到的页面
              }));

             //②返回上一级路由
             Navigator.of(context).pop();

            //③当然也可以通过路由传递参数:
            class SearchPage extends StatelessWidget {
               final String title;
               //构造函数中添加参数,用于接收从上一个页面传递过来的数据。
               const SearchPage({super.key, this.title = "Search"});
            }

            Navigator.of(context).push(MaterialPageRoute(builder: (context) {
                // return const SearchPage();
                return const SearchPage(title: "搜索vvvvv",);  //通过构造函数传值
              }));

            //④pop返回的时候也可以携带参数到上级页面
                Navigator.of(context).pop("a detail message");
             但这个数据如何拿到呢?
             在页面push跳转时,会返回一个Future。该Future会在详情页面调用pop时,回调对应的then函数,并且会携带结果    
            // 1.跳转代码
            final future = Navigator.of(context).push(MaterialPageRoute(
                     builder: (ctx) {
                          return DetailPage("a home message");
                     }
           ));

            // 2.获取结果
           future.then((res) {
                _message = res;
            });
 
  • 命名路由:我们可以通过创建一个新的Route,使用Navigator来导航到一个新的页面,但是如果在应用中很多地方都需要导航到同一个页面(比如在开发中,首页、推荐、分类页都可能会跳到详情页),那么就会存在很多重复的代码。在这种情况下,我们可以使用命名路由(named route)

  命名路由是一种将路由(页面)和名称关联映射起来的方式,在一个地方进行统一的管理。可以调用Navigator 的pushNamed方法,通过名称来导航到相应的页面。使用命名路由时,需要在应用程序的路由表中注册路由名称和对应的页面组件。在通过路由名字打开新路由时,应用会根据路由名字在路由表中查找到对应的WidgetBuilder回调函数,然后调用该回调函数生成路由widget并返回。

Future pushNamed(BuildContext context, String routeName,{Object arguments})

  相对于普通路由的拿起来就用,命名路由则多了一个步骤,需要设置管理(注册路由、设置初始路由),一般放到MaterialApp的 initialRouteroutes 中管理。
    initialRoute:设置应用程序从哪一个路由开始启动,设置了该属性,就不需要再设置home属性了
    routes:定义名称和路由之间的映射关系,类型为Map<String, WidgetBuilder>。key为路由的名字,是个字符串;value是个builder回调函数,用于生成相应的路由widget。

        return MaterialApp(
            title: 'Flutter Demo',
            theme: ThemeData(
              primarySwatch: Colors.blue, splashColor: Colors.transparent
            ),
            initialRoute: "/",
            routes: {
              "/home": (ctx) => HomePage(),
              "/detail": (ctx) => ProductionDetailPage()
            },
        );

  使用的话也简洁许多:

Navigator.of(context).pushNamed("/detail"); //直接跳转到ProductionDetailPage页面

  通过pushNamed函数的定义就可以知道,我们可以通过该方法的arguments参数直接传递参数,并不需要通过目标页面的构造函数传值:

Navigator.of(context).pushNamed("/shopDetailView",arguments: _model);

  在目标页面中获取参数:

_model = ModalRoute.of(context)?.settings.arguments as DataBean;

  因为pushNamed方法返回值也是一个Future参数,所以如果pop携带参数的话,同样可以解析拿到

 

  • 返回按钮的监听

  这里有一个问,点击返回按钮默认是直接执行pop方法的,但默认是不会携带任何参数的。所以如果用户是点击右上角的返回按钮,如何监听呢?
    方法一:自定义返回的按钮(在详情页中修改Scaffold的appBar)

            appBar: AppBar(
                title: Text("详情页"),
                leading: IconButton(
                    icon: Icon(Icons.arrow_back),
                    onPressed: () {
                       Navigator.of(context).pop("a back detail message");
                    },
                 ),
              )

    方法二:监听返回按钮的点击(给Scaffold(页面)包裹一个WillPopScope)
    WillPopScope有一个onWillPop的回调函数,当我们点击返回按钮时会执行。这个函数要求有一个Future的返回值:
      ·true:那么系统会自动帮我们执行pop操作
      ·false:系统不再执行pop操作,需要我们自己

                   return WillPopScope(
                        onWillPop: () {
                            Navigator.of(context).pop("a back detail message");
                            return Future.value(false);
                         },
                       child: Scaffold(
                           appBar: AppBar(title: Text("详情页"),),
                           body: ·······
                        );
                    );
  • 钩子函数onGenerateRoute、onUnknownRoute

  命名路由虽然使用方便,但是路由表里的页面都是默认构造函数创建的,无法在初始化的时候传参。这个时候我们可以在注册路由表时通过设置onGenerateRoute函数来进行个性化设置。当通过名称无法在路由表中找到对应的页面是,会首先来到这里查看是否做了特殊处理。所以我们可以在该函数中,手动创建对应的Route进行返回;
  该函数有一个参数RouteSettings,该类有两个常用的属性:
    name: 跳转的路径名称
    arguments:跳转时携带的参数

  如果在onGenerateRoute这里也没找到对应名称的路由跳转处理的话,就会来到另一个钩子函数onUnknownRoute。也就是如果我们打开的一个路由名称在路由表里不存在也没有在onGenerateRoute做特殊处理的话,就会来到onGenerateRoute。默认是没有实现的,所以如果跳转到了不存在的路由器时,Flutter就会报错。所以为了避免报错,我们可以创建一个错误提示的页面,让所有不存在的跳转都跳转到错误提示页面的路由。

     //测试路由的钩子函数创建的类
     class AboutPage extends StatelessWidget{

         AboutPage(value){//自定义构造函数
            print(value);
         }

        @override
        Widget build(BuildContext context) {
            return Container();
        }
     }

    ////路由异常跳转错误提示页面
     class UnknownPage extends StatelessWidget {
         @override
         Widget build(BuildContext context) {
             return Scaffold(
                appBar: AppBar(
                   title: Text("错误页面"),
                ),
               body: Container(
                   child: Center(
                       child: Text("页面跳转错误"),
                   ),
              ),
           );
        }
     }

     child: MaterialApp(
        //初始化主路由
          initialRoute:"/",
          routes: {"/editUserInfoPage":(BuildContext context) => EditUserInfoPage()},
          title: 'Flutter Demo',
          onGenerateRoute: (settings){ //pushNamed对应的name没有在routes中有映射关系,那么就会执行onGenerateRoute钩子函数;
            /*
            * 函数有一个参数RouteSettings,该类有两个常用的属性:
            * name: 跳转的路径名称
            * arguments:跳转时携带的参数
            * */
            if (settings.name == "/about") {  //在该函数中,对某个路由名称手动创建对应的Route进行返回;
              return MaterialPageRoute(
                  builder: (ctx) {
                    return AboutPage(settings.arguments);//可以传参初始化
                  }
              );
            }
            return null;
          },

          onUnknownRoute: (settings){ //路由名称不存在,先去会执行onGenerateRoute钩子函数看有没有对该名称做出处理,没有的话则来到这个钩子函数。我们一般在这里做出处理,跳转到一个统一的错误页面。
            return MaterialPageRoute(
                builder: (ctx) {
                  return UnknownPage();
                }
            );
          },
    );

  在使用路由(Navigator),有时候会报下述错误:Navigator operation requested with a context that does not include a Navigator.

void main() {
   runApp(const MyApp3());
}
class MyApp3 extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
      return MaterialApp(
          //初始化主路由
          initialRoute:"/",
          routes: {"/editUserInfoPage":(BuildContext context) => EditUserInfoPage()},
          home:Scaffold(
              appBar: AppBar( ),
            floatingActionButton: FloatingActionButton(
                onPressed: (){
                  Navigator.pushNamed(context, "/editUserInfoPage");
                }),
          ),
        );
    }
}

这是由于 Navigator 的查找机制导致的错误,
  Navigator 查找机制 : 这是由于调用了 Navigator.of(context) 代码获取 Navigator , 注意这里的 context 上下文关联的是 StatelessWidget 组件 , 也就是从该 StatelessWidget 组件开始 , 向上查找 Navigator ;

  但是实际的层级是这样的 , StatelessWidget 包裹 MaterialApp 包裹 Scaffold 包裹 floatingActionButton, 查找 Navigator 时 , 到了MaterialApp后,发现还有上层,所以越过了 MaterialApp , 直接从最顶层的 StatelessWidget 组件开始向上查找 , 肯定找不到 Navigator , 这里直接报错了 ;

  这是 , 解决这个问题也很简单 , 在 StatelessWidget 的外层再包裹一个 MaterialApp , 这样就可以解决问题了 , 这样从 StatelessWidget 组件开始向上查找 Navigator , 就可以找到 Navigator , 问题解决 。

void main() {
   runApp(
       MaterialApp(
        //初始化主路由
          initialRoute:"/",
          routes: {"/editUserInfoPage":(BuildContext context) => EditUserInfoPage()},
          home: MyAppScaffold()
   );
}

class MyAppScaffold  extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
      return Scaffold(
              appBar: AppBar( ),
              floatingActionButton: FloatingActionButton(
              onPressed: (){
                  Navigator.pushNamed(context, "/editUserInfoPage");
              }),
       );
    }
}

 

18、主题    Theme参数详解详见https://www.jianshu.com/p/d9b486074485

  • 全局主题

    全局Theme会影响整个app的颜色和字体样式。使用起来非常简单,只需要向MaterialApp构造器传入 ThemeData 即可。如果没有设置Theme,Flutter将会使用默认的预设样式。

        factory ThemeData({
           Brightness brightness, // 应用主题亮度,可选(dark、light)
           VisualDensity visualDensity, // 视觉密度
           MaterialColor primarySwatch, // 主要样式,设置primaryColor后该背景色会被覆盖
           Color primaryColor, // 主要部分背景颜色(导航和tabBar等)
           Brightness primaryColorBrightness, // primaryColor的亮度
           Color primaryColorLight, // primaryColor的浅色版
           Color primaryColorDark, // primaryColor的深色版
           Color accentColor, // 前景色(文本,按钮等)
           Brightness accentColorBrightness, // accentColor的亮度
           Color canvasColor, // MaterialType.canvas 的默认颜色
           Color shadowColor, // 阴影颜色
           Color scaffoldBackgroundColor, // Scaffold的背景颜色。典型Material应用程序或应用程序内页面的背景颜色
           Color bottomAppBarColor, // BottomAppBar的默认颜色
           Color cardColor, // Card的颜色
           Color dividerColor, // Divider和PopupMenuDivider的颜色,也用于ListTile之间、DataTable的行之间等。
           Color focusColor, // 焦点颜色
           Color hoverColor, // hoverColor
           Color highlightColor, // 高亮颜色,选中在泼墨动画期间使用的突出显示颜色,或用于指示菜单中的项。
           Color splashColor, // 墨水飞溅的颜色。InkWell
           InteractiveInkFeatureFactory splashFactory, // 定义由InkWell和InkResponse反应产生的墨溅的外观。
           Color selectedRowColor, // 用于突出显示选定行的颜色。
           Color unselectedWidgetColor, // 用于处于非活动(但已启用)状态的小部件的颜色。例如,未选中的复选框。通常与accentColor形成对比。也看到disabledColor。
           Color disabledColor, // 禁用状态下部件的颜色,无论其当前状态如何。例如,一个禁用的复选框(可以选中或未选中)。
           Color buttonColor, // RaisedButton按钮中使用的Material 的默认填充颜色。
           ButtonThemeData buttonTheme, // 定义按钮部件的默认配置,
           ToggleButtonsThemeData toggleButtonsTheme, // 切换按钮的主题
           Color secondaryHeaderColor, // 选定行时PaginatedDataTable标题的颜色。
           Color textSelectionColor, // 文本框中文本选择的颜色,如TextField
           Color cursorColor, // 文本框中光标的颜色,如TextField
           Color textSelectionHandleColor, // 调整当前选定的文本部分的句柄的颜色。
           Color backgroundColor, // 与主色形成对比的颜色,例如用作进度条的剩余部分。
           Color dialogBackgroundColor, // Dialog元素的背景颜色
           Color indicatorColor, // 选项卡中选定的选项卡指示器的颜色。
           Color hintColor, // 用于提示文本或占位符文本的颜色,例如在TextField中。
           Color errorColor, // 用于输入验证错误的颜色,例如在TextField中
           Color toggleableActiveColor, // 用于突出显示Switch、Radio和Checkbox等可切换小部件的活动状态的颜色。
           String fontFamily, // 文本字体
           TextTheme textTheme, // 文本的颜色、字号。
           TextTheme primaryTextTheme, // 与primaryColor形成对比的文本主题
           TextTheme accentTextTheme, // 与accentColor形成对比的文本主题。
           InputDecorationTheme inputDecorationTheme, // 基于这个主题的 InputDecorator、TextField和TextFormField的默认InputDecoration值。
           TabBarTheme tabBarTheme, // 用于自定义选项卡栏指示器的大小、形状和颜色的主题。
           TooltipThemeData tooltipTheme, // tooltip主题
           CardTheme cardTheme, // Card的颜色和样式
           AppBarTheme appBarTheme, // appBar主题
           ColorScheme colorScheme, // 拥有13种颜色,可用于配置大多数组件的颜色。
           NavigationRailThemeData navigationRailTheme, // 导航边栏主题
           // ...
       })

  我们可以看到, 主题里有太多的属性, 实际应用中, 我们不需要全部定制, 我们只需要把握关键的几个属性就可以为整个APP定调, 如果需要进一步个性化定制 再去研究更细的属性, 或者重新写属于自己的控件也可以。而且需要注意的是上面的属性是旧版的,所以有些属性以及过期或者不在支持了。比如primaryColor(设置无效果)和accentColor(不再支持)已经失效了,但是可以通过colorScheme属性中的primary和secondary来去进行设置

class MyApp6 extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
          // 1.整体明暗对比度: light-dark,设为light时,整体读取的是primary和onPrimary的颜色,设为dark时读取的是surface和onsurface的颜色
          brightness: Brightness.light,

          // 2.primarySwatch: primaryColor/accentColor的结合体,用于设置主要颜色:导航、tabBar、floatingActionButton等
          primarySwatch: Colors.red,

        colorScheme: ColorScheme(
            brightness: Brightness.light,//外观风格,需要与themeData中的brightness一样,否则会报错
            primary: Colors.brown,//同primaryColor,主要部分背景颜色(导航和tabBar等)
            onPrimary: Colors.green, //此颜色用于为原色之上的元素(例如文本、图标等)着色。
            secondary: Colors.pink,//同accentColor,前景色,用于UI中不太突出的组件的强调色,例如(按钮、文本、覆盖边缘效果等)
            onSecondary: Colors.purple,//该颜色用于为次要颜色上的元素着色。
            error: Colors.white,//用于输入验证错误的颜色,例如[InputDecoration.errorText]
            onError: Colors.yellow,//这是与 error 颜色相得益彰的文本颜色,例如红色标志上的白色文本,便于阅读。
            background: Colors.teal,//整个应用程序的主要背景色。将其视为放置所有其他 UI 元素的画布。
            onBackground: Colors.indigoAccent,//颜色用于为背景色上的元素着色。
            surface: Colors.orangeAccent,//影响组件表面的表面颜色,例如卡片、表格和菜单。
            onSurface: Colors.blue // 在表面颜色上显示的用于文本和图标的颜色。
        ),

          // 5.卡片主题
          cardTheme: CardTheme(
              color: Colors.greenAccent,
              elevation: 10,
              shape: Border.all(width: 3, color: Colors.red),
              margin: EdgeInsets.all(10)
          ),

          // 6.按钮主题
          buttonTheme: ButtonThemeData(
              minWidth: 0,
              height: 25
          ),

          // 7.文本主题
          textTheme: TextTheme(//Material3将文字元素按用途分为5种:display、headline、title、label、body。每种又分为small、medium、large三种尺寸。因此一共支持15种不同大小的字体。
            bodyMedium: TextStyle(fontSize: 30, color: Colors.blue),//body中文字主题
            titleLarge: TextStyle(fontSize: 20,color: Colors.white),//标题中文字主题
          )
      ),

      home: Scaffold(
        appBar: AppBar(title: Text("主题测试")),
        bottomNavigationBar: BottomNavigationBar(items: [
          BottomNavigationBarItem(icon: Icon(Icons.sailing),label: "first"),
          BottomNavigationBarItem(icon: Icon(Icons.save),label: "second"),
        ]),
        body: Container(
          color: Colors.white,
          child: Center(
            child: Column(
              children: [
                Text("我是正文"),
                TextButton(onPressed: (){}, child: Text("按钮"))
              ],
            )
          ),
        ),
        floatingActionButton: FloatingActionButton(
          child: Icon(Icons.bluetooth_connected_sharp),
          onPressed: (){},
        ),
      ),
    );
  }
}
  • 局部主题

  如果某个具体的Widget不希望直接使用全局的Theme,而希望自己来定义,应该如何做呢?
    非常简单,只需要在该Widget的父节点包裹一下Theme即可,这样创建出来新的页面,会使用新的主题:

      class SecondPage extends StatelessWidget {//在新的页面的Scaffold外,包裹了一个Theme,并且设置data为一个新的ThemeData
          @override
          Widget build(BuildContext context) {
              return Theme(
                 data: ThemeData(primarySwatch: Colors.orange),
                 child: Scaffold()
             );
          }
      }

    但是,我们很多时候并不是想完全使用一个新的主题,也可以在之前的主题基础之上进行修改:

      class HYSecondPage extends StatelessWidget {
           @override
           Widget build(BuildContext context) {
               return Theme(
                 data: Theme.of(context).copyWith(//它只会覆盖掉设置的这些主题属性,其他未修改的则仍然是之前设置好的样式
                     colorScheme: ColorScheme(brightness:Theme.of(context).brightness, primary: Colors.green,······· ),

                child: Scaffold(),
              );
           }
      }
  • 暗黑适配

  目前很多应用程序都需要适配暗黑模式,Flutter中如何做到暗黑模式的适配呢?
  事实上,MaterialApp中有theme和dartTheme两个参数,如果只设置theme的话,则没有适配暗黑模式,即白天晚上一个样式。如果两个都设置的话,那么就进行了暗黑模式的适配,自动根据系统的设置选择对应的主题。

  需要注意的一点是ThemeData中有个属性brightness,他有两个值: Brightness.light和 Brightness.dark,看到值可能以为这个是设置暗黑模式的,其实并不是。这个属性和适配暗黑模式没有关系,是设置整体明暗对比度的。设为light时,整体读取的是primary和onPrimary的颜色,设为dark时读取的是surface和onsurface的颜色。适配暗黑模式只能通过实现darkTheme和theme来实现

   class MyApp extends StatelessWidget {
          // This widget is the root of your application.
          @override
          Widget build(BuildContext context) {
              return MaterialApp(
                 title: 'Flutter Demo',
                 theme: ThemeData.light(),
                 darkTheme: ThemeData.dark(),
                 home: HomePage(),
               );
           }
       }

  在开发中,为了能适配两种主题(设置是更多的主题),我们可以封装一个AppTheme,封装一个亮色主题,封装一个暗黑主题,将公共的样式抽取成常量。

class AppTheme {
  // 1.抽取相同的样式
  staticconstdouble _titleFontSize = 20;
  
  // 2.亮色主题
  staticfinal ThemeData lightTheme = ThemeData(
    primarySwatch: Colors.pink,
    primaryTextTheme: TextTheme(
      title: TextStyle(
        color: Colors.yellow,
        fontSize: _titleFontSize
      )
    ),
    textTheme: TextTheme(
      body1: TextStyle(color: Colors.red)
    )
  );
  
  // 3.暗黑主题
  staticfinal ThemeData darkTheme = ThemeData(
    primaryColor: Colors.grey,
    primaryTextTheme: TextTheme(
      title: TextStyle(
        color: Colors.white,
        fontSize: _titleFontSize
      )
    ),
    textTheme: TextTheme(
      title: TextStyle(color: Colors.white),
      body1: TextStyle(color: Colors.white70)
    )
  );
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: AppTheme.lightTheme,
      darkTheme: AppTheme.darkTheme,
      home: HomePage(),
    );
  }
}

----------------------------------------------------ThemeData参数详解参考----------------------------------------------------
  一个颜色方案包含了所有为 MaterialTheme 命名的颜色参数。颜色方案旨在和谐一致,确保可访问的文本,并将 UI 元素和表面彼此区分开。有两个内置的基线方案,即 lightColorSchemedarkColorScheme,可以按原样使用或进行自定义。
  材料颜色系统和自定义方案为颜色提供了默认值,作为自定义的起点。

属性:
primary - 主要颜色是在您的应用程序的屏幕和组件上最频繁显示的颜色。
onPrimary - 在主要颜色上显示的用于文本和图标的颜色。
primaryContainer - 容器的首选色调颜色。
onPrimaryContainer - 应在 primaryContainer 顶部使用的内容的颜色(和状态变体)。
inversePrimary- 在需要反转颜色方案的地方用作“主要”颜色,例如 Snackbar 上的按钮。

secondary -次要颜色为您的产品提供了更多的强调和区分方式。次要颜色最适合:浮动操作按钮、选择控件,如复选框和单选按钮、突出显示选定的文本、链接和标题
onSecondary - 在次要颜色上显示的用于文本和图标的颜色。
secondaryContainer - 用于容器的色调颜色。
onSecondaryContainer - 应在 secondaryContainer 顶部使用的内容的颜色(和状态变体)。
tertiary - 可以用来平衡主要和次要颜色,或者对诸如输入字段等元素给予更高的关注。
onTertiary - 在三级颜色上显示的用于文本和图标的颜色。
tertiaryContainer - 用于容器的色调颜色。
onTertiaryContainer - 应在 tertiaryContainer 顶部使用的内容的颜色(和状态变体)。
background - 出现在可滚动内容后面的背景颜色。
onBackground - 在背景颜色上显示的用于文本和图标的颜色。
surface - 影响组件表面的表面颜色,例如卡片、表格和菜单。
onSurface - 在表面颜色上显示的用于文本和图标的颜色。
surfaceVariant - 具有与表面类似用途的另一种颜色选项。
onSurfaceVariant - 可用于表面上方内容的颜色(和状态变体)。
surfaceTint - 此颜色将由应用色调提升的组件使用,并在表面之上应用。海拔越高,使用这种颜色就越多。
inverseSurface - 与表面形成鲜明对比的颜色。对于位于其他具有表面颜色的表面之上的表面非常有用。
inverseOnSurface - 与 inverseSurface 形成良好对比的颜色。对于位于具有 inverseSurface 的容器之上的内容很有用。
error - 用于指示组件中的错误,例如文本字段中的无效文本的错误颜色。
onError - 在错误颜色上显示的用于文本和图标的颜色。
errorContainer - 错误容器的首选色调颜色。
onErrorContainer - 应在 errorContainer 顶部使用的内容的颜色(和状态变体)。
outline - 用于边界的微妙颜色。轮廓颜色角色为可访问性目的增加了对比度。
outlineVariant - 在不需要强烈对比度时用于边界的实用颜色,用于装饰元素的边界。
scrim - 遮挡内容的蒙板颜色。
surfaceBright - 始终比表面更亮的表面变体,无论是在亮模式还是暗模式下。
surfaceDim - 始终比表面更暗的表面变体,无论是在亮模式还是暗模式下。
surfaceContainer - 影响组件容器的表面变体,例如卡片、表格和菜单。
surfaceContainerHigh - 比 surfaceContainer 具有更高强调的容器的表面变体。将此角色用于需要比 surfaceContainer 更强调的内容。
surfaceContainerHighest - 比 surfaceContainerHigh 具有更高强调的容器的表面变体。将此角色用于需要比 surfaceContainerHigh 更强调的内容。
surfaceContainerLow - 比 surfaceContainer 强调程度低的容器的表面变体。将此角色用于需要比 surfaceContainer 更少强调的内容。
surfaceContainerLowest - 比 surfaceContainerLow 强调程度低的容器的表面变体。将此角色用于需要比 surfaceContainerLow 更少强调的内容。

ColorScheme参数解释

class ColorScheme(
/* 主色系 */
primary: Color, // 主色,用于应用的大部分 UI 元素,如按钮、选中的选项卡等。
onPrimary: Color, // 在主色上清晰显示的颜色,通常用于文本或图标。
primaryContainer: Color, // 主色的容器色,用于需要主色变体的元素背景。
onPrimaryContainer: Color, // 在主色容器上清晰显示的颜色,通常用于文本或图标。
inversePrimary: Color, // 主色的反色,用于在对比背景上需要主色时。

/* 次色系 */
secondary: Color, // 次级色,用于补充主色或用作次要的 UI 元素。
onSecondary: Color, // 在次级色上清晰显示的颜色,通常用于文本或图标。
secondaryContainer: Color, // 次级色的容器色,用于需要次级色变体的元素背景。
onSecondaryContainer: Color, // 在次级色容器上清晰显示的颜色,通常用于文本或图标。

/* 第三色系 */
tertiary: Color, // 第三色,用于需要注意或区分的 UI 元素。
onTertiary: Color, // 在第三色上清晰显示的颜色,通常用于文本或图标。
tertiaryContainer: Color, // 第三色的容器色,用于背景或填充色。
onTertiaryContainer: Color, // 在第三色容器上清晰显示的颜色,通常用于文本或图标。

/* 背景与表面色 */
background: Color, // 背景色,用于页面或组件的背景。
onBackground: Color, // 在背景色上清晰显示的颜色,通常用于文本或图标。
surface: Color, // 表面色,用于卡片、菜单和其他元素的背景。
onSurface: Color, // 在表面色上清晰显示的颜色,通常用于文本或图标。
surfaceVariant: Color, // 表面色的变体,用于需要区分的表面元素。
onSurfaceVariant: Color, // 在表面色变体上清晰显示的颜色。
surfaceTint: Color, // 表面色的着色,通常用于表面元素的图标或小组件。
inverseSurface: Color, // 表面色的反色,用于需要高对比度的背景。
inverseOnSurface: Color, // 在反表面色上清晰显示的颜色。

/* 错误处理色 */
error: Color, // 错误色,用于指示错误或警告状态,如输入校验失败。
onError: Color, // 在错误色上清晰显示的颜色,通常用于错误文本或图标。
errorContainer: Color, // 错误色的容器色,用于错误状态的背景。
onErrorContainer: Color, // 在错误容器色上清晰显示的颜色。

/* 其他 */
outline: Color, // 用于元素边框的颜色。
outlineVariant: Color, // 边框颜色的变体,可能用于更细微的分界线。
scrim: Color, // 遮罩层颜色,通常用于遮盖或暗化背景中的内容。
)

文本主题:TextTheme

displayLarge:显示大 - 显示大是最大的显示文本。
displayMedium:显示中 - 显示中是第二大的显示文本。
displaySmall:显示小 - 显示小是最小的显示文本。
headlineLarge:标题大 - 标题大是最大的标题,专为简短、重要的文本或数字保留。对于标题,您可以选择富有表现力的字体,例如展示、手写或手写体风格。这些非传统的字体设计具有细节和复杂性,有助于吸引眼球。
headlineMedium:标题中 - 标题中是第二大的标题,专为简短、重要的文本或数字保留。对于标题,您可以选择富有表现力的字体,例如展示、手写或手写体风格。这些非传统的字体设计具有细节和复杂性,有助于吸引眼球。
headlineSmall:标题小 - 标题小是最小的标题,专为简短、重要的文本或数字保留。对于标题,您可以选择富有表现力的字体,例如展示、手写或手写体风格。这些非传统的字体设计具有细节和复杂性,有助于吸引眼球。
titleLarge:标题大 - 标题大是最大的标题,通常为长度较短的中等强调文本保留。衬线或无衬线字体非常适合副标题。
titleMedium:标题中 - 标题中是第二大的标题,通常为长度较短的中等强调文本保留。衬线或无衬线字体非常适合副标题。
titleSmall:标题小 - 标题小是最小的标题,通常为长度较短的中等强调文本保留。衬线或无衬线字体非常适合副标题。
bodyLarge:主体大 - 主体大是最大的主体,通常用于长篇写作,因为它适用于较小的文本尺寸。对于较长的文本部分,建议使用衬线或无衬线字体。
bodyMedium:主体中 - 主体中是第二大的主体,通常用于长篇写作,因为它适用于较小的文本尺寸。对于较长的文本部分,建议使用衬线或无衬线字体。
bodySmall:主体小 - 主体小是最小的主体,通常用于长篇写作,因为它适用于较小的文本尺寸。对于较长的文本部分,建议使用衬线或无衬线字体。
labelLarge:标签大 - 标签大文本是用于不同类型按钮(如文本、轮廓和包含按钮)以及选项卡、对话框和卡片的行动呼吁。按钮文本通常为无衬线,使用全部大写文本。
labelMedium:标签中 - 标签中是最小字体之一。它很少用于注释图像或介绍标题。
labelSmall:标签小 - 标签小是最小字体之一。它很少用于注释图像或介绍标题。

 

19、屏幕适配
首先要弄懂两个概念:物理像素(pixel)和逻辑像素(point)
1、物理像素(px)呢就是手机屏幕真实的像素点,比如iPhone 3GS屏幕上有320x480=153600个像素点,而iphone4之后采用Retina显示屏,在物理尺寸不变的情况下,像素成倍增加,Phone 4屏幕上则有640 x 960 = 614400个像素点,像素个数是原来的4倍。

2、这样就出现了一个问题,怎么样让原来的App运行在新的手机上面? 为了运行之前的App,Apple引入了一个新的概念:point (点),也就是逻辑像素(pt)。在iPhone 3GS中,一个点等于一个像素,也就是说点与像素可以直接互换;在iPhone 4中,一个点等于两个像素;在iPhone 7 Plus中,一个点等于三个像素。
所以iPhone6的尺寸是375x667,但是它的分辨率其实是750x1334。其中375x667是逻辑像素(pt),750x1334是物理像素(px)

3、dpr(devicePixelRatio),分辨率比(物理像素:逻辑像素)。iPhone 3GS的dpr是1.0,iPhone6的dpr是2.0,iPhone6plus的dpr是3.0。

4、在ios开发者,我们设置尺寸或者字体时从不填单位,因为它使用单位就是点pt。Flutter开发也是一样,使用的是对应的逻辑分辨率

5、但是在UI设计稿里一般使用的都是物理像素px,所以我们经常除2才是开发用到的数字。比如UI设计图中经常选择中间尺寸750 x1334px作为基准,向下适配640x1136px,向上适配1242x2208px和750x1624px/1125x2436px。

  了解了这些基本知识后,我们再来看一下Flutter的屏幕适配。其实原理和iOS是一样的,都是【显示屏幕宽度/设计稿基准屏幕宽度=缩放系数,然后用缩放系数*长度(宽度、字号)来进行适配。
  那么首先就需要拿到屏幕的宽高度信息,我们可以通过MediaQuery.of(context).size和window.physicalSize两种方式来获取。但是MediaQuery需要获取上下文,不方便如果获取时机不当也容易报错,所以这里通过的方式获取。

    final width = window.physicalSize.width;
    final height = window.physicalSize.height;
    final dpr = window.devicePixelRatio;

  拿到了宽高和分辨率比,我们只需要进行一些运算就可以进行适配了,下面的GCSizeFit是自己封装的一个适配的工具类,并且对Int类和double类做了拓展,直接在数值后面点pt或者px就自动进行适配。需要注意的是:在使用GCSizeFit之前,需要先调用initialize构造函数进行初始化,这样才能获取像素信息。

//大多数都是以iphone6s的尺寸750 x1334px作为基准,假如实际开发中是以12Pro Max1284*2778出的基准稿,只需要初始化时设置standardWidth=1284和asset=3即可。
class GCSizeFit {
  static late double screenWidth;
  static late double screenHeight;
  static late double statusHeight;
  static late double bottomHeight;
  static late double rpx;
  static late double pt;
  //传入基准手机物理宽度,默认是6s的750,单位是px,asset:@2x就传2,@3x就传3,默认是6s的2
  static void initialize ({int standardWidth = 750, int asset = 2}) {
    //手机物理宽高
    final physicalWidth = window.physicalSize.width;
    final physicalHeight = window.physicalSize.height;
    //dpr
    final dpr = window.devicePixelRatio;
    //屏幕宽高
    screenWidth = physicalWidth / dpr;
    screenHeight = physicalHeight / dpr;
    //刘海及安全区域高度
    statusHeight = window.padding.top / dpr;
    //底部安全区域高度
    bottomHeight = window.padding.bottom / dpr;
    //计算rpx的大小
    rpx = screenWidth / standardWidth;
    //计算px的大小
    pt = rpx * asset;
    print('GCSizeFit{屏幕宽: $screenWidth, 屏幕高: $screenHeight, dpr:$dpr\n'
        '刘海及安全区域高: $statusHeight, \n'
        '底部安全区域高: $bottomHeight, \n'
        'rpx: $rpx, pt: $pt}');
  }

  //像素为单位的前端用这个,单位为px
  static double setRPX(num size) {
    return rpx * size;
  }
  //蓝湖pt为单位的移动端用这个  单位为pt
  static double setPT(num size) {
    return pt * size;
  }
}

/*
* 扩展double
* 使用 123.45.px 或123.45.pt
* */
extension DoubleFit on double {
  double get px {
    return GCSizeFit.setRPX(this);
  }
  double get pt {
    return GCSizeFit.setPT(this);
  }
}

/*
* 扩展int
* 使用 123.px 或123.pt
* */
extension IntFit on int {
  double get px {
    return GCSizeFit.setRPX(this);
  }
  double get pt {
    return GCSizeFit.setPT(this);
  }
}

使用:

//在使用GCSizeFit之前,需要先调用initialize构造函数进行初始化,这样才能获取像素信息。
GCSizeFit.initialize();

Container(
          //Flutter开发移动端使用的单位是pt
          width: GCSizeFit.setPT(200),
          height: 200.pt,
          color: Colors.lime,
 );

        当然也可以借助一些第三方库,比如flutter_screenutil(https://github.com/OpenFlutter/flutter_screenutil),其实现原理与我们的方法相同。常用的几个方法如下:
            尺寸适配:flutter_screenutil提供了setWidth()[根据屏幕宽度适配]和setHeight()[根据屏幕高度适配]方法。一般来说,控件尺寸都是根据宽度进行适配的,所以基本调用setWidth()就行,但某些特殊情况需要按高度适配的话,则需要执行setHeight()
            字体适配:flutter_screenutil提供了setSp()方法  

  如果获取一些设备相关的信息,可以使用官方提供的一个库:device_info_plus ,比如设备的名称、操作系统的版本、设备的型号、唯一标识符等等


20、应用信息

  • 设置应用包名(Bundle identifier)

    1.Android
      Android应用标识在对应的Android目录下:Android/app/build.gradleapplicationId:是打包时的应用标识。然后,快捷键Command + Shift + F全局搜索使用的包名,全部替换成新包名。
    2.iOS
      iOS应用标识在对应的iOS目录下:ios/Runner/Info.plis,,找到key为CFBundleIdentifier的键值对,对其值进行设置(但最好通过Xcode打开iOS工程来进行修改)。
      还有种方法就是打开ios/Runner.xcodeproj/project.pbxproj 文件,搜索PRODUCT_BUNDLE_IDENTIFIER,查看当前iOS使用的包名。然后全局搜索使用的包名,全部替换成新包名。

  • 设置应用名称

    1.Android
      编辑 android/app/src/main/AndroidManifest.xml 文件,并设置'android:label' 属性:
    2.iOS
      编辑 ios/Runner/Info.plist 文件,并设置CFBundleDisplayNameCFBundleName(可以通过Xcode打开来进行修改)。
      CFBundleName 是应用程序的内部标识符,用于文件系统中的标识和唯一性。而 CFBundleDisplayName 是应用程序的用户可见名称,用于在设备上显示给用户。
      在大多数情况下,开发者会将 CFBundleDisplayName 设置为更友好和描述性的名称,以便用户能够轻松识别和使用应用程序。

  • 设置应用版本号

    在 pubspec.yaml 文件中,您可以使用 version 字段来设置版本号和构建号,格式为 x.x.x+x,例如:1.0.0+1。这里 1.0.0 是版本号,+1 是构建号。每次发布新版本到应用商店时,都应该增加构建号。
    在pubspec.yaml 设置的版本信息会自动更新到安卓项目和iOS项目上,但我们也可以分别手动设置,方法如下:
      1.Android
        android/app/build.gradle 文件中,versionCodeversionName 这一对属性就是用来设置版本号和构架版本的,默认是从 pubspec.yaml 文件中自动获取
        当然也可以手动修改,其中versionCode是构建号,需要传一个int值,例如3;而versionName则是版本号,需要传一个字符串,例如"1.1.2"
      2.iOS
        而iOS可以通过ios/Runner/Info.plis 里面的CFBundleShortVersionString(版本号)和 CFBundleVersion(构建号)去进行设置,默认也是从 pubspec.yaml 文件中自动获取。
  -------------------------------题外话---------------------------------------
  我们在pubspec.yaml文件中修改version可以控制名称,但还有其他属性,比如namedescription 等。那是不是修改这里的name就是修改app的名称呢?并不是。
  这里的name表示包名(package name),引入其他文件时需要使用此包名:import 'package:flutter_app/home_page.dart';如果这里修改的话,那么引入文件的路径也需要随之修改;
  而description 属性是一个可选配置属性,是对当前项目的介绍。如果作为插件发布到 pub.dev 上,此值会显示在对应的网页位置

  • 设置最低支持系统版本和目标版本

  在Flutter开发中,设置应用的最低支持系统版本和目标版本需要在特定平台的项目设置中进行。这里分别介绍如何为Android和iOS设置这些版本。
  1.Android
    修改android/app/build.gradle文件,找到defaultConfig部分,然后设置minSdkVersion(最低支持版本)targetSdkVersion(目标版本)。与iOS只有一个最低版本的设置不同,安卓需要设置一个最低版本和目标版本。其中,
      minSdkVersion: 最小版本。它表示APP可以支持的Android SDK的最低版本. 意为小于该版本的Android系统上不保证APP正常运行。
      targetSdkVersion:目标版本。表示开发者已经测试过的最高的Android版本. 当新版本的Android可用的时候, 我们应在新版本上测试APP并更新这个值以匹配最新版本的API,从而使用新版本的功能。
    这里需要填的数值是Android SDK的API版本,并不是Android系统版本。Android SDK版本查询 https://developer.android.google.cn/tools/releases/platforms?hl=zh-cn。例如minSdkVersion设置为16表示应用程序最低支持Android 4.1(Jelly Bean)。targetSdkVersion设置为30表示应用程序针对的是Android 11。应该根据实际需要设置这些值。
  2.iOS
    方法1:全局搜索IPHONEOS_DEPLOYMENT_TARGET, 然后修改部署目标版本。
    方法2:在Xcode中打开iOS项目进行设置。

  需要注意的一点:在某些情况下,Flutter插件可能需要特定版本的平台SDK。这些要求通常在插件的pubspec.yaml文件中指定。确保你的项目pubspec.yaml文件中列出的所有依赖项都支持你设置的最低平台版本。查询最低支持,Flutter Packages 网站(https://pub.dev)。

  • 设置应用Icon

  1.Android
    1)Android通常需要多个图标大小以适应不同的设备屏幕密度。图标通常放在 android/app/src/main/res/ 目录下的不同的 mipmap-类型 文件夹中,例如mipmap-mdpi文件夹、mipmap-hdpi文件夹、mipmap-xhdpi文件夹、mipmap-xxhdpi文件夹、mipmap-xxhdpi文件夹。
    2)将图标文件名保持为 ic_launcher.png,替换所有的 mipmap-类型 文件夹中的 ic_launcher.png 文件
    3)如果你的图标名称或位置有所不同,更新 android/app/src/main/AndroidManifest.xml 文件中的 <application> 标签的 android:icon 属性。
  2.iOS
    iOS的应用图标在ios/Runner/Assets.xcassets/AppIcon.appiconset中管理(可以直接打开Xcode将对应的图标拖入
  3.使用自动化工具
    使用第三方工具来自动化这一过程。例如,flutter_launcher_icons,提供了一种简单的方式来同时为Android和iOS生成应用图标。使用步骤如下:
      ①将 flutter_launcher_icons 添加到 pubspec.yaml 文件中的 dev_dependencies 部分:

                 dev_dependencies:
                       flutter_launcher_icons: "^0.9.2"

                 flutter_icons:
                     android: true //是否为Android项目生成图标
                     ios: true  //是否为iOS项目生成图标
                     image_path: "assets/icon/app_icon.png" //图标Icon路径
                      # 你也可以为不同的平台指定不同的图标文件
                      # image_path_android: "assets/icon/app_icon_android.png"
                      # image_path_ios: "assets/icon/app_icon_ios.png"
                      # 可以添加更多的配置项,如适用于Android的adaptive_icon_background等

    ②运行以下命令来生成应用图标,flutter_launcher_icons 将根据你指定的源图标文件 app_icon.png 自动生成需要的各种尺寸的图标,并替换 iOS 和 Android 项目中的现有图标。

flutter pub get
flutter pub run flutter_launcher_icons:main

  这样就不需要手动进入 Xcode 或 Android Studio 设置应用图标,flutter_launcher_icons 已经自动完成了这些步骤。不过,请确保在运行上述命令之前关闭 Xcode 和 Android Studio,因为这些工具可能会锁定一些文件,导致 flutter_launcher_icons 无法正确写入新图标。

  • 设置启动页

  1.Android
    Android中默认的启动图是一片空白的,这是Flutter的默认设置效果。如果需要修改的话,在android/app/src/main/res/drawable/launch_background.xml进行修改。
      1)首先将不同尺寸的启动图分别添加到我们设置Icon时用到的 mipmap-类型 文件夹中,统一命名为launch_image.png
      2)将launch_background.xml中的android:src这一部分代码注释打开,然后就可以了。

    但有时候,我们是之后发现没有效果。这个时候需要看一下res目录中,是不是有多个drawable文件夹(比如文件夹drawable、文件夹drawable-v21),分别进行修改。
    这是因为应用加载drawable会根据API的不同,分别到对应的drawable-v数字文件夹下去进行查找。比如 drawable-v21: 这个文件夹用于存放针对Android 5.0(API级别21)及更高版本的特定版本的可绘制资源。当应用运行在 Android 5.0 及更高版本时,系统会优先加载这个文件夹下的资源
    drawable-v21文件名后面的数字代表安卓的版本API ,v21代表的安卓5.0

    另外在aunch_background.xml中还有个属性android:drawable,这是控制启动页的背景色的,可以进行修改。
    我们可以在res/values文件夹下创建一个colors.xml,然后定义一个颜色,比如splash_color:

<resources>
<color name="splash_color">#FF00FF</color>
</resources>

    然后修改android:drawable的值即可:<item android:drawable="@color/splash_color"/>

  2.iOS
    在iOS中,我们可以直接替换ios\Runner\Assets.xcassets\LaunchImage.imageset中的三张启动图就行。但最好还是通过Xcode打开项目,通过故事板(Storyboard)来配置启动页。

  3.使用自动化工具
    同样,我们也可以使用一些三方库来进行配置,简化操作。flutter_native_splash 是一个流行的 Flutter 插件,可以自动帮我们完成上述操作,用于轻松地生成和配置本地化的启动页
      1)首先,要在项目的 pubspec.yaml 文件中添加 flutter_native_splash 作为一个开发依赖项。

      2)在 pubspec.yaml 文件中,你可以配置启动页的各种属性,如背景颜色、图片、文字等

                  flutter_native_splash:
                      color: "#42a5f5"  //用于设置启动图的背景颜色
                      image: "assets/icons/icon_launch.jpg"  //指定启动图资源文件的路径
                      android: true    //是否为android平台生成闪屏界面
                      ios: true    //是否为iOS平台生成闪屏界面
                      duration: 2500, //闪屏时间为2.5s
 
                        # 从 Android 12 开始,在所有应用的冷启动和温启动期间,系统一律会应用Android 系统的默认启动画面。系统默认启动画面由应用的启动器图标元素和主题的 windowBackground(如果是单色)构成。
                        # 官网说明https://developer.android.google.cn/develop/ui/views/launch/splash-screen?hl=zh-cn
                      android_12:
                        # image参数设置闪屏图标图像。 如果不指定该参数,将使用应用程序的启动器图标。
                        # 请注意,初始屏幕将被裁剪为屏幕中心的圆圈。
                        # 带有图标背景的应用程序图标:这应该是 960×960 像素,并且适合一个圆圈
                        # 没有图标背景的应用程序图标:这应该是 1152×1152 像素,并且适合一个圆圈
                        image: "assets/icons/icon_launch.jpg"

                        # 启动画面背景颜色。
                        color: "#161517"

                        # 应用程序图标背景颜色。
                        #icon_background_color: "#111111"

                        # 该属性允许你指定图像作为商标在闪屏界面显示。它必须是 png 文件。现在它只支持 Android 和 iOS 。
                       #branding: assets/dart.png

    3)生成启动页:配置好 pubspec.yaml 文件后,运行以下命令以生成启动页:

flutter pub get
dart run flutter_native_splash:create

    4)如果不想要启动图了,想恢复 Flutter 默认的白色启动页,可以执行下面指令:

dart run flutter_native_splash:remove
  • 环境判断

  常量kReleaseMode,它会根据你的应用是以什么模式编译的来获取值。kReleaseMode是foundation库的一部分,这意味着你不需要手动定义它,可以直接使用。有以下三种模式:
    kDebugMode: 当应用在Debug模式下运行时为true。Debug模式可以同时在物理设备、仿真器或者模拟器上运行应用。默认情况下,使用flutter run命令运行应用程序时就是使用的Debug模式。
    kProfileMode: 当应用在Profile模式下运行时为true。此模式只能在物理设备上运行,不能在模拟器上运行。使用flutter run --release命令运行应用程序时就是使用的Release模式。
    kReleaseMode: 当应用在Release模式下运行时为true。 Profile模式只能在物理设备上运行,不能在模拟器上运行。此模式主要用于应用性能分析,一些应用调试能力是被保留的,目的是分析应用存在的性能问题。
  由于 Profile 与 Release 在编译过程上几乎无差异,因此我们今天只讨论 Debug 和 Release 模式。

/* 当应用以Release模式编译时(例如运行flutter build apk或flutter build ios),kReleaseMode会被设置为true。
  当应用在Debug模式或Profile模式下运行时,kReleaseMode会被设置为false。*/
if (kReleaseMode) {
print("dart.vm.product-现在是release环境.");
} else {
print("dart.vm.product-现在是debug环境.");
}

  在Xcode中,默认情况下运行或构建应用会使用Debug配置,这意味着如果你直接通过Xcode的运行按钮(通常是顶部左侧的一个播放按钮)启动应用,它将默认使用Debug模式。如果修改的话,可以在前往Xcode的顶部菜单栏,选择Product > Scheme > Edit Scheme > Archive > Build Configuration 进行设置。
  Android Studio 运行项目,默认安装到手机上的 app 也属于debug 包,而且在Android Studio 没找到运行release版本的入口,现在需要连接上真机,通过命令行flutter run --release运行release模式。

  • 平台判断

  Flutter中,你可以使用Platform类来检测应用程序正在哪个操作系统平台上运行。这个类位于dart:io库中。

        import 'dart:io' show Platform;
        if (Platform.isAndroid) {
            // Android平台的代码
        } else if (Platform.isIOS) {
            // iOS平台的代码
        } else if (Platform.isLinux) {
            // Linux平台的代码
        } else if (Platform.isMacOS) {
            // macOS平台的代码
        } else if (Platform.isWindows) {
            // Windows平台的代码
        } else if (Platform.isFuchsia) {
            // Fuchsia平台的代码
        }
  • 权限申请

  App在原生功能访问中都需要申请权限后才能使用,比如存储权限、网络访问权限、定位权限等等。在 flutter 开发中,则需要一个跨平台(iOS, Android)的 API 来请求权限和检查他们的状态,这时候就需要使用 flutter 插件permission_handler来帮忙了,它允许您查看和申请相关权限
  使用介绍,详见印象笔记:https://app.yinxiang.com/fx/c03ec6da-09b8-4c0d-8109-e90a01c649dc   


21、国际化

  • Widget组件国际化

  Flutter给我们提供的Widget默认情况下就是支持国际化,比如日历组件。但是在没有进行特别的设置之前,它们无论在什么环境都是以英文的方式显示的。
  如果想要添加其他语言,应用必须指定额外的 MaterialApp 属性并且添加一个单独的 package,叫做 flutter_localizations
    1、在 pubspec.yaml 文件中添加它作为依赖,这个和设置三方库依赖不同:

    dependencies:
      #添加国际化包
      flutter_localizations:
        sdk: flutter

    2、设置MaterialApp:参数localizationsDelegates中指定哪些Widget需要进行国际化;参数supportedLocales指定要支持哪些国际化

      MaterialApp(
       localizationsDelegates: [ //我们这里指定了Material、Widgets、Cupertino都使用国际化
         GlobalMaterialLocalizations.delegate, // 指定本地化的字符串和一些其他的值
         GlobalCupertinoLocalizations.delegate, // 对应的Cupertino风格
         GlobalWidgetsLocalizations.delegate // 指定默认的文本排列方向, 由左到右或由右到左
       ],
       supportedLocales: [//我们这里指定中文和英文(也可以指定国家编码)
         Locale("en"),
         Locale("zh")
       ],
      )

    3、完成以上两步,安卓项目就完成了Widget的国家化,但是iOS还不行,需要对iOS项目中对应的info.plist文件进行修改

      用Xcode打开iOS项目中对应的info.plist文件,选择 Information Property List 项;
      从 Editor 菜单中选择 Add Item,然后从弹出菜单中选择 Localizations
      为array添加一项选择 Add Item,选择Chinese;

  • 文本国际化

  App中除了有默认的Widget,我们也希望对自己的文本进行国际化,其原理和iOS做国家化是一样的,就是不同的环境加载不同的语言文件包。比较流行的方法是通过Flutter Intl插件实现【也可以通过GetX框架实现国际化】。
    1.在Android Studio的Plugins中安装Flutter Intl插件

    2.初始化intl,选择工具栏Tools - Flutter Intl - Initialize for the Project。完成上面的操作之后会自动生成如下文件目录:
      *generated是自动生成的dart代码
      *I10n是对应的arb文件目录(arb全称Application Resource Bundle,表示应用资源包),我们适配的语言文件就是在这里。

    3.使用intl
      ①在localizationsDelegates添加对应的delegate:S.delegate
      ②supportedLocales使用S.delegate.supportedLocales。(我们在widget国际化时,需要设置为Locale("en")和Locale("zh")。设置为S.delegate.supportedLocales,它会根据添加的语言环境去自动适配widget的国际化,比如intl增加了中文语言,widget也会增加中文的支持,所以只设置为S.delegate.supportedLocales就行。)
      ③在intl_en.arb文件中进行编写保存

{
"title": "home",
"greet": "hello~",
"picktime": "Pick a time"
}

      ④增加其他语言支持,Tools - Flutter Intl - add local,比如如果希望添加中文支持,在弹出框中输入zh即可。然后在intl_zh_.arb文件中进行编写保存:

{
"title": "首页",
"greet": "您好~",
"picktime": "选择一个时间"
}

      ⑤在代码中使用即可,按照如下格式:S.of(context).title

intl_zh.arb文件
     {
          "title" : "标题"
     }

intl_en.arb文件
     {
       "title" : "title"
     }

main.dart文件
void main(){
  runApp(
      MaterialApp(
          localizationsDelegates: [
            GlobalMaterialLocalizations.delegate, // 指定本地化的字符串和一些其他的值
            GlobalCupertinoLocalizations.delegate, // 对应的Cupertino风格
            GlobalWidgetsLocalizations.delegate, // 指定默认的文本排列方向, 由左到右或由右到左
            S.delegate
          ],
          // supportedLocales: [//我们这里指定中文和英文(也可以指定国家编码)
          //   Locale("en"),
          //   Locale("zh")
          // ],
          supportedLocales: S.delegate.supportedLocales,
          home:myAppInternationalScaffold()
      )
  );
}

_showDatePicker(context) async{
  var date =await showDatePicker(
      context: context,
      initialDate: DateTime.now(),
      firstDate:DateTime(1900),
      lastDate:DateTime(2050)
  );
  if(date==null) return;
  print(date);
}

class myAppInternationalScaffold extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    return Scaffold(
          appBar: AppBar(
            title: Text(S.of(context).title),
          ),
          body:Center(
            child:Container(
              color: Colors.red,
              width: 200,
              height: 200,
            ),
          ),
          floatingActionButton: FloatingActionButton(onPressed: (){
            _showDatePicker(context);
          })
      );
  }
}

 

22、数据存储(数据持久化)
数据持久化是指将应用程序中的数据保存在持久存储介质(如硬盘、数据库等)中的过程。Flutter中的数据持久化方式:

  • Shared Preferences,用于存储简单的数据类型(如字符串、整数、布尔值等)的键值对,类似于Android中的SharedPreferences或iOS中的UserDefaults。适用于存储简单的配置、设置和用户偏好数据。

    * 1.首先在pubspec.yaml文件中添加依赖:shared_preferences: ^2.0.0
    * 2.通过SharedPreferences.getInstance()创建一个单例对象;
    * 3.通过set方法设置键值对,通过get方法通过key取出对应的值。支持存储五种数据格式:setBool、setDouble、setInt、setString、setStringList

  • 文件存储(File Storage),文件存储适用于存储结构化数据或需要较长时间存储的数据。可以使用Dart的dart:io库来操作文件。常用的存储格式包括JSON、XML等。

    * 1.首先我们需要先添加path provider的依赖,这个包有助于获取文件存储的目录路径。根据getApplicationDocumentsDirectory()方法获取根路径,然后在拼接文件的保存路径;
    * 2.通过File类,创建对应路径的文件。
    * 3.通过File的write方法进行文件写入,可以写入Bytes和String两种数据
    * 4.读取也是一样,给File类输入对应的路径,然后执行read方法就能读取数据

  • SQLite数据库,一种嵌入式关系数据库,适用于需要复杂数据查询和关系操作的情况,但使用相对较复杂,需要熟悉SQL语法。Flutter可以通过第三方库sqflite来访问SQLite数据库。

    * 1.添加sqflite依赖:sqflite: ^2.0.0
    * 2.通过openDatabase方法根据路径创建或打开一个数据库,通过version参数置设版本,通过onCreate参数执行CREATE TABLE test(id INTEGER PRIMARY KEY, name TEXT)指令新建一个表test
    * 3.然后分别通过insert、delete、update和query方法实现增删改查的功能;当然也可以通过rawInsert、rawDelete、rawUpdate和rawQuery方法实现,但是这几个方法需要直接使用SQL语句。而不带row的则是Android自己封装的查询API,会根据内容帮你拼写 SQL 语句。

  • 使用第三方数据库服务(如Hive)

  因为使用SQLite比较麻烦,所以我们可以使用第三方数据库服务,比如Hive。这是一个轻量级、键值对存储的数据库,适合在移动设备上高效存储数据。它不需要初始化异步操作,因此在很多情况下比SQLite更简单和高效。
    *1、首先在pubspec.yaml文件中添加依赖,在dependencies中添加了hive的库。

dependencies:
  hive: ^2.2.3
  hive_flutter: ^1.1.0

    * 2、在main方法里初始化:await Hive.initFlutter();
    * 3、在Hive中,所有的数据都被存储到box中。Box是Hive中的最小存储单元,类似于文件系统中的文件夹,我们可以使用它来存储和检索数据。在使用box之前,必须先打开它。我们可以使用Hive.openBox()方法打开一个box:var box = await Hive.openBox<E>('testBox');
    * 4、在Hive中,数据是以键值对的形式存储的。我们可以使用box的put方法,将数据存储到Box里面
    * 5、使用 get() 方法可以获取数据,如果key对应的value不存在的话,会返回 null,为了避免这种情况的发生,我们可以设置一个返回值:_box.get('key', defaultValue: 'No Value')。
    * 6、删除数据可以选择用null覆盖或者直接删除:_box.delete("age");box.put("age", null);
    * 7、使用完后要记得关闭,所有活动的读写操作完成后,盒子的所有缓存键和值将从内存中删除,并且盒子文件将关闭:await box.close();。

---------------------------------------------示例代码---------------------------------------------

                      TextEditingController _controller = TextEditingController();
                      Database? _database;
                      late Box _box;

                    //保存数据
                    /*Shared Preferences,用于存储简单的数据类型(如字符串、整数、布尔值等)的键值对,类似于Android中的SharedPreferences或iOS中的UserDefaults。适用于存储简单的配置、设置和用户偏好数据。
                     *
                     * 1.首先在pubspec.yaml文件中添加依赖:shared_preferences: ^2.0.0
                     * 2.通过SharedPreferences.getInstance()创建一个单例对象;
                     * 3.通过set方法设置键值对,通过get方法通过key取出对应的值。支持存储五种数据格式:setBool、setDouble、setInt、setString、setStringList
                     * */
                    Container(
                      width: 200,
                      height: 44,
                      color: Colors.blueGrey,
                      child: TextButton(
                        child:Row(
                            mainAxisAlignment: MainAxisAlignment.center,
                            children:[
                              Icon(Icons.save,color: Colors.white,),
                              Text("  保存姓名",style: TextStyle(color: Colors.white,fontSize: 20))
                            ]),
                        onPressed: () async{
                          //获取输入结果
                          print(_controller.text);
                          SharedPreferences prefs = await SharedPreferences.getInstance();
                          prefs.setString("UserName", _controller.text);
                        },
                      ),
                    ),

                    /*
                    * File Storage,文件存储适用于存储结构化数据或需要较长时间存储的数据。可以使用Dart的dart:io库来操作文件。常用的存储格式包括JSON、XML等。
                    *
                    * 1.首先我们需要先添加path provider的依赖,这个包有助于获取文件存储的目录路径。根据getApplicationDocumentsDirectory()方法获取根路径,然后在拼接文件的保存路径;
                    * 2.通过File类,创建对应路径的文件。
                    * 3.通过File的write方法进行文件写入,可以写入Bytes和String两种数据
                    * 4.读取也是一样,给File类输入对应的路径,然后执行read方法就能读取数据了
                    * */
                    Container(
                      width: 200,
                      height: 44,
                      color: Colors.orange,
                      child: TextButton(
                        child:Row(
                            mainAxisAlignment: MainAxisAlignment.center,
                            children:[
                              Icon(Icons.save_alt_outlined,color: Colors.white,),
                              Text("  保存文件",style: TextStyle(color: Colors.white,fontSize: 20))
                            ]),
                        onPressed: () async{
                          //获取根路径
                          final directory  = await getApplicationDocumentsDirectory();
                          final rootPath = directory.path;
                          //拼接设置文件要保存的路径
                          final filePath = "$rootPath/data.txt";
                          print(filePath);// /data/user/0/com.example.gao_progect/app_flutter/data.txt

                          //通过File类,创建对应路径的文件。
                          final file = File(filePath);

                          //通过File的write方法进行文件写入
                          file.writeAsString("这里是测试数据---这里是测试数据");
                        },
                      ),
                    ),

                    /*SQLite,一种嵌入式关系数据库,适用于需要复杂数据查询和关系操作的情况,但使用相对较复杂,需要熟悉SQL语法。Flutter可以通过第三方库sqflite来访问SQLite数据库。
                    *
                    * 1.添加sqflite依赖:sqflite: ^2.0.0
                    * 2.通过openDatabase方法根据路径创建或打开一个数据库,通过version参数置设版本,通过onCreate参数执行CREATE TABLE test(id INTEGER PRIMARY KEY, name TEXT)指令新建一个表test。
                    * 3.然后分别通过insert、delete、update和query方法实现增删改查的功能;当然也可以通过rawInsert、rawDelete、rawUpdate和rawQuery方法实现,但是这几个方法需要直接使用SQL语句。而不带row的则是Android自己封装的查询API,会根据内容帮你拼写 SQL 语句。
                    * */
                    Container(
                      width: 200,
                      height: 44,
                      color: Colors.deepPurple,
                      child: TextButton(
                        child:Row(
                            mainAxisAlignment: MainAxisAlignment.center,
                            children:[
                              Icon(Icons.table_chart_outlined,color: Colors.white,),
                              Text("  数据库保存",style: TextStyle(color: Colors.white,fontSize: 20))
                            ]),
                        onPressed: () async{

                          //获取根路径
                          final directory  = await getApplicationDocumentsDirectory();
                          final rootPath = directory.path;
                          //设置数据库的路径
                          final filePath = "$rootPath/example.db";

                          //通过openDatabase方法根据路径创建或打开一个数据库,通过version参数置设版本,通过onCreate参数执行CREATE TABLE test(id INTEGER PRIMARY KEY, name TEXT)指令新建一个表。
                          _database = await openDatabase(
                              filePath,
                              version: 1,
                              onCreate: (db, version) {
                                return db.execute(
                                   "CREATE TABLE test(id INTEGER PRIMARY KEY, name TEXT)",
                                );
                              }
                          );

                          _database?.insert("test", {'id': 1, 'name': 'Flutter'});

                          /*安卓版增删改查
                          _database.insert(table, values)
                          _database.delete(table)
                          _database.update(table, values)
                          _database.query(table)
                           */

                          /*SQL语句版增删改查
                          _database.rawInsert(sql)
                          _database.rawDelete(sql)
                          _database.rawUpdate(sql)
                          _database.rawQuery(sql)
                           */
                        },
                      ),
                    ),

                    /*因为使用SQLite比较麻烦,所以我们可以使用第三方数据库服务,比如Hive。Hive是一个轻量级的、键值对存储的数据库,适合在移动设备上高效存储数据。它不需要初始化异步操作,因此在很多情况下比SQLite更简单和高效。
                    *
                    * 1、首先在pubspec.yaml文件中添加依赖,在dependencies中添加了hive的库。
                    *     dependencies:
                    *       hive: ^2.2.3
                    *       hive_flutter: ^1.1.0
                    * 2、在main里初始化:await Hive.initFlutter();
                    * 3、在Hive中,所有的数据都被组织到box中。Box是Hive中的最小存储单元,类似于文件系统中的文件夹,我们可以使用它来存储和检索数据。在使用box之前,必须先打开它。我们可以使用Hive.openBox()方法打开一个box。var box = await Hive.openBox<E>('testBox');
                    * 4、在Hive中,数据是以键值对的形式存储的。我们可以使用box的put方法,将数据存储到Box里面。
                    * 5、使用 get() 方法可以获取数据,如果key对应的value不存在的话,会返回 null,为了避免这种情况的发生,我们可以设置一个返回值:_box.get('key', defaultValue: 'No Value')。
                    * 6、可以选择用null覆盖或者直接删除:_box.delete("age");或box.put("age", null);
                    * 7、使用完后要记得关闭,所有活动的读写操作完成后,盒子的所有缓存键和值将从内存中删除,并且盒子文件将关闭:await box.close();
                    * */
                    Container(
                      width: 200,
                      height: 44,
                      color: Colors.blueAccent,
                      child: TextButton(
                        child:Row(
                            mainAxisAlignment: MainAxisAlignment.center,
                            children:[
                              Icon(Icons.table_chart_sharp,color: Colors.white,),
                              Text("  Hive",style: TextStyle(color: Colors.white,fontSize: 20))
                            ]),
                        onPressed: () async{
                          
                          //打开box
                          final boxName = "test";
                          if (!Hive.isBoxOpen(boxName)) {
                            _box = await Hive.openBox(boxName); // 打开 Hive Box 实例
                          } else {
                            _box = Hive.box(boxName); // 如果已经打开,则获取实例
                          }
                          //将数据存储到Box里面
                          _box.put("saveKey", ["testValue1","testValue2"]);
                        },
                      ),
                    ),


//读取保存的数据
onPressed: ()async{
          //Shared Preferences
          //调用SharedPreferences单例对象的get方法,获取key对应的数据
          SharedPreferences prefs = await SharedPreferences.getInstance();
          var value = prefs.getString("UserName");
          print("保存的数据为----$value");


          //File Storage读取也是一样,给File类输入对应的路径,然后执行read方法就能读取数据了
          final directory  = await getApplicationDocumentsDirectory();
          final rootPath = directory.path;
          //拼接设置文件要保存的路径
          final filePath = "$rootPath/data.txt";

          try{//防止读取时发生错误
            //通过File类,获取对应路径的文件。
            final file = File(filePath);

            //通过File的read方法进行文件写入
            final content = await file.readAsString();
            print("File文件保存内容----$content");
          }catch(e){//读取文件发生错误
            print("Error reading data!");
          }
          
          
          //数据库读取
          final maps = await _database?.rawQuery("SELECT * FROM test");
          print("数据库读取---$maps");


          //Hive数据读取
          final result = _box.get("saveKey",defaultValue: "empty value");
          print("Hive读取---$result");
 }

 

23、动画
FLutter 中的动画主要分为:隐式动画、显式动画、自定义隐式动画、自定义显式动画和 Hero 动画。

  • 所谓隐式动画就是只需要设置动画目标,过程控制由系统实现。一般是简单点的动画,比如只是简单的宽高变化,没有循环重播,不用随时中断,没有多方协调,就是从开始运行到结束。使用flutter提供的api,隐式动画一般是Animated...开头。AnimatedContainer、AnimatedPadding、AnimatedPositioned、AnimatedOpacity、AnimatedDefaultTextStyle、AnimatedSwitcher都属于隐式动画,虽然他们关注动画的侧重点不同,但也支持其他部分的动画。比如在AnimatedPadding设置尺寸变化动画也是支持的,但必须得设置aliment属性值。
         * AnimatedOpacity 在透明度opacity发生变化时执行过渡动画到新状态,Opacity属性是必传值。
         * AnimatedContainer 当Container属性发生变化时会执行过渡动画到新的状态,比如颜色值、大小等。
         * AnimatedPadding 在padding发生变化时会执行过渡动画到新状态,Padding属性是必传值
         * AnimatedPositioned 配合Stack一起使用,当定位状态发生变化时会执行过渡动画到新的状态。
         * AnimatedAlign 当alignment发生变化时会执行过渡动画到新的状态,alignment属性是必传值
         * AnimatedSwitcher 当内部组件发生变化时会执行过渡动画到新的状态。参数transitionBuilder:一个回调函数,用于定义子元素切换时的过渡效果。需要注意的是,必须给子控件指定一个UniqueKey,这样才能强制渲染刷新

      动画效果图详见https://blog.csdn.net/Taonce/article/details/136790922

  隐式动画中可以通过 duration 配置动画时长、可以通过 Curve (曲线)来配置动画过程onEnd参数表示的是动画结束的回调

Curves 类提供了一系列预定义的曲线,用于控制动画的速度变化。以下是一些常用的 Curves 组件的值及简单解释:
Curves.linear:线性曲线,动画以恒定的速度进行,没有加速或减速。
Curves.decelerate:减速曲线,动画开始时速度较快,然后逐渐减速。
Curves.ease:标准的加速减速曲线,动画开始和结束时速度较慢,中间时速度较快。
Curves.easeIn:加速曲线,动画开始时速度较慢,然后逐渐加速。
Curves.easeOut:减速曲线,动画开始时速度较快,然后逐渐减速。
Curves.easeInOut:加速减速曲线,动画开始和结束时速度较慢,中间时速度较快,类似于Curves.ease。
Curves.fastOutSlowIn:快出慢入曲线,动画开始时速度较快,然后逐渐减速到结束。
Curves.bounceIn:弹簧效果曲线,动画开始时速度为0,然后加速进入动画,到达最大速度后反弹一次。
Curves.elasticIn:弹性效果曲线,动画开始时速度为0,然后加速进入动画,到达最大速度后会有一些超过目标值的回弹效果。
  • 显式动画指的是需要手动设置动画的时间,运动曲线,取值范围的动画,将值传递给动画部件,使用 Animation(AnimatedWidget)AnimationController 来实现。相比隐式动画,虽然是使用上麻烦了,但显示动画提供了更大的灵活性,能够更精确地控制动画的进程、效果和交互。

  常见的显式动画有 RotationTransition(旋转)、FadeTransition(透明度)、ScaleTransition(缩放)、SlideTransition(移动)、AnimatedIcon(改变常见图标)。

RotationTransition用于在子组件进行旋转动画。它可以根据指定的旋转角度来对子组件进行旋转,并且可以在动画执行过程中实时更新旋转角度以创建平滑的动画效果。
FadeTransition用于在子组件进行透明度渐变动画。它可以根据指定的透明度值来对子组件进行渐变动画,并且可以在动画执行过程中实时更新透明度值以创建平滑的动画效果。
ScaleTransition用于在子组件进行缩放动画。它可以根据指定的缩放比例来对子组件进行缩放动画,并且可以在动画执行过程中实时更新缩放比例以创建平滑的动画效果。
SlideTransition是负责平移的显示动画组件,使用时需要通过 position 属性传入一个 Animated 表示位移程度,通常借助 Tween 实现。
AnimatedIcon是一个用于提供动画图标的组件,它的名字虽然是以 Animated 开头,但是他是一个显式动画组件,需要通过 progress 属性传入动画控制器,另外需要由 Icon 属性传入动画图标数据。

  使用步骤:

    1.创建AnimationController
    2.创建动画组件,绑定Conroller,设置Tween、Curve效果
    3.Controller 控制动画的开始、暂停、重置、跳转、倒播等
    4.监听动画
    5.销毁控制器
  几个概念:
    AnimationController 是一个控制动画的类,它管理动画的状态,如开始、停止、正向或反向播放等。
    Animation 表示动画的当前状态,它是一个在指定范围内的可变值。动画的值会随时间变化,可以用于控制 UI 元素的属性,从而创建动画效果。它是一个抽象类
    AnimatedWidget可以理解为动画Animation的辅助类,可以理解为创建一个Widget自带动画效果,也可以理解为使用Widget来封装复杂的组合的自定义动画实现
    Tween: 默认情况下,AnimationController动画生成的值所在区间是0.0到1.0,如果希望使用这个以外的值,或者其他的数据类型,就需要使用Tween

  • 过渡动画Hero,用于在两个页面之间平滑地传递共享元素。比如微信朋友圈点击小图片的时候会有一个动画效果到大图预览,这个动画效果就可以使用 Hero 动画实现。简单来说 Hero 动画就是在两个路由之间传递一个元素,使其看起来像是在连续移动

    1、使用也比较简单,只需要用Hero组件包裹住需要传递的共享组件即可。
    2、然后设置一个tag标签,用于在多个 Hero widget 之间创建关联

 

  • 物理动画,Flutter 提供了 Simulation 类,用于创建具有初始状态和演化规则的基于物理的模拟,这个类使你能够制作各种基于物理的动画,包括基于弹簧动力学、摩擦力和重力的动画。

  Simulation是个抽象类,我们一般使用它以下的几个子类:

BouncingScrollSimulation 弹性的滚动模拟
BoundedFrictionSimulation 边界摩擦模拟引擎
ClampedSimulation 区间模拟引擎
ClampingScrollSimulation 区间滚动模拟引擎
FrictionSimulation 摩擦参数的的滚动模拟
GravitySimulation 下落重力模拟引擎
SpringSimulation 弹簧弹力的模拟
ScrollSpringSimulation 弹簧滚动模拟

  实际开发中,常常将Simulation类结合AnimationController来实现物理动画,使用流程如下:
    1.定义一个AnimationController,用来控制动画信息;
    2.通过控制器指定我们需要的动画起点和终点;
    3.创建Simulation对象,设置动画中的物理属性;
    4.通过控制器执行动画

------------------------------------------------------------示例代码--------------------------------------------------------------------

//AnimatedAlign 当Container属性发生变化时会执行过渡动画到新的状态,比如宽高
class animalClass1 extends StatefulWidget{
  @override
  State<animalClass1> createState() => _animalState1();
}
class _animalState1 extends State<animalClass1> {
  bool flag = false;
  @override
  Widget build(BuildContext context) {
    return Center(
      child: GestureDetector(
        child: AnimatedContainer(
          duration: const Duration(milliseconds: 250),
          width: flag ? 100 : 200,
          height: flag ? 100 : 200,
          color: flag ?Colors.green:Colors.red,
          curve: Curves.ease, //标准的加速减速曲线,动画开始和结束时速度较慢,中间时速度较快。
        ),
        onTap: (){
          setState(() {
            flag = !flag;
          });
        },
      )
    );
  }
}


//AnimatedAlign 当alignment发生变化时会执行过渡动画到新的状态,alignment是必传值,
class animalClass2 extends StatefulWidget{
  @override
  State<animalClass2> createState() => _animalState2();
}
class _animalState2 extends State<animalClass2> {
  bool flag = false;
  @override
  Widget build(BuildContext context) {
    return Center(
        child: GestureDetector(
          child: AnimatedAlign(
            child: Container(height: flag?100:200,width: 100,color: flag?Colors.orange:Colors.blueAccent),//AnimatedAlign主要用来设置alignment动画,alignment是必传值,但是也支持其他部分的动画
            duration: const Duration(milliseconds: 250),
            curve: Curves.ease,
            alignment: flag?Alignment.bottomRight:Alignment.topLeft, //标准的加速减速曲线,动画开始和结束时速度较慢,中间时速度较快。
          ),
          onTap: (){
            setState(() {
              flag = !flag;
            });
          },
        )
    );
  }
}

//AnimatedSwitcher 当内部组件发生变化时会执行过渡动画到新的状态。
class animalClass3 extends StatefulWidget{
  @override
  State<animalClass3> createState() => _animalState3();
}
class _animalState3 extends State<animalClass3> {
  var _value = "开始显示的内容";
  @override
  Widget build(BuildContext context) {
    return Center(
        child: GestureDetector(
          child: AnimatedSwitcher(
            child: Text(_value,key: UniqueKey()),//child用于设置变化前显示的组件
            duration: const Duration(milliseconds: 250),
            transitionBuilder: (child, animation) {
              return FadeTransition(
                opacity: animation,
                child: ScaleTransition(
                  scale: animation,
                  child: child
                ),
              );
            }
          ),
          onTap: (){
            setState(() {
              _value = "内容发生变化";
            });
          },
        )
    );
  }
}

//AnimatedSwitcher 当内部组件发生变化时会执行过渡动画到新的状态。
class animalClass4 extends StatefulWidget{
  @override
  State<animalClass4> createState() => _animalState4();
}
class _animalState4 extends State<animalClass4> with SingleTickerProviderStateMixin{
  late AnimationController _controller;
  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    // 1.创建AnimationController
    //Vsync 机制可以理解为是显卡与显示器的通信桥梁,显卡在渲染每一帧之前会等待垂直同步信号,只有显示器完成了一次刷新时,发出垂直同步信号,
    //显卡才会渲染下一帧,确保刷新率和帧率保持同步,以达到供需平衡的效果,防止卡顿现象。
    _controller =
        AnimationController(vsync: this, duration: const Duration(seconds: 1));

    //4.监听动画
    _controller.addListener(() {//addListener方法,每当动画的状态值发生变化时,动画都会通知所有通过 addListener 添加的监听器。
      print(_controller.value);
    });

    _controller.addStatusListener((status) {//addStatusListener,当动画的状态发生变化时,会通知所有通过 addStatusListener 添加的监听器。
      if (status == AnimationStatus.completed) {//正序播放完成,倒序播放
        _controller.reverse();
      } else if (status == AnimationStatus.dismissed) {//倒叙播放完了,正序播放
        _controller.forward();
      }
    });
  }
  @override
  Widget build(BuildContext context) {
    return Center(

    child: GestureDetector(
          //2.创建动画组件,绑定Conroller,设置Tween、Curve效果
          child:ScaleTransition(
              scale: _controller.drive(Tween(begin: 1, end: 2)),//设置放大倍数是1到2倍
              child: Icon(Icons.favorite,color: Colors.red,size: 100),
          ),
          onTap: (){
            setState(() {
              //3.Controller 控制动画的开始、暂停、重置、跳转、倒播等
              _controller.forward();//正序播放
            });
          },
        )
    );
  }

  @override
  void dispose() {
    //5.销毁控制器
    _controller.dispose();
    super.dispose();
  }

}


//Hero 动画就是在两个路由之间传递一个元素,使其看起来像是在连续移动
class animalClass5 extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        Navigator.of(context).push(MaterialPageRoute(
          builder: (context) => SecondPage(),
        ));
      },
      //1、使用也比较简单,只需要用Hero组件包裹住需要传递的共享组件即可。
      child: Hero(
        //2、然后设置一个tag标签,用于在多个 Hero widget 之间创建关联
        tag: 'iconTag',
        child: Icon(Icons.star,size: 80,color: Colors.pink,),
      ),
    );
  }
}
class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: Hero(
          tag: 'iconTag',
          child: Icon(Icons.star,size: 80,color: Colors.pink),
        ),
      ),
    );
  }
}


//物理动画,Simulation类结合AnimationController实现。动画效果:拖拽widget,松手后,widget会按照弹簧效果回到中心位置。
class animalClass6 extends StatefulWidget{
  @override
  State<animalClass6> createState() => _animalState6();
}
class _animalState6 extends State<animalClass6> with SingleTickerProviderStateMixin{

  late AnimationController _controller;

  late Animation<Alignment> _animation;

  Alignment _dragAlignment = Alignment.center;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();


    //1.定义一个AnimationController,用来控制动画信息
    _controller = AnimationController(vsync: this, duration: const Duration(seconds: 1));

    _controller.addListener(() {
      setState(() {
        _dragAlignment = _animation.value;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return  GestureDetector(
      onPanUpdate: (details) {//拖拽时
        setState(() {

          //获取屏幕尺寸
          final size = MediaQuery.of(context).size;

          _dragAlignment += Alignment(
            details.delta.dx / (size.width / 2),
            details.delta.dy / (size.height / 2),
          );
        });
      },

      onPanEnd: (details) {//松手后执行物理动画

         //2.通过控制器指定我们需要的动画起点和终点:
         _animation = _controller.drive(
          AlignmentTween( //Alignment有一个专门表示位置信息的类叫做AlignmentTween
            begin: _dragAlignment,
            end: Alignment.center,
          ),
        );

        //3.创建Simulation对象,设置动画中的物理属性
        final spring = SpringDescription(//弹簧的属性配置
          mass: 10,  //质量
          stiffness: 5,  //硬度
          damping: 0.75,  //阻尼系数
        );

        SpringSimulation simulation = SpringSimulation(spring, 0, 1, -1);

        //4.通过控制器执行动画
        _controller.animateWith(simulation);
      },
      child: Align(
        alignment: _dragAlignment,
        child: Container(
          height: 100,
          width: 100,
          color: Colors.green,
        )
      ),
    );
  }


  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

 

24、订阅、Stream、EventBus
  在 Flutter 中有两种处理异步操作的方式 Future 和 Stream。Future 用于处理单个异步操作,Stream 用来处理连续的异步操作。在执行异步任务时,Stream可以通过多次触发成功或失败事件来传递结果数据或错误异常。所以Stream常用于会多次读取数据的异步任务场景,如网络内容下载、文件读写
  Stream的字面意思上是流,那什么是流呢?流提供了一种接收一系列事件的方法。每个事件要么是一个数据事件,也称为流的元素,要么是一个错误事件,即某事已失败的通知。
  Stream主要是把事件放在流上面去处理,可以接受任何类型的数据,值、事件、对象、集合、映射、错误、甚至是另一个Stream。
  通过StreamController中的sink作为入口,往Stream中插入数据,然后自定义监听StreamSubscription对象,接受数据变化的通知。如果需要对输出数据进行处理,可以使用StreamTransformer,它可以对输出数据进行过滤、重组、修改、将数据注入其他流等等任何类型的数据操作。

  • stream都有哪些类型?

  Stream 分单订阅流和广播流。

    单订阅流在发送完成事件之前只允许设置一个监听器,并且只有在流上设置监听器后才开始产生事件,取消监听器后将停止发送事件。即使取消了第一个监听器,也不允许在单订阅流上设置其他的监听器。一般创建的Stream都是单订阅模式
    广播流(多订阅模式)则允许设置多个监听器,也可以在取消上一个监听器后再次添加新的监听器。在创建StreamController时添加broadcast就变为多订阅模式
  另外Stream 有同步流和异步流之分。它们的区别在于同步流会在执行 add,addError 或 close 方法时立即向流的监听器 StreamSubscription 发送事件,而异步流总是在事件队列中的代码执行完成后在发送事件。

  • Stream五大元素:

  StreamController:作为整个流的控制器,用于创建和管理一个流(Stream),它允许你添加数据到流中,并且可以监听这些数据。sync 参数决定这个流是同步流还是异步流。
  StreamSink:流事件入口,提供 add,addError,addStream 方法向流发送事件。
  Stream:事件源
  StreamSubscription:订阅管理(流的监听器),提供 cacenl、pause, resume 等方法管理。
  StreamBuilder:StreamBuilder是Flutter中的一个Widget,它可以跟Steam结合起来使用。StreamBuilder监听到更新然后会自动触发 Widget 的刷新,相比StatefulWidget,刷新时使用setstate会将整个item 进行重新构建,StreamBuilder更节约开销

  • Stream的创建

  Stream可以通过两种形式去创建,一种是通过StreamController,StreamController中是有一个Stream,只需要构造出StreamController对象,通过这个对象的.stream就可以得到Stream。
  如果我们不想使用Controller的Stream,可以通过构造方法去创建,然后通过StreamSink添加到Controller去管理,而Stream的构造方法分为3种:
    Stream.fromFuture,通过传递一个异步任务来创建Stream
    Stream.fromFutures,通过传递多个异步任务来创建Stream
    Stream.fromIterable 通过传递一个集合来创建Stream,集合中的每一个数据都会有自己的回调

  • stream有哪些好处?

  1.随意操作数据流。
    刚才在stream定义那里已经说过了,stream是基于数据流的,从skin管道入口到StreamController提供stream属性作为数据的出口之间,可以对数据做任何操作,包括过滤、重组、修改等等。
  2 当数据流变化时,可以刷新小部件。
    Stream是一种订阅者模式,当数据发生变化时,通知订阅者发生改变,重新构建小部件,刷新UI。

  • 使用过程:

  1.创建StreamController,管理流。单订阅只能添加一个listen,而多订阅则无限制
  2.通过stream的listen方法监听数据变化,并获取StreamSubscription
  3.通过StreamSink往Stream中添加数据,如果不使用Controller的Stream,而是通过构造方法自建Stream的话,则需要通过StreamSink的addStream将自定义Stream绑定到Controller上。
  4.在dispose方法中取消订阅,关闭流

  • EventBus,在实际开发中,我们很少直接使用Stream,一般都是通过三方库去简化操作,比如Event_Bus库。主要作用是各组件间的通信,能有效的分离事件发送方和接收方(解耦),类似通知。

  EventBus是全局事件总线,底层通过Stream来实现。EventBus对象初始化实际上初始化了一个_streamController对象,而这个对象是通过StreamController的broadcast(sync: sync)方法初始化的。EventBus可以实现不同页面的跨层访问,通过Stream的机制来实现不同widget之间的状态共享
  使用方法如下:
    1.在pubsec.yaml文件导入依赖:event_bus: ^1.1.0
    2.初始化EventBus(发送端),创建一个全局的 EventBus,通常每个应用只有一个事件总线,但如果需要多个事件总线的话可以在初始化时设置 sync = false;
    3.监听相应的响应事件(接收端,可以放在initState()中),可以通过 on(event).listen() 来监听;其中若 on() 可以监听所有事件也可以监听固定的事件,区别是是否限制当前广播;
    4.销毁event_bus对象(接收端,放在dispose()中), 为了防止内存泄漏,一般在应用销毁时都需要对 EventBus 进行销毁;
    5.发出事件(发送端) eventBus.fire(传递的数据)  _event.cancel();   _eventBus.destroy();

------------------------------------------------------------示例代码--------------------------------------------------------------------

//2.首先创建一个全局的 EventBus
EventBus _eventBus = EventBus();

void main(){
  runApp(MaterialApp(
    home: streamScaffold()
  ));
}

class streamScaffold extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: streamClass(),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.navigate_next,size: 30,color: Colors.blueGrey),
        onPressed: (){
          Navigator.of(context).push(MaterialPageRoute(
            builder: (context) => test1Page(),
          ));
        },
      ),
    );
  }
}

class test1Page extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("第二个页面"),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.navigate_next,size: 30,color: Colors.blueGrey),
        onPressed: (){
          Navigator.of(context).push(MaterialPageRoute(
            builder: (context) => test2Page(),
          ));
        },
      ),
    );
  }
}

class test2Page extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("第三个页面"),
      ),
      body: Center(
        child: TextButton(
          child: Text("更改首页文字"),
          onPressed: (){
            // 5.发出事件(发送端)  eventBus.fire(传递的数据)
            _eventBus.fire("来自第三页的文字问候");
          },
        ),
      ),
    );
  }
}


class streamClass extends StatefulWidget{
  @override
  State<StatefulWidget> createState() => _streamState();
}

class _streamState extends State<streamClass>{

  late StreamController _streamCtrl;

  //Stream的订阅对象
  late StreamSubscription _subscription;

  var _content = "初始内容";

  var _eventBusContent = "";

  late StreamSubscription _event;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();

    //1.创建StreamController
    // _streamCtrl = StreamController(); //单订阅
    _streamCtrl = StreamController.broadcast();  //广播订阅(多订阅)

    /*如果是直接使用StreamController中的Stream的话,比较简单,直接用就行,如果是自定义Strean的话则多了异步创建和绑定的步骤
    //通过构造方法创建Stream
    Stream stream = Stream.fromFuture(
        Future.delayed(const Duration(milliseconds: 500)).then((value) {
          return '我是Stream的future执行结果';
        }));

    //添加到StreamController中管理
    _streamCtrl.sink.addStream(stream);

    //2.监听用做添加事件的入口
    _subscription = stream.listen((event) {
    });
  * */

    //2.监听用做添加事件的入口
    _subscription = _streamCtrl.stream.listen((event) {
      print(event);
      // setState(() {
      //   _content = event;
      // });
    });

    //如果是单订阅的话,下面这段代码就会报错,因为上面已经有了个listen,无法在用其他的订阅了
    // _streamCtrl.stream.listen((event) {
    //   print(event);
    //   // setState(() {
    //   //   _content = event;
    //   // });
    // });


    //eventBus的使用
    //3.监听相应的响应事件(接收端,可以放在initState()中),可以通过 on(event).listen() 来监听;拿到订阅者方便管理
    _event = _eventBus.on().listen((event) {
      print(event);
      setState(() {
        _eventBusContent = event;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          // Text(_content),
          //如果我们希望值变化后刷新某个Widget的话,最好使用StreamBuilder构造器,每次值改变的时候都会引起StreamBuilder的监听,StreamBuilder重建并刷新。这样不用每次都通过setState重构整个item
          StreamBuilder(
              stream:_streamCtrl.stream, //...需要监听的stream...
              initialData: "初始内容",//初始数据,尽量不要填null,初始化时会将该值作为snapshot的值传过去初始化
              builder: (BuildContext context, AsyncSnapshot snapshot) { //AsyncSnapShot是快照的意思,保存着此刻最新的事件。
                if (snapshot.hasData){ //...基于snapshot.hasData返回的控件
                  // 根据 snapshot 的数据处理返回
                  var data = snapshot.data;
                  print(data);
                  return Text(data);
                }
                return Text("空内容");//...没有数据的时候返回的控件
              }
          ),
          SizedBox(height: 20),
          TextButton(
              onPressed: (){
                //3.往Stream中添加数据
                _streamCtrl.sink.add("new Value");
              },
              child: Text("变更")
          ),

          SizedBox(height: 20),

          Text(_eventBusContent)
        ],
      )
    );
  }

  @override
  void dispose() {
    // TODO: implement dispose
    super.dispose();
    //4.在dispose方法中取消订阅,关闭流
    _subscription.cancel();
    _streamCtrl.close();

    //4.销毁event_bus对象(接收端,放在dispose()中)  _event.dispose();
    _event.cancel();
    _eventBus.destroy();
  }

}

 

25、混合开发、Flutter 是如何与原生Android、iOS进行通信的?

分两种情况:

  • 项目直接由Flutter开发,但有时候需要用到一些原生的能力相机、相册、位置信息、Map,这个时候分两种情况

  *像相机、相册、定位这种功能,pub.dev上有一些比较好的第三方插件,我们可以直接使用三方库来操作,这样就不需要写原生代码了

  *但有些原生能力,Flutter并没有提供对应的api,更没有很好的第三方插件或者某些SDK不支持flutter但项目又必须要用,我们可以通过platform channels(平台通道)来获取信息。平台通道允许Flutter代码与原生平台代码(如Java、Kotlin、Objective-C、Swift)相互调用,从而实现Flutter与原生代码之间的数据和功能交互。 也就是在Dart中调用原生代码(i0S、Android)

    使用比较简单:
      1、在Flutter端通过MethodChannel建立一个通道,Flutter端和原生平台会借助这个通道去通信:MethodChannel("gao.com/battery");这里需要传入一个name,该name是区分多个通信的名称,每个通信通道的名称都要唯一,一般是域名+功能
      2、在Flutter端,直接通过创建好的通道给原生平台发消息,要求它执行某个代码并等待结果返回platform.invokeMethod("getBatteryInfo"); 这里需要传入原生平台中的要执行方法名称
      3.原生平台实现方法并作出反应:
        iOS端:用Xcode打开Flutter项目中的iOS代码,在Appdelegate文件的didFinishLaunchingWithOptions方法中通过FlutterMethodChannel监听事件请求并做出响应:
          1.获取FlutterViewController(是应用程序的默认Controller),用于设置MethodChannel的binaryMessenger
          2.获取FlutterMethodChannel(方法通道),注意:这里需要根据我们创建通道时的名称来获取
          3.通过setMethodCallHandler方法监听方法调用并作出响应(有两个参数:call是用来获取方法调用者的相关信息,result是用于方法响应的数据返回)
        Android端:安卓端的实现思路和iOS一致。只不过安卓是在MainActivity文件的configureFlutterEngine方法中通过MethodChannel做出响应;
          1.获取MethodChannel(方法通道),注意:这里需要根据我们创建通道时的名称来获取
          2.通过setMethodCallHandler方法监听方法调用并作出响应(有两个参数:call是用来获取方法调用者的相关信息,result是用于方法响应的数据返回)

      如需携带参数传递给原生端,可以直接拼接在方法调用时方法名的后面
        platform.invokeMethod('yourMethod', [{"fileName": "fName"}]);
      原生端获取参数直接通过setMethodCallHandler方法中的call参数的arguments属性来获取:
        call.arguments

  • 项目原本就有原生代码(i0S、Android )编写,但新模块打算用Flutter开发节约成本,或者原有模块由Flutter重构。也就是在原生代码(iOS、Android)调用Dart

  对于需要进行混合开发的原有项目,Flutter可以作为一个库或者模块,继承进现有项目中。其使用流程也比较简单,跟iOS中使用第三方库差不多,使用方法如下:
    1.首先通过命令行创建Flutter Module(注意这里是创建模块而不是项目):【flutter create --template module 模块名称】。创建完成后,该模块和普通的Flutter项目一样,可以通过Android Studio或VSCode打开、开发、运行;
    2.模块开发完成之后,该怎么嵌入移动端呢?跟三方库一样,使用 CocoaPods 依赖管理和已安装的 Flutter SDK在Posfile文件中设置Flutter模块的路径这个路径是相对Podfile文件来说的相对路径

               # Uncomment the next line to define a global platform for your project
               # platform :ios, '9.0'

               # 添加模块所在路径
               flutter_application_path = '../my_flutter'
               load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')

               target 'ios_my_test' do
                  # Comment the next line if you don't want to use dynamic frameworks
                  use_frameworks!

                 # 安装Flutter模块
                 install_all_flutter_pods(flutter_application_path)

                 # Pods for ios_my_test
              end

    3.重新执行安装CocoaPods的依赖:pod install
    4.将Flutter模块导入到iOS项目中后,接下来就是使用了。一般作为一个模块使用的话,就是由某个控制器push或者present出来Flutter模块页面。为了在既有的iOS应用中展示Flutter页面,需要启动 Flutter Engine和 FlutterViewController。
    5.在应用启动的 app delegate 中创建一个 FlutterEngine并在didFinishLaunchingWithOptions方法中启动Flutter引擎,并作为属性暴露给外界。(也可以省略预先创建的 FlutterEngine,在创建Controller是引擎传空。但这样可能会出现明显的延迟,导致显示Flutter页面时会暂时空白)

                  class AppDelegate: UIResponder, UIApplicationDelegate {
                      // 1.创建一个FlutterEngine对象
                      lazy var flutterEngine = FlutterEngine(name: "my flutter engine")

                     func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
                        // 2.启动flutterEngine
                        flutterEngine.run()
                        return true
                    }
                 }

    6.在需要弹出Flutter模块的地方直接创建FlutterController,弹出Flutter页面

               @objc func showFlutter() {
                  // 创建FlutterViewController对象(需要先获取flutterEngine)
                  let flutterEngine = (UIApplication.shared.delegate as! AppDelegate).flutterEngine;
                  let flutterViewController = FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil);
                  navigationController?.pushViewController(flutterViewController, animated: true);
              }

    7.将Flutter模块嵌入原生项目中,尤其是iOS的原生项目,如果想保留flutter的Hot Reload的优势,可以通过flutter attach 调试 :
      1、利用Xcode启动以及嵌入好Fluttre模块的原生项目
      2、Android Studio中打开终端输入 flutter attach命令,如果有多个应用或者多个设备的话,可以通过--app-id是指定哪一个应用程序,通过-d是指定连接哪一个设备
        flutter attach --app-id com.coderwhy.ios-my-test -d 3D7A877C-B0DD-4871-8D6E-0C5263B986CD
      3、这样在 Android Studio 中修改 flutter 模块代码 ,在终端中执行 r 或者 R,在模拟器中就能看到最新的样式了 。

------------------------------------------------------------示例代码--------------------------------------------------------------------

iOS端代码(AppDelegate.swift)
@objc
class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { // 1.获取FlutterViewController(是应用程序的默认Controller) 用于设置MethodChannel的binaryMessenger let controller : FlutterViewController = window?.rootViewController as! FlutterViewController // 2.获取MethodChannel(方法通道) 根据我们创建通道时的名称来获取 let batteryChannel = FlutterMethodChannel(name: "flutter_test_project/getBattery", binaryMessenger: controller.binaryMessenger) // 3.通过方法通道的setMethodCallHandler方法监听方法调用并作出响应 (有两个参数:call是用来获取方法调用者的相关信息,result是用于方法响应的数据返回) batteryChannel.setMethodCallHandler({ [weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in // call.arguments // 3.1.判断是否是getBatteryInfo的调用,告知Flutter端没有实现对应的方法 guard call.method == "getPhontBatteryInfo" else { result(FlutterMethodNotImplemented) return } // 3.2.如果调用的是getBatteryInfo的方法, 那么通过封装的另外一个方法实现回调 self?.receiveBatteryLevel(result: result) }) } //纯原生代码,用于获取电池信息 private func receiveBatteryLevel(result: FlutterResult) { // 1.iOS中获取信息的方式 let device = UIDevice.current device.isBatteryMonitoringEnabled = true // 2.如果没有获取到,那么返回给Flutter端一个异常 if device.batteryState == UIDevice.BatteryState.unknown { result(FlutterError(code: "UNAVAILABLE", message: "Battery info unavailable", details: nil)) } else { // 3.通过result将结果回调给Flutter端 result(Int(device.batteryLevel * 100)) } } }
Flutter端代码:
class _channelState extends State{ late MethodChannel _channel; String _currentBattery = "当前电量为:0"; @override void initState() { super.initState(); //1、在Flutter端通过MethodChannel建立一个通道,Flutter端和原生平台会借助这个通道去通信:MethodChannel("gao.com/battery"); _channel = MethodChannel("flutter_test_project/getBattery"); } @override Widget build(BuildContext context){ return Scaffold( appBar: AppBar(title: Text(_currentBattery)), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(_currentBattery), SizedBox(height: 20), TextButton( onPressed: ()async{ //2.在Flutter端,直接通过创建好的通道给原生平台发消息,要求它执行某个代码并等待结果返回: final String result = await _channel.invokeMethod("getPhontBatteryInfo",["传递参数1","传递参数2"]); setState(() { _currentBattery = result; }); }, child: Text("获取电量信息")), ], ), ), ); } }

 

26、测试
Flutter官方对Flutter应用测试类型做了三个阶段划分,分别为Unit(单元)测试、Widget(组件)测试、Integration(集成)测试

  • 单元测试:单元测试通常是测试一个函数或者类,这个函数或者类被称之为是一个单元。比如我们需要检测某个类的初始化是否正确,执行某个方法的结果是否正确?其逻辑就是在test方法里执行方法或者类,判断其执行结果与理想结果是否一致

  示例类:

class Counter {
  int value = 0;

  void increment() => value++;
  void decrement() => value--;
}

  单元测试如下:
  我们在test目录下(注意:不是lib目录下),创建一个测试文件:counter_test.dart(测试文件通常以xx _test.dart 命名)

import 'package:flutter_test/flutter_test.dart';
import 'package:test_demo/counter.dart';

void main() {
  test("Counter Class test", () {
    // 1.创建Counter并且执行操作
    final counter = Counter();
    counter.increment();
    // 2.通过expect来监测结果正确与否  
    expect(counter.value, 1);  //expect方法的作用是执行参数1的代码,然后和参数2的预期结果做比对,从而达到测试目的
  });
}

  如果对同一个类或函数有多个测试,我们希望它们关联在一起进行测试,可以使用group

void main() {
  group("Counter Test", () {
      test("Counter Default Value", () {
          expect(Counter().value, 0);
      });

      test("Counter Increment test", () {
          final counter = Counter();
          counter.increment();
          expect(counter.value, 1);
      });

      test("Counter Decrement test", () {
          final counter = Counter();
          counter.decrement();
          expect(counter.value, -1);
      });
  });
}
  • widget 测试:Widget测试主要是针对某一个封装的Widget进行单独测试。(我们要开发一个UI界面,需要通过组合其它Widget来实现,Flutter中,一切都是Widget!)其原理是,在testWidgets方法中通过创建一个待测试的Widget,然后查看其组成控件是否正确,流程如下:

  1.创建一个 testWidgets 方法
  2.用 tester.pumpWidget创建一个待测试Widget
  3.用 finder 方法来在 Widget tree 中查找Widget中对应的子控件数量
  4.用 expect 来测试子控件出现的结果是否正确
    findsOneWidget 只有一个对应的 Widget
    findsNothing 没有找到对应的 Widget
    findsWidgets 找到一个或一个以上对应的 Widget
    findsNWidgets 找到特定数量对应的Widget

  示例Widget

import 'package:flutter/material.dart';

class HYKeywords extends StatelessWidget {
  final List<String> keywords;
  HYKeywords(this.keywords);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView(
        children: keywords.map((key) {
          return ListTile(
            leading: Icon(Icons.people),
            title: Text(key),
          );
        }).toList(),
      ),
    );
  }
}

  测试代码:

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:test_demo/keywords.dart';

void main() {
  testWidgets("KeywordWidget Test", (WidgetTester tester) async {
    await tester.pumpWidget(MaterialApp(title: "demo", home: HYKeywords(["abc""cba""nba"]),));

    final abcText find.text("abc"); //查找内容为abc的Text控件
    final cbaText = find.text("cba");//查找内容为cba的Text控件
    final icons = find.byIcon(Icons.people);//查找内容为Icons.people的Icon控件

    expect(abcText, findsOneWidget); //内容为abc的Text控件只找到一个
    expect(cbaText, findsOneWidget); //内容为cba的Text控件只找到一个
    expect(icons, findsNWidgets(3)); //内容为Icons.people的Text控件只找到三个
  });
)
  • 集成测试: 测试一个完整的应用程序或应用程序的很大一部分。通常,集成测试可以在真实设备或OS仿真器上运行,例如iOS Simulator或Android Emulator。模拟用户的点击、输入操作,从而完成功能的验证。

  使用流程如下:
    1、创建一个可以运行在模拟器或者真实设备的应用程序。比如默认创建的Demo工程,其中包括悬浮按钮和中间的文本显示。
    2、给测试中可能会用到的组件添加Key(ValueKey),在这里我们给 Text 和 FloatingActionButton 添加了 ValueKey 以便在测试时识别这些特点的 Widgets。
    3、在 pubspec.yaml 中加入集成测试中要用到 flutter_driver,同时,也添加 test ,因为要用到这里面的方法和断言。(flutter_driver并不是创建项目标配的,需要你额外安装。)

    dev_dependencies:
        flutter_driver:
           sdk: flutter
        test: any

    4、创建和 lib 文件同级的文件夹 test_driver(文件夹名称必须是test_driver)。
    5、在driver文件夹下创建两个文件:一个是创建指令化的 Flutter 应用程序,使我们能 "运行" 这个app,并记录运行的(app.dart),另一个用于写测试来判断app 是不是按预期运行(app_test.dart),目录如下:

    lib/
         main.dart
    test_driver/
        app.dart
        app_test.dart

    6、在 app.dart中编写安装应用代码,启动带测试的应用程序。写法固定如下:

import 'package:flutter_driver/driver_extension.dart';
import 'package:test_demo/main.dart' as app;

void main() {
  // 开启Flutter Driver的扩展
  enableFlutterDriverExtension();

  // 手动调用main函数, 启动应用程序
  app.main();
}

    7、在app_test.dart中编写集成测试代码,使用Flutter Driver API告诉应用程序执行什么操作,然后验证应用程序是否执行了此操作。这包含了四个步骤:
      创建 SerializableFinders 定位指定组件
      在 setUpAll() 函数中运行测试案例前,先与待测应用建立连接
      测试一些重要的流程
      完成测试后,在 teardownAll() 函数中与待测应用断开连接

// Imports the Flutter Driver API
import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';

void main() {
  group('Counter App', () {
    // 通过 Finders 找到对应的 Widgets
    final counterTextFinder = find.byValueKey('counter');
    final buttonFinder = find.byValueKey('increment');

    FlutterDriver driver;

    // 初始化操作  测试代码执行之前调用
    setUpAll(() async {
      driver = await FlutterDriver.connect();// 连接 Flutter driver 
    });

    // 测试结束操作  测试代码执行完成后调用
    tearDownAll(() async {
      if (driver != null) {
        driver.close();// 当测试完成断开连接
      }
    });

    test('starts at 0', () async {
      // 用 `driver.getText` 来判断 counter 初始化是 0
      expect(await driver.getText(counterTextFinder), "0");
    });

     // 编写测试代码
    test('increments the counter', () async {
      // 首先,点击按钮
      await driver.tap(buttonFinder);

      // 然后,判断是否增加了 1
      expect(await driver.getText(counterTextFinder), "1");
    });
  });
}

    8、 运行集成测试
      启动安卓模拟器或者 iOS 模拟器,或者直接把 iOS 或 Android 真机连接到电脑上。接着,在项目的根文件夹下运行下面的命令:flutter drive --target=test_driver/app.dart     这个指令的作用:创建 --target 目标应用并且把它安装在模拟器或真机中启动应用程序运行位于 test_driver/ 文件夹下的 app_test.dart 测试套件
      运行结果:我们会发现正常运行,并且结果app中的FloatingActionButton自动被点击了一次。

posted @ 2024-09-05 00:56  高晓牛  阅读(82)  评论(0编辑  收藏  举报