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 中主要有两个作用:
- 为 JavaScript 提供运行环境。
- 是 JavaScript 与原生应用之间通信的桥梁,作用和 JsBridge 一样,事实上,在 iOS 中,很多 JsBridge 的实现都是基于 JavaScriptCore 。
而 RN 中将虚拟 DOM 映射为原生控件的过程主要分两步:
-
布局消息传递; 将虚拟 DOM 布局信息传递给原生;
-
原生根据布局信息通过对应的原生控件渲染;
至此,React Native 便实现了跨平台。 相对于混合应用,由于React Native是 原生控件渲染,所以性能会比混合应用中 H5 好一些,同时 React Native 提供了很多原生组件对应的 Web 组件,大多数情况下开发者只需要使用 Web 技术栈 就能开发出 App。这样也就做到了维护一份代码,便可以跨平台了。
1.3.2 总结:
JavaScript 开发 + 原生渲染 的方式主要优点如下:
- 采用 Web 开发技术栈,社区庞大、上手快、开发成本相对较低。
- 原生渲染,性能相比 H5 提高很多。
- 动态化较好,支持热更新。
不足:
- 渲染时需要 JavaScript 和原生之间通信,在有些场景如拖动可能会因为通信频繁导致卡顿。
- JavaScript 为脚本语言,执行时需要解释执行 (这种执行方式通常称为 JIT,即 Just In Time,指在执行时实时生成机器码),执行效率和编译类语言(编译类语言的执行方式为 AOT ,即 Ahead Of Time,指在代码执行前已经将源码进行了预处理,这种预处理通常情况下是将源码编译为机器码或某种中间码)仍有差距。
- 由于渲染依赖原生控件,不同平台的控件需要单独维护,并且当系统更新时,社区控件可能会滞后;除此之外,其控件系统也会受到原生UI系统限制,例如,在 Android 中,手势冲突消歧规则是固定的,这在使用不同人写的控件嵌套时,手势冲突问题将会变得非常棘手。这就会导致,如果需要自定义原生渲染组件时,开发和维护成本过高。
1.4 Flutter
1.4.1 自绘UI + 原生
自绘UI + 原生。这种技术的思路是:通过在不同平台实现一个统一接口的渲染引擎来绘制UI,而不依赖系统原生控件,所以可以做到不同平台UI的一致性。
这种平台技术的优点如下:
- 性能高; 由于自绘引擎是直接调用系统 API 来绘制 UI ,所以性能和原生控件接近。
- 灵活、组件库易维护、 UI 外观保真度和一致性高;由于UI渲染不依赖原生控件,也就不需要根据不同平台的控件单独维护一套组件库,所以代码容易维护。由于组件库是同一套代码、同一个渲染引擎,所以在不同平台,组件显示外观可以做到 高保真 和高一致性;另外,由于不依赖原生控件,也就不会受原生布局系统的限制,这样布局系统会非常灵活。
Flutter 正是实现一套自绘引擎,并拥有一套自己的 UI 布局系统,且同时在开发效率上有了很大突破
1.4.2 Flutter出世
Flutter 是 Google 发布的一个用于创建跨平台、高性能移动应用的框架。没有使用原生控件,相反都实现了一个自绘引擎,使用自身的布局、绘制系统。
主要特点:
-
跨平台自绘引擎
-
Flutter 与用于构建移动应用程序的其他大多数框架不同,因为 Flutter 既不使用 WebView,也不使用操作系统的原生控件。
- 相反,Flutter 使用自己的高性能渲染引擎来绘制 Widget(组件)。这样不仅可以保证在 Android 和iOS 上 UI 的一致性,也可以避免对原生控件依赖而带来的限制及高昂的维护成本。
-
Flutter 底层使用 Skia 作为其 2D 渲染引擎,Skia 是 Google的一个 2D 图形处理函数库,包含字型、坐标转换,以及点阵图,它们都有高效能且简洁的表现。
-
目前 Flutter 已经支持 iOS、Android、Web、Windows、macOS、Linux等众多平台。
-
高性能
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 引擎 ”安装“ 到特定平台上。嵌入层采用了当前平台的语言编写,例如 :
- Android 使用的是 Java 和 C++
- iOS 和 macOS 使用的是 Objective-C 和 Objective-C++
- Windows 和 Linux 使用的是 C++
Flutter 代码可以通过嵌入层,以模块方式集成到现有的应用中,也可以作为应用的主体。Flutter 本身包含了各个常见平台的嵌入层。
-
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)有两点不同:
-
Stateful widget 可以拥有状态,这些状态在 widget 生命周期中是可以变的,而 Stateless widget 是不可变的。
-
Stateful widget 至少由两个类组成:
- 一个
StatefulWidget类。 - 一个
State类;StatefulWidget类本身是不变的,但是State类中持有的状态在 widget 生命周期中可能会发生变化。
- 一个
-
_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 框架的处理流程是这样的:
- Widget树,即Flutter提供的用来布局页面的组件或者说标签。
- 根据 Widget 树生成一个 Element 树,Element 树中的节点都继承自
Element类。 - 根据 Element 树生成 Render 树(渲染树),渲染树中的节点都继承自
RenderObject类。 - 根据渲染树生成 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 中的保存的状态信息可以:
- 在 widget 构建时可以被同步读取。
- 在 widget 生命周期中可以被改变,当State被改变时,可以手动调用其
setState()方法通知Flutter 框架状态发生改变,Flutter 框架在收到消息后,会重新调用其build方法重新构建 widget 树,从而达到更新UI的目的。
state有以下生命周期:
-
initState:当 widget 第一次插入到 widget 树时会被调用,对于每一个State对象,Flutter 框架只会调用一次该回调。 -
didChangeDependencies():当State对象的依赖发生变化时会被调用。 -
build():它主要是用于构建 widget 树的,会在如下场景被调用:- 在调用
initState()之后。 - 在调用
didUpdateWidget()之后。 - 在调用
setState()之后。 - 在调用
didChangeDependencies()之后。 - 在State对象从树中一个位置移除后(会调用deactivate)又重新插入到树的其他位置之后。
- 在调用
-
reassemble():此回调是专门为了开发调试而提供的,在热重载(hot reload)时会被调用。 -
didUpdateWidget ():在 widget 重新构建时,Flutter 框架会调用。 -
deactivate():当 State 对象从树中被移除时,会调用此回调。 -
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 通过路由打开新的路由页
要通过路由名称来打开新路由,可以使用Navigator 的pushNamed方法,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;
//...省略无关代码
}
}
-
基础组件
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:文本的对齐方式。 -
maxLines、overflow:指定文本显示的最大行数,默认情况下,文本是自动折行的,如果指定此参数,则文本最多不会超过指定的行。 -
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 内容发生变化时都会自动校验合法性,并直接显示错误信息。onChanged:Form的任意一个子FormField内容发生变化时会触发此回调。
1. FormField
Form的子孙元素必须是FormField类型,FormField是一个抽象类,定义几个属性,FormState内部通过它们来完成操作,FormField部分定义如下:
const FormField({
...
FormFieldSetter<T> onSaved, //保存回调
FormFieldValidator<T> validator, //验证回调
T initialValue, //初始值
bool autovalidate = false, //是否自动校验。
})
2. FormState
FormState为Form的State类,可以通过Form.of()或GlobalKey获得。我们可以通过它来对Form的子孙FormField进行统一操作。我们看看其常用的三个方法:
FormState.validate():调用此方法后,会调用Form子孙FormField的validate回调,如果有一个校验失败,则返回false,所有校验失败项都会返回用户返回的错误提示。FormState.save():调用此方法后,会调用Form子孙FormField的save回调,用于保存表单内容FormState.reset():调用此方法后,会将子孙FormField的内容清空。
4.4 图片
4.4.1 从asset中加载图片
- 在工程根目录下创建一个
images目录,并将图片 avatar.png 拷贝到该目录。 - 在
pubspec.yaml中的flutter部分添加如下内容:
assets:
- images/avatar.png
- 加载该图片
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,
);
-
布局组件
5.1 线性布局(Row和Column)
所谓线性布局,即指沿水平或垂直方向排列子组件。Flutter 中通过Row和Column来实现线性布局,Row和Column都继承自Flex。
5.1.1 主轴和纵轴
对于线性布局,有主轴和纵轴之分,在线性布局中,有两个定义对齐方式的枚举类MainAxisAlignment和CrossAxisAlignment,分别代表主轴对齐和纵轴对齐。
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中使用Stack和Positioned这两个组件来配合实现绝对定位。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)或部分定位的子组件。所谓部分定位,在这里特指没有在某一个轴上定位:left、right为横轴,top、bottom为纵轴,只要包含某个轴上的一个定位属性就算在该轴上有定位。textDirection:用于确定alignment对齐的参考系,即:textDirection的值为TextDirection.ltr,则alignment的start代表左,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,
})
left、top 、right、 bottom分别代表离Stack左、上、右、底四边的距离。width和height用于指定需要定位元素的宽度和高度。
示例:
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"),
)
],
),
-
网络&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 例子
- 在请求阶段弹出loading
- 请求结束后,如果请求失败,则展示错误信息;如果成功,则将项目名称列表展示出来。
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。它接收一个BuildContext和AsyncSnapshot<T>对象作为参数,返回一个小部件(Widget)。
根据 AsyncSnapshot<T> 的状态,可以在 builder 函数中构建不同的 UI,例如显示加载状态、错误状态或成功状态的 UI。
总结
- Flutter 的主打特点就是“一次编写,处处运行”。通过 Flutter,可以用一套代码构建 iOS 和 Android 应用,这大大提高了开发效率。此外,Flutter 对原生功能的支持也十分强大,可以调用各种设备的 API,如 GPS、摄像头、传感器等,使得开发的应用功能丰富,用户体验良好。
- 在 Flutter 中,一切都是 Widget,另外,Flutter 的丰富的 Widget 库能够轻松地构建各种复杂的界面和动画而不必担心性能的开销。
- Flutter 的生态系统相当成熟。可以在 pub.dev 找到各种丰富的包,这些包提供了很多实用的功能,如网络请求、图片处理、本地存储等。此外,Flutter 的社区也十分活跃,可以在 StackOverflow、GitHub 等平台上找到很多有用的资源和解决问题的方法。
- 学习成本相较于RN高。需要学习一门新的Dart语言,掌握Flutter的声明式UI方法,以及各种Widget的特性,之前没有运用的企业尝试运用Flutter开发项目还需要一个探索的过程,比如基建以及和其他部门比如Android、Ios团队的协调。