Flutter 简介

243 阅读13分钟

一、优势

  • 提高开发效率
    1. 在应用程序运行时更改代码并重新加载(通过热重载)
    2. 修复崩溃并继续从应用程序停止的地方进行调试
  • 创建美观,高度定制的用户体验 受益于使用Flutter框架提供的丰富的Material Design和Cupertino(iOS风格)的widget, 实现定制、美观、品牌驱动的设计,而不受原生控件的限制。

二、配置环境 + 创建第一个工程 + 运行

  1. vim ~/.bash_profile

    export PUB_HOSTED_URL=https://pub.flutter-io.cn
    export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
    export PATH=下载的SDK的路径/flutter/bin:$PATH
    

    编辑完成后,按“Esc”,再按“:wq”,保存这个文件

  2. source ~/.bash_profile :让配置立即生效

  3. source ~/.zshrc :生效.zshrc文件
    如果不存在.zshrc文件
    // 进入根目录
    $ cd ~
    // 打开/新建 .zshrc 文件
    vim ~/.zshrc
    粘贴下面保存

    # Add RVM to PATH for scripting. Make sure this is the last PATH variable change.
    export PATH="$PATH:$HOME/.rvm/bin"
    source ~/.bash_profile
    

    source ~/.zshrc

  4. flutter -h : 检测是否配置成功输入

  5. flutter doctor : 命令检测环境

  6. flutter create 工程名字 : 创建工程

  7. vscode 打开工程,输入open -a Simulator,然后flutter run命令,运行成功

报错
  • flutter doctor 报错 Downloaded executables cannot execute on host. 解决: rm -rf 下载的SDK的路径/flutter/bin/cache flutter doctor --verbose

二、Flutter 基础知识

核心原则

Flutter包括一个现代的响应式框架、一个2D渲染引擎、现成的widget和开发工具。

Flutter 核心原理

它提供了一套Dart API,然后在底层通过OpenGL这种跨平台的绘制库(内部会调用操作系统API)实现了一套代码跨多端。由于Dart API也是调用操作系统API,所以它的性能接近原生。

注意,虽然Dart是先调用了OpenGL,OpenGL才会调用操作系统API,但是这仍然是原生渲染,因为OpenGL只是操作系统API的一个封装库。

Flutter 系统架构图

Flutter 框架分为三层: Framework 层、 Engine 层和 Embedder 层

Framework 层: 下面详细介绍
Engine 层: Engine使用C++实现,主要包括:Skia, Dart 和 Text。 Embedder层: Embedder是一个嵌入层,通过该层把Flutter嵌入到各个平台上去,Embedder的主要工作包括渲染Surface设置, 线程设置,以及插件等。平台(如iOS)只是提供一个画布,剩余的所有渲染相关的逻辑都在Flutter内部,这就使得它具有了很好的跨端一致性。

Flutter 中的层级结构

Framework 层

Widgets

我们大多数情况下使用的是Widget。 Widget库:

  • Widget库是Widget的抽象层。这个库中所有的Widget都属于以下三种使用适当的RenderObject处理的Widget之一。
    1. Layout 例如Column和Row Widgets用来帮助我们轻松的处理其他Widget的布局。
    2. Painting 例如Text和Image Widgets允许我们展示(绘制)一些内容在屏幕上。
    3. Hit-Testing 例如GestureDetector允许我们识别出不同的手势,例如点击和滑动。
  • Flutter团队还创建了两个包含常用的Material和Cupertino风格的Widgets的库。
