flutter初步入门介绍

flutter1.x

简介

Flutter 是 Google推出并开源的移动应用开发框架,主打跨平台、高保真、高性能。开发者可以通过 Dart语言开发 App,一套代码同时运行在 iOS 和 Android平台。 Flutter提供了丰富的组件、接口,开发者可以很快地为 Flutter添加 native扩展。同时 Flutter还使用 Native引擎渲染视图,这无疑能为用户提供良好的体验。

特点

  • 跨平台自绘引擎

    不使用 webview,不使用操作系统原生空间,自己的高性能渲染引擎来绘制widget

    Skia作为其2D渲染引擎

  • 高性能

    采用 dart 语言,dart 在 JIT(ust-in-time) 下编译速度与 JavaScript 持平,Dart 支持 AOT(Ahead Of Time),以 AOT 模式运行速度更快,参考 JIT 与 AOT 的区别

    自己绘制的引擎开发,统一由 Dart 控制,不需要javasript 与 nativt 通信

  • Dart 语言开发

    开发时可以 JIT,避免每次改动都编译,几乎秒安装

    基于 AOT 发布,发布时通过 AOT 保证性能

    快速内存分配,底层高效的内存分配器,屏蔽了 UI 绘制细节

    类型安全,支持静态类型检测

实例演示

flutter学习

注意看 执行逻辑都是从run->build

架构

对比一下android的架构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PIpfOzMC-1620921831417)(https://km.woa.com/files/photos/pictures/202105/1620916021_26_w600_h419.png)]

flutter 的架构
图1-1
是要简单很多
Foundation 与 Animation,Painting,Gestures 合称为 ui 层,在 dart:ui 中

Rendering 层,是抽象的布局层,依赖于 dart UI,构建一个 UI 树,当 UI 树有变化的时候,计算出变化的部分,并更新 UI 树,绘制到屏幕上。主要负责确定 UI 位置,大小,坐标变换等,是核心部分

Widgets 是一套基础组件库,在此之上还有 Material 与 Cupetino 两种组件库,我们写代码主要在这上面

关于 Engine,由于在 底层 C++,本篇不进行详细介绍

实现

路由

Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
      ... //省略无关代码
      FlatButton(
         child: Text("open new route"),
         textColor: Colors.blue,
         onPressed: () {
          //导航到新路由   
          Navigator.push( context,
           MaterialPageRoute(builder: (context) {
              return NewRoute();
           }));
          },
         ),
       ],
 )

效果如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DCwAtod5-1620921831419)(https://km.woa.com/files/photos/pictures/202105/1620916874_63_w1510_h1322.png)]

基础组件

Dart 是一个真正面向对象的语言,几乎所有的对象都可以用 Widget 表示,而且比原生的控件更加彻底

包括主题在内的很多非 UI 元素同样可以用 Widget 表示,但是他的本质是”描述一个 UI 元素的配置数据"

Widget

具体绘制 UI 的是 Element,一个 Widget 对应多个 Element,Element 就是UI 树 的每一个结点

查看 Widget 类的源码

@immutable
abstract class Widget extends DiagnosticableTree {
  const Widget({ this.key });
  final Key key;
    
  @protected
  Element createElement();

  @override
  String toStringShort() {
    return key == null ? '$runtimeType' : '$runtimeType-$key';
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
  }
  
  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }
}
  • Widget类继承自DiagnosticableTreeDiagnosticableTree即“诊断树”,主要作用是提供调试信息。
  • Key: 这个key属性类似于React/Vue中的key,主要的作用是决定是否在下一次build时复用旧的widget,决定的条件在canUpdate()方法中。
  • createElement():正如前文所述“一个Widget可以对应多个Element”;Flutter Framework在构建UI树时,会先调用此方法生成对应节点的Element对象。此方法是Flutter Framework隐式调用的,在我们开发过程中基本不会调用到。
  • debugFillProperties(...) 复写父类的方法,主要是设置诊断树的一些特性。
  • canUpdate(...)是一个静态方法,它主要用于在Widget树重新build时复用旧的widget,其实具体来说,应该是:是否用新的Widget对象去更新旧UI树上所对应的Element对象的配置;通过其源码我们可以看到,只要newWidgetoldWidgetruntimeTypekey同时相等时就会用newWidget去更新Element对象的配置,否则就会创建新的Element
StatelessWidget

继承自 Widget,重写

