移动应用开发框架-Flutter

573 阅读26分钟

Flutter 是 Google 推出并开源的移动应用开发框架,主打跨平台、高保真、高性能。开发者可以通过 Dart 语言开发 App,一套代码同时运行在 iOS 、Android、Web、macOs、Linux、Windows等多个平台。

(注:本文主要针对Android 和 iOS 两个平台进行阐述)

那么为什么会产生Flutter? 接下来我们一起看一下移动端开发技术的发展历史。

1. 移动开发技术简介

1.1 原生开发与跨平台技术

1.1.1 原生开发

原生应用程序是指某一个移动平台(比如iOS或安卓)所特有的应用,使用相应平台支持的开发工具和语言,并直接调用系统提供的SDK API。比如:

  • Android原生应用就是指使用Java或Kotlin语言直接调用Android SDK开发的应用程序;
  • iOS原生应用就是指通过Objective-C或Swift语言直接调用iOS SDK开发的应用程序。

原生开发有以下主要优势

  • 可访问平台全部功能(GPS、摄像头);
  • 速度快、性能高、可以实现复杂动画及绘制,整体用户体验好;

主要缺点

  • 平台特定,开发成本高;由于原生开发一般都要维护Android、iOS两个开发团队,版本迭代时,无论人力成本,还是测试成本都会变大;
  • 内容固定,动态化弱,大多数情况下,有新功能更新时只能发版;

纯原生开发主要面临动态化和开发成本两个问题,而针对这两个问题,诞生了一些跨平台的动态化框架。

1.1.2 跨平台技术简介

针对原生开发面临的问题,业界一直都在努力寻找好的解决方案,而时至今日,已经有很多跨平台框架,根据其原理,主要分为三类:

  • H5 + 原生(Cordova、Ionic、微信小程序)
  • JavaScript 开发 + 原生渲染 (React Native)
  • 自绘UI + 原生 (Qt for mobile、Flutter)

1.2 Hybrid技术简介

1.2.1 H5+原生

这类框架主要原理就是将 App 中需要动态变动的内容通过HTML5(简称 H5)来实现,通过原生的网页加载控件WebView (Android)或 WKWebView(iOS)来加载。我们称这种 H5 + 原生 的开发模式为混合开发 ,采用混合模式开发的App我们称之为混合应用HTMLybrid App ,如果一个应用的大多数功能都是 H5 实现的话,我们称其为 ****Web App

大多数 App 中都会有一些功能是 H5 开发的,至少目前为止,HTMLybrid App 仍然是最通用且最成熟的跨端解决方案。

核心

混合框架一般都会在原生代码中预先实现一些访问系统能力的 API , 然后暴露给 WebView 以供 JavaScript 调用。这样一来,WebView 中 JavaScript 与原生 API 之间就需要一个通信的桥梁,主要负责 JavaScript 与原生之间传递调用消息。这种依赖于 WebView 的用于在 JavaScript 与原生之间通信并实现了某种消息传输协议的工具就是 WebView JavaScript Bridge , 简称 JsBridge,它也是混合开发框架的核心。

优势

  • 动态化强,可以随时改变而不用发版。
  • 开发成本小,开发一次,可同时在 Android 和 iOS 两个平台运行

缺点

  • 性能体验不佳,对于复杂用户界面或动画,WebView 有时会不堪重任
  • WebView实质上是一个浏览器内核,其JavaScript依然运行在一个权限受限的沙箱中,对于大多数系统能力都没有访问权限,如无法访问文件系统、不能使用蓝牙等

1.3 JavaScript开发 + 原生渲染

1.3.1 以React Native为例:

React Native (简称 RN )是 Facebook 于 2015 年 4 月开源的跨平台移动应用开发框架,是 Facebook 早先开源的 Web 框架 React 在原生移动应用平台的衍生产物,目前支持 iOS 和 Android 两个平台。

React React Native 的区别:

主要的区别在于虚拟 DOM 映射的对象是什么。React中虚拟 DOM 最终会映射为浏览器 DOM 树,而 RN 中虚拟 DOM会通过 JavaScriptCore 映射为原生控件。

JavaScriptCore 是一个 JavaScript 解释器 ,它在 React Native 中主要有两个作用:

  1. 为 JavaScript 提供运行环境。
  2. 是 JavaScript 与原生应用之间通信的桥梁,作用和 JsBridge 一样,事实上,在 iOS 中,很多 JsBridge 的实现都是基于 JavaScriptCore 。

RN 中将虚拟 DOM 映射为原生控件的过程主要分两步

  1. 布局消息传递; 将虚拟 DOM 布局信息传递给原生;

  2. 原生根据布局信息通过对应的原生控件渲染;

至此,React Native 便实现了跨平台。 相对于混合应用,由于React Native是 原生控件渲染,所以性能会比混合应用中 H5 好一些,同时 React Native 提供了很多原生组件对应的 Web 组件,大多数情况下开发者只需要使用 Web 技术栈 就能开发出 App。这样也就做到了维护一份代码,便可以跨平台了。

