1.路由与导航
在lib下,新建pages目录,用来存放页面
- 首页-Home.dart
- 学习-Study.dart
- 我-Mine.dart ```dart import ‘package:flutter/material.dart’; import ‘home/Home.dart’; import ‘study/Study.dart’; import ‘mine/Mine.dart’;
class Index extends StatefulWidget { Index({Key key}) : super(key: key);
@override _IndexState createState() => _IndexState(); }
class _IndexState extends State
final List
final List pages = [ { “appBar”: AppBar( title: Text(“拉勾教育”), centerTitle: true, elevation: 0, ), “page”: Home(), }, { “appBar”: AppBar( title: Text(“学习中心”), centerTitle: true, elevation: 0, ), “page”: Study(), }, { “appBar”: AppBar( title: Text(“个人中心”), centerTitle: true, elevation: 0, ), “page”: Mine(), }, ];
int currentIndex = 0;
@override Widget build(BuildContext context) { return Scaffold( appBar: pages[currentIndex][‘appBar’], bottomNavigationBar: BottomNavigationBar( items: bottomNavItems, currentIndex: currentIndex, selectedItemColor: Colors.green, type: BottomNavigationBarType.fixed, onTap: (index) { _changePage(index); } }, ), body: pages[currentIndex][‘page’], ); }
void _changePage(int index) { if (index != currentIndex) { setState(() { currentIndex = index; }); } }
@override void dispose() { super.dispose(); } }
**Fluro路由**<br />首页完成后,我们需要点击课程列表,跳转到课程详情页。并且,需要动态传递参数,来动态获取详情页的内容。此时,我们需要声明详情页的路由。之前我们学过 Flutter 内置的路由方案(Navigator)。 <br />这里我们介绍一款企业级的路由框架 - Fluro
- 安装
```yaml
dependencies:
fluro: ^1.7.8
- 声明路由处理器
我们将所有路由文件,统一放到lib/routes中
lib routes RoutesHandler.dart 路由处理器 Routes.dart 路由
创建lib/routes/RoutesHandler.dart
import 'package:flutter/material.dart';
import 'package:fluro/fluro.dart';
import '../pages/Index.dart';
import '../pages/notfound/NotFound.dart';
import '../pages/mine/ProviderTest.dart';
import '../pages/course/CourseDetail.dart';
import '../pages/mine/Profile.dart';
/// 空页面
var notfoundHandler = Handler(
handlerFunc: (BuildContext context, Map<String, List<String>> params) {
return NotFound();
}
);
/// 首页
var indexHandler = Handler(
handlerFunc: (BuildContext context, Map<String, List<String>> params) {
return Index();
}
);
// 个人中心页面
var mineHandler = new Handler(
handlerFunc: (BuildContext context, Map<String, List<Object>> params) {
return Mine();
}
);
// 学习页面
var studyHandler = new Handler(
handlerFunc: (BuildContext context, Map<String, List<Object>> params) {
return Study();
}
);
- 声明路由
创建lib/routes/Routes.dart
import 'package:flutter/material.dart';
import 'package:fluro/fluro.dart';
import 'RouteHandler.dart';
class Routes {
static void configureRoutes(FluroRouter router) {
router.define('/', handler: indexHandler);
router.define('/course_detail', handler: courseDetailHandler);
router.define('/mine', handler: mineHandler);
router.define('/study', handler: studyHandler);
router.notFoundHandler = unknownHandler; // 未知页面
}
}
然后把路由相关的内容,也放到 lib/utils/Global.dart 中
import 'package:fluro/fluro.dart';
class G {
/// Fluro路由
static FluroRouter router;
}
在入口文件(lib/main.dart)中初始化 router
import 'package:fluro/fluro.dart';
import 'routes/Routes.dart';
import 'utils/global.dart';
//final MyRouter router = MyRouter();
void main() {
FluroRouter router = FluroRouter();
Routes.configureRoutes(router);
G.router = router;
// 初始化全局中的 router
/// ...
}
- 使用路由
首页跳转到详情页
/// course 是文章详情
Map<String, dynamic> p = {
'id': course['id'],
'title': course['courseName'],
};
// print("/course_detail?id=123&title=课程名称");
G.router.navigateTo(context, "/course_detail"+G.parseQuery(params: p));
上述代码中的parseQuery,是将Map类型,转成URL中query字符串,代码详情:
// lib/utils/Global.dart
import 'package:flutter/material.dart';
class G {
/// 将请求参数,由 Map 解析成 query
static parseQuery({Map<String, dynamic> params}) {
String query = "";
if (params != null) {
int index = 0;
for (String key in params.keys) {
final String value = Uri.encodeComponent(params[key].toString());
if (index == 0) {
query = "?";
} else {
query = query + "\&";
}
query += "$key=$value";
index++;
}
}
return query.toString();
}
}
2.状态管理
Provider
- 安装Provider
https://pub.dev/packages/provider
- 创建数据模型 ```dart import ‘package:flutter/material.dart’;
class currentIndexProvider with ChangeNotifier { int currentIndex = 0;
changeIndex(int index) { currentIndex = index; notifyListeners(); } }
- 注册数据模型
- 注册单个数据模型
```dart
ChangeNotifierProvider(
create: (BuildContext context) => new UserProvider(),
child: MyApp()
)
- 注册多个数据模型
MultiProvider(
providers: [
changNotifierProvider.value(value: CurrentIndexProvider()),
changNotifierProvider.value(value: UserProvider()),
],
child: MyApp()
)
- 在具体组件中使用Provider中的数据
访问Provider时,有两种方式i:监听和取消监听
监听
- 监听方法只能用来[StatelessWidget.build]和[State.build]中使用,监听值发生变化时,会重建组件
Provider.of<T>(context)
// 语法糖
context.watch<T>(context)
- 监听方法只能用来[StatelessWidget.build]和[State.build]中使用,监听值发生变化时,会重建组件
取消监听
- 取消监听,不能在[StatelessWidget.build]和[State.build]中使用,换句话说,它可以在上述两个方法之外的所有方法中使用。监听值发生变化时,不会重建组件。
访问数据Provider.of<T>(context, listen: false)
// 语法糖 context
context.read<T>
访问方法Provider.of<CurrentIndexProvider>(context).currentIndex;
// 取消监听
Provider.of<CurrentIndexProvider>(context, listen: false).changeIndex(index);
3.数据接口
Dio
安装Dio: https://pub.dev/packages/dio
如果是使用http协议的接口地址,那么会产生报错,因为平台不支持不安全的HTTP协议,即不允许访问HTTP协议的地址。
- 取消监听,不能在[StatelessWidget.build]和[State.build]中使用,换句话说,它可以在上述两个方法之外的所有方法中使用。监听值发生变化时,不会重建组件。
- Andriod解决
- 打开andriod/app/src/main/AndriodManifest.xml
```dart
- 打开andriod/app/src/main/AndriodManifest.xml
```dart
- IOS解决
- 打开ios/Runner/Info.plist。添加如下代码
```dart
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
Dio 手册: https://github.com/flutterchina/dio/blob/master/README-ZH.md
初始化Dio
我们把所有接口操作相关的代码都集中放到api目录下。例如
lib api initDio.dart 初始化Dio AdAPI.dart 广告API UserAPI.dart 用户API API.dart 所有API
接下来,我们来创建具体的文件。首先,创建lib/api/initDio.dart
Dio initDio() {
BaseOptions _baseOptions = BaseOptions(
baseUrl: 'http://xxx.com'
)
// 初始化
Dio dio = Dio(_baseOptions);
// 添加请求拦截
dio.interceptors.add(
Interceptorswrapper(
// 请求拦截
onRequest: (RequestOptions options) async {
// 在请求被发送之前做的一些事情
// 将access_token封装到header中,注意这里时dio文件因此不存在构建上下文,
// 因此我们需要去获取,这里我们是在全局变量中定义了getCurrentContext()方法
var user = G.getCurrentContext().read<UserProvider>().user;
if (user.isNotEmpty) {
optiosn.headers['Authorizatoin'] = user['access_token'];
}
return options;
},
// 响应拦截
onResponse: (Resoponse response) async {
// 在返回响应数据之前做一些预处理
if (response.data['state'] != 1) {
print('响应失败' + response.data['message']);
response.data = null;
}
return response;
},
// 请求错误处理
onError: (DioError e) async {
return e;
}
)
)
}
如何在Flutter的任意位置获取构建上下文? 我们可以声明路由的全局唯一键(navigatorKey),然后,通过navigatorKey来获取构建上下文 在lib/utils/Global.dart中声明全局key
class G {
// 导入唯一key
static final Globalkey<NavigatorState> navigatorKey = Globalkey();
// 获取构建上下文
static BuildContext getCurrentContext() =>
navigatorkey.curentContext;
}
在materialApp中注册navigatorKey
MaterialApp(
navigatorKey: G.navigatorKey
)
然后调用G.getCurrentContext()就可以取到上下文
使用Dio
获取首页广告列表,创建lib/api/AdAPI.dart
import 'package:dio/dio.dart';
class AdAPI {
final Dio _dio;
AdAPI(THIS._dio);
// 广告列表
Future<dynamic> adList({
String xxx = '999'
}) async {
Response res = await _dio.get('/xxx/xxx',
queryParameters: {
"xxx": xxx
}
);
List adList = res.data;
return adList;
}
}
创建lib/api/API.dart
import 'package:dio/dio.dart';
import 'initDio.dart';
import 'AdAPI.dart';
class API {
Dio _dio;
API() {
// 初始化dio
_dio = initDio();
}
// 广告接口
AdAPI get ad => AdAPI(_dio);
// 课程接口
// CourseAPI get course => CourseAPI(_dio);
}
为了操作方便,我们可以把常用内容统一放到一个全局文件中
列如,创建lib/utils/Global.dart。然后,把我们写好的接口放到Global.dart中
import 'package:flutter/material.dart';
import '../api/API.dart';
class G {
// 初始化API
static final API api = API();
}
在首页中调用接口adList()
import '../api/API.dart';
List adList = [];
@override
void initState() {
super.initState();
// 广告列表
G.api.ad.adList().then(value) {
setState() {
adList = value.where((ad) => ad['status'] == 1).toList();
}
}
}
首页
首页包含两部分内容:
- 广告轮播
数据接口已经完成。想要展示轮播的话需要使用flutter_swiper插件。
- 课程列表
创建lib/CourseAPI.dart。创建方式与AdAPI.dart一致,下面给出关键代码
// 课程列表
Future<dynamic> courseList() async {
Response res = await _dio.get('/front/course/getAllCourse');
List target = res.data['content'];
return target;
}
如果是初次创建lib/api/CourseAPI.dart,需要在lib/api/API.dart中,添加对应的gettter。
import 'package:dio/dio.dart';
import 'initDio.dart';
class API {
Dio _dio;
API() {
_dio = initDio();
}
CourseAPI get course => courseAPI(_dio);
}
调用接口
List courseList = [];
@override
void initState() {
super.initState();
// 课程列表
G.api.course.courseList().then(value) {
setState(() {
courseList = value;
})
}
}
展示数据
// 展示数据的组件有多种,列如:ListView, GridView。我们这里使用SliverList:
SliverList(
delegate: SliverChildBuilderDelegate(BuildContext context, int index) {
var course = courseList[index];
// 创建列表想
return GestureDetector(
onTap: () {},
child: 具体组件
)
}
);
4.屏幕适配
除了内容展示之外还有屏幕适配的问题。例如,我的应用可能在手机打开,也可能在pad上打开。终端屏幕尺寸大小不易,如何进行屏幕适配呢?
适配原理
- 设计尺寸(初始化指定,一般是设计师出图的尺寸,是可以预先知道的)
- designWidth: 750px
- designHeight: 1334px
- 终端尺寸(动态获取)
- deviceWidth: 1080px
- deviceHeight: 1920px
- 缩放比例
- scaleWidth = deviceWidth / designWidth
- scalHeight = deviceHeight / designHeight
明确了缩放比例后,我们可以适配终端了。
例如终端宽度是1080px,此时,如果声明50%的宽度,可以写成375.w(因为设计尺寸的宽度是750px,所以50%的宽度是375),375.w会根据缩放比例,计算处实际终端的宽度。计算公式为
实际宽度 50% = 375 _scaleWidth = 375 _(1080 / 750) = 540
flutter_screenutil
flutter_screenutil是用来解决屏幕适配的包。
flutter_screenutil的工作原理是:在具体设备上,把原型图的尺寸,等比例放大或缩小
具体用法:
初始化设计尺寸
ScreenUtilInit(
designSize: Size(750, 1334), // 初始化设计尺寸
allowFontScaling: false, // 字体大学奥是否跟随终端(这个有查阅文档,最新版本好像把这个属性删除了)
builder: () => MaterialApp(
title: 'Flutter_demo',
onGenerateRoute: G.router.generator,
initialRoute: '/'
)
);
在实际使用过程中,以Flutter1.2为分割线,有两种不同的写法
Flutter1.2之前
width: ScreenUtil().setWidth(50);
height: ScreenUtil().setHeight(200);
Flutter1.2之后
width: 50.w;
height: 200.h;
5.富文本展示
课程详情是一些HTML代码,但HTML不能直接在Flutter中展示。因此我们需要将HTML代码,转换成Flutter支持的Dart代码。这里我们借助flutter_html来完成课程详情的展示
flutter_html
使用步骤:
- 安装
在pubspec.yaml中设置依赖
dependencies:
flutter_html: ^1.3.0
安装依赖
VS Code中,保存pubspec.yaml会自动安装图依赖
或者:
在Flutter项目根目录下运行
flutter pub get
- 配置gradle-plugin的中文镜像
为了能够通过Flutter的项目构建,我们需要对两个文件进行配置(目的是修改gradle-plugin的中文镜像)
一个是Flutter安装路径下的文件。列如,我,本地把Flutter安装到D:\flutter,我的文件路径是D:\flutter\packages\flutter_tools\gradle\flutter.gradle
buildscript {
repositories {
// google()
// jcenter()
maven { url 'https://maven.aliyun.com/repository/google' }
maven { url 'https://maven.aliyun.com/repository/jcenter' }
maven { url 'https://maven.aliyun.com/repository/public' }
maven { url 'https://maven.aliyun.com/repository/gradle-plugin/' }
// 新加 }dependencies
{ classpath 'com.android.tools.build:gradle:4.1.0' }
}
修改flutter项目/android/build.gradle ```dart buildscript { ext.kotlin_version = ‘1.3.50’ repositories { // google() // jcenter() maven { url ‘https://maven.aliyun.com/repository/google‘ } maven { url ‘https://maven.aliyun.com/repository/jcenter‘ } maven { url ‘https://maven.aliyun.com/repository/public‘ } maven { url ‘https://maven.aliyun.com/repository/gradle-plugin/‘ } // 新加 } dependencies { classpath ‘com.android.tools.build:gradle:4.1.0’ classpath “org.jetbrains.kotlin:kotlin-gradle- plugin:$kotlin_version” } }
allprojects { repositories { // google() // jcenter() maven { url ‘https://maven.aliyun.com/repository/google‘ } maven { url ‘https://maven.aliyun.com/repository/jcenter‘ } maven { url ‘https://maven.aliyun.com/repository/public‘ } maven { url ‘https://maven.aliyun.com/repository/gradle-plugin/‘ } // 新加 } }
- 使用
```dart
import 'package:flutter_html/flutter_html.dart';
Html(data: "<h1>标题</h1>"); // 在flutter中展示HTML代码
6.用户提示
toast常用于展示用户一些不那么重要的信息,会弹出并显示文字一段时间,时间一到就会消失,相较于snackbar和dialog,对屏幕的入侵较少。Flutter中最常用的toast组件时fluttertoast
fluttertoast的用法:
- 安装
https://pub.dev/packages/fluttertoast
引入
import 'package:fluttertoast/fluttertoast.dart';
使用
ElevatedButton(
child: Text('弹出toast'),
onPressed: () {
Fluttertoast: showToast(
msg: '弹出消息内容', // 弹出信息
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM, // 展示位置
timeInSecForIosweb: 1, // 秒数
backgroundColor: Colors.black45, // 背景色
fontSize: 16.0 // 字体大小
)
}
)
7.调用系统摄像头或打开相册
调用终端的摄像头或者在相册中选取图片,我们需要使用第三方插件image_picker
安装
在pubspec.yaml中,配置合适的版本
- 配置权限
编辑andriod/app/src/main/AndriodManifest.xml
<!-- 调用摄像头权限 -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- 获取 SD 卡内容(访问相册)权限 -->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
<application android:requestLegacyExternalStorage="true"
<!-- Android API 29+ 添加这一行 -->
其他配置项
>
- 声明调用函数 ```dart import ‘package:image_picker/image_picker.dart’; import ‘dart:io’;
final picker = ImagePicker(); File _image;
// 拍照获取图片 Future _takePhoto() async { final pickedFile = await picker.getImage(source: ImageSource: camera);
setState() { if (pickedFile != null) { _image = File(pickedFile.path); } else { print(‘No image’); } } }
// 在相册中选取一张图片 Future _openGallery() async { final pickedFile = await picker.getImage(source: ImageSource.gallery);
setState(() { if (pickedFile != null) { _image = File(pickedFile.path); } else { print(‘No Image’); } }); }
- 声明调用菜单
```dart
// 当触碰的时候拉起菜单,注册点击事件
onTap: () {
showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return renderBottomSheet(context);
}
)
}
Widget renderBottomSheet(BuildContext context) {
return Container(
height: 160,
child: Column(
children: [
Inkwell(
onTap: () {
_takePhoto();
G.router.pop(context);
},
child: Container(
child: Text('拍照'),
height: 50,
alignment: Alignment.center
)
),
Inkwell(
onTap: () {
_takeGallery();
G.router.pop(context);
},
child: Container(
child: Text('从相册中选取'),
height: 50,
alignment: Alignment.center
)
),
Contaienr(
color: Colors.grey[200],
height: 10
),
Inkwell(
onTap: () {
G.router.pop(context);
},
child: Container(
child: Text('取消'),
height: 50,
alignment: Alignment.center
)
)
]
)
);
}
8.支付
创建订单
// 创建订单接口
Future<dynamic> createOrder({ int goodsId }) async {
Response res = await _dio.post('/front/order/saveOrder', data: {"goodsId": goodsId});
return res.data['content'];
}
发起支付
// 发起支付接口
Future<dynamic> createPay({
String orderNo,
int channel,
String returnUrl = 'http://edufront.lagou.com'
}) async {
Map payData = {
"goodsOrderNo": orderNo,
"channel": channel == 1 ? 'weChat' : 'aliPay',
"returnUrl": returnUrl
}
Response res = await _dio.post('/front/pay/saveOrder', data: payData);
if (res.data != null) {
return res.data['content'];
} else {
return false;
}
}
如果发起支付成功,上述接口会返回支付链接。接着,只要跳转到支付链接进行支付就可以了。
在APP中,跳转到指定URL地址,需要使用第三方插件url_launcher。
import 'package:url_launcher/url_launcher.dart';
// 跳转到指定页面
void _launchURL(_url) async =>
await canLaunch(_url)
?
await launch(_url)
:
throw '不能跳转到 $_url';
// 确定支付
doPay() {
// 发起支付
G.api.order.createPay(orderNo: orderNo, channel: payment).then(value) {
if (value != false) {
_launchURL(value['payUrl']); // 跳转到支付链接
} else {
print('支付失败');
}
}
}
9.Splash页面
Splash页面就是打开APP时,看到的第一个广告页。主要技术点是倒计时,默认展示广告图片,倒计时时间到了之后,跳转到首页。
import 'package:flutter/material.dart';
import 'dart.async';
import '../../utils/Global.dart';
class Splash extends StatefulWidget {
Splash({Key key}) : super(key: key);
@override
_SplashState createState() => _SplashState();
}
class _SplashState extends State<Splash> {
Timer _timer;
int counter = 3;
// 倒计时
countDown() async {
var _duration = Duration(seconds: 1);
Timer(_duration, () {
// 等待1秒之后,倒计时
_timer = Timer.periodic(_duration, timer) {
counter--;
if (counter == 0) {
// 执行跳转
goHome();
} else {
setState(() {});
}
}
return _timer;
});
}
void goHome() {
_timer.cancel();
G.router.navigateTo(context, '/');
}
@override
void initState() {
super.initState();
countDown(); // 指定倒计时
}
@override
void initState() {
super.initState();
countDown();
// 指定倒计时
}
@override Widget build(BuildContext context) {
return Stack( alignment: Alignment(1.0, -1.0),
children: [
ConstrainedBox( constraints: BoxConstraints.expand(),
child: Image.asset( "lib/assets/images/splash.jpeg",
fit: BoxFit.fill ) ),
Container( color: Colors.grey,
margin: EdgeInsets.fromLTRB(0, 50, 10, 0),
padding: EdgeInsets.all(5),
child: TextButton(
onPressed: () {
goHome();
},
child: Text( "$counter 跳过广告",
style: TextStyle(
color: Colors.white,
fontSize: 14 ) ), ) ), ] ); }
@override
void dispose() {
super.dispose();
}
}
10.项目优化
虽然我们已经完成了项目的基本功能,但仍有很多细节,需要优化。
异步UI更新
试想这样一种场景:异步请求接口,在数据还未请求回来的时候,UI就已经更新了。此时,UI会因为拿不到数据而报错。
而异步UI更新,就是为了解决这一问题的。其基本思路是:先等待数据请求,后刷新UI
FutureBuilder 是对 Future 的封装。我们先来看看它的构造方法
FutureBuilder({
Key key,
Future<dynamic> future,
dynamic initialData,
widget Function(BuildContext, AsyncSnapshot<dynamic>) builder
})
- future 接收 Future
类型的值,实际上就是我们的异步函数,通常是接口请求函数 - initialData 初始数据,在异步请求完成之前使用
builder:是一个回调函数,接收两个参数一个 AsyncWidgetBuilder
类型的值 builder: (
BuildContext context,
AsyncSnapshot<dynamic> snapshot
) {
/// ...
}
AsyncSnapshot (即 snapshot)中封装了三个内容:
connectionState(连接状态 - 一共有四个)
- none :当前未连接到任何异步计算。
- waiting : 连接成功等待交互
- active :正在交互中,可以理解为正在返回数据
- done :交互完成,可以理解为数据返回完成。通过 snapshot.data 获取数据
- data(实际上就是 future 执行后返回的数据)
- error(实际上就是 future 错误时返回的错误信息)
保持页面状态
默认情况,我们进行页面跳转时。都会重新刷新页面(包括请求后代数据接口)。但是,有些页面的数据不会频繁变化(或及时性要求不高),此时,我们可以将页面数据暂时保存起来,从能避免页面频繁的刷新。
保持页面状态相当于缓存数据,是一种常规的优化手段。具体实现方案有如下几种
- IndexedStack
IndexedStack 的逻辑是,一次加载多有的 Tab 页面,但同时,只展示其中一个。
body: IndexedStack(
index: curIndex,
// children: _listViews,
children: pages.map<Widget>((e) => e['widget']).toList(),
)
AutomaticKeepAliveClientMixin ```dart // home page class Home extends StatefulWidget { Home({Key key}) : super(key: key);
@override _HomeState createState() => _HomeState(); }
// 1. 使用 AutomaticKeepAliveClientMixin
class _HomeState extends State
// 2. 声明 wantKeepAlive // 避免 initState 重复调用 @override void initState() { super.initState(); print(‘333333’); } @override Widget build(BuildContext context) { super.build(context); // 3. 在构造方法中调用父类的 build 方法 } /// … }
- Tab 中,只保持某些页面的状态(需要修改 Tab 实现)
- 声明 PageController
```dart
PageController _pageController;
初始化 PageController
@override
void initState() {
// 2. 初始化 PageController
_pageController = PageController(
initialPage: G.getCurrentContext().watch<CurrentIndexProvider> ().currentIndex
);
super.initState();
}
修改 Tab 的 body
body: PageView(
controller: _pageController,
children: pages.map<Widget>((e) => e['page']).toList(),
)
跳转到指定页面
onTap: (index) async {
// 4. 跳转到指定页面
setState(() {
_pageController.jumpToPage(index);
});
},
11.DevTools
DevTools 是一套 Dart 和 Flutter 性能调试工具。
在开始 DevTools 之前,我们先来介绍一下 Flutter 的运行模式。
Flutter 有四种运行模式:Debug、Release、Profile和test,这四种模式在build的时候是完全独立的。debug
Debug 模式可以在真机和模拟器上同时运行:会打开所有的断言,包括 debugging 信息。debug 模式适合调试代码,但是不适合做性能分析。
命令 flutter run 就是以这种模式运行的。
- release
Release 模式只能在真机上运行,不能在模拟器上运行:会关闭所有断言和 debugging 信息。关闭所有debugger工具。优化了快速启动、快速执行和减小包体积。
命令 flutter run —release 就是以这种模式运行的
- profile
Profile 模式只能在真机上运行,不能在模拟器上运行:基本和 Release 模式一致,除了启用了服务扩展和tracing。
命令 flutter run —profile 就是以这种模式运行的
- test
headless test 模式只能在桌面上运行:基本和 Debug 模式一致,除了是 headless 的而且你能在桌面运行。
命令 flutter test 就是以这种模式运行的
判断当前运行环境
// 当 App 运行在 Release 环境时,inProduction 为 true
// 当 App 运行在 Debug 和 Profile 环境时,inProduction 为 false
const bool inProduction = const bool.fromEnvironment("dart.vm.product");
目前,DevTools支持的功能有如下一些:
- 检查和分析应用程序的UI布局和状态。
- 诊断应用的UI性能问题。
- 检测和分析应用程序的CPU使用情况。
- 分析应用程序的网络使用情况。
- Flutter或Dart应用程序的源代码级调试。
- 调试Flutter或Dart应用程序的内存使用情况和分析内存问题。
- 查看运行的Flutter或Dart应用程序的一般日志和诊断信息。
安装DevTools
- 编辑器中
在 Android Studio 或 VS Code 中,只要你安装了 Flutter 插件,则 DevTools 也已经默认安装了。
命令行中
如果在你的环境变量 PATH 中有 pub , 可以运行:
pub global activate devtools
如果环境变量 PATH 中有 flutter , 可以运行:
flutter pub global activate devtools
启动 DevTools
VS Code中
命令行中
如果在你的环境变量 PATH 中有 pub , 可以运行:
pub global run devtools
如果环境变量 PATH 中有 flutter , 可以运行:
flutter pub global run devtools
启动应用
debug 模式启动
flutter run
profile 模式启动
flutter run --profile
使用DevTools
Flutter Inspector
这是一款用于可视化和浏览 Flutter Widget 树的工具。
- Performance
性能分析
- CPU Profiler
CPU 分析器,可以通过此视图记录应用的运行会话,并查看 CPU 在哪些方法中耗费了大量时间,然后就可以决定应该在哪里进行优化。
- Memory
查看应用在特定时刻的内存使用情况
- Debugger
调试器
- Network
网络请求调试,例如:接口调试,HTTP 分析等
- Logging
查看日志,支持关键词搜索。日志内容包括:
- Dart 运行时的垃圾回收事件。
- Flutter 框架事件,比如创建帧的事件。
- 应用的 stdout 和 stderr 输出。
- 应用的自定义日志事件。
- App Size
App 打包后,可以对 APP 的大小进行分析
以 profile 方式启动 flutter (flutter run —profile)报错: Flutter Profile mode is not supported by sdk gphone x86. 原因:模拟器是 x86 的,不支持 profile 方式运行,将模拟器换成 x64 的即可。
12.架构原理
系统架构
首先,我们先来了解一下移动端架构的演进。只有了解了之前的技术架构,才能体会Flutter 的优势。
- Native APP
- Android
- iOS
- Web APP
- SPA
- PWA
- HyBird APP
- WebView(Cordova | Ionic | 微信小程序)
- JSBridge(React Native | Weex)
- 自绘制(Flutter)
原生架构
webview架构
没有使用原生组件,在 WebView 中通过 H5 实现所需的界面效果,对前端友好,上手快,成本低。 硬件通信,通过 Bridge 来完成。
JSBridge架构
Flutter架构
通信机制
Flutter跨端的通信效率也是高出JSBridge许多许多。Flutter通过Channel进行通信。其中: 1.BasicMessageChannel,用于传递字符串和半结构化的信息,是全双工的,可以双向请求数据。 2.MethodChannel,用于传递方案调用,即Dart侧可以调用原生侧的方法并通过Result接口回调结果数据。 3.EventChannel,用户数据流的通信,即Dart侧监听原生侧的实时消息,一旦原生侧产生了数据,立即回调给Dart侧
为什么我们说Channel的性能高呢。我们来看一下MethodChannel调用时的调用栈
整个流程中都是机器码的传递,而JNI的通信又和JavaVM内部通信效率一样,整个流程通信的流程相当于原生端的内部通信。因此,其通信效率比JSBridge要高。
具体来说,Flutter的系统架构共分三层,如下图:
Flutter Framework(框架层)
这是一个纯 Dart 实现的 SDK。从上往下包括了两大风格组件库(Material 和 Cupertino)、基础组件库、图形绘制、手势识别、动画等。
- Widget
在 Flutter 中,可以把一切都看作一个组件,组件式的构建 UI。
- Rendering
Rendering 是渲染库,在 Flutter 中,界面的渲染主要包括三个阶段:
- 布局(Layout)
- 绘制(Painting)
- 合成(Composite)
- Animation(动画)
Animation 是一个动画相关类,可以通过这个类创建一些基础的动画。
- Painting(绘制)
Painting 封装了来自 Engine 层的绘制接口
- Gesture(手势)
处理手势动作和手势相关交互
- Foundation(基础)
底层框架,定义底层工具类和方法,提供其他层使用
Flutter Engine(引擎层)
这是一个 C++ 实现的 SDK,其中包括了 Skia 引擎(Google开源图形库)、Dart 运行时、文字排版引擎等。在代码调用 dart:ui 库时,调用最终会走到 Engine 层,然后实现真正的绘制逻辑。
Skia 是谷歌出品的开源二维图形库,提供常用的 API,并且可以在多种软硬件平台上运行。谷歌Chrome 浏览器、Chorme OS、Android、火狐浏览器和操作系统,及其他许多产品都使用它作为图形引擎。 和其他跨平台方案不同。Flutter 没有使用原生的 UI 和绘制框架,以此来保证 Flutter 的高性能体验。
Embedder(嵌入层)
嵌入层是操作系统适配层,其中主要负责的工作有:surface 渲染设置,线程的管理,原生插件管理,事件循环的交互等。
嵌入层位于整个框架的最底层,说明 Flutter 的平台相关层非常低,大部分的渲染操作在 Flutter 本身内部完成,各个平台(Android,iOS等)只需要提供一个画布,这就让 Flutter 本身有了很好的跨端一致性。
渲染过程
首先,我们从官网拿到下面这张图。这张图从用户的角度,来解释 Flutter 的渲染过程
另外,从源码的角度,对上述过程进行解剖,会得到下图
三颗树
从创建到渲染的大体流程是:
当应用启动时 Flutter 会遍历并创建所有的 Widget 形成 Widget Tree,同时与 Widget Tree 相对应,通过调用 Widget 上的 createElement() 方法创建每个 Element 对象,形成 Element Tree。最后调用Element 的 createRenderObject() 方法创建每个渲染对象,形成一个 Render Tree。
这里我们可以做一个类比:
- Widget Tree 是设计原型(设计师)
- Element Tree 是产品原型(产品经理)
- RenderObject Tree 要实现产品(工程师)
Widget Tree
第一棵树,是 Widget Tree。程序员写的用来构建页面的组件树。
需要注意的是,Widget 是不可变的(immutable),当 Widget 发生变化时,Flutter 会重建 Widget来进行更新。
那为什么将 Widget Tree 设计为 immutable?Flutter 主张 simple is fast,不用关心数据与节点之间的关系。
相当于牺牲空间换时间,从而保证性能
Element Tree
Element 就是 Widget 在 UI 树具体位置的一个实例化对象。持久存在于Dart Runtime上下文之中。它承载了构建的上下文数据,是Widget Tree 和 RenderObject Tree 的桥梁。
之所以让它持久地存在于 Dart 上下文中,而不是像 Widget 那样重新构建,因为Element Tree 的重新创建和重新渲染的开销会非常大,所以 Element Tree 到 RenderObject Tree 也有一个 Diff 环节,来计算最小重绘区域。
需要注意的是,Element 同时持有 Widget 和 RenderObject,但无论是 Widget 还是 Element,其实都不负责最后的渲染,它们只是“发号施令”,真正对配置信息进行渲染的是 RenderObject。
RenderObject Tree
渲染树的任务就是做具体渲染工作。RenderObject 用于应用界面的布局(Layout)和绘制 (Paint),保存了元素的大小,布局等信息。RenderObject 主要属性和方法如下:
- constraints 对象,从其父级传递给它的约束
- parentData 对象,其父对象附加有用的信息。
- performLayout 方法,计算此渲染对象的布局。
- paint 方法,绘制该组件及其子组件。
布局过程
Flutter 中的组件在屏幕上绘制渲染之前,需要先进行布局(Layout)操作。其具体可分为两个过程:
- Constraints Down(从顶部向下传递约束)
父节点给每个子节点传递约束,这些约束是每个子节点在布局阶段必须要遵守的规则。
常见的约束包括,规定子节点最大最小宽度或者子节点最大最小的高度。这种约束会向下延伸,子组件也会产生约束传递给自己的孩子,一直到叶子结点。
- Layout Up(从底部向上传递布局信息)
子节点接受到来自父节点的约束后,会依据这些约束,产生自己的布局信息;
例如:父节点规定我的最小宽度是 500 的单位像素,子节点按照这个规则,可能定义自己的宽度为 500 个像素,或者大于 500 像素的任何一个值。
确定好自己的布局信息之后,将这些信息告诉父节点。父节点也会继续此操作向上传递,一直到最顶部。
Flutter 中有两种主要的布局协议:Box 盒子协议和 Sliver 滑动协议。RenderObject 作为一个抽象类。每个节点需要实现它才能进行实际渲染。扩展 RenderOject 的两个最重要的类是 RenderBox 和RenderSliver。这两个类分别是应用了 Box 协议和 Sliver 协议。
绘制过程
RenderObject 可以通过 paint() 方法来完成具体绘制逻辑,流程和布局流程相似,子类可以实现 paint() 方法来完成自身的绘制逻辑。
void paint(PaintingContext context, Offset offset) { }
通过 context.canvas 可以取到 Canvas 对象,接下来就可以调用 Canvas API 来实现具体的绘制逻辑。 如果节点有子节点,它除了完成自身绘制逻辑之外,还要通过 paintChild() 方法来调用子节点的绘制方法。 如此递归完成整个节点树的绘制,最终调用栈为: paint() > paintChild() > paint() … 。
另外,Flutter 使用 Composited Layer 来对 RenderObject 的绘制进行组织,通常一个 Composited Layer 对应一棵 RenderObject 子树,Composited Layer 的 Display List 记录了这棵 RenderObject 子树的绘制指令。
为什么需要三棵树?
使用三棵树的目的是尽可能复用 Element。复用 Element 对性能非常重要,因为 Element 拥有两份关键数据:StatefulWidget 的状态对象及底层的 RenderObject。当应用的结构很简单时,或许体现不出这种优势,一旦应用复杂起来,构成页面的元素越来越多,重新创建 3 棵树的代价是很高的,所以需要最小化更新操作。
Element Tree的定位,有点象Web端的Virtual DOM
在开始 Flutter 的渲染机制之前,我们先介绍一下屏幕绘制的原理
我们知道显示器以固定的频率刷新,比如 iPhone的 60Hz。当一帧图像绘制完毕后,准备绘制下一帧时,显示器会发出一个垂直同步信号(VSync)。一般地来说,计算机中,CPU、GPU 和显示器以一种特定的方式协作:
CPU 将计算好的显示内容提交给 GPU,GPU 渲染后放入帧缓冲区,然后视频控制器按照 VSync 信号从帧缓冲区取帧数据,传递给显示器显示。屏幕上的每一帧的绘制过程,实际上是 Engine 通过接收的VSync 信号不断地触发帧的绘制。
绘制管线
Flutter 只关心向 GPU 提供视图数据,GPU 的 VSync信号同步到 UI 线程,UI 线程使用 Dart 来构建抽象的视图结构,这份视图结构在 GPU 线程进行图层合成,视图数据提供给 Skia 引擎渲染为 GPU 数据,这些数据通过 OpenGL 或 Vulkan提供给 GPU。
Flutter 的渲染流水线也包括两个线程 —— UI 线程 和 GPU 线程。
UI 线程主要负责的是根据 UI 界面的描述生成 UI 界面的绘制指令,而 GPU 线程负责光栅化和合成。
渲染管线
在 Flutter 框架中存在着一个渲染流水线(Rendering pipline)。这个渲染流水线是由垂直同步信号 (Vsync)驱动的,而 Vsync 信号是由系统提供的,如果你的 Flutter 是运行在 Android 上的话,Flutter 会向 Android 系统的 Choreographer 注册并接收 VSync 信号,GPU 硬件产生 VSync 信号以后,系统便会触发回调,并驱动 UI 线程进行渲染工作。
- 动画(Animate)阶段:因为动画会随每个Vsync信号的到来而改变状态(State),所以动画阶段是流水线的第一个阶段。
- 构建(Build)需要被重新构建的 Widget 会在此时被重新构建。也就是我们熟悉的 StatelessWidget.build() 或者 State.build() 被调用的时候。
- 布局(Layout)阶段,这时会确定各个显示元素的位置,尺寸。此时是RenderObject.performLayout() 被调用的时候。
- 绘制(Paint)阶段,此时是 RenderObject.paint() 被调用的时候。
渲染流水线
启动过程分析
接下来,我们以 Flutter 的启动过程为例,来分析一下 Flutter 的源码。
上述图比较复杂,你可以先简单了解下,等下我们会详细拆分来讲解。我们先来看下这几个关键函数的作用。
首先我们从 runApp() 开始
void main() { runApp(MyApp()); }
runApp() 函数声明在 widgets/binding.dart 中
void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized() /// 单例模式初始化
..scheduleAttachRootWidget(app) /// 将 app 添加到根组件
..scheduleWarmUpFrame(); /// 调度热身帧
}
ensureInitialized 实例化过程中,实现了很多绑定
class WidgetsFlutterBinding extends BindingBase with
GestureBinding, /// 手势绑定
ServicesBinding, /// 服务绑定
SchedulerBinding, /// 调度绑定
PaintingBinding, /// 绘制绑定
SemanticsBinding, /// 语义绑定(辅助功能)
RendererBinding, /// 渲染绑定
WidgetsBinding /// 组件绑定
{ static WidgetsBinding ensureInitialized() {
/// 单例模式实例化
if (WidgetsBinding.instance == null) WidgetsFlutterBinding()
; return WidgetsBinding.instance;
}
}
scheduleAttachRootWidget,创建根 widget ,并且从根 widget 向子节点递归创建元素Element,对子节点为 RenderObjectWidget 的小部件创建 RenderObject 树节点,从而创建出View 的渲染树,这里源代码中使用 Timer.run 事件任务的方式来运行,目的是避免影响到微任务的执行。
void scheduleAttachRootWidget(Widget rootWidget) {
Timer.run(() {
attachRootWidget(rootWidget);
});
}
attachRootWidget 与 scheduleAttachRootWidget 作用一致,首先是创建根节点,然后调用attachToRenderTree 循环创建子节点。
void attachRootWidget(Widget rootWidget) {
_readyToProduceFrames = true;
_renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
container: renderView,
debugShortDescription: '[root]',
child: rootWidget,
).attachToRenderTree(buildOwner, renderViewElement as RenderObjectToWidgetElement<RenderBox>);
}
attachToRenderTree,该方法中有两个比较关键的调用,我只举例出核心代码部分,这里会先执行 buildScope ,但是在 buildScope 中会优先调用第二个参数(回调函数,也就是element.mount ),而 mount 就会循环创建子节点,并在创建的过程中将需要更新的数据标记为dirty。
owner.buildScope(element, () {
element.mount(null, null);
});
buildScope,如果首次渲染 dirty 是空的列表,因此首次渲染在该函数中是没有任何执行流程的, 该函数的核心还是在第二次渲染或者 setState 后,有标记 dirty 的 Element 时才会起作用,该函数的目的也是循环 dirty 数组,如果 Element 有 child 则会递归判断子元素,并进行子元素的 build创建新的 Element 或者修改 Element 或者创建 RenderObject。
- updateChild,该方法非常重要,所有子节点的处理都是经过该函数,在该函数中 Flutter 会处理Element 与 RenderObject 的转化逻辑,通过 Element 树的中间状态来减少对 RenderObject 树的影响,从而提升性能。具体这个函数的代码逻辑,我们拆解来分析。
该函数的输入参数,包括三个参数:
- Element child
child 为当前节点的 Element 信息
- Widget newWidget
newWidget 为 Widget 树的新节点
- dynamic newSlot
newSlot 为节点的新位置
在了解参数后,接下来看下核心逻辑,首先判断是否有新的 Widget 节点。
if (newWidget == null) {
if (child != null)
deactivateChild(child);
return null;
}
如果不存在,则将当前节点的 Element 直接销毁
如果 Widget 存在该节点,并且 Element 中也存在该节点,那么就首先判断两个节点是否一致,
- 如果一致只是位置不同,则更新位置即可。
其他情况下判断是否可更新子节点,如果可以则更新,如果不可以则销毁原来的 Element 子节点,并重新创建一个。
if (hasSameSuperclass && child.widget == newWidget) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
newChild = child; } else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
child.update(newWidget);
// 根据不同的节点类型,调用不同的
Update assert(child.widget == newWidget);
assert(() {
child.owner._debugElementWasRebuilt(child);
return true;
}());
newChild = child;
} else {
deactivateChild(child);
assert(child._parent == null);
newChild = inflateWidget(newWidget, newSlot);
}
上面代码的第 8 行非常关键,在 child.update 函数逻辑里面,会根据当前节点的类型,调用不同 的 update ,可参考上图中的 update 下的流程,每一个流程也都会递归调用子节点,并循环返回 到 updateChild 中。有以下三个核心的函数会重新进入 updateChild 流程中,分别是 performRebuild、inflateWidget 和 markNeedsBuild,接下来我们看下这三个函数具体的作用。
performRebuild 是非常关键的一个代码,这部分就是我们在组件中写的 build 逻辑函数,StatelessWidget 和 StatefulWidget 的 build 函数都是在此执行,执行完成后将作为该节点的子节点,并进入 updateChild 递归函数中。
- inflateWidget 创建一个新的节点,在创建完成后会根据当前 Element 类型,判断是
RenderObjectElement 或者 ComponentElement 。根据两者类型的不同,调用不mount, 挂载 到当前节点上,在两种类型的 mount 中又会循环子节点,调用 updateChild 重新进入子节点更新流程。这里还有一点,当为 RenderObjectElement 的时候会去创建 RenderObject 。
- markNeedsBuild,标记为 dirty ,并且调用 scheduleBuildFor 等待下一次 buildScope 操作。
首次Build
当我们首次加载一个页面组件的时候,由于所有节点都是不存在的,因此这时候的流程大部分情况下都是创建新的节点,如下图:
runApp 到 RenderObjectToWidgetElement(mount) 逻辑都是一样的,在 _rebuild 中会调用updateChild 更新节点,由于节点是不存在的,因此这时候就调用 inflateWidget 来创建 Element。
当 Element 为 Component 时,会调用 Component.mount ,在 Component.mount 中会创建Element 并挂载到当前节点上,其次会调用 _firstBuild 进行子组件的 build ,build 完成后则将 build 好的组件作为子组件,进入 updateChild 的子组件更新。
当 Element 为 RenderObjectElement 时,则会调用 RenderObjectElement.mount,在
RenderObjectElement.mount 中会创建 RenderObjectElement 并且调用createRenderObject 创建RenderObject,并将该 RenderObject 和 RenderObjectElement 分别挂载到当前节点的 Element 树和 RenderObject 树,最后同样会调用 updateChild 来递归创建子节点。
以上就是首次 build 的逻辑,单独来看还是非常清晰的,接下来我们看下 setState 的逻辑
setState
首先,我们查看 StatefulWidget 中的 setState
void setState(VoidCallback fn) {
/// ...
_element.markNeedsBuild();
// 标记需要构建的 element
}
然后,我们来看一下 markNeedsBuild
void markNeedsBuild() {
/// ...
if (dirty) return;
_dirty = true;
owner.scheduleBuildFor(this);
}
Widget 对应的 element 将自身标记为 dirty 状态,并调用 owner.scheduleBuildFor(this); 通知 buildOwner 进行处理。
void scheduleBuildFor(Element element) {
...
if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
_scheduledFlushDirtyElements = true;
onBuildScheduled();
// 这是一个callback,调用的方法是下面的
_handleBuildScheduled }
_dirtyElements.add(element);
// 把当前 element 添加到 _dirtyElements 数组里面,后 面重新build会遍历这个数组
element._inDirtyList = true;
}
此时 buildOwner 会将所有 dirty 的 Element 添加到 _dirtyElements 当中经过 Framework 一连串的调用后,最终调用 scheduleFrame 来通知 Engine 需要更新 UI,Engine 就会在下个 vSync 到达的时候通过调用 _drawFrame 来通知 Framework,然后 Framework 就会通过 BuildOwner 进行Build 和PipelineOwner 进行 Layout,Paint,最后把生成 Layer,组合成 Scene 提交给 Engine。
void _drawFrame() {
// Engine 回调 Framework 入口
_invoke(window.onDrawFrame, window._onDrawFrameZone);
}
void initInstances() {
super.initInstances();
_instance = this;
ui.window.onBeginFrame = _handleBeginFrame;
// 初始化的时候把 onDrawFrame 设置为 _handleDrawFrame
ui.window.onDrawFrame = _handleDrawFrame;
SystemChannels.lifecycle.setMessageHandler(_handleLifecycleMessage);
}
void _handleDrawFrame() {
if (_ignoreNextEngineDrawFrame) {
_ignoreNextEngineDrawFrame = false;
return;
}
handleDrawFrame();
}
void handleDrawFrame() {
_schedulerPhase = SchedulerPhase.persistentCallbacks;
// 记录当前更新UI的状态
for (FrameCallback callback in _persistentCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp);
}
}
void initInstances() {
/// ....
addPersistentFrameCallback(_handlePersistentFrameCallback);
}
void _handlePersistentFrameCallback(Duration timeStamp) { drawFrame(); }
void drawFrame() {
/// ...
if (renderViewElement != null)
buildOwner.buildScope(renderViewElement);
// 先重新build widget
super.drawFrame();
buildOwner.finalizeTree();
}
核心方法 buildScope
void buildScope(Element context, [VoidCallback callback]){
/// ...
}
需要传入一个 Element 的参数,这个方法通过字面意思应该理解就是对这个 Element 以下范围 rebuild
void buildScope(Element context, [VoidCallback callback]) {
/// ...
try {
/// ...
_dirtyElements.sort(Element._sort);
// 1.排序 /// ...
int dirtyCount = _dirtyElements.length;
int index = 0;
while (index < dirtyCount) {
try { _dirtyElements[index].rebuild();
// 2.遍历 rebuild
} catch (e, stack) {} index += 1; }
} finally {
for (Element element in _dirtyElements) {
element._inDirtyList = false;
}
_dirtyElements.clear();
// 3.清空 /// ...
}
}
- 第1步:按照 Element 的深度从小到大,对 _dirtyElements 进行排序
由于父 Widget 的 build 方法必然会触发子 Widget 的 build,如果先 build 了子 Widget,后面再build 父Widget 时,子 Widget 又要被 build 一次。所以这样排序之后,可以避免子 Widget 的重复 build。
- 第2步:遍历执行 _dirtyElements 当中 element 的 rebuild 方法
值得一提的是,遍历执行的过程中,也有可能会有新的 element 被加入到 _dirtyElements 集合中,此时会根据 dirtyElements 集合的长度判断是否有新的元素进来了,如果有,就重新排序。
element 的 rebuild 方法最终会调用 performRebuild(),而 performRebuild() 不同的 Element 有不同的实现。
- 第3步:遍历结束之后,清空 dirtyElements 集合
因此 setState() 的主要工作是记录所有的脏元素,添加到 BuildOwner 对象的 _dirtyElements 中,然后调用scheduleFrame 来注册 Vsync 回调。 当下一次 Vsync 信号到来时,会执行 handleBeginFrame()和handleDrawFrame() 来更新 UI。