abstract class StatelessWidget extends Widget {
  /// Initializes [key] for subclasses.
  const StatelessWidget({ Key? key }) : super(key: key);

  /// Creates a [StatelessElement] to manage this widget's location in the tree.
  ///
  /// It is uncommon for subclasses to override this method.
  @override
  StatelessElement createElement() => StatelessElement(this);

 
  @protected
  Widget build(BuildContext context);
}

StatelessWidget用于不需要维护状态的场景,它通常在build方法中通过嵌套其它Widget来构建UI,在构建过程中会递归的构建其嵌套的Widget。

StatefulWidget

继承自 Widget,重写

abstract class StatefulWidget extends Widget {
  /// Initializes [key] for subclasses.
  const StatefulWidget({ Key? key }) : super(key: key);

  /// Creates a [StatefulElement] to manage this widget's location in the tree.
  ///
  /// It is uncommon for subclasses to override this method.
  @override
  StatefulElement createElement() => StatefulElement(this);

  
  @protected
  @factory
  State createState(); // ignore: no_logic_in_create_state, this is the original sin
}

State

一个StatefulWidget类会对应一个State类,State表示与其对应的StatefulWidget要维护的状态,State中的保存的状态信息可以:

  1. 在widget 构建时可以被同步读取。
  2. 在widget生命周期中可以被改变,当State被改变时,可以手动调用其setState()方法通知Flutter framework状态发生改变,Flutter framework在收到消息后,会重新调用其build方法重新构建widget树,从而达到更新UI的目的。

State中有两个常用属性:

  1. widget,它表示与该State实例关联的widget实例,由Flutter framework动态设置。注意,这种关联并非永久的,因为在应用生命周期中,UI树上的某一个节点的widget实例在重新构建时可能会变化,但State实例只会在第一次插入到树中时被创建,当在重新构建时,如果widget被修改了,Flutter framework会动态设置State.widget为新的widget实例。
  2. context。StatefulWidget对应的BuildContext,作用同StatelessWidget的BuildContext。

代码演示,切换 LayoutDemo 与 CounterWidget

图3-2

  • initState :当Widget第一次插入到Widget树时会被调用,对于每一个State对象,Flutter framework只会调用一次该回调,所以,通常在该回调中做一些一次性的操作,如状态初始化、订阅子树的事件通知等。
  • didChangeDependencies() :当State对象的依赖发生变化时会被调用;例如:在之前 build() 中包含了一个 InheritedWidget ,然后在之后的 build()InheritedWidget 发生了变化,那么此时 InheritedWidget 的子widget的 didChangeDependencies() 回调都会被调用。典型的场景是当系统语言Locale或应用主题改变时,Flutter framework会通知widget调用此回调。
  • build() :它主要是用于构建Widget子树的,会在如下场景被调用:
  1. 在调用 initState() 之后。
  2. 在调用 didUpdateWidget() 之后。
  3. 在调用 setState() 之后。
  4. 在调用 didChangeDependencies() 之后。
  5. 在State对象从树中一个位置移除后(会调用deactivate)又重新插入到树的其它位置之后。
  • reassemble() :此回调是专门为了开发调试而提供的,在热重载(hot reload)时会被调用,此回调在Release模式下永远不会被调用。
  • didUpdateWidget() :在widget重新构建时,Flutter framework会调用 Widget.canUpdate 来检测Widget树中同一位置的新旧节点,然后决定是否需要更新,如果 Widget.canUpdate 返回 true 则会调用此回调。正如之前所述, Widget.canUpdate 会在新旧widget的key和runtimeType同时相等时会返回true,也就是说在在新旧widget的key和runtimeType同时相等时 didUpdateWidget() 就会被调用。
  • deactivate() :当State对象从树中被移除时,会调用此回调。在一些场景下,Flutter framework会将State对象重新插到树中,如包含此State对象的子树在树的一个位置移动到另一个位置时(可以通过GlobalKey来实现)。如果移除后没有重新插入到树中则紧接着会调用 dispose() 方法。
  • dispose() :当State对象从树中被永久移除时调用;通常在此回调中释放资源。

UI 绘制过程

UI 系统

Flutter 能够统一 android, ios, web端,使用同一套代码实现 UI,证明这些终端的 UI实现流程其实是相似的