1.3.2 总结:

JavaScript 开发 + 原生渲染 的方式主要优点如下

  1. 采用 Web 开发技术栈,社区庞大、上手快、开发成本相对较低。
  2. 原生渲染,性能相比 H5 提高很多。
  3. 动态化较好,支持热更新。

不足:

  1. 渲染时需要 JavaScript 和原生之间通信,在有些场景如拖动可能会因为通信频繁导致卡顿。
  2. JavaScript 为脚本语言,执行时需要解释执行 (这种执行方式通常称为 JIT,即 Just In Time,指在执行时实时生成机器码),执行效率和编译类语言(编译类语言的执行方式为 AOT ,即 Ahead Of Time,指在代码执行前已经将源码进行了预处理,这种预处理通常情况下是将源码编译为机器码或某种中间码)仍有差距。
  3. 由于渲染依赖原生控件,不同平台的控件需要单独维护,并且当系统更新时,社区控件可能会滞后;除此之外,其控件系统也会受到原生UI系统限制,例如,在 Android 中,手势冲突消歧规则是固定的,这在使用不同人写的控件嵌套时,手势冲突问题将会变得非常棘手。这就会导致,如果需要自定义原生渲染组件时,开发和维护成本过高。

1.4 Flutter

1.4.1 自绘UI + 原生

自绘UI + 原生。这种技术的思路是:通过在不同平台实现一个统一接口的渲染引擎来绘制UI,而不依赖系统原生控件,所以可以做到不同平台UI的一致性。

这种平台技术的优点如下:

  1. 性能高; 由于自绘引擎是直接调用系统 API 来绘制 UI ,所以性能和原生控件接近
  2. 灵活、组件库易维护、 UI 外观保真度和一致性高;由于UI渲染不依赖原生控件,也就不需要根据不同平台的控件单独维护一套组件库,所以代码容易维护。由于组件库是同一套代码、同一个渲染引擎,所以在不同平台,组件显示外观可以做到 高保真 和高一致性;另外,由于不依赖原生控件,也就不会受原生布局系统的限制,这样布局系统会非常灵活。

Flutter 正是实现一套自绘引擎,并拥有一套自己的 UI 布局系统,且同时在开发效率上有了很大突破

1.4.2 Flutter出世

Flutter 是 Google 发布的一个用于创建跨平台、高性能移动应用的框架。没有使用原生控件,相反都实现了一个自绘引擎,使用自身的布局、绘制系统。

主要特点:

  1. 跨平台自绘引擎
  • Flutter 与用于构建移动应用程序的其他大多数框架不同,因为 Flutter 既不使用 WebView,也不使用操作系统的原生控件。

    •   相反,Flutter 使用自己的高性能渲染引擎来绘制 Widget(组件)。这样不仅可以保证在 Android 和iOS 上 UI 的一致性,也可以避免对原生控件依赖而带来的限制及高昂的维护成本。
  • Flutter 底层使用 Skia 作为其 2D 渲染引擎,Skia 是 Google的一个 2D 图形处理函数库,包含字型、坐标转换,以及点阵图,它们都有高效能且简洁的表现。

  • 目前 Flutter 已经支持 iOS、Android、Web、Windows、macOS、Linux等众多平台。

  1. 高性能

Flutter 高性能主要靠两点来保证:

  • 第一:Flutter App 采用 Dart 语言开发。Dart 在 JIT(即时编译)模式下,执行速度与 JavaScript 基本持平。但是 Dart 支持 AOT,当以 AOT模式运行时,JavaScript 便远远追不上了。执行速度的提升对高帧率下的视图数据计算很有帮助。

  • 第二:Flutter 使用自己的渲染引擎来绘制 UI ,布局数据等由 Dart 语言直接控制,所以在布局过程中不需要像 RN 那样要在 JavaScript 和 Native 之间通信,这在一些滑动和拖动的场景下具有明显优势,因为在滑动和拖动过程往往都会引起布局发生变化,所以 JavaScript 需要和 Native 之间不停地同步布局信息,这和在浏览器中JavaScript 频繁操作 DOM 所带来的问题是类似的,都会导致比较可观的性能开销。

1.5 各跨平台技术对比

1.5.1 方案对比

技术类型UI 渲染方式性能开发效率动态化优势不足框架代表
原生原生控件渲染不支持-可访问平台全部功能;-速度快、性能高、可以实现复杂动画及绘制,整体用户体验好;-开发成本高;不 同平台必须维护不 同代码,人力成 本、测试成本大;-内容固定,动态 化弱,有新功能需 要发版,但应用上 架、审核是需要周期的android/Ios
H5 + 原生WebView渲染一般支持-开发成本低,上手快-跨平台、支持动态化-社区生态完善-对于大多数系统 能力都没有访问权 限,需要原生去 做,人力成本较高Cordova、Ionic
JavaScript + 原生渲染原生控件渲染支持-开发成本低,上手快-跨平台、动态化较好,支持热更新-社区生态完善-兼容性好-渲染时需要 JavaScript和原生 之间通信,容易造 成卡顿—JIT执行效率低—渲染依赖原生控 件,不同平台的控 件需要单独维护RN
自绘UI + 原生调用系统API渲染支持-跨平台、跨端-自研渲染引擎,性能高-兼容性好-没有单独的样式文件-学习成本高Flutter

