文章目录
一、Flutter基础知识
1.Flutter简介和发展历史
Flutter是一款由Google开发的移动应用程序开发框架,可用于开发高性能、跨平台的移动应用程序。Flutter使用Dart编程语言,并提供了丰富的UI组件和工具,使得开发者可以快速构建漂亮、响应式的用户界面和高质量的应用程序。
Flutter的发展历史可以分为以下几个阶段:
-
2011年,Dart语言项目启动,旨在提供一种适用于Web开发的高性能编程语言。
-
2015年,Flutter项目启动,旨在使用Dart语言构建高性能、跨平台的应用程序。
-
2017年,Flutter的第一个Alpha版本发布,开发者可以开始尝试使用Flutter构建应用程序。
-
2018年,Flutter的第一个稳定版本发布,得到了广泛的认可和采用。
-
2019年,Flutter 1.5发布,增加了对iOS13和Android Q的支持,以及许多其他改进和 新功能。
-
2020年,Flutter 1.20发布,引入了新的Web支持和iOS14和Android 11的支持,以及其他改进和新功能。
-
2021年,Flutter2.0发布,引入了许多重要的新功能,包括全新的Web支持、桌面应用程序支持、Flutter for Windows等。
-
2023年,Flutter3.0发布,引入了许多重要的新功能,包括全新的Web支持、桌面应用程序支持、Flutter for Windows等。
截止到2023年3月,由于Flutter生态系统的不断发展,Flutter在Google Play Store和Apple App Store上已经有超过400,000款应用程序上线。同时,Flutter还正在扩展到其他领域,例如Web开发和桌面应用程序开发,这些领域的应用程序数量也在不断增长。由于Flutter是一个开源框架,许多公司和个人也在使用Flutter构建自己的应用程序,并且Flutter的生态系统也在不断扩大,因此Flutter上线的应用程序数量可能会随着时间的推移而继续增长。
2.Flutter安装和配置
以下是安装和配置Flutter的步骤,以Windows为例:
- 下载Flutter SDK
可以在Flutter的官方网站上下载Flutter SDK。下载完成后,将其解压到您选择的目录中,例如在Windows上,您可以将其解压到 C:\src\flutter。
-
配置Flutter环境变量
将Flutter SDK的路径添加到PATH环境变量中,以便在终端中可以使用flutter命令。例如,在Windows上,您可以将C:\src\flutter\bin添加到系统环境变量的PATH变量中。
-
下载开发工具并安装Flutter插件
如果您使用的是Android Studio或IntelliJ IDEA,您需要安装Flutter插件以便在IDE中开发Flutter应用程序。打开IDE,进入插件设置,搜索“Flutter”,然后安装并重启IDE即可。
-
运行Flutter Doctor
打开终端或命令提示符,并输入“flutter doctor”命令,以检查Flutter环境的配置情况。Flutter doctor会检查您的环境并给出建议以解决任何缺失的依赖项或配置问题。
-
配置Android开发环境
如果您要在Flutter中开发Android应用程序,您需要安装并配置Android开发环境。您可以在Android开发者网站上获取有关如何安装和配置Android开发环境的更多信息。
以上是在Windows上安装和配置Flutter的基本步骤。完成这些步骤后,您就可以开始使用Flutter构建应用程序了。
3.Dart语言基础知识
Dart语言特性
-
Dart 的特性
Dart 是少数同时支持 JIT(Just In Time,即时编译)和 AOT(Ahead of Time,运行前编译)的语言之一。语言在运行之前通常都需要编译,JIT 和 AOT 则是最常见的两种编译模式。
-
JIT 在运行时即时编译,在开发周期中使用,可以动态下发和执行代码,开发测试效率高,但运行速度和执行性能则会因为运行时即时编译受到影响。
-
AOT 即提前编译,可以生成被直接执行的二进制代码,运行速度快、执行性能表现好,但每次执行前都需要提前编译,开发测试效率低。
总结来讲,在开发期使用 JIT 编译,可以缩短产品的开发周期。Flutter 最受欢迎的功能之一热重载,正是基于此特性。而在发布期使用 AOT,就不需要像 React Native 那样在跨平台 JavaScript 代码和原生 Android、iOS 代码之间建立低效的方法调用映射关系。所以说,Dart 具有运行速度快、执行性能好的特点。
-
-
内存分配与垃圾回收
Dart VM 的内存分配策略比较简单,创建对象时只需要在堆上移动指针,内存增长始终是线性的,省去了查找可用内存的过程。
Dart 的垃圾回收,则是采用了多生代算法。新生代在回收内存时采用“半空间”机制,触发垃圾回收时,Dart 会将当前半空间中的“活跃”对象拷贝到备用空间,然后整体释放当前空间的所有内存。回收过程中,Dart 只需要操作少量的“活跃”对象,没有引用的大量“死亡”对象则被忽略,这样的回收机制很适合 Flutter 框架中大量 Widget 销毁重建的场景。
-
单线程模型
Dart 中并没有线程,只有 Isolate(隔离区)。Isolates 之间不会共享内存,就像几个运行在不同进程中的 worker,它们通过事件循环(Event Looper)在事件队列(Event Queue)上传递消息通信。所以如果想要在 Dart 中实现并发是可以通过 Isolate 实现的。Isolate 的这种类似于线程但不共享内存,独立运行的 worker的机制,就可以让 Dart 实现无锁的快速分配。
-
无需单独的声明式布局语言
Dart 声明式编程布局易于阅读和可视化,使得 Flutter 并不需要类似 JSX 或 XML 的声明式布局语言。所有的布局都使用同一种格式,也使得 Flutter 很容易提供高级工具使布局更简单,就突出一个上手简单。
Dart基本语法
- 主函数(入口函数)
void main(List arguments) {print('Hello world! ${arguments}');}
- 函数
// 1,函数创建// 2,函数传值// 3. 可选参数printName(String name, int age, [String sex = '女']) {print("name is ${name}, age is ${age}, sex is ${sex}")}// 4. 命名函数printInfo({name, age=4, sex}) {print("name is ${name}, age is ${age}, sex is ${sex}")}void main(List arguments) {printName('果果', 18);printInfo({name: '果果', age: 4, sex: '女'});}
-
基本类型
Dart支持的基础数据类型和其他语言基本一样
// 基本类型// intint age = 20;// doubledouble count = 10.0;// StringString name="果果";// boolbool flag = true;// ListList list = [1,2,3,4,5,6];// SetSet set = new Set();set.addAll(list);// MapMap user = {'name': 'bajie', 'age': 18};// 常量constfinal // 变量var// 类型可推导=> var user = {'name': '果果', 'age': 4};print("${user['name']}")
- 面向对象
// 创建一个类// lib/animal.dartclass Animal { String? name; Animal({this.name}); Animal.fromJson(Map json) { name = json['name']; } Map toJson() { final Map data = {}; data['name'] = name; return data; } void eat() { print('$name is eating!'); }}// main.dartvoid main() {Animal a = Animal(name: '果果');a.eat();}输出结果:果果 is eating
- 类的继承
// 继承一个类// lib/cat.dartclass Cat extends Animal { }void main() { Cat cat = Cat(); cat.name = "colala"; cat.eat();}输出结果:colala is eating
多态
// lib/cat.dartclass Cat {// 重写一个方法drink() { print('$name is drinking!'); }} void main() { Cat cat = Cat(); cat.name = "colala"; cat.eat(); cat.drink();}输出结果:colala is eating输出结果:colala is drinking
- 类的实现
Dart语言中没有接口(interface)的关键字,但是有实现(implements)关键字,Dart中可以将类(是否为抽象无关)当做隐式接口直接使用,当需要使用接口时,可以声明类来代替。
// 抽象类作为接口abstract class Sleep{ void sleep();}// 普通类作为接口class Play{ void play(){}}class Cat extends Animal implements Sleep, Play { @override void eat() { print("eat"); } @override void sleep() { print(" is sleeping"); } @override void play() { print("is beating colala"); }}void main() { var cat = = Cat(); cat.name = "果果"; cat.eat(); cat.sleep(); cat.play();}输出结果:果果 is eating输出结果:果果 is sleeping输出结果:果果 is beating colala
-
类的混入
mixin一般用于描述一种具有某种功能的组块,而某一对象可以拥有多个不同功能的组块。
mixin用于修饰类,和abstract类似,该类可以拥有成员变量、普通方法、抽象方法,但是不可以实例化。mixins不是一种在经典意义上获得多重继承的方法。
mixins是一种抽象和重用一系列操作和状态的方法。
它类似于扩展类所获得的重用,但它与单继承兼容,因为它是线性的。
- 简单应用
最简单的mixin由mixin & with关键字组成。
‘教师‘ 一种能力是 ‘绘画’
void main() { Teacher().draw();}mixin DrawFunc { String content = '..'; String what(); void draw() { print('I can draw ${what()}'); }}class Teacher with DrawFunc { String what() => "car";}
- 限定类型(mixin…on)
当我们在mixin上使用了on关键字,那么mixin只能在那个类的子类上使用,而mixin可以调用那个类的方法。
限定 ‘绘画’ 这种能力只能够用在 ‘人类’ 上面
void main() { Teacher().draw();}class Person {}mixin DrawFunc on Person { String content = '..'; String what(); void draw() { print('I can draw ${what()}'); }}class Teacher extends Person with DrawFunc { String what() => "car";}
- 多个类型
在 ‘绘画’ 的基础上,我们增加一种新的能力 ‘唱歌’
void main() { Teacher().draw(); Teacher().sing();}class Person {}mixin DrawFunc on Person { String content = '..'; String what(); void draw() { print('I can draw ${what()}'); }}mixin SingFunc on Person { void sing() { print('I can sing'); }}class Teacher extends Person with DrawFunc, SingFunc { String what() => "car";}
- 组合组块
- mixin:定义了组块。
- on:限定了使用mixin组块的宿主必须要继承于某个特定的类;在mixin中可以访问到该特定类的成员和方法。
- with:负责组合组块,而with后面,这一点需要注意,例如下面这样:
void main() { Teacher().draw(); Teacher().sing(); Teacher().dance();}class Person {}mixin DrawFunc on Person { String content = '..'; String what(); void draw() { print('I can draw ${what()}'); }}mixin SingFunc on Person { void sing() { print('I can sing'); }}abstract class DanceFunc { void dance() { print('I can dance'); }}class Teacher extends Person with DrawFunc, SingFunc, DanceFunc { String what() => "car";}
总结就是,mixin可以理解为一个个的功能组块,哪些宿主需要哪些功能就with到上去。
on关键字一方面是为了限制组块的应用场景,也可以为多个组块提供公共的基础功能。
空安全 Null safety
- 空安全的目的是让开发人员对代码中的 Null 可见且可控,并且确保它不会传递至某些位置从而引发崩溃。相关关键字有以下这些:
- ?
- !
- late
如果可以为空值的变量(null), 在类型声明处加上 ?。
String? name = null;
在您已经明确一个非空变量一定会在使用前初始化,而 Dart 分析器仍然无法明确的情况下,您可以在变量的类型前加上late。
void main() { late TextEditingController textEditingController; init() { textEditingController = TextEditingController(); }}
当您正在调用一个可空的变量或者表达式时,请确保您自己处理了空值。例如:您可以使用 if 条件句、?? 操作符 或是 ?. 操作符来处理可能为空的值。
// 使用 ?? 操作符来避免将非空变量赋予空值Cat cat = Cat();String name = cat.name ?? "果果";
如果您能确定一条可空的表达式不为空,您可以在其后添加 ! 让 Dart 处理为非空。
Cat cat = Cat(); cat.name = "果果"; String name = cat.name!;
一旦您开始使用空安全,当操作对象可能为空时,您将不再能使用成员访问符(.),取而代之的是可空版本的?.
Cat? cat; String? name = cat?.name;
Dart异步原理
Dart 是一门单线程编程语言。异步 IO + 事件循环,Dart异步主要可以通过以下关键字来实现:
- Future
- async
- await
Future 对象封装了Dart 的异步操作,它有未完成(uncompleted)和已完成(completed)两种状态。completed 状态也有两种:一种是代表操作成功,返回结果;另一种代表操作失败,返回错误。
Future fetchUserName() { //想象这是个耗时的获取用户名称操作 return Future(() => '果果');}void main() { fetchUserName().then((result){print(result)}) print('after fetch user name ...');}
通过.then来回调成功结果,main会先于Future里面的操作,输出结果:
flutter: after fetch user name ...flutter: 果果
那如果我们想要先打印名称咋办,换一下掉用方式
Future fetchUserName() { //想象这是个耗时的获取用户名称操作 return Future(() => '果果');}void main() { fetchUserName().then((result){ print(result); print('after fetch user name ...'); } )}
输出结果:
flutter: 果果flutter: after fetch user name ...
Future 同名构造器是 factory Future(FutureOr computation()),它的函数参数返回值为 FutureOr 类型,我们发现还有很多 Future 中的方法比如Future.then、Future.microtask 的参数类型也是 FutureOr,这个对象其实是个特殊的类型,它没有类成员,不能实例化,也不可以继承,只是一个语法糖。
abstract class FutureOr { FutureOr._() { throw new UnsupportedError("FutureOr can't be instantiated"); }}
想象一个这样的场景:
- 调用登录接口;
- 根据登录接口返回的token获取用户信息;
- 缓存用户信息到本机。
Future login(String name,String password){ //登录}Future fetchUserInfo(String token){ //获取用户信息}Future saveUserInfo(User user){ // 保存用户信息}
用 Future 可以这样写:
login('name','password') .then((token) => fetchUserInfo(token)) .then((user) => saveUserInfo(user));
但是这种看着有点别扭,所以我们可以换成关键字处理,换成async和await则可以这样:
void doLogin() async { String token = await login('name','password'); //await 必须在 async 函数体内 User user = await fetchUserInfo(token); await saveUserInfo(user); }
需要注意的是如果声明了 async 的函数,返回值是必须是 Future 对象。即便你在 async 函数里面直接返回 T 类型数据,编译器会自动帮你包装成 Future 类型的对象,如果是 void 函数,则返回 Future 对象。在遇到 await 的时候,又会把 Futrue 类型拆包,又会原来的数据类型暴露出来,所以请注意,await 所在的函数必须添加 async 关键词。
await 的代码发生异常,捕获方式跟同步调用函数一样:
void doLogin() async { try { var token = await login('name','password'); var user = await fetchUserInfo(token); await saveUserInfo(user); } catch (err) { print('Caught error: $err'); }}
语法糖
Future getUserInfo() async { return 'aaa';}//等价于:Future getUserInfo() async { return Future.value('aaa');}
Completer
在Flutter中,Completer是一个类,用于实现异步操作的等待和结果处理。Completer允许您创建一个Future对象,并在该对象的值(或错误)可用时解决该对象。
Completer对象包含两个主要方法:complete()和completeError()。complete()方法将Future对象标记为已完成,并将其结果设置为指定的值,而completeError()方法将Future对象标记为已完成,并将其结果设置为指定的错误。使用Completer时,您可以通过Future对象的then()方法或await关键字来等待异步操作的结果。
以下是一个示例,演示如何使用Completer来处理异步操作的结果:
Completer completer = Completer();// 假设在两秒钟后异步返回一个值Future.delayed(Duration(seconds: 2), () { completer.complete(42);});// 使用then()方法等待异步操作的结果completer.future.then((value) { print('异步操作的结果为:$value');});
在上面的示例中,创建了一个Completer对象,并通过Future.delayed()方法模拟一个异步操作,该操作在两秒钟后返回一个值。然后,使用then()方法等待异步操作的结果,并在结果可用时打印该结果。
Completer是Flutter中处理异步操作的重要工具之一,特别是在需要处理多个异步操作的情况下,Completer可以更好地控制异步操作的执行顺序和结果处理。
Isolate
在Flutter中,Isolate是一种轻量级的、独立的执行线程,它与主线程(也称为UI线程)并行运行,并可以执行CPU密集型任务,而不会阻塞UI线程。Flutter中的每个Isolate都有自己的内存空间,可以独立于其他Isolate进行操作。
Isolate可以在Flutter应用程序中实现并发性,可以将一些耗时的任务(如网络请求、文件操作、复杂计算等)分配给Isolate,从而使UI线程不会被阻塞,从而提高应用程序的性能和响应速度。
Flutter中可以通过使用compute()函数或Isolate.spawn()方法来创建和使用Isolate。compute()函数是Flutter提供的一个方便的API,它允许您在后台Isolate中执行耗时的计算,并在完成后返回结果。例如,以下代码演示了如何使用compute()函数来计算斐波那契数列的第20项:
int fibonacci(int n) { if (n <= 0) { return 0; } else if (n == 1) { return 1; } else { return fibonacci(n - 1) + fibonacci(n - 2); }}Future calculateFibonacci(int n) async { return await compute(fibonacci, n);}int result = await calculateFibonacci(20);
Isolate.spawn()方法允许您创建新的Isolate,并将指定的函数作为Isolate的入口点来执行。例如,以下代码演示了如何使用Isolate.spawn()方法来创建一个新的Isolate,并在其中执行耗时的计算:
Future calculate() async { ReceivePort receivePort = ReceivePort(); await Isolate.spawn(calculateFibonacci, 20, onExit: receivePort.sendPort); int result = await receivePort.first; print('计算结果为:$result');}void calculateFibonacci(int n) { // 计算斐波那契数列的第n项,并将结果发送到主Isolate int result = fibonacci(n); SendPort sendPort = IsolateNameServer.lookupPortByName('main_send_port'); sendPort.send(result);}
在上面的示例中,首先创建一个ReceivePort对象,然后使用Isolate.spawn()方法创建一个新的Isolate,并将calculateFibonacci函数作为入口点来执行。在calculateFibonacci函数中,计算斐波那契数列的第n项,并将结果发送到主Isolate。在calculate()函数中,使用ReceivePort来等待新Isolate的结果,并将其打印出来。
Isolate是Flutter中处理并发性和异步操作的重要工具之一,特别是在需要执行耗时的计算或操作的情况下,使用Isolate可以使应用程序保持流畅并响应迅速。
那么这两个如何联合使用呢?
通常,Completer对象用于异步操作的等待和结果处理。当需要执行耗时的操作时,可以创建一个新的Isolate来执行该操作,并使用Completer来等待操作完成并获取结果。在Isolate中,可以使用SendPort来将结果发送回主Isolate,然后在主Isolate中使用Completer来获取结果。
以下是一个示例,演示如何使用Completer和Isolate一起实现异步操作和并发执行:
Future calculateFibonacci(int n) async { Completer completer = Completer(); ReceivePort receivePort = ReceivePort(); await Isolate.spawn(calculate, [n, receivePort.sendPort]); receivePort.listen((result) { completer.complete(result); receivePort.close(); }); return completer.future;}void calculate(List args) { int n = args[0]; SendPort sendPort = args[1]; // 计算斐波那契数列的第n项 int result = fibonacci(n); // 将结果发送回主Isolate sendPort.send(result);}int fibonacci(int n) { if (n <= 0) { return 0; } else if (n == 1) { return 1; } else { return fibonacci(n - 1) + fibonacci(n - 2); }}int result = await calculateFibonacci(20);
在上面的示例中,首先创建一个Completer对象和一个ReceivePort对象。然后使用Isolate.spawn()方法创建一个新的Isolate,并将calculate函数作为入口点来执行。在calculate函数中,计算斐波那契数列的第n项,并将结果发送到主Isolate。在主Isolate中,通过监听ReceivePort来等待新Isolate的结果,并使用Completer来获取结果。
使用Completer和Isolate一起实现异步操作和并发执行可以提高应用程序的性能和响应速度,特别是在需要执行耗时的操作时。但是需要注意,使用Isolate也会带来一些额外的开销和复杂性,因此应根据实际需求进行权衡和选择。
4.Flutter项目结构和文件组织方式
在哪里归档图片资源以及如何处理不同分辨率?
虽然Android将resources和assets区别对待,但在Flutter中它们都会被作为assets处理,所有存在于Androidres/drawable文件夹中的资源都放在Flutter的assets文件夹中。与Android类似,iOS同样将images和assets作为不同的东西,而Flutter中只有assets。被放到iOS中Assets.xcassets文件夹下的资源在Flutter中被放到了assets文件夹中。在Flutter中assets可以是任意类型的文件,而不仅仅是图片。例如,你可以把json文件data.json放置到assets文件夹中。
在pubspec.yaml文件中声明assets
assets:-assets/data.json
然后在代码中我们可以通过AssetBundle来访问它:
import 'dart:async; import package flutter services.dart;FutureloadAsset() async {retum await rootBundle.loadString("assets/data.json");}
对于图片,Flutter像iOS一样,遵循了一个简单的基于像素密度的格式。Imageassets可能是1.0x 2.0x 3.0x或是其他的任何倍数。这个device PixelRatio表示了物理像素到单个逻辑像素的比率。
Android不同像素密度的图片和Flutter的像素比率的对应关系
- ldpi 0.75x
- mdpi 1.0x
- hdpi 1.5x
- xhdpi 2.0x
- xxhdpi 3.0x
- xxxhdpi 4.0x
Assets可以被放置到任何属性文件夹中,Flutter并没有预先定义的文件结构。我们需要在pubspec.yaml文件中声明assets的位置,然后 Flutter会把他们识别出来。举个例子🌰,要把一个名为my_icon.png的图片放到Flutter工程中,你可能想要把它放到images文件夹中。把图片(1.0x)放置到images文件夹中,并把其它分辨率的图片放在对应的子文件夹中,并接上合适的比例系数,就像这样:
- images/my_icon.png // 1.0ximage
- images/2.0x/my_icon.png // 2.0x image
- images/3.0x/my_icon.png // 3.0ximage
接下来就可以在pubspec.yaml文件中这样声明这个图片资源:
assets:-assets/images
现在,我们就可以来访问它了。
Image.asset("assets/images/my_icon.png")
但是,这种一直用手写路径的方式就很麻烦而且不安全对吧,那怎么能够我们不手写直接引用路径呢,这个时候我们就需要引用flutter_gen,他可以帮助我们完成这个工作,具体使用可参考我的另外一篇博客 使用FlutterGen快乐生成配置文件
如何归档strings资源,以及如何处理不同语言?
不像iOS拥有一个Localizable.strings文件,Flutter目前没有专门的字符串资源系统。目前,最佳做法是将strings资源作为静态字段保存在中。 例如:
class Strings {static String welcomeMessage="Welcome To Flutter”:}
然后像如下方式来访问它:
Text(Strings.welcomeMessage)
默认情况下,Flutter只支持美式英语字符串。如果你要支持其他语言,请引入flutter localizations包。你可能也要引入intl包来支持其他的 i10n机制,比如日期/时间格式化。
dependencies: flutter: sdk: flutter flutter_localizations:
配置完成之后运行flutter pub get,如果要使用flutter localizations包,还需要在appwidget中指定localizationsDelegates和supportedLocales。
MaterialApp( title: 'Flutter Demo', localizationsDelegates: const [ GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, S.delegate ], supportedLocales: const [ Locale('zh', 'CN'), Locale('en', 'US'), ], locale: const Locale('en', 'US'), theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(), )
这些代理包括了实际的本地化值,并目supportedLocales定义了App支持哪些地区。上面的例子用了一个MaterialApp,所以它既有GlobalWidgetsLocalizations 用于基础widgets,也有MaterialWidgetsLocalizations用于Material wigets的本地化。如果你使用WidgetsApp,则无需包括后者。注意,这两个代理虽然包括了“默认”值,但如果你想让你的App本地化,你仍需要提供一或多个代理作为你的App本地化副本。
当初始化时,MaterialApp会使用你指定的代理为你创建一个Localizations widget,Localizations widget可以随时从当前上下文中访问设备的地点,或者使用Window.locale。
要访问本地化文件,使用Localizations.of()方法来访问提供代理的特定本地化类。如需翻译,使用intl_translation包来取出翻译副本到arb文件中。把它们引入App中,并用intl来使用它们。
具体使用可以参考我的另一篇博客Flutter国际化
- 如何添加Flutter项目所需的依赖?
在Android中,你可以在Gradle文件来添加依赖项,在ios中,通常把依赖添加到Podfile中,
在RN中,通常是由packagejson来管理项目依赖,如何添加Flutter项目所需的依赖?
Flutter 使用 Dart 构建系统和Pub包管理器来处理依赖。这些工具将Android和iOS native包装应用程序的构建委派给相应的构建系统。
dependencies: flutter:sdk: fluttergoogle_sign_in: ^3.0.3
在Flutter中,虽然在Flutter项目中的Android文件夹下有Gradle文件,但只有在添加平台相关所需的依赖关系时才使用这些文件,否则应该使用pubspec.yaml来声明用于Flutter的外部依赖项。
iOS也是一样,如果你的Flutter工程中的iOS文件夹中有Podfile,请仅在添加iOS平台相关的依赖时使用它。
5.Flutter Widgets和布局基础
-
初识StatelessWidget和StatefulWidget
-
StatelessWidget是一个不需要状态更改的widget, 它没有要管理的内部状态。当您描述的用户界面部分不依赖于对象本身中的配置信息以及widget的BuildContext 时,无状态widget非常有用。
-
StatefulWidget 是可变状态的widget。 使用setState方法管理StatefulWidget的状态的改变。调用setState告诉Flutter框架,某个状态发生了变化,Flutter会重新运行build方法,以便应用程序可以应用最新状态。状态是在构建widget时可以同步读取的信息,可能会在widget的生命周期中发生变化。确保在状态改变时及时通知状态变化是widget实现者的责任。当widget可以动态更改时,需要使用StatefulWidget。
-
Flutter常用布局
Flutter中拥有30多种预定义的布局widget,常用的有Container、Padding、Center、Flex、Row、Colum、ListView、GridView。用一个表格列出它们的特性和使用。
name | usage |
---|---|
Container | 只有一个子 Widget,默认充满,包含 padding,margin,color,width,height,decoration 等设置 |
Padding | 只有一个子 Widget,只用于设置 Padding,用来嵌套child,给child 设置padding |
Center | 只有一个子 Widget,只用于居中显示,常用于嵌套 child,给 child 设置居中 |
Stack | 可以有多个子Widget,子Widget堆在一起 ,类似android的RelativeLayout |
Colum | 垂直排列布局,可以有多个子 Widget,类似android的vertical方向的LinearLayout |
Row | 水平排列布局,可以有多个子 Widget,类似android的horizontal方向的LinearLayout |
Expanded | 只有一个子Widget,把Colum 和Row填满 |
ListView | 可设置水平排列和竖直排列 |
GridView | 网格布局,可设置水平或者竖直排列 |
Container:一个拥有绘制、定位、调整大小的widget。使用如下:
Container( width: 88, height: 88, margin: const EdgeInsets.fromLTRB(8, 8, 8, 8), padding: const EdgeInsets.fromLTRB(8, 8, 8, 8), decoration: BoxDecoration( color: Colors.yellow, borderRadius: BorderRadius.circular(8)), child: const Text("果果"), )
Padding:可以给其子节点添加填充(留白),和边距效果类似。
Padding( padding: EdgeInsets.fromLTRB(8, 8, 8, 8), child: Text("果果"), )
Center:将其子widget居中显示在自身内部的widget##
Center( child: Text("果果"), )
Colum:在垂直方向上排列子Widget。
Column( children: [ Container( width: 88, height: 88, margin: const EdgeInsets.fromLTRB(8, 8, 8, 8), padding: const EdgeInsets.fromLTRB(8, 8, 8, 8), decoration: BoxDecoration( color: Colors.yellow, borderRadius: BorderRadius.circular(8)), child: const Text("有装饰的果果"), ), const Center( child: Text("居中的果果"), ), ], )
Row:在水平方向上排列子widget。
Row( children: [ Container( width: 88, height: 88, margin: const EdgeInsets.fromLTRB(8, 8, 8, 8), padding: const EdgeInsets.fromLTRB(8, 8, 8, 8), decoration: BoxDecoration( color: Colors.yellow, borderRadius: BorderRadius.circular(8)), child: const Text("有装饰的果果"), ), const Center( child: Text("居中的果果"), ), ], )
Expanded组件:可以使Row、Column、Fiex等子组件在其主轴上方向展开并填充可用的空间,这里注意:Expanded组件必须用在Row、Column、Fiex内,并且从Expanded到封装它的Row、Column、Flex的路径必须只包括StatelessWidgets或者StatefulWidgets(不能是其他类型的组件,像RenderObjectWidget,它是渲染对象,不再改变尺寸,因此Expanded不能放进RenderObjectWidget)
Row( children: [ Container( width: 88, height: 88, margin: const EdgeInsets.fromLTRB(8, 8, 8, 8), padding: const EdgeInsets.fromLTRB(8, 8, 8, 8), decoration: BoxDecoration( color: Colors.yellow, borderRadius: BorderRadius.circular(8)), child: const Text("中黄黄"), ), const Expanded(child: Text("中黄黄")) ], )
ListView:是最常用的可滚动组件之一,它可以沿一个方向线性排布所有子组件。
默认构造函数有一个children参数,它接受一个Widget列表(List)。这种方式适合只有少量的子组件的情况,因为这种方式需要将所有children都提前创建好(这需要做大量工作),而不是等到子widget真正显示的时候再创建,也就是说通过默认构造函数构建的ListView没有应用基于Sliver的懒加载模型。实际上通过此方式创建的ListView和使用SingleChildScrollView+Column的方式没有本质的区别.
ListView( children: [ Container( width: 88, height: 88, margin: const EdgeInsets.fromLTRB(8, 8, 8, 8), padding: const EdgeInsets.fromLTRB(8, 8, 8, 8), decoration: BoxDecoration( color: Colors.yellow, borderRadius: BorderRadius.circular(8)), child: const Text("有装饰的果果"), ), const Text("居中的果果") ], )
ListView.builder:适合列表项比较多(或者无限)的情况,因为只有当子组件真正显示的时候才会被创建,也就说通过该构造函数创建的ListView是支持基于Sliver的懒加载模型的
ListView.builder( itemCount: 88, itemBuilder: (context, index) { return const Text( "果果", ); })
GridView可以构建一个二维网格列表
GridView( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 8, mainAxisSpacing: 8, childAspectRatio: 8, crossAxisSpacing: 8), children: const [ Text( "胖果果", ), Text( "瘦果果", ), Text( "不胖不瘦的果果", ), ], )
这里子元素的大小是通过crossAxisCount和childAspectRatio两个参数共同决定的。注意,这里的子元素指的是子组件的最大显示空间,注意确保子组件的实际大小不要超出子元素的空间。
GridView.Builder:适合表格比较或无限的情况
GridView.builder( itemCount: 88, shrinkWrap: true, padding: EdgeInsets.zero, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 8, mainAxisSpacing: 8, childAspectRatio: 8, crossAxisSpacing: 8), itemBuilder: (context, index) { return const Text( "果果", ); })
二、Flutter进阶知识
1.Flutter状态管理和数据传递
数据同步关系有以下三种类型
- 由上往下,传递给子孙节点
- 由下往上,传递给祖宗节点
- 兄弟节点传递
同步可能需要满足以下场景: 组件A共享数据给组件B时,
- 组件B可以实时拿到组件A的变化值,
- 可以监听到数据变更,
- 组件B可以通知组件A进行数据更改,
- 组件A可以决定是否需要重建。
Flutter提供了数据传递的几种方案:
-
InheritedWidget: 适用于父组件传递给子组件的场景, 可跨层级
-
Notification:适用于子组件通知父组件数据改变的场景
-
Broadcast: 消息广播机制
InheritedWidget适用于父组件传递给子组件的场景, 可跨层级,是一个功能型组件, 意味着它仅提供数据操作的功能,不提供UI构建。
使用InheritedWidget需要提供一个组件用来包装数据,它继承InheritedWidget类,把需要共享的数据和数据改变方法包装进来,并提供一个of方法方便在子widget往上找到这个功能组件。可以重写updateShouldNotify方法,在flutter中判断InheritedWidget是否需要重建,从而通知下层观察者组件更新数据时被调用。
子组件使用.of(context).XXX方法来获得共享的数据,同时可以覆写State对象的didChangeDependencies回调,该回调会在组件所依赖的数据发生变化时调用
下面用计数器的例子来说明InheritedWidget如何使用
class TestPage extends StatefulWidget { const TestPage({Key? key}) : super(key: key); @override State createState() => _TestPageState();}class _TestPageState extends State { int count = 0; @override Widget build(BuildContext context) { return Scaffold( body: Center( child: MyInheritedWidget( count: count, child: Column( mainAxisSize: MainAxisSize.min, children: [ const TestWidget(), IconButton( onPressed: () { setState(() { count++; }); }, icon: const Icon(Icons.add), ) ], ), ), )); }}class TestWidget extends StatefulWidget { const TestWidget({Key? key}) : super(key: key); @override State createState() => _TestWidgetState();}class _TestWidgetState extends State { @override Widget build(BuildContext context) { return Text( MyInheritedWidget.of(context)?.count.toString() ?? "", style: const TextStyle( color: Colors.black, fontSize: 14, fontWeight: FontWeight.w400, ), ); }}class MyInheritedWidget extends InheritedWidget { final int count; const MyInheritedWidget( {super.key, required this.count, required Widget child}) : super(child: child); static MyInheritedWidget? of(BuildContext context) { return context.dependOnInheritedWidgetOfExactType(); } @override bool updateShouldNotify(covariant MyInheritedWidget oldWidget) { return oldWidget.count != count; }}
点击Button之后,count会增加,即实现了数据从上到下的共享。
Notification适用于子组件通知父组件数据改变的场景,是通过dispatch方法将消息由子到父派发出来的,这种机制叫做通知冒泡,会通知到所有通过NotificationListener来监听的父节点,也可以通过中间的某个节点来中止。
Flutter中已经实现了很多类型的通知,比如滑动通知派发ScrollNotification,这些通知都是继承自基类Notifaction, 所以Flutter也支持自定义通知,派发消息给父类。
下面展示自定义消息派发的步骤:
//自定义通知class CustomNotification extends Notification { CustomNotification(this.msg); final String msg;}在子组件中通过dispatch派发消息class CustomChild extends StatelessWidget { @override Widget build(BuildContext context) { return RaisedButton( child: Text("Fire Notification"), onPressed: () => CustomNotification("lala").dispatch(context), ); }}在父组件中通过NotificationListener设置对自定义通知的监听class CustomNotificationRoute extends StatefulWidget { @override _CustomNotificationRouteState createState() => new _CustomNotificationRouteState();}class _CustomNotificationRouteState extends State { String _msg = "通知: "; @override Widget build(BuildContext context) { return Scaffold( body: NotificationListener( onNotification: (notification) { setState(() { _msg += notification.msg + " "; }); }, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [Text(_msg), CustomChild()], ) ) ); }}
dispatch方法为什么需要传入context?
因为context实际上是操作element的一个接口,它和element树的结构是保持一致的,当通知派发出来后,会从当前的context一直往上找。所以这里如果我们直接把子组件的实现写在父组件的类定义里边的话,父组件是没有办法接收到通知的,因为这个时候dispatch(context)的context是_CustomNotificationRouteState的,位于NotificationListener定义的上层,已经在监听的范围之外了
class _CustomNotificationRouteState extends State { String _msg = "通知: "; @override Widget build(BuildContext context) { return Scaffold( body: NotificationListener( onNotification: (notification) { setState(() { _msg += notification.msg + " "; }); }, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(_msg), RaisedButton( child: Text("Fire Notification"), onPressed: () => CustomNotification("lala").dispatch(context), ) ], ) ) ); }}
如何停止通知冒泡?
在onNotification函数中返回true即可。默认情况下onNotification返回false表示不阻止冒泡。
在 Flutter 中,Broadcast 是一种消息广播机制,可以用于多个 Widget 之间的通信。当 Widget 发送广播时,所有监听该广播的 Widget 都能够接收到这个消息。Flutter 中的 Broadcast 机制基于 Dart 中的 Stream API 实现,它的工作原理如下:
Widget A 发送广播,通过 StreamController 发送一个事件。
所有监听该广播的 Widget B、Widget C、Widget D 等都会接收到这个事件。
Widget B、Widget C、Widget D 等根据事件内容进行处理,并更新自身状态。
当 Widget B、Widget C、Widget D 等状态更新后,Flutter 框架会调用它们的 build() 方法进行重构,以更新 Widget 的显示。
下面是一个简单的 Broadcast 示例代码,假设有一个名为 MyBroadcast 的广播:
import 'dart:async';class MyBroadcast { static final StreamController _controller = StreamController.broadcast(); static Stream get stream => _controller.stream; static void sendEvent(String message) { _controller.sink.add(message); } static void dispose() { _controller.close(); }}
上述代码中,MyBroadcast 类是一个广播类,它包含一个静态的 _controller,用于创建广播流,并通过 stream 属性公开该流。sendEvent 方法用于发送广播事件,dispose 方法用于关闭广播流。
在需要接收广播的 Widget 中,可以使用 StreamBuilder 或 StreamSubscription 来监听 MyBroadcast 广播,例如:
StreamBuilder( stream: MyBroadcast.stream, builder: (context, snapshot) { if (!snapshot.hasData) { return Container(); } return Text(snapshot.data); },),
上述代码中,使用 StreamBuilder 监听 MyBroadcast 广播,当广播事件发生时,会根据事件内容构建一个 Text Widget,以显示广播消息
两者的区别是啥呢?
Flutter 中的 Notification 和 Broadcast 都是用于 Widget 之间通信的机制,但它们之间有一些区别。
发送者和接收者的关系不同
Notification 是一种由 Widget 主动发送的通知,它通常是由某个 Widget 发起,然后由 Widget 树中的其他 Widget 监听和处理。而 Broadcast 是一种广播机制,它是由全局事件总线(Global Event Bus)发起的,任何 Widget 都可以注册成为广播接收者,接收来自全局事件总线的广播消息。
传递方式不同
Notification 是一种向上冒泡的机制,即通知从发送者 Widget 开始向上逐级传递,直到找到一个处理该通知的 Widget 为止。这种方式可以保证只有处于 Widget 树中某个特定位置的 Widget 才能接收到通知。而 Broadcast 则是一种向全局发送的机制,广播消息会被所有注册为广播接收者的 Widget 接收到。
传递内容不同
Notification 和 Broadcast 的传递内容也不同。Notification 通常是一个自定义的类实例,包含一些要传递的数据,而 Broadcast 则只是一个字符串,用于标识广播的类型或名称。在接收到广播消息后,Widget 可以根据广播的名称来进行不同的操作。
综上所述,Notification 和 Broadcast 都是 Flutter 中用于 Widget 之间通信的机制,它们都有各自的优缺点和适用场景。如果需要向上冒泡传递通知,并且只想让处于 Widget 树中某个特定位置的 Widget 接收到通知,则可以使用 Notification;如果需要向所有注册为广播接收者的 Widget 发送广播消息,则可以使用 Broadcast。
其实还有一个最基础的setState(),但是慎用,还是简单说一下:
在 Flutter 中,Widget 的状态可以通过 StatefulWidget 和 StatelessWidget 来实现。其中,StatefulWidget 是有状态的 Widget,它可以在 Widget 的生命周期内保持状态的变化,而 StatelessWidget 是无状态的 Widget,它在 Widget 构建完成后不再保留状态。
对于 StatefulWidget,当 Widget 的状态发生变化时,需要通过调用 setState() 方法来触发 Widget 的重构,以更新 Widget 的状态并重新绘制 Widget。setState() 方法是 StatefulWidget 中一个重要的方法,可以通过它来告知 Flutter 框架状态发生了改变,需要重新绘制 Widget。
setState() 方法内部的工作原理如下:
首先,Flutter 框架会记录需要重建的 Widget。
然后,Flutter 框架会调用 build() 方法来重建 Widget。
在 build() 方法中,Flutter 框架会根据 Widget 的新状态来构建 Widget 树,并返回一个新的 Widget 树。
最后,Flutter 框架会比较新旧 Widget 树的差异,并将差异应用到渲染树中,以更新 Widget 的显示。
需要注意的是,setState() 方法并不是立即执行的,而是将其标记为“脏”状态,等到下一次构建时再执行。因此,如果在 setState() 方法调用后立即访问 Widget 的状态,可能得到的还是旧的状态。为了避免这种情况,可以使用 Future.microtask() 或 WidgetsBinding.instance.addPostFrameCallback() 方法来在下一次构建之后获取 Widget 的最新状态。
setState()的刷新区域如果控制不好的话,会引起大范围的重绘,慎用。
Flutter还有一些其他方式能够进行数据传递,例如:
- Event Bus:事件广播,可适于于各种类型的数据通知同步
- Provider,GetX :状态管理方案,适用于复杂应用,可以方便的实现多种类型的数据同步。
Eventbus,provider,Getx的使用官网已经有了很好的讲解,这里就不赘述了
2.Flutter动画和过渡效果
隐式(全自动)动画
所谓隐式动画就是只需要设置动画目标,过程控制由系统实现。
一般是简单点的动画,比如只是简单的宽高变化。当然使用简单不代表功能就简单,下面会有体现
没有循环重播,不用随时中断,没有多方协调,就是从开始运行到结束。这种动画一般就是隐式动画,使用Flutter提供的api,隐式动画一般是Animated…开头,便于学习记忆。还可以自定义隐式动画:TweenAnimationBuilder。
AnimatedContainer
动画盒子,是作用到盒子属性上的,所以盒子有的一些属性,是可以动画效果的,但是每个widget都有自己的管理,所以,如果想实现盒子里widget的动画就需要别的方式了,实现很简单,就两行代码,AnimatedContainer,duration变量。flutter会隐式的帮我们处理动画过程。实现简单,但是功能可不简单,container有个decoration属性,该属性能实现的功能是很丰富的。所以,只要是盒子这个级别的所有变化都是可以动画的。
不同控件间切换的过渡动画:AnimatedSwitcher依然是个widget,是个盒子,但是该盒子是用于处理内部child组件切换时候动画的。也是只有一个属性:duration。flutter会隐式帮我们处理动画过程但是当子组件没有变化的时候,就没有动画了。
比如:AnimatedSwitcher内的child组件是个text,text内容有变化的时候,就不会有动画,这是因为flutter判断child子组件没有发生变化,就没有触发动画过程
return AnimatedContainer( duration: const Duration(milliseconds: 300), width: 200, height: 300, child: AnimatedSwitcher( duration: const Duration(milliseconds: 400), //当内容有变化的时候就会触发动画 child: Text('content', key: UniqueKey(),), // 丰富的动画控制 transitionBuilder: (child, animation) { return FadeTransition( opacity: animation, child: ScaleTransition( scale: animation, child: child, ), ); } ), );
其他的一些可以自行尝试,一般就是Animated…开头的动画组件
- AnimatedOpacity
- AnimatedPadding
- …
TweenAnimationBuilder
补间动画,重点在于开始和结束值,flutter帮我们补帧。
代码流程:基于设备帧率,比如60帧,那在duration内flutter帮我们从begin值到end值补上58帧图画。builder回调1s内会被调60次,把tween当前值传给builder用于控制ui当前状态。
补间动画每次都是从当前值到end的动画过程,所以begin值就相当于一个动画初始值了
tween值设置的时候要结合具体动画控制的范围。比如Opacity是从0-1,rotate是0-6.28。
return TweenAnimationBuilder( tween: Tween(begin: 0.0, end: 1.0), duration: const Duration(seconds: 2), builder: (context, double value, widget) { return Opacity( opacity: value, child: Container( width: 200, height: 300, color: Colors.red, ), ); } );
显示(手动控制)动画
隐式动画的流程是系统控制的,大部分工作都是由系统来做的。只是需要我们设置begin和end值。但是有些是需要动画一直运行,或者其他的需要我们来控制更多的流程,就需要显示动画了,我们手动控制动画流程一般我们使用AnimationController的时候,直接在as中键入stanim使用模板代码:
class xxx extends StatefulWidget { const xxx({Key? key}) : super(key: key); @override _xxxState createState() => _xxxState();}class _xxxState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; @override void initState() { super.initState(); _controller = AnimationController(vsync: this); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Container(); }}
vsync:垂直信号,由于不同设备刷新频率不一样,告诉系统何时同步动画状态。with SingleTickerProviderStateMixin。vsync:this即可
controller是什么?
动画控制器,是跟系统有绑定的关系,通过vsync:this实现屏幕刷新回调builder回调。所以动画的实现最关心的是_controller持续的变动以及builder回调刷新,具体的动画控制可以自行实现,用Tween修改动画范围
比如我们使用RotationTransition的时候turns参数就可以传_controller,turns的入参就可以用AnimationController做一个映射,自己来控制转的圈数:turns:_controller.drive(Tween(end: 4)) 这样实现了_controller的duration内旋转4圈
同理其他…Transition以及AnimatedTransition也一样可以这样用。比如FadeTransition,opacity就可以直接用_controller。
另外可以addListener监听它的值变化,也可以用.value获取动画过程中当前值
turns传的controller怎么会是个double呢?
上面说的RotatationTransition,turns参数怎么可以用_controller呢?它是继承自extends Animation的。
比如ScaleTransition,scale入参Tween(),可以用_controller.drive(Tween())包装为Animation类型。作用就是_controller的duration范围内Tween补帧缩放范围.
比如AlignTransition,alignment入参是Animation类型,也可以用_controller.drive()驱动
return SlideTransition( position:_controller.drive(Tween(begin: Offset(0, 0), end: Offset(1, 1))), ); return AlignTransition( alignment: _controller.drive(Tween(begin: Alignment.topLeft, end: Alignment.bottomRight)), child: const Text("d"), );
ticker是什么?
英文释义是嘀嗒声,形象的表述为屏幕刷新频率,屏幕要刷新一帧的时候就发一个tick。
AnimationController初始化的时候设置的duration范围,有几种控制方式:
-
初始化controller时候,设置lowerBound,upperBound
-
scale:_controller.drive(Tween(begin:0.0, end:1.0)),
这种方式解决的是将Tween转为Animation -
还有种写法:交错动画
Tween(begin:Offset(0, -0.5), end:Offset(0, 8)).chain(CurveTween(curve: Curves.elasticInOut)).chain(CurveTween(curve: Interval(0.0, 0.2))).animate(_controller)
chain可以理解为一个函数,就好比g(h(f(x))),函数嵌套,_controller就像是动画的持续过程,这个是跟屏幕刷新有关,但是可以通过多函数复合作用于实际动画效果。比如上面的代码:offset移动和移动速度曲线同时作用于动画过程,就有了从开始到结束沿移动速度曲线进行移动的动画效果
自定义显示动画:AnimatedBuidler
自定义显示动画,一是把动画控制交给AnimationController,二是可以做一些性能优化。比如把动画渲染过程中不需要重绘的组件交给AnimatedBuidler
@override Widget build(BuildContext context) { return AnimatedBuilder( animation: _controller, //_controller控制内持续的调用builder builder: (context, child) { //每次调用builder,内部所有组件都会重新渲染,注意考虑优化性能 return Opacity( //这里value超过1也没关系,有自动纠错机制。并不会报错。透明度是0~1,如果超过1就还是1不透明的效果 opacity: _controller.value, child: Container( width: 300, height: 200 + 100 * _controller.value, color: Colors.red, child: child, ), ); }, //动画执行过程中,不需要变化的部分可以传给AnimatedBuilder, // 每次builder回调时候会再传出来,这样就避免了child部分的重新渲染 child: const Center( child: Text( "hello", style: TextStyle(fontSize: 24), ), ), ); }
还有一些动画,例如嵌入式lottie、Rive/Flare插件动画,感兴趣的小伙伴们可以自己去了解一下
3.Flutter网络请求和数据解析
网络请求框架
这两种框架都提供了和后端交互的功能,dio在封装上比http丰富一点,http可自行扩展。
数据解析
Flutter目前数据解析有三种方式,
4.Flutter本地存储和数据缓存
Flutter作为了一个跨平台框架,所以也提供了其他平台应该有的本地存储的方式,包含简单的key-value,数据库等,目前用的比较多的就是
对比一下优缺点,Shared_preferences使用起来最为简单,能够保存Key-Value形式的数据,Sqflite是 Flutter 的 SQLite 包,当你需要处理复杂关系的强关系数据时,是一个不错的选择数据库。但是我这里推荐Hive,它 是一个用纯 Dart 编写的轻量级、超快的键值数据库,它集成了Shared_preferences和Hive的功能,并且非常轻量,而且由于是纯Dart语言编写,Hive对于多平台的兼容性也比其他两种好。
5.Flutter渲染和性能优化
渲染流程
Flutter的渲染流程大致分为以下几个步骤:
Flutter通过Dart语言的代码来构建应用程序的widget树。Widget是Flutter应用程序的基本构建块,表示用户界面的一部分,可以是一些简单的控件,也可以是复杂的自定义控件。
在widget树中,每个widget都有一个对应的Element对象,Element是Flutter框架内部用于管理widget的对象。Flutter会将widget树转换成Element树,然后进行布局和渲染。
布局:Flutter会对Element树进行布局,根据每个widget的尺寸和约束条件计算其位置和大小。这一过程通常在布局阶段就已经完成,但是如果widget的尺寸或约束条件发生变化,布局过程也会重新执行。
绘制:在布局完成后,Flutter会将Element树转换成RenderObject树,然后进行绘制。RenderObject是Flutter内部用于管理widget绘制的对象,每个Element都会对应一个或多个RenderObject。绘制过程涉及到图形渲染引擎,包括OpenGL等。
将渲染结果显示在屏幕上:Flutter将绘制结果生成Bitmap位图,并将其显示在屏幕上,以呈现给用户。
需要注意的是,Flutter使用了类似于React的响应式框架,当widget的状态发生变化时,Flutter会自动重新构建widget树,然后进行布局和渲染。由于Flutter的布局和渲染是高效的,因此即使widget树的变化非常频繁,也可以实现流畅的用户界面。
常见的内存溢出和内存泄漏的场景
内存溢出和内存泄漏是Flutter应用程序中常见的问题,以下是一些可能导致内存溢出和内存泄漏的场景:
图片加载:在加载大量图片时,如果不及时释放已经加载的图片,可能会导致内存溢出。为了避免这个问题,可以使用Flutter提供的Image.asset或Image.network等方法来加载图片,并使用缓存机制来避免重复加载。
状态管理:如果在状态管理中不小心保留了过多的状态或状态引用,可能会导致内存泄漏。例如,如果使用InheritedWidget或Provider来管理状态,并且未及时释放不再需要的状态或引用,可能会导致内存泄漏。
异步操作:在执行异步操作时,如果未及时取消或清除未完成的操作,可能会导致内存泄漏。例如,在使用Future或Stream时,需要及时取消或清除已经不需要的操作。
自定义控件:在编写自定义控件时,如果不小心保留了过多的引用或状态,可能会导致内存泄漏。为了避免这个问题,可以使用StatefulWidget和State对象来管理状态,并确保在不需要时及时释放引用。
第三方库:在使用第三方库时,需要注意库是否存在内存泄漏的问题。如果发现第三方库存在内存泄漏问题,可以考虑使用其他库或自行修改源代码来解决问题。
以上是一些可能导致内存溢出和内存泄漏的场景,需要在开发过程中注意检查和解决这些问题。
优化检测工具
Flutter编译模式
- Release
- Profile
- Debug
Release模式,使用AOT预编译模式,预编译为机器码,通过编译生成对应架构的代码,在用户设备上直接运行对应的机器码,运行速度快,执行性能好;此模式关闭了所有调试工具,只支持真机。
Profile模式,和Release模式类似,使用AOT预编译模式,此模式最重要的作用是可以用DevTools来检测应用的性能,做性能调试分析。
Debug模式,使用JIT(Just in time)即时编译技术,支持常用的开发调试功能hot reload,在开发调试时使用,包括支持的调试信息、服务扩展、Observatory、DevTools等调试工具,支持模拟器和真机。
通过以上介绍我们可以知道,flutter为我们提供 profile模式启动应用,进行性能分析,profile模式在Release模式的基础之上,为分析工具提供了少量必要的应用追踪信息。
如何开启profile模式?
如果是独立flutter工程可以使用flutter run --profile启动。如果是混合 Flutter 应用,在 flutter/packages/flutter_tools/gradle/flutter.gradle 的 buildModeFor 方法中将 debug 模式改为 profile即可。
检测工具
Flutter Inspector (debug模式下)
Flutter Inspector有很多功能,其中有两个功能更值得我们去关注,例如:“Select Widget Mode” 和 “Highlight Repaints”。
Select Widget Mode点击 “Select Widget Mode” 图标(上图的左上角高亮的图标),可以在手机上查看当前页面的布局框架与容器类型。
通过“Select Widget Mode”我们可以快速查看陌生页面的布局实现方式。
Select Widget Mode模式下,也可以在app里点击相应的布局控件查看
Highlight Repaints
点击 “Highlight Repaints” 图标(上图右上角高亮的图标),它会 为所有 RenderBox 绘制一层外框,并在它们重绘时会改变颜色。
这样做帮你找到 App 中频繁重绘导致性能消耗过大的部分。
例如:一个小动画可能会导致整个页面重绘,这个时候使用 RepaintBoundary Widget 包裹它,可以将重绘范围缩小至本身所占用的区域,这样就可以减少绘制消耗。
Performance Overlay(性能图层)
在完成了应用启动之后,接下来我们就可以利用 Flutter 提供的渲染问题分析工具,即性能图层(Performance Overlay),来分析渲染问题了。
我们可以通过以下方式开启性能图层
MaterialApp( title: 'Flutter Demo', showPerformanceOverlay: true,//开启性能图层 localizationsDelegates: const [ GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, S.delegate ], supportedLocales: const [ Locale('zh', 'CN'), Locale('en', 'US'), ], locale: const Locale('en', 'US'), theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(), )
性能图层会在当前应用的最上层,以 Flutter 引擎自绘的方式展示 GPU 与 UI 线程的执行图表,而其中每一张图表都代表当前线程最近 300 帧的表现,如果 UI 产生了卡顿,这些图表可以帮助我们分析并找到原因。
下图演示了性能图层的展现样式。其中,GPU 线程的性能情况在上面,UI 线程的情况显示在下面,蓝色垂直的线条表示已执行的正常帧,绿色的线条代表的是当前帧:
如果有一帧处理时间过长,就会导致界面卡顿,图表中就会展示出一个红色竖条。
如果红色竖条出现在 GPU 线程图表,意味着渲染的图形太复杂,导致无法快速渲染;而如果是出现在了 UI 线程图表,则表示 Dart 代码消耗了大量资源,需要优化代码执行时间。
CPU Profiler(UI 线程问题定位)
在视图构建时,在 build 方法中使用了一些复杂的运算,或是在主 Isolate 中进行了同步的 I/O 操作。
我们可以使用 CPU Profiler 进行检测:
你需要手动点击 “Record” 按钮去主动触发,在完成信息的抽样采集后,点击 “Stop” 按钮结束录制。这时,你就可以得到在这期间应用的执行情况了。
其中:
x 轴:表示单位时间,一个函数在 x 轴占据的宽度越宽,就表示它被采样到的次数越多,即执行时间越长。
y 轴:表示调用栈,其每一层都是一个函数。调用栈越深,火焰就越高,底部就是正在执行的函数,上方都是它的父函数。
通过上述CPU帧图我们可以大概分析出哪些方法存在耗时操作,针对性的进行优化
一般的耗时问题,我们通常可以 使用 Isolate(或 compute)将这些耗时的操作挪到并发主 Isolate 之外去完成。
例如:复杂JSON解析子线程化
Flutter的isolate默认是单线程模型,而所有的UI操作又都是在UI线程进行的,想应用多线程的并发优势需新开isolate 或compute。无论如何await,scheduleTask 都只是延后任务的调用时机,仍然会占用“UI线程”, 所以在大Json解析或大量的channel调用时,一定要观测对UI线程的消耗情况。
Flutter布局优化
常规优化
常规优化即针对 build() 进行优化,build() 方法中的性能问题一般有两种:耗时操作和 Widget 层叠。
- 在 build() 方法中执行了耗时操作
我们应该尽量避免在 build() 中执行耗时操作,因为 build() 会被频繁地调用,尤其是当 Widget 重建的时候。
此外,我们不要在代码中进行阻塞式操作,可以将一般耗时操作等通过 Future 来转换成异步方式来完成。
对于 CPU 计算频繁的操作,例如图片压缩,可以使用 isolate 来充分利用多核心 CPU。
- build() 方法中堆叠了大量的 Widget
这将会导致三个问题:
-
代码可读性差:画界面时需要一个 Widget 嵌套一个 Widget,但如果 Widget 嵌套太深,就会导致代码的可读性变差,也不利于后期的维护和扩展。
-
复用难:由于所有的代码都在一个 build(),会导致无法将公共的 UI 代码复用到其它的页面或模块。
-
影响性能:我们在 State 上调用 setState() 时,所有 build() 中的 Widget 都将被重建,因此 build() 中返回的 Widget 树越大,那么需要重建的 Widget 就越多,也就会对性能越不利。
所以,你需要 控制 build 方法耗时,将 Widget 拆小,避免直接返回一个巨大的 Widget,这样 Widget 会享有更细粒度的重建和复用。
- 尽可能地使用 const 构造器
当构建你自己的 Widget 或者使用 Flutter 的 Widget 时,这将会帮助 Flutter 仅仅去 rebuild 那些应当被更新的 Widget。
因此,你应该尽量多用 const 组件,这样即使父组件更新了,子组件也不会重新进行 rebuild 操作。特别是针对一些长期不修改的组件,例如通用报错组件和通用 loading 组件等。
- 列表优化
尽量避免使用 ListView默认构造方法
不管列表内容是否可见,会导致列表中所有的数据都会被一次性绘制出来
建议使用 ListView 和 GridView 的 builder 方法
它们只会绘制可见的列表内容,类似于 Android 的 RecyclerView。
其实,本质上,就是对列表采用了懒加载而不是直接一次性创建所有的子 Widget,这样视图的初始化时间就减少了。
深入光栅化优化
优化光栅线程
屏幕显示器一般以60Hz的固定频率刷新,每一帧图像绘制完成后,会继续绘制下一帧,这时显示器就会发出一个Vsync信号,按60Hz计算,屏幕每秒会发出60次这样的信号。CPU计算好显示内容提交给GPU,GPU渲染好传递给显示器显示。
Flutter遵循了这种模式,渲染流程如图:
flutter通过native获取屏幕刷新信号通过engine层传递给flutter framework
所有的 Flutter 应用至少都会运行在两个并行的线程上:UI 线程和 Raster 线程。
UI 线程
构建 Widgets 和运行应用逻辑的地方。
Raster 线程
用来光栅化应用。它从 UI 线程获取指令将其转换成为GPU命令并发送到GPU。
我们通常可以使用Flutter DevTools-Performance 进行检测,步骤如下:
在 Performance Overlay 中,查看光栅线程和 UI 线程哪个负载过重。
在 Timeline Events 中,找到那些耗费时间最长的事件,例如常见的 SkCanvas::Flush,它负责解决所有待处理的 GPU 操作。
找到对应的代码区域,通过删除 Widgets 或方法的方式来看对性能的影响。
Flutter内存优化
-
const 实例化
const 对象只会创建一个编译时的常量值。在代码被加载进 Dart Vm 时,在编译时会存储在一个特殊的查询表里,仅仅只分配一次内存给当前实例。我们可以使用 flutter_lints 库对我们的代码进行检测提示
-
检测消耗多余内存的图片
Flutter Inspector:点击 “Highlight Oversizeded Images”,它会识别出那些解码大小超过展示大小的图片,并且系统会将其倒置,这些你就能更容易在 App 页面中找到它。
通过下面两张图可以清晰的看出使用“Highlight Oversizeded Images”的检测效果
针对这些图片,你可以指定 cacheWidth 和 cacheHeight 为展示大小,这样可以让 flutter 引擎以指定大小解析图片,减少内存消耗
-
针对 ListView item 中有 image 的情况来优化内存
ListView 不会销毁那些在屏幕可视范围之外的那些 item,如果 item 使用了高分辨率的图片,那么它将会消耗非常多的内存。
ListView 在默认情况下会在整个滑动/不滑动的过程中让子 Widget 保持活动状态,这一点是通过 AutomaticKeepAlive 来保证,在默认情况下,每个子 Widget 都会被这个 Widget 包裹,以使被包裹的子 Widget 保持活跃。其次,如果用户向后滚动,则不会再次重新绘制子 Widget,这一点是通过 RepaintBoundaries 来保证,在默认情况下,每个子 Widget 都会被这个 Widget 包裹,它会让被包裹的子 Widget 仅仅绘制一次,以此获得更高的性能。但这样的问题在于,如果加载大量的图片,则会消耗大量的内存,最终可能使 App 崩溃。
通过将这两个选项置为 false 来禁用它们,这样不可见的子元素就会被自动处理和 GC。
-
多变图层与不变图层分离
在日常开发中,会经常遇到页面中大部分元素不变,某个元素实时变化。如Gif,动画。这时我们就需要RepaintBoundary,不过独立图层合成也是有消耗,这块需实测把握。
这会导致页面同一图层重新Paint。此时可以用RepaintBoundary包裹该多变的Gif组件,让其处在单独的图层,待最终再一块图层合成上屏。
-
降级CustomScrollView,ListView等预渲染区域为合理值
默认情况下,CustomScrollView除了渲染屏幕内的内容,还会渲染上下各250区域的组件内容,例如当前屏幕可显示4个组件,实际仍有上下共4个组件在显示状态,如果setState(),则会进行8个组件重绘。实际用户只看到4个,其实应该也只需渲染4个, 且上下滑动也会触发屏幕外的Widget创建销毁,造成滚动卡顿。高性能的手机可预渲染,在低端机降级该区域距离为0或较小值。
6.Flutter代码结构和代码质量把控
Flutter应用程序的代码结构和代码质量对应用程序的可维护性和可扩展性有很大影响。以下是一些关于Flutter代码结构和代码质量的建议:
代码结构:
分层:将应用程序代码分层,将UI逻辑、业务逻辑、数据逻辑分别放在不同的层中,有助于代码的组织和维护。
模块化:将应用程序拆分为多个模块,每个模块都应该独立且可重用,以便于应对日后的功能扩展和需求变化。
文件命名:为每个文件命名时,应该清晰明了,表明文件的用途和所属模块,以便于其他开发者理解和维护代码。
代码质量:
可读性:编写易读易懂的代码可以使其他开发人员更容易理解和维护代码。
可测试性:编写可测试的代码可以使测试更容易实现和维护,从而提高代码质量和稳定性。
代码风格:统一的代码风格可以使代码更易读、易懂,也有助于多人协作开发。
错误处理:代码应该及时检测和处理错误,避免产生异常和崩溃。
内存管理:避免内存泄漏和过度消耗内存,使应用程序运行更加高效和稳定。
总之,良好的Flutter代码结构和代码质量是保证应用程序质量和可维护性的关键。
三、常用插件和第三方库
好用的状态管理
好用的网络请求
好用的图片加载
好用的音视频
-
视频播放
-
音频播放
-
录音
好用的控件
- 侧滑操作
- 上拉刷新
- loading
- html
- 发光条动效
好用的工具类
- 屏幕适配
- 权限申请
- 日志打印
- 国际化
- 时间格式化
- 软件相关信息
- 路径获取
- 二维码
- 应用商店跳转
- 封装好的工具类
好用的动画
好用的数据存储
好用的事件传递
好用的三方sdk
好用的编译器插件
国际化插件
- Flutter intl
代码模版生成插件
- Flutter Snippets
数据模型生成插件
- Flutter JsonBeanFactory
四、可能有用的小技巧
- Flutter json_serializable快乐生成数据模型
- 使用FlutterGen快乐生成配置文件
- 使用Idea运行Flutter项目中Android项目的Release模式
- Flutter国际化
好了,这期的分享就到这里,希望对大家有所帮助。
来源地址:https://blog.csdn.net/qq_33183882/article/details/129818175