我们可以看到,无论是Android SDK还是iOS的UIKit 的职责都是相同的,它们只是语言载体和底层的系统不同而已。那么可不可以实现这么一个UI系统:可以使用同一种编程语言开发,然后针对不同操作系统API抽象一个对上接口一致,对下适配不同操作系统的的中间层,然后在打包编译时再使用相应的中间层代码?如果可以做到,那么我们就可以使用同一套代码编写跨平台的应用了。而Flutter的原理正是如此,它提供了一套Dart API,然后在底层通过OpenGL这种跨平台的绘制库(内部会调用操作系统API)实现了一套代码跨多端。由于Dart API也是调用操作系统API,所以它的性能接近原生。

绘制流程

我们知道最终的UI树其实是由一个个独立的Element节点构成。我们也说过组件最终的Layout、渲染都是通过RenderObject来完成的,从创建到渲染的大体流程是:根据Widget生成Element,然后创建相应的RenderObject并关联到Element.renderObject属性上,最后再通过RenderObject来完成布局排列和绘制。

图14-0

从启动到显示

Flutter的入口在"lib/main.dart"的main()函数中,它是Dart应用程序的起点。在Flutter应用中,main()函数最简单的实现如下:

void main() {
  runApp(MyApp());
}

可以看main()函数只调用了一个runApp()方法,我们看看runApp()方法中都做了什么:

截图

void runApp(Widget app) {
  WidgetsFlutterBinding.ensureInitialized()
    ..scheduleAttachRootWidget(app)
    ..scheduleWarmUpFrame();
} 

参数app是一个widget,它是Flutter应用启动后要展示的第一个Widget。而WidgetsFlutterBinding正是绑定widget 框架和Flutter engine的桥梁,定义如下:

class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {
  static WidgetsBinding ensureInitialized() {
    if (WidgetsBinding.instance == null)
      WidgetsFlutterBinding();
    return WidgetsBinding.instance;
  }
}

可以看到WidgetsFlutterBinding继承自BindingBase 并混入了很多Binding,在介绍这些Binding之前我们先介绍一下Window,下面是Window的官方解释:

The most basic interface to the host operating system’s user interface.

很明显,Window正是Flutter Framework连接宿主操作系统的接口。

现在我们再回来看看WidgetsFlutterBinding混入的各种Binding。通过查看这些 Binding的源码,我们可以发现这些Binding中基本都是监听并处理Window对象的一些事件,然后将这些事件按照Framework的模型包装、抽象然后分发。可以看到WidgetsFlutterBinding正是粘连Flutter engine与上层Framework的“胶水”。

  • GestureBinding:提供了window.onPointerDataPacket 回调,绑定Framework手势子系统,是Framework事件模型与底层事件的绑定入口。
  • ServicesBinding:提供了window.onPlatformMessage 回调, 用于绑定平台消息通道(message channel),主要处理原生和Flutter通信。
  • SchedulerBinding:提供了window.onBeginFramewindow.onDrawFrame回调,监听刷新事件,绑定Framework绘制调度子系统。
  • PaintingBinding:绑定绘制库,主要用于处理图片缓存。
  • SemanticsBinding:语义化层与Flutter engine的桥梁,主要是辅助功能的底层支持。
  • RendererBinding: 提供了window.onMetricsChangedwindow.onTextScaleFactorChanged 等回调。它是渲染树与Flutter engine的桥梁。
  • WidgetsBinding:提供了window.onLocaleChangedonBuildScheduled 等回调。它是Flutter widget层与engine的桥梁。
渲染

回到runApp的实现中,当调用完attachRootWidget后,最后一行会调用 WidgetsFlutterBinding 实例的 scheduleWarmUpFrame() 方法,该方法的实现在SchedulerBinding 中,它被调用后会立即进行一次绘制(而不是等待"vsync" 信号),在此次绘制结束前,该方法会锁定事件分发,也就是说在本次绘制结束完成之前Flutter将不会响应各种事件,这可以保证在绘制过程中不会再触发新的重绘。下面是scheduleWarmUpFrame() 方法的部分实现(省略了无关代码):

void scheduleWarmUpFrame() {
    if (_warmUpFrame || schedulerPhase != SchedulerPhase.idle)
      return;

    _warmUpFrame = true;
    Timeline.startSync('Warm-up frame');
    final bool hadScheduledFrame = _hasScheduledFrame;
    // We use timers here to ensure that microtasks flush in between.
    Timer.run(() {
      assert(_warmUpFrame);
      handleBeginFrame(null);
    });
    Timer.run(() {
      assert(_warmUpFrame);
      handleDrawFrame();
    
      resetEpoch();
      _warmUpFrame = false;
      if (hadScheduledFrame)
        scheduleFrame();
    });

    // Lock events so touch events etc don't insert themselves until the
    // scheduled frame has finished.
    lockEvents(() async {
      await endOfFrame;
      Timeline.finishSync();
    });
  }