1.5.2 性能对比

技术类型代表性能跨平台内存使用CPU耗电量冷启动
原生android/Ios
H5 + 原生Cordova、Ionic一般
JavaScript + 原生渲染RN
自绘UI + 原生Flutter

参考:Flutter vs React Native vs Native:深度性能比较 - 老孟Flutter - 博客园

2. Flutter框架结构

简单来讲,Flutter 从上到下可以分为三层:框架层、引擎层和嵌入层:

2.1 框架层

Flutter Framework,即框架层。这是一个纯 Dart实现的 SDK,它实现了一套基础库,自底向上:

  • 底下两层(Foundation 和 Animation、Painting、Gestures)在 Google 的一些视频中被合并为一个dart UI层,对应的是Flutter中的dart:ui包,它是 Flutter Engine 暴露的底层UI库,提供动画、手势及绘制能力
  • Rendering 层即渲染层,这一层是一个抽象的布局层,它依赖于 Dart UI 层,渲染层会构建一棵由可渲染对象组成的渲染树, 当动态更新这些对象时,渲染树会找出变化的部分,然后更新渲染。渲染层可以说是Flutter 框架层中最核心的部分,它除了确定每个渲染对象的位置、大小之外还要进行坐标变换、绘制。
  • Widgets 是 Flutter 提供的一套基础组件库, 在基础组件库之上,Flutter 还提供了 Material 和 Cupertino 两种视觉风格的组件库,它们分别实现了 Material 和 iOS 设计规范。

2.2. 引擎层

Engine,即引擎层。毫无疑问是 Flutter 的核心, 该层主要是 C++ 实现,其中包括了 Skia 引擎、Dart 运行时(Dart runtime)、文字排版引擎等。在代码调用 dart:ui库时,调用最终会走到引擎层,然后实现真正的绘制和显示。

2.3 嵌入层

Embedder,即嵌入层。Flutter 最终渲染、交互是要依赖其所在平台的操作系统 API,嵌入层主要是将 Flutter 引擎 ”安装“ 到特定平台上。嵌入层采用了当前平台的语言编写,例如 :

  1. Android 使用的是 Java 和 C++
  2. iOS 和 macOS 使用的是 Objective-C 和 Objective-C++
  3. Windows 和 Linux 使用的是 C++

Flutter 代码可以通过嵌入层,以模块方式集成到现有的应用中,也可以作为应用的主体。Flutter 本身包含了各个常见平台的嵌入层。

  1. Flutter应用模版初识

以“计数器”demo为例子,主要Dart代码是在 lib/main.dart 文件中:

// 计数器
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('You have pushed the button this many times:'),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}// This trailing comma makes auto-formatting nicer for build methods.);}

3.1 模版代码分析

3.1.1 导入包

import 'package:flutter/material.dart';

此行代码作用是导入了 Material UI 组件库。Material 是一种标准的移动端和web端的视觉设计语言, Flutter 默认提供了一套丰富的 Material 风格的UI组件。

3.1.2 应用入口

void main() => runApp(MyApp());

与 C/C++、Java 类似,Flutter 应用中 main 函数为应用程序的入口。main 函数中调用了runApp 方法,它的功能是启动Flutter应用。runApp它接受一个 Widget参数,在本示例中它是一个MyApp对象,MyApp()是 Flutter 应用的根组件。

3.1.3 应用结构

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
       //蓝色主题  
        primarySwatch: Colors.blue,
      ),
      //应用首页路由  
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}
  • MyApp类代表 Flutter 应用,它继承了 StatelessWidget类,这也就意味着应用本身也是一个widget。
  • 在 Flutter 中,大多数东西都是 widget(同“组件”或“部件”),包括对齐(Align)、填充(Padding)、手势处理(GestureDetector)等,它们都是以 widget 的形式提供。
  • Flutter 在构建页面时,会调用组件的build方法,widget 的主要工作是提供一个 build() 方法来描述如何构建 UI 界面(通常是通过组合、拼装其他基础 widget )。
  • MaterialApp 是Material 库中提供的 Flutter APP 框架,通过它可以设置应用的名称、主题、语言、首页及路由列表等。MaterialApp也是一个 widget。
  • home 为 Flutter 应用的首页,它也是一个 widget。

3.2 首页

3.2.1 首页结构

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;
  
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
 ...
}