Rendering
  1. Rendering library 是 dart:ui library 上第一个抽象层。它替你做了所有繁重的数学计算工作(例如跟踪需要不断计算的坐标)。它使用RenderObjects来处理这些工作。
  2. Rendering tree(渲染树)中的所有RenderObjects都会被Flutter分层和绘制。RenderObject的层级结构被Flutter Widgets库使用来实现其布局和后台的绘制。
  3. 渲染优化: 为了优化这个复杂的过程,Flutter使用了一个智能算法来缓存这些实例化很耗费性能的对象从而实现在性能最优化。 大多数情况,Flutter 使用 RenderBox 而不是 RenderObject。这是因为项目的构建者发现使用一个简单和盒布局约束就能够成功的构建出有效稳定的UI。想象一下所有的Widget都被放置在它们的盒中。这个盒中的相关参数都计算好了,然后被放置到其他已经整理好的盒中间。所以如果在你的布局中仅有一个Widget改变了,只需要装载其的盒被系统重新计算即可。 尽管你可能会使用RenderBox来在你的应用中实现自定义的效果,但是大多数情况下我们唯一与 RenderObject 的交互就是在调试布局信息的时候。
dart:ui

暴露的最底层的服务,它负责处理与Engine层的交流沟通。用来引导Application,例如用来驱动输入、绘制文字、布局和渲染子系统。

四、Flutter Widget

1. 简介

Flutter Widget采用响应式框架构建,中心思想是用widget构建你的UI。

Flutter中的Widget的概念更广泛,它不仅可以表示UI元素,也可以表示一些功能性的组件如:用于手势检测的 GestureDetector widget、用于APP主题数据传递的Theme等等。

widget的主要工作是实现一个build函数,用以构建自身。

  • StatelessWidget 和 StatefulWidget 在编写应用程序时,通常会创建新的widget,这些widget是无状态的StatelessWidget或者是有状态的StatefulWidget, 具体的选择取决于您的widget是否需要管理一些状态。 有状态和无状态的 widget 之间一个非常重要的区别是,StatefulWidget 拥有一个 State 对象来存储它的状态数据,并在 widget 树重建时携带着它,因此状态不会丢失。 我们一般都不用直接继承Widget类来实现一个新组件,我们通常会通过继承StatelessWidget或StatefulWidget来间接继承Widget类来实现。

  • State 定义了StatefulWidget实例的行为,它包含了用于 ”交互/干预“ Widget 信息的行为和布局。应用于State的任何更改都会强制重建Widget。

  • Context build方法有一个context参数,它是BuildContext类的一个实例,表示当前widget在widget树中的上下文,每一个widget都会对应一个context对象(因为每一个widget都是widget树上的一个节点)。并且一个context只从属于一个widget,它和widget一样是链接在一起的,并且会形成一个context树。

  • Widget树 Widget以树结构进行组织。包含其他Widget的widget被称为父Widget(或widget容器)。包含在父widget中的widget被称为子Widget。 该runApp函数接受给定的Widget并使其成为widget树的根

      import 'package:flutter/material.dart';
    
      void main() {
        runApp(
          new Center(
            child: new Text(
              'Hello, world!',
              textDirection: TextDirection.ltr,
            ),
          ),
        );
      }
    

2. Widget 和 element 和 RenderObject

  • Widget 和 RenderObject Widget 只是一个配置,RenderObject 负责管理布局、绘制等操作。 RenderObject中包含了所有用来渲染实例Widget的逻辑。它负责layout、painting和hit-testing。它的生成十分耗费性能,所以我们应该尽可能的缓存它。我们把它在内存中尽可能的保存更长的时间,甚至回收利用它们(因为它们的实例化真的很耗费资源)。这个时候Element就登场了。

  • Element Element是存在于可变Widget树和不可变RenderObject树之间的桥梁。 Element擅长比较两个Object,在Flutter里面就是Widget和RenderObject。它的作用是配置好Widget在树中的位置,并且保持对于相对应的RenderObject和Widget的引用。 Flutter创建Element的可见树,相对于Widget来说,是可变的,通常界面开发中,我们不用直接操作Element,而是由框架层实现内部逻辑。

  • Widget 和 Element 在Flutter中,Widget的功能是“描述一个UI元素的配置数据”,它就是说,Widget其实并不是表示最终绘制在设备屏幕上的显示元素,而它只是描述显示元素的一个配置数据。Widget实际上就是Element的配置数据,Widget树实际上是一个配置树。真正代表屏幕上显示元素的类是Element,一个Widget对象可以对应多个Element对象。 Widget类是一个抽象类,其中最核心的就是定义了createElement()接口。 在第一次创建 Widget 的时候,会对应创建一个 Element, 然后将该元素插入树中。如果之后 Widget 发生了变化,则将其与旧的 Widget 进行比较,并且相应地更新 Element。重要的是,Element 不会被重建,只是更新而已。