绘制

渲染和绘制逻辑在RendererBinding中实现,查看其源码,发现在其initInstances()方法中有如下代码:

void initInstances() {
  ... //省略无关代码
      
  //监听Window对象的事件  
  ui.window
    ..onMetricsChanged = handleMetricsChanged
    ..onTextScaleFactorChanged = handleTextScaleFactorChanged
    ..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged
    ..onSemanticsAction = _handleSemanticsAction;
   
  //添加PersistentFrameCallback    
  addPersistentFrameCallback(_handlePersistentFrameCallback);
}

我们看最后一行,通过addPersistentFrameCallbackpersistentCallbacks队列添加了一个回调 _handlePersistentFrameCallback:

void _handlePersistentFrameCallback(Duration timeStamp) {
  drawFrame();
}

该方法直接调用了RendererBindingdrawFrame()方法:

void drawFrame() {
  assert(renderView != null);
  pipelineOwner.flushLayout(); //布局
  pipelineOwner.flushCompositingBits(); //重绘之前的预处理操作,检查RenderObject是否需要重绘
  pipelineOwner.flushPaint(); // 重绘
  renderView.compositeFrame(); // 将需要绘制的比特数据发给GPU
  pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
}

drawFrame最后会调用底层的flushPaint 方法,此方法进入到 Flutter engine 调用提供的 Canvas API 来完成

flutter2.0

Flutter2.0最大的变化是除了之前已经处于stable渠道的移动设备支持外,桌面和Web支持也正式宣布进入stable渠道。
另外还有一个很重要的变化,就是版本升级为 2.0,之前的1.x版本给人以刚出来,不稳定的感觉,升级成为2.0后,意味着flutter团队认为flutter已经成熟,可以在企业中推广使用。