MyHomePage 是应用的首页,它继承自StatefulWidget类,表示它是一个有状态的组件(Stateful widget)现在我们只需简单认为有状态的组件(Stateful widget) 和无状态的组件(Stateless widget)有两点不同:

  1. Stateful widget 可以拥有状态,这些状态在 widget 生命周期中是可以变的,而 Stateless widget 是不可变的。

  2. Stateful widget 至少由两个类组成:

    1. 一个StatefulWidget类。
    2. 一个 State类; StatefulWidget类本身是不变的,但是State类中持有的状态在 widget 生命周期中可能会发生变化。
  3. _MyHomePageState类是MyHomePage类对应的状态类。

3.2.2 State类

_MyHomePageState 类解析

_MyHomePageState中都包含:

  • 组件的状态。

    •   由于我们只需要维护一个点击次数计数器,所以定义一个_counter状态:
    • int _counter = 0; //用于记录按钮点击的总次数
      
    •   _counter 为保存屏幕右下角带“+”号按钮点击次数的状态。
  • 设置状态的自增函数。

    •   void _incrementCounter() {
          setState(() {
            _counter++;
          });
        }
      
    • 当按钮点击时,会调用此函数,该函数的作用是先自增_counter,然后调用setState 方法。setState方法的作用是通知 Flutter 框架,有状态发生了改变,Flutter 框架收到通知后,会执行 build 方法来根据新的状态重新构建界面, Flutter 对此方法做了优化,使重新执行变的很快,所以你可以重新构建任何需要更新的东西,而无需分别去修改各个 widget。
  • 构建UI界面的build方法

    • Scaffold 是 Material 库中提供的页面脚手架,它提供了默认的导航栏、标题和包含主屏幕 widget 树(后同“组件树”或“部件树”)的body属性,组件树可以很复杂。
    • body的组件树中包含了一个Center 组件,Center 可以将其子组件树对齐到屏幕中心。此例中, Center 子组件是一个Column 组件,Column的作用是将其所有子组件沿屏幕垂直方向依次排列; 此例中Column子组件是两个 Text,第一个Text 显示固定文本 “You have pushed the button this many times:”,第二个Text 显示_counter状态的数值。
    • floatingActionButton是页面右下角的带“+”的悬浮按钮,它的onPressed属性接受一个回调函数,代表它被点击后的处理器,本例中直接将_incrementCounter方法作为其处理函数。
  int _counter = 0;
  
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('You have pushed the button this many times:'),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }

当右下角的floatingActionButton按钮被点击之后,会调用_incrementCounter方法。在_incrementCounter方法中,首先会自增_counter计数器(状态),然后setState会通知 Flutter 框架状态发生变化,接着,Flutter 框架会调用build方法以新的状态重新构建UI,最终显示在设备屏幕上。

3.3 Widget 简介

Flutter中几乎所有的对象都是一个 widget (组件)。与原生开发中“控件”不同的是,Flutter 中的 widget 的概念更广泛,它不仅可以表示UI元素,也可以表示一些功能性的组件如:用于手势检测的 GestureDetector 、用于APP主题数据传递的 Theme 等等。

3.3.1 Flutter中的四棵树

既然 Widget 只是描述一个UI元素的配置信息,Flutter 框架的处理流程是这样的:

  1. Widget树,即Flutter提供的用来布局页面的组件或者说标签。
  2. 根据 Widget 树生成一个 Element 树,Element 树中的节点都继承自 Element 类。
  3. 根据 Element 树生成 Render 树(渲染树),渲染树中的节点都继承自RenderObject 类。
  4. 根据渲染树生成 Layer 树,然后上屏显示,Layer 树中的节点都继承自 Layer 类。

真正的布局和渲染逻辑在 Render 树中,Element 是 Widget 和 Render 的粘合剂,可以理解为一个中间代理。

Container( // 一个容器 widget
  color: Colors.blue, // 设置容器背景色
  child: Row( // 可以将子widget沿水平方向排列
    children: [
        Image.network('https://www.example.com/1.png'), // 显示图片的 widget
        const Text('A'),
    ],
   ),
 );

注意,如果 Container 设置了背景色,Container 内部会创建一个新的 ColoredBox 来填充背景:

if (color != null)
  current = ColoredBox(color: color!, child: current);

而 Image 内部会通过 RawImage 来渲染图片、Text 内部会通过 RichText 来渲染文本,所以最终的 Widget树、Element 树、渲染树结构如图所示:

3.3.2 state生命周期

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

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

state有以下生命周期

  1. initState:当 widget 第一次插入到 widget 树时会被调用,对于每一个State对象,Flutter 框架只会调用一次该回调。

  2. didChangeDependencies() :当State对象的依赖发生变化时会被调用。

  3. build() :它主要是用于构建 widget 树的,会在如下场景被调用:

    1. 在调用initState()之后。
    2. 在调用didUpdateWidget()之后。
    3. 在调用setState()之后。
    4. 在调用didChangeDependencies()之后。
    5. 在State对象从树中一个位置移除后(会调用deactivate)又重新插入到树的其他位置之后。
  4. reassemble() :此回调是专门为了开发调试而提供的,在热重载(hot reload)时会被调用。

  5. didUpdateWidget () :在 widget 重新构建时,Flutter 框架会调用。

  6. deactivate() :当 State 对象从树中被移除时,会调用此回调。

  7. dispose() :当 State 对象从树中被永久移除时调用;通常在此回调中释放资源。