3. 渲染

当widget的状态发生变化时,widget会重新构建UI,Flutter会对比前后变化的不同,以确定底层渲染树从一个状态转换到下一个状态所需的最小更改。这种自动比较非常有效,可以实现高性能的交互式应用程序。

1. 加载举例

当runApp()被调用时,第一时间会在后台发生以下事件。

  1. Flutter会构建包含这三个Widget的Widgets树。
  2. Flutter遍历Widget树,然后根据其中的Widget调用createElement()来创建相应的Element对象,最后将这些对象组建成Element树。
  3. 第三个树被创建,这个树中包含了与Widget对应的Element通过createRenderObject()创建的RenderObject。
  4. 最终Flutter创建了三个不同的树,一个对应着Widget,一个对应着Element,一个对应着RenderObject。每一个Element中都有着相对应的Widget和RenderObject的引用。

Flutter最终状态图

  • 为什么使用三个树而不是一个树呢? 为了性能。当Widget树改变的时候,Flutter使用Element树来比较新的Widget树和原来的RenderObject树。如果某一个位置的Widget和RenderObject类型不一致,才需要重新创建RenderObject。如果其他位置的Widget和RenderObject类型一致,则只需要修改RenderObject的配置,不用进行耗费性能的RenderObject的实例化工作了。 因为Widget是非常轻量级的,实例化耗费的性能很少。重量级的RenderObject(创建十分耗费性能)则需要尽可能少的创建,并尽可能的复用。
2. 发生状态改变后的渲染和举例

Flutter遵循一个最基本的原则:判断新的Widget和老的Widget是否是同一个类型。 如果不是同一个类型,那就把Widget、Element、RenderObject分别从它们的树(包括它们的子树)上移除,然后创建新的对象。 如果是一个类型,那就仅仅修改RenderObject中的配置,然后继续向下遍历。

  • 具体例子 2和1比较: SimpleApp Widget是和原来一样的类型,它的配置也是和原来的SimpleAppRender一样的,所以什么都不会发生。下一个item在Widget树中是SimpleContainer Widget,它的类型和原来是一样的,但是它的颜色变化了,RenderObject的配置发生变化了。因为SimpleObject仍然需要一个SimpleContainerRender来渲染,Flutter只是更新了SimpleContainerRender的颜色属性,然后要求它重新渲染。其他的对象都保持不变。 3和1比较: 因为SimpleButton的类型与Element树中相对应位置的Element的类型不同(实际上还是与RenderObject的类型进行比较),Flutter将会从各自的树上删除这个Element和相对应的SimpleTextRender。然后Flutter将会重建与SimpleButton相对应的Element和RenderObject。

五、代码相关

启动

Flutter的入口在"lib/main.dart"的main()函数中,它是Dart应用程序的起点。

常见目录结构 lib 文件夹下

common: 一些工具类,如通用方法类、网络接口类、保存全局变量的静态类等 l10n: 国际化相关的类都在此目录下 models: Json文件对应的Dart Model类会在此目录下 states: 保存APP中需要跨组件共享的状态类 routes: 存放所有路由页面类 widgets: APP内封装的一些Widget组件都在该目录下

main.dart

MaterialApp可以理解为ui的风格,而其中theme就是主题,比如primarySwatch表示主题色调,上面颜色为blue。

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

所有实现有状态控件StatefulWidget的类都必须重写该方法,而前面的“_”在dart语言中代表私有,只能内部访问。

_MyHomePageState createState() => _MyHomePageState();

