Flutter2新特性:空安全 标准linter 轻量级多引擎 全平台支持 性能优化 navigator2(声明式路由管理)
迁移工作:解决破坏改动 代码迁移到空安全
自动生成迁移后的代码命令:dart migrate
查看三方库版本是否支持空安全命令:dart pub outdated —node=null-safety
Late:把编译器检查转移到运行期检查
适配参考:Flutter 升级 2.0 填坑指导,带你原地起飞_恋猫de小郭的博客-CSDN博客
适配视频参考:Flutter 2 迁移实践 - 王鑫磊_哔哩哔哩_bilibili
从Flutter 2开始,Flutter便在配置中默认启用了空安全,通过将空检查合并到类型系统中,可以在开发过程中捕获这些错误,从而防止再生产环境导致的崩溃。
时至今日,空安全已经是一个屡见不鲜的话题,目前像主流的编程语言Kotlin、Swift、Rust 等都对空安全有自己的支持。Dart从2.12版本开始支持了空安全,通过空安全开发人员可以有效避免null错误崩溃。空安全性可以说是Dart语言的重要补充,它通过区分可空类型和非可空类型进一步增强了类型系统。
Dart 的空安全支持基于以下三条核心原则:
在引入空安全前Dart的类型系统是这样的:
这意味着在之前,所有的类型都可以为Null,也就是Nul类型被看作是所有类型的子类。
在引入空安全之后:
可以看出,最大的变化是将Null类型独立出来了,这意味着Null不在是其它类型的子类型,所以对于一个非Null类型的变量传递一个Null值时会报类型转换错误。
提示:在使用了空安全的Flutter或Dart项目中你会经常看到
?.、!、late
的大量应用,那么他们分别是什么又改如何使用呢?请看下文的分析
我们可以通过将?
跟在类型的后面来表示它后面的变量或参数可接受Null:
class CommonModel {
String? firstName; //可空的成员变量
int getNameLen(String? lastName /*可空的参数*/) {
int firstLen = firstName?.length ?? 0;
int lastLen = lastName?.length ?? 0;
return firstLen + lastLen;
}
}
对于可空的变量或参数在使用的时候需要通过Dart 的避空运算符?.
来进行访问,否则会抛出编译错误。
当程序启用空安全后,类的成员变量默认是不可空的,所以对于一个非空的成员变量需要指定其初始化方式:
class CommonModel {
List names=[];//定义时初始化
final List colors;//在构造方法中初始化
late List urls;//延时初始化
CommonModel(this.colors);
...
对于无法在定义时进行初始化,并且又想避免使用?.
,那么延迟初始化可以帮到你。通过late
修饰的变量,可以让开发者选择初始化的时机,并且在使用这个变量时可以不用?.
。
late List urls;//延时初始化
setUrls(List urls){
this.urls=urls;
}
int getUrlLen(){
return urls.length;
}
延时初始化虽然能为我们编码带来一定便利,但如果使用不当会带来空异常的问题,所以在使用的时候一定保证赋值和访问的顺序,切莫颠倒。
延迟初始化(late)使用范式
在Flutter中State的initState
方法中初始化的一些变量是比较适合使用late来进行延时初始化的,因为在Widget生命周期中initState
方法是最先执行的,所以它里面初始化的变量通过late
修饰后既能保障使用时的便利,又能防止空异常,例子:
class _SpeakPageState extends State
with SingleTickerProviderStateMixin {
String speakTips = '长按说话';
String speakResult = '';
late Animation animation;
late AnimationController controller;
@override
void initState() {
controller = AnimationController(
super.initState();
vsync: this, duration: Duration(milliseconds: 1000));
animation = CurvedAnimation(parent: controller, curve: Curves.easeIn)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
});
}
...
使用 !
来暂时完成适配,比如某个参数你确定不会为 null
,你可以在使用时通过 !
表示强行使用(就是任性不判空)
当我们排除变量或参数的可空的可能后,可以通过!
来告诉编译器这个可空的变量或参数不可空,这对我们进行方法传参或将可空参数传递给一个不可空的入参时特别有用:
Widget get _listView {
return ListView(
children: [
_banner,
Padding(
padding: EdgeInsets.fromLTRB(7, 4, 7, 4),
child: LocalNav(localNavList: localNavList),
),
if (gridNavModel != null)
Padding(
padding: EdgeInsets.fromLTRB(7, 0, 7, 4),
child: GridNav(gridNavModel: gridNavModel!)),
Padding(
padding: EdgeInsets.fromLTRB(7, 0, 7, 4),
child: SubNav(subNavList: subNavList)),
if (salesBoxModel != null)
Padding(
padding: EdgeInsets.fromLTRB(7, 0, 7, 4),
child: SalesBox(salesBox: salesBoxModel!)),
],
);
}
上述代码在确保变量不为空的情况下使用了空值断言操作符!
。
除此之外,
!
还有一个常见的用处:
bool isEmptyList(Object object) {
if (object is! List) return false;
return object.isEmpty;
}
用在这里表示取反,上述代码等价于:
bool isEmptyList(Object object) {
if (!(object is List)) return false;
return object.isEmpty;
}
Flutter 2默认启用了空安全,所以通过Flutter 2创建的项目是已经开启了空安全的检查的,另外,小伙伴也可以可以通过下面命令来查看你的Flutter SDK版本:
flutter doctor
那么,如何手动开启和关闭空区安全的?
environment:
sdk: ">=2.12.0 <3.0.0" //sdk >=2.12.0表示开启空安全检查
提示:一旦项目开启了空安全检查,那么你的代码包括项目所依赖的三方插件必须是要支持空安全的否则是无法正常编译的。
如果想关闭空安全检查,可以将SDK的支持范围调整到2.12.0
以下即可,如:
environment:
sdk: ">=2.7.0 <3.0.0"
开启空安全之后,然后运行下项目你会看到很多的报错,然后定位到报错的文件,通过本章所讲技能进行适配。首先需要对文件进行分类:
自定义Widget的空安全适配分两种情况:
对于自定的Widget无论是页面的某控件还是整个页面,通常都会为Widget定义一些属性。在进行空安全适配时要对属性进行一下分类:
?
进行修饰required
进行修饰class WebView extends StatefulWidget {
String? url;
final String? statusBarColor;
final String? title;
final bool? hideAppBar;
final bool backForbid;
WebView(
{this.url,
this.statusBarColor,
this.title,
this.hideAppBar,
this.backForbid = false})
...
提示:如果构造方法中使用了
@required
那么需要改成required
。
State的空安全适配主要是根据它的成员变量是否可空进行分类:
?
进行修饰late
修饰为延时变量代码效果可以参考下:
class _TravelPageState extends State with TickerProviderStateMixin {
late TabController _controller; //延时初始
List tabs = []; //定义时初始化
...
@override
void initState() {
super.initState();
_controller = TabController(length: 0, vsync: this);
...
数据模型(Model)空安全适配主要以下两种情况:
含有命令构造函数的模型
接下来以含有命令构造函数的模型的空安全适配技巧:
适配前:
///旅拍页模型
class TravelItemModel {
int totalCount;
List resultList;
TravelItemModel.fromJson(Map json) {
totalCount = json['totalCount'];
if (json['resultList'] != null) {
resultList = new List();
json['resultList'].forEach((v) {
resultList.add(new TravelItem.fromJson(v));
});
}
}
Map toJson() {
final Map data = new Map();
data['totalCount'] = this.totalCount;
if (this.resultList != null) {
data['resultList'] = this.resultList.map((v) => v.toJson()).toList();
}
return data;
}
}
适配之前首先要和服务端协商好,模型中那些字段可空,那些字段是一定会下发的。对于这个案例假如:totalCount字段是一定会下发的,resultList字段是不能保证一定会下发,那么我们可以这样来适配:
适配后:
///旅拍页模型
class TravelItemModel {
late int totalCount;
List? resultList;
//命名构造方法
TravelItemModel.fromJson(Map json) {
totalCount = json['totalCount'];
if (json['resultList'] != null) {
resultList = new List.empty(growable: true);
json['resultList'].forEach((v) {
resultList!.add(new TravelItem.fromJson(v));
});
}
}
Map toJson() {
final Map data = new Map();
data['totalCount'] = this.totalCount;
data['resultList'] = this.resultList!.map((v) => v.toJson()).toList();
return data;
}
}
late
来修饰为延迟初始化的字段以方便访问?
将其修饰为可空的变量含有命名工厂构造函数的模型
命名工厂构造函的数据模型也是比较常见的数据模型之一,公共数据模型为例来分享含有命名工厂构造函的数据模型的空安全适配技巧:
适配前:
class CommonModel {
final String icon;
final String title;
final String url;
final String statusBarColor;
final bool hideAppBar;
CommonModel(
{this.icon, this.title, this.url, this.statusBarColor, this.hideAppBar});
factory CommonModel.fromJson(Map json) {
return CommonModel(
icon: json['icon'],
title: json['title'],
url: json['url'],
statusBarColor: json['statusBarColor'],
hideAppBar: json['hideAppBar']
);
}
}
含有命名工厂构造函数的模型通常需要有自己的构造函数,构造函数通常采用可选参数,所以在进行适配时首先要明确哪些字段一定不为空,哪些字段可空,确认好之后就可以进行下面适配了:
适配后:
class CommonModel {
final String? icon;
final String? title;
final String url;
final String? statusBarColor;
final bool? hideAppBar;
CommonModel(
{this.icon,
this.title,
required this.url,
this.statusBarColor,
this.hideAppBar});
//命名工厂构造函数必须要有返回值,类似static 函数无法访问成员变量和方法
factory CommonModel.fromJson(Map json) {
return CommonModel(
icon: json['icon'],
title: json['title'],
url: json['url'],
statusBarColor: json['statusBarColor'],
hideAppBar: json['hideAppBar']
);
}
}
?
进行修饰required
修饰符来表示这个参数是必传参数单例是Flutter开发中使用最广的一种设计模式,那么单例该如何适配空安全呢?
适配前:
///缓存管理类
class HiCache {
SharedPreferences prefs;
static HiCache _instance;
HiCache._() {
init();
}
HiCache._pre(SharedPreferences prefs) {
this.prefs = prefs;
}
static Future preInit() async {
if (_instance == null) {
var prefs = await SharedPreferences.getInstance();
_instance = HiCache._pre(prefs);
}
return _instance;
}
static HiCache getInstance() {
if (_instance == null) {
_instance = HiCache._();
}
return _instance;
}
void init() async {
if (prefs == null) {
prefs = await SharedPreferences.getInstance();
}
}
setString(String key, String value) {
prefs.setString(key, value);
}
setDouble(String key, double value) {
prefs.setDouble(key, value);
}
setInt(String key, int value) {
prefs.setInt(key, value);
}
setBool(String key, bool value) {
prefs.setBool(key, value);
}
setStringList(String key, List value) {
prefs.setStringList(key, value);
}
T get(String key) {
return prefs?.get(key) ?? null;
}
}
适配后:
class HiCache {
SharedPreferences? prefs;
static HiCache? _instance;
HiCache._() {
init();
}
HiCache._pre(SharedPreferences prefs) {
this.prefs = prefs;
}
static Future preInit() async {
if (_instance == null) {
var prefs = await SharedPreferences.getInstance();
_instance = HiCache._pre(prefs);
}
return _instance!;
}
static HiCache getInstance() {
if (_instance == null) {
_instance = HiCache._();
}
return _instance!;
}
void init() async {
if (prefs == null) {
prefs = await SharedPreferences.getInstance();
}
}
setString(String key, String value) {
prefs?.setString(key, value);
}
setDouble(String key, double value) {
prefs?.setDouble(key, value);
}
setInt(String key, int value) {
prefs?.setInt(key, value);
}
setBool(String key, bool value) {
prefs?.setBool(key, value);
}
setStringList(String key, List value) {
prefs?.setStringList(key, value);
}
remove(String key) {
prefs?.remove(key);
}
T? get(String key) {
var result = prefs?.get(key);
if (result != null) {
return result as T;
}
return null;
}
}
核心适配的地方主要有两点:
目前在Dart的官方插件平台上的主流插件都陆续进行了空安全支持,如果你的项目开启了空安全那么所有使用的插件也必须是要支持空安全的,否则会导致无法编译:
Xcode's output:
↳
Error: Cannot run with sound null safety, because the following dependencies
don't support null safety:
- package:flutter_splash_screen
遇到这个问题后可以到Dart的官方插件平台查看这个flutter_splash_screen插件是否有支持了空安全的版本。如果插件支持了空安全插件平台会为其打上空安全的标:
如果你所使用的某个插件还不支持空安全,而且你又必须要使用这个插件,那么可以通过上文所讲的方式来关闭空安全检查。
通过Flutter进阶拓展:开发包和插件开发的学习,有不少小伙伴已经开发并发布了一些插件,那么该如何为你所开发的插件适配空安全呢?
回顾我对一些插件的适配的整个过程来讲,可以分为三个关键步骤:
type ‘Null’ is not a subtype of type ‘xxx’
问题描述
运行APP后控制台输出上述log
问题分析
导致此问题的主要原因将一个null值传递给了一个不能为null的参数,常见在使用model时,如:
type 'Null' is not a subtype of type 'String'.
type 'Null' is not a subtype of type 'bool'.
解决方案
上图是进行网络请求时在将json数据转换成model的时候报的错误,通过这种方式定位到报错是发生在:
package:flutter_trip/model/travel_model.dart
文件的第272行late bool isWaterMarked;
//改成
bool? isWaterMarked;