3.4 状态管理

以TapboxA、TapboxB来说明管理状态的不同方式。 这两个例子功能是相似的 ——创建一个盒子,当点击它时,盒子背景会在绿色与灰色之间切换。状态 _active确定颜色:绿色为true ,灰色为false

3.4.1 Widget 管理自己的状态。

// TapboxA 管理自身状态.
//------------------------- TapboxA ----------------------------------
class TapboxA extends StatefulWidget {
  TapboxA({Key? key}) : super(key: key);
  @override
  _TapboxAState createState() => _TapboxAState();
}

class _TapboxAState extends State<TapboxA> {
  bool _active = false;
  
  void _handleTap() {
    setState(() {
      _active = !_active;
    });
  }

  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _handleTap,
      child: Container(
        child: Center(
          child: Text(
            _active ? 'Active' : 'Inactive',
            style: TextStyle(fontSize: 32.0, color: Colors.white),
          ),
        ),
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
          color: _active ? Colors.lightGreen[700] : Colors.grey[600],
        ),
      ),
    );
  }
}

3.4.2 父Widget管理子Widget的状态

// ParentWidget 为 TapboxB 管理状态.

//------------------------ ParentWidget --------------------------------

class ParentWidget extends StatefulWidget {
  @override
  _ParentWidgetState createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
  bool _active = false;

  void _handleTapboxChanged(bool newValue) {
    setState(() {
      _active = newValue;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      child: TapboxB(
        active: _active,
        onChanged: _handleTapboxChanged,
      ),
    );
  }
}

//------------------------- TapboxB ----------------------------------

class TapboxB extends StatelessWidget {
  TapboxB({Key? key, this.active: false, required this.onChanged})
      : super(key: key);

  final bool active;
  final ValueChanged<bool> onChanged;

  void _handleTap() {
    onChanged(!active);
  }

  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _handleTap,
      child: Container(
        child: Center(
          child: Text(
            active ? 'Active' : 'Inactive',
            style: TextStyle(fontSize: 32.0, color: Colors.white),
          ),
        ),
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
          color: active ? Colors.lightGreen[700] : Colors.grey[600],
        ),
      ),
    );
  }
}

3.5 路由管理

3.5.1 注册路由表

路由表的注册方式比较简单,在MyApp类的build方法中找到MaterialApp,添加routes属性,代码如下:

MaterialApp(
    title: 'Shrine',
    initialRoute: '/login',
    routes: {
        '/login': (BuildContext context) => const LoginPage(),
        '/': (BuildContext context) => const HomePage(),
    },
    theme: _kShrineTheme,
    home: MyHomePage(title: 'Flutter Demo Home Page'),
);

3.5.2 通过路由打开新的路由页

要通过路由名称来打开新路由,可以使用NavigatorpushNamed方法,Navigator 除了pushNamed方法,还有pushReplacementNamed等其他管理命名路由的方法。

onPressed: () {
  Navigator.pushNamed(context, "/login");
},

3.5.3 路由参数传递

在打开路由时传递参数

Navigator.of(context).pushNamed("new_page", arguments: "hi");

在路由页通过RouteSetting对象获取路由参数:

class EchoRoute extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    //获取路由参数  
    var args=ModalRoute.of(context).settings.arguments;
    //...省略无关代码
  }
}
  1. 基础组件

4.1 文本及样式

Text 用于显示简单样式文本,它包含一些控制文本显示样式的一些属性,一个简单的例子如下:


Text("Hello world",
  textAlign: TextAlign.left,
);

Text("Hello world! I'm Jack. "*4,
  maxLines: 1,
  overflow: TextOverflow.ellipsis,
);

Text("Hello world",
  textScaleFactor: 1.5,
);
  • textAlign:文本的对齐方式。

  • maxLinesoverflow:指定文本显示的最大行数,默认情况下,文本是自动折行的,如果指定此参数,则文本最多不会超过指定的行。

  • textScaleFactor:代表文本相对于当前字体大小的缩放因子,相对于去设置文本的样式style属性的fontSize,它是调整字体大小的一个快捷方式。

TextStyle用于指定文本显示的样式如颜色、字体、粗细、背景等。

Text("Hello world",
  style: TextStyle(
    color: Colors.blue,
    fontSize: 18.0,
    height: 1.2,  
    fontFamily: "Courier",
    background: Colors.yellow,
    decoration:TextDecoration.underline,
    decorationStyle: TextDecorationStyle.dashed
  ),
);

效果图:

更多属性:

  • height:该属性用于指定行高。
  • fontFamily :由于不同平台默认支持的字体集不同。
  • fontSize:通常用于单个文本,字体大小不会跟随系统字体大小变化。

4.2 按钮

4.2.1 ElevatedButton