定义标题栏,可以看到这里定义了标题栏的标题,就是MyHomePage刚传入进入的标题,widget其实就是MyHomePage。

appBar: AppBar(
    // Here we take the value from the MyHomePage object that was created by
    // the App.build method, and use it to set our appbar title.
    title: Text(widget.title),
),

六、其他

1. Flutter线程管理

Flutter Engine自己不创建, 管理线程。Flutter Engine线程的创建和管理是由embedder负责的。 Embeder提供四个Task Runner, 每个Task Runner负责不同的任务。

  • Platform Task Runner 是Flutter Engine的主Task Runner,运行Platform Task Runner的线程可以理解为是主线程。类似于Android Main Thread或者iOS的Main Thread。 可以同时启动多个Engine实例,每个Engine对应一个Platform Runner,每个Runner跑在各自的线程里。这也是Fuchsia(Google正在开发的操作引擎)里Content Handler的工作原理。一般情况下,一个Flutter应用启动的时候会创建一个Engine实例,Engine创建的时候会创建一个线程供Platform Runner使用。 跟Flutter Engine的所有交互(接口调用)必须发生在Platform Thread,试图在其它线程中调用Flutter Engine会导致无法预期的异常。

2. Flutter运行模式

Flutter常见的种运行模式:DebugReleaseProfile

Debug模式:使用JIT编译,支持模拟器和设备。打开了断言支持,包括所有的调试信息,服务扩展和Observatory等调试辅助。此模式为快速开发和运行做了优化,但并未对执行速度,包大小和部署做优化。所以能实现秒级别的hot reload。

Release模式:使用AOT编译,只支持真机,不支持模拟器。关闭了所有断言,尽可能多地去掉了调试信息,关闭了所有调试工具。为快速启动,快速执行,包大小做了优化。禁止了所有调试辅助手段,服务扩展。

3. State生命周期

一个StatefulWidget类会对应一个State类,State表示与其对应的StatefulWidget要维护的状态, initState(): 界面初始化状态时调用 didChangeDependencies(): 当state状态对象发生变化时调用(典型的场景是当系统语言Locale或应用主题改变时,Flutter framework会通知widget调用此回调。) build(): 主要是用于构建Widget子树,调用时机如下:

  • 在调用initState()之后。
  • 在调用didUpdateWidget()之后。
  • 在调用setState()之后。
  • 在调用didChangeDependencies()之后。
  • 在State对象从树中一个位置移除后(会调用deactivate)又重新插入到树的其它位置之后。 reassemble(): 主要用于调试,热重载时调用,release环境下不会调用 didUpdateWidget(): 用于更新widget ,Widget.canUpdate返回true则会调用此回调 deactivate(): 从widget树中移除State对象t时调用(位置交换) dispose(): 从widget树中移除State对象,并不再插入此State对象时调用(一般用于释放资源)

4. 常用的命令行

run: 在附加设备上运行你的Flutter应用程序。
create: 创建一个新的Flutter项目。
clean: 删除构建/目录。
doctor: 展示了有关安装工具的信息。
upgrade: 升级你的Flutter副本。
--version: 查看Flutter版本
-h或者--help: 打印所有命令行用法信息
analyze: 分析项目的Dart代码。
build: Flutter构建命令。
channel: 列表或开关Flutter通道。
config: 配置Flutter设置。
devices: 列出所有连接的设备。
drive: 为当前项目运行Flutter驱动程序测试。
format: 格式一个或多个Dart文件。
fuchsia_reload: 在Fuchsia上进行热重载。
help: 显示帮助信息的Flutter。
install: 在附加设备上安装Flutter应用程序。
logs: 显示用于运行Flutter应用程序的日志输出。
packages: 命令用于管理Flutter包。
precache: 填充了Flutter工具的二进制工件缓存。
screenshot: 从一个连接的设备截图。
stop: 停止在附加设备上的Flutter应用。
test: 对当前项目的Flutter单元测试。
trace: 开始并停止跟踪运行的Flutter应用程序。