为什么?

  • 在探究具体细节之前,我们有必要了解一下 Flutter 团队为什么要不惜这些代价对 Navigator API 做这次的重构,主要有如下几点原因。

    • 原始 API 中的 initialRoute 参数,即系统默认的初始页面,在应用运行后就不能再更改了。这种情况下,如果用户接收到一个系统通知,点击后想要从当前的路由栈状态 [Main -> Profile -> Settings] 重启切换到新的 [Main -> List -> Detail[id=24] 路由栈,旧的 Navigator API 并没有一种优雅的实现方式实现这种效果。
    • 原始的命令式 Navigator API 只提供给了开发者一些非常针对性的接口,如 push()pop() 等,而没有给出一种更灵活的方式让我们直接操作路由栈。这种做法其实与 Flutter 理念相违背,试想如果我们想要改变某个 widget 的所有子组件只需要重建所有子组件并且创建一系列新的 widget 即可,而将此概念应用在路由中,当应用中存在一系列路由页面并想要更改时,我们只能调用 push()pop() 这类接口来回操作, 这样的 Flutter 食之无味
    • 嵌套路由下,手机设备自带的回退按钮只能由根 Navigator 响应。在目前的应用中,我们很多场景都需要在某个子 tab 内单独管理一个子路由栈。假设有这个场景,用户在子路由栈中做一系列路由操作之后,点击系统回退按钮,消失的将是整个上层的根路由,我们当然可以使用某种措施来避免这种状况,但归咎起来,这也不应该是应用开发者应该考虑的问题。

    于是,Navigator 2.0 就肩负着这千里之任来了。

在我看来,这就是一种关于路由的解耦,以前由于只有 push pop 这些,高度定制化,虽然简单,丧失了灵活性,现在,进行了改进

Navigator 2.0

操作多样化

https://mp.weixin.qq.com/s?__biz=MzIzMjYyNzQ2Ng==&mid=2247484244&idx=1&sn=dd4eed8a60e1d9a612b7937e699cf49e&scene=21#wechat_redirect

原来 : pop,push

现在:

图片

大致流程如下:

  1. 当系统打开新页面(如 “books / 2”)时,RouteInformationParser 会将其转换为应用中的具体数据类型 T(如 BooksRoutePath)。
  2. 该数据类型会被传递给 RouterDelegatesetNewRoutePath 方法,我们可以在这里更新路由状态(如通过设置 selectedBookId)并调用 notifyListeners 响应该操作。
  3. notifyListeners 会通知 Router 重建 RouterDelegate(通过 build() 方法).
  4. RouterDelegate.build() 返回一个新的 Navigator 实例,并最终展示出我们想要打开的页面(如 selectedBookId)。

原来无法实现精准进入某个页面,现在在 web 上能够通过变更 url 实现

图片

实例演示

代码demo展示

image-20210506130115313

异步执行

让我们来看一些会使一个程序“冻结”的代码:

// Synchronous code
// 同步执行的代码
void printDailyNewsDigest() {
  // var newsDigest = gatherNewsReports(); // Can take a while.
  var newsDigest = gatherNewsReports(); // 执行该函数会耗费一定时间。
  print(newsDigest);
}

main() {
  printDailyNewsDigest();
  printWinningLotteryNumbers();
  printWeatherForecast();
  printBaseballScore();
}

上述的程序中我们搜集当天的新闻以及用户感兴趣的其它一些信息并打印输出到控制台:

<gathered news goes here>

<搜集的新闻信息在这(省略……)>

Winning lotto numbers: [23, 63, 87, 26, 2]

乐透中奖号码:[23、63、87、26、2]

Tomorrow's forecast: 70F, sunny.

明天天气预报:21摄氏度,晴。

Baseball score: Red Sox 10, Yankees 0

棒球赛比分:红袜队 10,洋基队 0

上述的代码的问题在于:由于 gatherNewsReports() 函数阻塞,不管要等多久,剩下的代码都会在 gatherNewsReports() 函数返回了文件的内容后才会运行。如果读取文件需要耗费很长的时间,即便用户想马上知道他们是否中了乐透?明天天气如何?以及今天谁赢了比赛?都不得不等待 gatherNewsReports() 函数执行完毕。

为了帮助保持应用的响应速度,Dart 库的创作者们在定义可能需要执行耗时操作的函数时使用一种异步模型。这类函数使用一个 future 对象返回它们的值。

future是什么?

future 是 Future 类的对象,其表示一个 T 类型的异步操作结果。如果异步操作不需要结果,则 future 的类型可为 Future<void>。当一个返回 future 对象的函数被调用时,会发生两件事:

  1. 将函数操作列入队列等待执行并返回一个未完成的 Future 对象。
  2. 不久后当函数操作执行完成,Future 对象变为完成并携带一个值或一个错误。

当你写的代码依赖于 future 对象时,你有两种可选的实现方式:

  • 使用关键字 asyncawait
  • 使用 Future API

关键字 async 和 await

关键字 asyncawait 是 Dart 语言 异步支持 的一部分。它们允许你不使用 Future 的 API 编写看起来与同步代码一样的异步代码。异步函数 即在函数头中包含关键字 async 的函数。关键字 await 只能用在异步函数中。

接下来的应用使用关键字 asyncawait

这里需要注意,虽然函数 printDailyNewsDigest() 是第一个被调用的函数,但是新闻信息却是最后才打印输出的,即便该新闻信息只是一行模拟文本。这是因为代码在读取和打印输出该信息时是异步的。

在这个例子中,函数 printDailyNewsDigest() 调用函数 gatherNewsReports()的过程是非阻塞的。调用函数 gatherNewsReports() 时会将该函数操作列入队列执行但不会阻止其它代码的执行。程序打印输出乐透号码、天气、以及棒球赛比分;当 gatherNewsReports() 函数搜集完新闻后程序也将其打印输出。即使 gatherNewsReports() 函数需要花费一点时间来完成它的工作,这也不会造成太大的影响:用户可以在每日新闻摘要打印输出前查看其它信息。

注意函数的返回类型。函数 gatherNewsReports() 的返回类型是 Future<String>,这表示其返回一个完成时包含一个字符串值的 future 对象。而函数 printDailyNewsDigest() 因为本身没有返回值,所以在变为异步函数后其返回值类型为 Future<void>

下面的图示展示了代码的执行流程。每个数字对应图示下面的一个步骤。

diagram showing flow of control through the main() and printDailyNewsDigest functions

  1. 应用开始执行。

  2. main() 函数开始以同步的方式执行并调用异步函数 printDailyNewsDigest()

  3. printDailyNewsDigest()函数开始执行,并使用关键字 await 调用 gatherNewsReports() 函数。

  4. 函数 gatherNewsReports() 返回一个未完成的 future 对象( Future<String> 类的一个实例)。

    The gatherNewsReports() function returns an uncompleted future (an instance of Future<String>).

  5. 因为函数 printDailyNewsDigest() 是一个异步函数并且它在等待一个值(该值由第 4 步中函数 gatherNewsReports() 返回的 future 对象提供),所以它暂停其函数内代码的执行并返回一个未完成的 future 对象(在此例中,该 future 对象为 Future<void> 的一个实例)给它的调用者(main() 函数)。

  6. 执行其它的打印输出函数。因为这些函数是同步的,所以每一个函数完整地执行完后才会执行下一个函数。例如乐透的中奖号码一定会在天气预报之前打印出来。

  7. main() 函数完成执行时,异步函数将会恢复执行。首先,函数 gatherNewsReports() 返回的 future 对象完成。然后函数 printDailyNewsDigest() 继续执行,最后打印出新闻。

  8. printDailyNewsDigest() 函数完成执行时,其刚开始返回的 future 对象也完成,并且应用退出。

请注意异步函数是立即开始执行的(同步地),其将会在下述情况之一首次出现时暂停执行并返回一个未完成的 future 对象:

  • 函数中第一个 await 表达式出现时(在该函数从 await 表达式获取到未完成的 future 之后)。
  • 函数中任何 return 语句的出现时。
  • 函数体的结束。

Async 将函数标记为异步函数, await 仅在 异步函数中工作,将被标记函数暂停,直到异步函数调用其返回结果,在这里,需要暂停list.add,直到文件读取完成

Future<List>getFromFile() async {
  List list = [];
  Stream stream = Stream.fromIterable([1,2,3,4]); //假设这里是文件读取
  await stream.forEach((element) {
    list.add(element);
  });
  print ("future");
  return list;

}

Stream

FutureStream 类是 Dart 异步编程的核心。

Future 表示一个不会立即完成的计算过程。与普通函数直接返回结果不同的是异步函数返回一个将会包含结果的 Future。该 Future 会在结果准备好时通知调用者。

Stream 是一系列异步事件的序列。其类似于一个异步的 Iterable,不同的是当你向 Iterable 获取下一个事件时它会立即给你,但是 Stream 则不会立即给你而是在它准备好时告诉你。

Stream 可以通过许多方式创建,这个话题我们会在另一篇文章详述,而这些所有的创建方式都可以相同的方式在代码中使用:像使用 for 循环 迭代一个 Iterable 一样,我们可以使用 异步 for 循环 (通常我们直接称之为 await for)来迭代 Stream 中的事件。例如:

Future<int> sumStream(Stream<int> stream) async {
  var sum = 0;
  await for (var value in stream) {
    sum += value;
  }
  return sum;
}

什么是Stream

Stream是Dart语言中的所谓异步数据序列的东西,简单理解,其实就是一个异步数据队列而已。我们知道队列的特点是先进先出的,Stream也正是如此

在这里插入图片描述

更形象的比喻,Stream就像一个传送带。可以将一侧的物品自动运送到另一侧。如上图,在另一侧,如果没有人去抓取,物品就会掉落消失。

在这里插入图片描述

但如果我们在末尾设置一个监听,当物品到达末端时,就可以触发相应的响应行为。

在Dart语言中,Stream有两种类型,一种是点对点的单订阅流(Single-subscription),另一种则是广播流。

单订阅流

单订阅流的特点是只允许存在一个监听器,即使该监听器被取消后,也不允许再次注册监听器。

创建 Stream

创建一个Stream有9个构造方法,其中一个是构造广播流的,这里主要看一下其中5个构造单订阅流的方法

periodic
void main(){
  test();
  
}

test() async{
  // 使用 periodic 创建流,第一个参数为间隔时间,第二个参数为回调函数
  Stream<int> stream = Stream<int>.periodic(Duration(seconds: 1), callback);
  // await for循环从流中读取
  await for(var i in stream){
    print(i);
  }
}

// 可以在回调函数中对值进行处理,这里直接返回了
int callback(int value){
  return value;
}

打印结果:

0
1
2
3
4
...

该方法从整数0开始,在指定的间隔时间内生成一个自然数列,以上设置为每一秒生成一次,callback函数用于对生成的整数进行处理,处理后再放入Stream中。这里并未处理,直接返回了。要注意,这个流是无限的,它没有任何一个约束条件使之停止。在后面会介绍如何给流设置条件。

fromFuture
void main(){
  test();
}

test() async{
  print("test start");
  Future<String> fut = Future((){
      return "async task";
  });

  // 从Future创建Stream
  Stream<String> stream = Stream<String>.fromFuture(fut);
  await for(var s in stream){
    print(s);
  }
  print("test end");
}

打印结果:

test start
async task
test end
复制代码

该方法从一个Future创建Stream,当Future执行完成时,就会放入Stream中,而后从Stream中将任务完成的结果取出。这种用法,很像异步任务队列。

fromFutures

从多个Future创建Stream,即将一系列的异步任务放入Stream中,每个Future按顺序执行,执行完成后放入Stream

import  'dart:io';

void main() {
  test();
}

test() async{
  print("test start");
  Future<String> fut1 = Future((){
      // 模拟耗时5秒
      sleep(Duration(seconds:5));
      return "async task1";
  });
    Future<String> fut2 = Future((){
      return "async task2";
  });

  // 将多个Future放入一个列表中,将该列表传入
  Stream<String> stream = Stream<String>.fromFutures([fut1,fut2]);
  await for(var s in stream){
    print(s);
  }
  print("test end");
}

hot reload

Framework.dart
  
@mustCallSuper
  @protected
  void reassemble() {
    markNeedsBuild();  //重新执行 build
    visitChildren((Element child) {
      child.reassemble(); 
    });
  }

原理

热重载是指,在不中断 App 正常运行的情况下,动态注入修改后的代码片段。而这一切的背后,离不开 Flutter 所提供的运行时编译能力。为了更好地理解 Flutter 的热重载实现原理,我们先简单回顾一下 Flutter 编译模式背后的技术吧。

JIT

JIT(Just In Time),指的是即时编译或运行时编译,在 Debug 模式中使用,可以动态下发和执行代码,启动速度快,但执行性能受运行时编译影响。

img

AOT

AOT(Ahead Of Time),指的是提前编译或运行前编译,在 Release 模式中使用,可以为特定的平台生成稳定的二进制代码,执行性能好、运行速度快,但每次执行均需提前编译,开发调试效率低。

img

可以看到,Flutter 提供的两种编译模式中,AOT 是静态编译,即编译成设备可直接执行的二进制码;而 JIT 则是动态编译,即将 Dart 代码编译成中间代码(Script Snapshot),在运行时设备需要 Dart VM 解释执行。

而热重载之所以只能在 Debug 模式下使用,是因为 Debug 模式下,Flutter 采用的是 JIT 动态编译(而 Release 模式下采用的是 AOT 静态编译)。JIT 编译器将 Dart 代码编译成可以运行在 Dart VM 上的 Dart Kernel,而 Dart Kernel 是可以动态更新的,这就实现了代码的实时更新功能,原理如下图。

img

总体来说,完成热重载的可以分为扫描工程改动、增量编译、推送更新、代码合并、Widget 重建 5 个步骤。

  1. 工程改动。热重载模块会逐一扫描工程中的文件,检查是否有新增、删除或者改动,直到找到在上次编译之后,发生变化的 Dart 代码。
  2. **增量编译。**热重载模块会将发生变化的 Dart 代码,通过编译转化为增量的 Dart Kernel 文件。
  3. 推送更新。热重载模块将增量的 Dart Kernel 文件通过 HTTP 端口,发送给正在移动设备上运行的 Dart VM。
  4. 代码合并。Dart VM 会将收到的增量 Dart Kernel 文件,与原有的 Dart Kernel 文件进行合并,然后重新加载新的Dart Kernel 文件。
  5. Widget 重建。在确认 Dart VM 资源加载成功后,Flutter 会将其 UI 线程重置,通知 Flutter Framework 重建 Widget。

可以看到,Flutter 提供的热重载在收到代码变更后,并不会让 App 重新启动执行,而只会触发 Widget 树的重新绘制,因此可以保持改动前的状态,这就大大节省了调试复杂交互界面的时间。

比如,我们需要为一个视图栈很深的页面调整 UI 样式,若采用重新编译的方式,不仅需要漫长的全量编译时间,而为了恢复视图栈,也需要重复之前的多次点击交互,才能重新进入到这个页面查看改动效果。但如果是采用热重载的方式,不仅没有编译时间,而且页面的视图栈状态也得以保留,完成热重载之后马上就可以预览 UI 效果了,相当于进行了局部界面刷新。

失效情况:

  • 替换掉根节点

  • 非 build 方法改动的数据

  • inal 变量内容的修改 (可通过修改变量名完成 hot reload)

空安全

Dart 的空安全支持基于以下三条核心原则:

  • 默认不可空。除非您将变量显式声明为可空,否则它一定是非空的类型。我们在研究后发现,非空是目前的 API 中最常见的选择,所以选择了非空作为默认值。
  • 渐进迁移。您可以自由地选择何时进行迁移,多少代码会进行迁移。您可以使用混合模式的空安全,在一个项目中同时使用空安全和非空安全的代码。我们也提供了帮助您进行迁移的工具。
  • 完全可靠。Dart 的空安全是非常可靠的,意味着编译期间包含了很多优化。如果类型系统推断出某个变量不为空,那么它 永远 不为空。当您将整个项目和其依赖完全迁移至空安全后,您会享有健全性带来的所有优势——更少的 BUG、更小的二进制文件以及更快的执行速度。

原来的 Null 类型

img

Null 是所有类型的子类,在 null 上可以调用任何方法,但是会导致 NPE

现在的解决方案

img

Null 被独立出 Object,从根源上排除了 NPE

使用可空类型

Null 被独立出 Object,是不是就意味这 Null 无法使用了?

不是,在一些情况下,我们需要有可空类型的参数,与 kotlin 一样,在后面增加?

bad(String? maybeString) {
  print(maybeString.length);
}

main() {
  bad(null);
}

这样代码运行会直接崩溃,由于非空类型的值传递给可空类型一定是安全的,所以,可以将非空类型作为可空类型的子类传递

img

反之则是不安全的,所以,此时需要显示转换

// Hypothetical unsound null safety:
requireStringNotNull(String definitelyString) {
  print(definitelyString.length);
}

main() {
  String? maybeString = null; // Or not!
  requireStringNotNull(maybeString);
}

在编译时会运行报错,另外,由于不同类型的隐式转换也有运行报错的风险,所以被强制更改为显示转换

// Using null safety:
requireStringNotObject(String definitelyString) {
  print(definitelyString.length);
}

main() {
  Object maybeString = 'it is';
  requireStringNotObject(maybeString as String);
}

所以,在移除了 null 作为子类和隐式转换后,null 在代码中会极大的减少,如果需要一个表示任何值的变量,我们可以使用 object?,所有东西的超类成为了 Object?,而所有东西的子类替换为 Never

现在的类型

img

其他的可空情况

现在还有两种情况可空

  • 未初始化的变量

    顶层变量和静态字段必须包含一个初始化方法

    实例的字段也必须在声明时包含初始化方法,可以为常见初始化形式,也可以在实例的构造方法中进行初始化

    局部变量必须*确保在使用前被赋值*

    可选参数必须具有默认值

  • 无效的返回值

关于 Never

Never 是一底层的子类,没有任何值,因为他不可能同时是 List,double,int,所以表示这个表达式无法被成功执行,必须要抛出异常

类型提升

判定了不为 null 的可空变量,Dart 会自动提升为非空

// Using null safety:
String makeCommand(String executable, [List<String>? arguments]) {
  var result = executable;
  if (arguments != null) {
    result += ' ' + arguments.join(' ');
  }
  return result;
}

其他特性

独一无二的应用构建能力集合

可移植性

Dart的高效编译器可以生成针对x86&ARM的机器码,以及针对Web优化过的JS。其广泛支持了各种目标: 移动设备、桌面PC、后端应用以及更多。

高开发效率

Dart提供的HotReload特性,支持快速的,可交互的开发体验,不论是原生设备还是Web应用均如此。Dart也提供丰富的对象用于应用开发,包括Isolate模型,async/await并发处理,以及事件驱动的开发模式。

健壮

Dart的健全空安全类型系统可以在编译期捕获错误,这一切高度可伸缩可信赖,并被用于支持大量的应用,如高度重要的Google Ads,Google Assistant,运行长达长达十年以上

参考资料

深入理解 Flutter 应用启动

Flutter 实战电子书

dart中文文档

flutter 中文网

Dart 语言异步之Stream详解

[Flutter HotReload](

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值