ElevatedButton 即"漂浮"按钮,它默认带有阴影和灰色背景。按下后,阴影会变大。

使用ElevatedButton非常简单,如:

ElevatedButton(
  child: Text("normal"),
  onPressed: () {},
);

4.2.2 TextButton

TextButton即文本按钮,默认背景透明并不带阴影。按下后,会有背景色。

使用 TextButton 也很简单,代码如下:

TextButton(
  child: Text("normal"),
  onPressed: () {},
)

4.2.3 OutlinedButton

OutlinedButton默认有一个边框,不带阴影且背景透明。按下后,边框颜色会变亮、同时出现背景和阴影。

使用OutlinedButton也很简单,代码如下:

OutlinedButton(
  child: Text("normal"),
  onPressed: () {},
)

4.2.4 IconButton

IconButton是一个可点击的Icon,不包括文字,默认没有背景,点击后会出现背景。

代码如下:

IconButton(
  icon: Icon(Icons.thumb_up),
  onPressed: () {},
 )

4.3 输入框和表单

4.3.1 TextField

TextField用于文本输入,它提供了很多属性,通过一个示例来演示一下关键属性的用法。

const TextField({
  ...
  TextEditingController controller, 
  FocusNode focusNode,
  InputDecoration decoration = const InputDecoration(),
  TextInputType keyboardType,
  TextInputAction textInputAction,
  TextStyle style,
  TextAlign textAlign = TextAlign.start,
  bool autofocus = false,
  bool obscureText = false,
  int maxLines = 1,
  int maxLength,
  this.maxLengthEnforcement,
  ToolbarOptions? toolbarOptions,
  ValueChanged<String> onChanged,
  this.cursorWidth = 2.0,
  this.cursorRadius,
  this.cursorColor,
  ...
})
  • controller:编辑框的控制器,通过它可以设置/获取编辑框的内容、选择编辑内容、监听编辑文本改变事件。大多数情况下我们都需要显式提供一个controller来与文本框交互。如果没有提供controller,则TextField内部会自动创建一个。
  • focusNode:用于控制TextField是否占有当前键盘的输入焦点。
  • InputDecoration:用于控制TextField的外观显示,如提示文本、背景颜色、边框等。
  • keyboardType:用于设置该输入框默认的键盘输入类型,取值如下:text,multiline,number,phone等
  • style:正在编辑的文本样式。
  • textAlign: 输入框内编辑文本在水平方向的对齐方式。
  • autofocus: 是否自动获取焦点。

.......

4.3.2 Form

Form继承自StatefulWidget对象,它对应的状态类为FormState

Form({
  required Widget child,
  bool autovalidate = false,
  VoidCallback onChanged,
})
  • autovalidate:是否自动校验输入内容;当为true时,每一个子 FormField 内容发生变化时都会自动校验合法性,并直接显示错误信息。
  • onChangedForm的任意一个子FormField内容发生变化时会触发此回调。
1. FormField

Form的子孙元素必须是FormField类型,FormField是一个抽象类,定义几个属性,FormState内部通过它们来完成操作,FormField部分定义如下:

const FormField({
  ...
  FormFieldSetter<T> onSaved, //保存回调
  FormFieldValidator<T>  validator, //验证回调
  T initialValue, //初始值
  bool autovalidate = false, //是否自动校验。
})
2. FormState

FormStateFormState类,可以通过Form.of()GlobalKey获得。我们可以通过它来对Form的子孙FormField进行统一操作。我们看看其常用的三个方法:

  • FormState.validate():调用此方法后,会调用Form子孙FormField的validate回调,如果有一个校验失败,则返回false,所有校验失败项都会返回用户返回的错误提示。
  • FormState.save():调用此方法后,会调用Form子孙FormFieldsave回调,用于保存表单内容
  • FormState.reset():调用此方法后,会将子孙FormField的内容清空。

4.4 图片

4.4.1 从asset中加载图片

  1. 在工程根目录下创建一个images目录,并将图片 avatar.png 拷贝到该目录。
  2. pubspec.yaml中的flutter部分添加如下内容:
  assets:
      - images/avatar.png
  1. 加载该图片
Image(
  image: AssetImage("images/avatar.png"),
      width: 100.0
  );

Image也提供了一个快捷的构造函数Image.asset用于从asset中加载、显示图片:

Image.asset("images/avatar.png",
  width: 100.0,
 )

4.4.2 从网络加载图片

NetworkImage 可以加载网络图片,例如:

Image(
  image: NetworkImage("https://avatars2.githubusercontent.com/u/20411648?s=460&v=4"),
  width: 100.0,
  )

Image也提供了一个快捷的构造函数Image.network用于从网络加载、显示图片:

Image.network("https://avatars2.githubusercontent.com/u/20411648?s=460&v=4",
  width: 100.0,
  )

4.4.3 参数

Image在显示图片时定义了一系列参数,通过这些参数我们可以控制图片的显示外观、大小、混合效果等。我们看一下 Image 的主要参数:

const Image({
  ...
  this.width, //图片的宽
  this.height, //图片高度
  this.color, //图片的混合色值
  this.colorBlendMode, //混合模式
  this.fit,//缩放模式
  this.alignment = Alignment.center, //对齐方式
  this.repeat = ImageRepeat.noRepeat, //重复方式
  ...
})
Image(
  image: AssetImage("images/avatar.png"),
  width: 100.0,
  color: Colors.blue,
  colorBlendMode: BlendMode.difference,
 );
  1. 布局组件

5.1 线性布局(Row和Column)

所谓线性布局,即指沿水平或垂直方向排列子组件。Flutter 中通过RowColumn来实现线性布局,RowColumn都继承自Flex

5.1.1 主轴和纵轴

对于线性布局,有主轴和纵轴之分,在线性布局中,有两个定义对齐方式的枚举类MainAxisAlignmentCrossAxisAlignment,分别代表主轴对齐和纵轴对齐。

5.1.2 Row

Row可以沿水平方向排列其子widget。定义如下:

Row({
  ...  
  TextDirection textDirection,    
  MainAxisSize mainAxisSize = MainAxisSize.max,    
  MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
  VerticalDirection verticalDirection = VerticalDirection.down,  
  CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
  List<Widget> children = const <Widget>[],
})
  • textDirection 表示水平方向子组件的布局顺序(是从左往右还是从右往左)。

  • mainAxisSize:表示Row在主轴(水平)方向占用的空间。

    •   默认是MainAxisSize.max,表示尽可能多的占用水平方向的空间,此时无论子 widgets 实际占用多少水平空间,Row的宽度始终等于水平方向的最大宽度;
    •   而MainAxisSize.min表示尽可能少的占用水平空间,当子组件没有占满水平剩余空间,则Row的实际宽度等于所有子组件占用的水平空间;
  • mainAxisAlignment

    •   MainAxisAlignment.start表示沿textDirection的初始方向对齐
    •   MainAxisAlignment.end后方对齐
    •   MainAxisAlignment.center表示居中对齐
  • verticalDirection:表示Row纵轴(垂直)的对齐方向,默认是VerticalDirection.down,表示从上到下。

  • crossAxisAlignment:表示子组件在纵轴方向的对齐方式。

  • children :子组件数组

示例:

    Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Text(" hello world "),
        Text(" I am Jack "),
      ],
    ),
    Row(
      mainAxisSize: MainAxisSize.min,
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Text(" hello world "),
        Text(" I am Jack "),
      ],
    ),

5.1.3 Column

Column可以在垂直方向排列其子组件。参数和Row一样,不同的是布局方向为垂直,主轴纵轴正好相反:

Column(
    crossAxisAlignment: CrossAxisAlignment.center,
    children: <Widget>[
        Text("hi"),
        Text("world"),
    ],
 );

5.2 层叠布局

层叠布局和 Web 中的绝对定位、Android 中的 Frame 布局是相似的,子组件可以根据距父容器四个角的位置来确定自身的位置。层叠布局允许子组件按照代码中声明的顺序堆叠起来。Flutter中使用StackPositioned这两个组件来配合实现绝对定位。Stack允许子组件堆叠,而Positioned用于根据Stack的四个角来确定子组件的位置。

5.2.1 Stack

Stack组件定义如下:

Stack({
  this.alignment = AlignmentDirectional.topStart,
  this.textDirection,
  this.fit = StackFit.loose,
  List<Widget> children = const <Widget>[],
})
  • alignment:此参数决定如何去对齐没有定位(没有使用Positioned)或部分定位的子组件。所谓部分定位,在这里特指没有在某一个轴上定位:leftright为横轴,topbottom为纵轴,只要包含某个轴上的一个定位属性就算在该轴上有定位。
  • textDirection:用于确定alignment对齐的参考系,即:textDirection的值为TextDirection.ltr,则alignmentstart代表左,end代表右,即从左往右的顺序;textDirection的值为TextDirection.rtl,则alignment的start代表右,end代表左,即从右往左的顺序。
  • fit:此参数用于确定没有定位的子组件如何去适应Stack的大小。StackFit.loose表示使用子组件的大小,StackFit.expand表示扩伸到Stack的大小。

5.2.2 Positioned

Positioned 的默认构造函数如下:

const Positioned({
  Key? key,
  this.left, 
  this.top,
  this.right,
  this.bottom,
  this.width,
  this.height,
  required Widget child,
})

lefttoprightbottom分别代表离Stack左、上、右、底四边的距离。widthheight用于指定需要定位元素的宽度和高度。

示例:

Stack(
    alignment:Alignment.center , //指定未定位或部分定位widget的对齐方式
    children: <Widget>[
      Container(
        child: Text("Hello world",style: TextStyle(color: Colors.white)),
        color: Colors.red,
      ),
      Positioned(
        left: 18.0,
        child: Text("I am Jack"),
      ),
      Positioned(
        top: 18.0,
        child: Text("Your friend"),
      )        
    ],
  ),

  1. 网络&http

Flutter直接使用HttpClient发起网络请求是比较麻烦的,很多事情得我们手动处理,如果再涉及到文件上传/下载、Cookie管理等就会非常繁琐。幸运的是,Dart社区有一些第三方http请求库,用它们来发起http请求将会简单的多,接下来主要使用dio 库。

dio是开发者维护的一个强大的Dart Http请求库,支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时等。

6.1 Http请求库-dio

6.1.1 引入dio

dependencies:
    dio: ^5.3.2 

导入并创建dio实例:

import 'package:dio/dio.dart';
Dio dio =  Dio();

接下来就可以通过 dio实例来发起网络请求了,注意,一个dio实例可以发起多个http请求,一般来说,APP只有一个http数据源时,dio应该使用单例模式。

6.1.2 通过dio发起请求

发起 GET 请求 :

response=await dio.get("/test?id=12&name=wendu")
print(response);

对于GET请求我们可以将query参数通过对象来传递,上面的代码等同于:

response=await dio.get("/test",queryParameters:{"id":12,"name":"wendu"})
print(response);

发起一个 POST 请求:

response=await dio.post("/test",data:{"id":12,"name":"wendu"})

发起多个并发请求:

response= await Future.wait([dio.post("/info"),dio.get("/token")]);

下载文件:

response=await dio.download("https://www.google.com/",_savePath);

发送 FormData:

FormData formData = FormData.from({
    "name": "wendux",
    "age": 25,
});
response = await dio.post("/info", data: formData)

6.1.3 例子

  1. 在请求阶段弹出loading
  2. 请求结束后,如果请求失败,则展示错误信息;如果成功,则将项目名称列表展示出来。
class _FutureBuilderRouteState extends State<FutureBuilderRoute> {
  Dio _dio = Dio();

  @override
  Widget build(BuildContext context) {

    return Container(
      alignment: Alignment.center,
      child: FutureBuilder(
          future: _dio.get("https://api.github.com/orgs/flutterchina/repos"),
          builder: (BuildContext context, AsyncSnapshot snapshot) {
            //请求完成
            if (snapshot.connectionState == ConnectionState.done) {
              Response response = snapshot.data;
              //发生错误
              if (snapshot.hasError) {
                return Text(snapshot.error.toString());
              }
              //请求成功,通过项目信息构建用于显示项目名称的ListView
              return ListView(
                children: response.data.map<Widget>((e) =>
                    ListTile(title: Text(e["full_name"]))
                ).toList(),
              );
            }
            //请求未完成时弹出loading
            return CircularProgressIndicator();
          }
      ),
    );
  }
}

FutureBuilder 是 Flutter 中的一个小部件(Widget),用于在异步操作完成后构建 UI。它是一个非常有用的工具,用于处理异步任务的结果,并根据不同的状态显示不同的 UI。

FutureBuilder 接收一个 Future 对象作为参数,并根据 Future 对象的执行状态来构建 UI。当 Future 对象处于不同的状态时,FutureBuilder 会根据指定的构建函数构建相应的 UI。

参数说明:

  • future:要监视的 Future 对象,通常是一个异步操作。
  • builder:一个回调函数,用于根据不同的 Future 状态构建 UI。它接收一个 BuildContextAsyncSnapshot<T> 对象作为参数,返回一个小部件(Widget)。

根据 AsyncSnapshot<T> 的状态,可以在 builder 函数中构建不同的 UI,例如显示加载状态、错误状态或成功状态的 UI。

总结

  1. Flutter 的主打特点就是“一次编写,处处运行”。通过 Flutter,可以用一套代码构建 iOS 和 Android 应用,这大大提高了开发效率。此外,Flutter 对原生功能的支持也十分强大,可以调用各种设备的 API,如 GPS、摄像头、传感器等,使得开发的应用功能丰富,用户体验良好。
  2. 在 Flutter 中,一切都是 Widget,另外,Flutter 的丰富的 Widget 库能够轻松地构建各种复杂的界面和动画而不必担心性能的开销。
  3. Flutter 的生态系统相当成熟。可以在 pub.dev 找到各种丰富的包,这些包提供了很多实用的功能,如网络请求、图片处理、本地存储等。此外,Flutter 的社区也十分活跃,可以在 StackOverflow、GitHub 等平台上找到很多有用的资源和解决问题的方法。
  4. 学习成本相较于RN高。需要学习一门新的Dart语言,掌握Flutter的声明式UI方法,以及各种Widget的特性,之前没有运用的企业尝试运用Flutter开发项目还需要一个探索的过程,比如基建以及和其他部门比如Android、Ios团队的协调。

参考文档

给 Web 开发者的 Flutter 指南

Flutter 开发文档

第二版序 | 《Flutter实战·第二版》

Flutter vs React Native vs Native:深度性能比较 - 老孟Flutter - 博客园

Dart 编程语言概览

国内大厂应用在移动端 Flutter 框架使用分析 - 掘金