一巫橄、走進(jìn)空安全(空安全最小必備知識(shí))
從Flutter 2開(kāi)始椰棘,F(xiàn)lutter便在配置中默認(rèn)啟用了空安全,通過(guò)將空檢查合并到類(lèi)型系統(tǒng)中壳坪,可以在開(kāi)發(fā)過(guò)程中捕獲這些錯(cuò)誤暇昂,從而防止再生產(chǎn)環(huán)境導(dǎo)致的崩潰莺戒。
什么是空安全
時(shí)至今日,空安全已經(jīng)是一個(gè)屢見(jiàn)不鮮的話題急波,目前像主流的編程語(yǔ)言Kotlin从铲、Swift、Rust 等都對(duì)空安全有自己的支持澄暮。Dart從2.12版本開(kāi)始支持了空安全名段,通過(guò)空安全開(kāi)發(fā)人員可以有效避免null錯(cuò)誤崩潰∑茫空安全性可以說(shuō)是Dart語(yǔ)言的重要補(bǔ)充伸辟,它通過(guò)區(qū)分可空類(lèi)型和非可空類(lèi)型進(jìn)一步增強(qiáng)了類(lèi)型系統(tǒng)。
引入空安全的好處
- 可以將原本運(yùn)行時(shí)的空值引用錯(cuò)誤將變?yōu)榫庉嫊r(shí)的分析錯(cuò)誤馍刮;
- 增強(qiáng)程序的健壯性信夫,有效避免由Null而導(dǎo)致的崩潰;
- 跟隨Dart和Flutter的發(fā)展趨勢(shì)卡啰,為程序的后續(xù)迭代不留坑静稻;
空安全最小必備知識(shí)
- 空安全的原則
- 引入空安全前后Dart類(lèi)型系統(tǒng)的變化
- 可空(?)類(lèi)型的使用
- 延遲初始化(late)的使用
- 空值斷言操作符(!)的使用
空安全的原則
Dart 的空安全支持基于以下三條核心原則:
- 默認(rèn)不可空:除非您將變量顯式聲明為可空,否則它一定是非空的類(lèi)型碎乃;
- 漸進(jìn)遷移:您可以自由地選擇何時(shí)進(jìn)行遷移,多少代碼會(huì)進(jìn)行遷移惠奸;
- 完全可靠:Dart 的空安全是非趁肥模可靠的,意味著編譯期間包含了很多優(yōu)化佛南,
- 如果類(lèi)型系統(tǒng)推斷出某個(gè)變量不為空梗掰,那么它 永遠(yuǎn) 不為空。當(dāng)您將整個(gè)項(xiàng)目和其依賴(lài)完全遷移至空安全后嗅回,您會(huì)享有健全性帶來(lái)的所有優(yōu)勢(shì)——更少的 BUG及穗、更小的二進(jìn)制文件以及更快的執(zhí)行速度。
引入空安全前后Dart類(lèi)型系統(tǒng)的變化
在引入空安全前Dart的類(lèi)型系統(tǒng)是這樣的:
這意味著在之前绵载,所有的類(lèi)型都可以為Null埂陆,也就是Nul類(lèi)型被看作是所有類(lèi)型的子類(lèi)苛白。
在引入空安全之后:
可以看出,最大的變化是將Null類(lèi)型獨(dú)立出來(lái)了焚虱,這意味著Null不在是其它類(lèi)型的子類(lèi)型购裙,所以對(duì)于一個(gè)非Null類(lèi)型的變量傳遞一個(gè)Null值時(shí)會(huì)報(bào)類(lèi)型轉(zhuǎn)換錯(cuò)誤。
提示:在使用了空安全的Flutter或Dart項(xiàng)目中你會(huì)經(jīng)尘樵裕看到
?.躏率、!、late
的大量應(yīng)用民鼓,那么他們分別是什么又改如何使用呢薇芝?請(qǐng)看下文的分析
可空(?)類(lèi)型的使用
我們可以通過(guò)將?
跟在類(lèi)型的后面來(lái)表示它后面的變量或參數(shù)可接受Null:
class CommonModel {
String? firstName; //可空的成員變量
int getNameLen(String? lastName /*可空的參數(shù)*/) {
int firstLen = firstName?.length ?? 0;
int lastLen = lastName?.length ?? 0;
return firstLen + lastLen;
}
}
對(duì)于可空的變量或參數(shù)在使用的時(shí)候需要通過(guò)Dart 的避空運(yùn)算符?.
來(lái)進(jìn)行訪問(wèn),否則會(huì)拋出編譯錯(cuò)誤丰嘉。
當(dāng)程序啟用空安全后夯到,類(lèi)的成員變量默認(rèn)是不可空的,所以對(duì)于一個(gè)非空的成員變量需要指定其初始化方式:
class CommonModel {
List names=[];//定義時(shí)初始化
final List colors;//在構(gòu)造方法中初始化
late List urls;//延時(shí)初始化
CommonModel(this.colors);
...
延遲初始化(late)的使用
對(duì)于無(wú)法在定義時(shí)進(jìn)行初始化供嚎,并且又想避免使用?.
黄娘,那么延遲初始化可以幫到你。通過(guò)late
修飾的變量克滴,可以讓開(kāi)發(fā)者選擇初始化的時(shí)機(jī)逼争,并且在使用這個(gè)變量時(shí)可以不用?.
。
late List urls;//延時(shí)初始化
setUrls(List urls){
this.urls=urls;
}
int getUrlLen(){
return urls.length;
}
延時(shí)初始化雖然能為我們編碼帶來(lái)一定便利劝赔,但如果使用不當(dāng)會(huì)帶來(lái)空異常的問(wèn)題誓焦,所以在使用的時(shí)候一定保證賦值和訪問(wèn)的順序,切莫顛倒着帽。
延遲初始化(late)使用范式
在Flutter中State的initState
方法中初始化的一些變量是比較適合使用late來(lái)進(jìn)行延時(shí)初始化的杂伟,因?yàn)樵赪idget生命周期中initState
方法是最先執(zhí)行的,所以它里面初始化的變量通過(guò)late
修飾后既能保障使用時(shí)的便利仍翰,又能防止空異常赫粥,下面就以Flutter從入門(mén)到進(jìn)階-語(yǔ)音搜索模塊為例來(lái)看下具體的用法:
class _SpeakPageState extends State<SpeakPage>
with SingleTickerProviderStateMixin {
String speakTips = '長(zhǎng)按說(shuō)話';
String speakResult = '';
late Animation<double> 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();
}
});
}
...
空值斷言操作符(!)的使用
當(dāng)我們排除變量或參數(shù)的可空的可能后,可以通過(guò)!
來(lái)告訴編譯器這個(gè)可空的變量或參數(shù)不可空予借,這對(duì)我們進(jìn)行方法傳參或?qū)⒖煽諈?shù)傳遞給一個(gè)不可空的入?yún)r(shí)特別有用:
Widget get _listView {
return ListView(
children: <Widget>[
_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!)),
],
);
}
上述代碼是Flutter從入門(mén)到進(jìn)階-首頁(yè)模塊根據(jù)gridNavModel與salesBoxModel模塊數(shù)據(jù)是否為空時(shí)動(dòng)態(tài)創(chuàng)建的列表越平,在確保變量不為空的情況下使用了空值斷言操作符!
。
除此之外灵迫,
!
還有一個(gè)常見(jiàn)的用處:
bool isEmptyList(Object object) {
if (object is! List) return false;
return object.isEmpty;
}
用在這里表示取反秦叛,上述代碼等價(jià)于:
bool isEmptyList(Object object) {
if (!(object is List)) return false;
return object.isEmpty;
}
二、Flutter如何做空安全適配
Step 1: 啟用空安全
Flutter 2默認(rèn)啟用了空安全瀑粥,所以通過(guò)Flutter 2創(chuàng)建的項(xiàng)目是已經(jīng)開(kāi)啟了空安全的檢查的挣跋,另外,小伙伴也可以可以通過(guò)下面命令來(lái)查看你的Flutter SDK版本:
flutter doctor
那么狞换,如何手動(dòng)開(kāi)啟和關(guān)閉空區(qū)安全的避咆?
environment:
sdk: ">=2.12.0 <3.0.0" //sdk >=2.12.0表示開(kāi)啟空安全檢查
提示:一旦項(xiàng)目開(kāi)啟了空安全檢查舟肉,那么你的代碼包括項(xiàng)目所依賴(lài)的三方插件必須是要支持空安全的否則是無(wú)法正常編譯的。
如果想關(guān)閉空安全檢查牌借,可以將SDK的支持范圍調(diào)整到2.12.0
以下即可度气,如:
environment:
sdk: ">=2.7.0 <3.0.0"
Step 2: 進(jìn)行空安全適配
開(kāi)啟空安全之后,然后運(yùn)行下項(xiàng)目你會(huì)看到很多的報(bào)錯(cuò)膨报,然后定位到報(bào)錯(cuò)的文件磷籍,通過(guò)本章所講技能進(jìn)行適配。首先需要對(duì)文件進(jìn)行分類(lèi):
- 自定義Widget(包含你所創(chuàng)建的Flutter頁(yè)面)如:
- Flutter高級(jí)進(jìn)階實(shí)戰(zhàn) 仿嗶哩嗶哩APP中的:登錄现柠、注冊(cè)院领、首頁(yè)、收藏够吩、排行比然、詳情等頁(yè)面,以及
video_card.dart
周循、login_effect.dart
强法、hi_tab.dart
、hi_banner.dart
等自定義widget湾笛。 - Flutter從入門(mén)到進(jìn)階 實(shí)戰(zhàn)攜程網(wǎng)App中的:搜索饮怯、旅拍、我的嚎研、首頁(yè)等頁(yè)面蓖墅,以及
grid_nav.dart
、loading_container.dart
临扮、local_nav.dart
论矾、sales_box.dart
、search_bar.dart
杆勇、sub_nav.dart
等自定義widget贪壳。
- Flutter高級(jí)進(jìn)階實(shí)戰(zhàn) 仿嗶哩嗶哩APP中的:登錄现柠、注冊(cè)院领、首頁(yè)、收藏够吩、排行比然、詳情等頁(yè)面,以及
- 數(shù)據(jù)模型(Model)如:
- Flutter高級(jí)進(jìn)階實(shí)戰(zhàn) 仿嗶哩嗶哩APP中的:
home_mo.dart
、notice_mo.dart
蚜退、profile_mo.dart
闰靴、ranking_mo.dart
、video_detail_mo.dart
关霸、video_model.dart
等model传黄。 - Flutter從入門(mén)到進(jìn)階 實(shí)戰(zhàn)攜程網(wǎng)App中的:
common_model.dart
杰扫、config_model.dart
队寇、grid_nav_model.dart
、home_model.dart
章姓、sales_box_model.dart
佳遣、seach_model.dart
识埋、travel_model.dart
、travel_tab_model.dart
等model零渐。
- Flutter高級(jí)進(jìn)階實(shí)戰(zhàn) 仿嗶哩嗶哩APP中的:
- 單例如:
- Flutter高級(jí)進(jìn)階實(shí)戰(zhàn) 仿嗶哩嗶哩APP中的:
hi_navigator.dart
窒舟、hi_cache.dart
、hi_net.dart
等诵盼。
- Flutter高級(jí)進(jìn)階實(shí)戰(zhàn) 仿嗶哩嗶哩APP中的:
然后結(jié)合著后面對(duì)應(yīng)小節(jié)的教程進(jìn)行適配即可惠豺。
三、自定義Widget的空安全適配技巧
自定義Widget的空安全適配分兩種情況:
- Widget的空安全適配
- State的空安全適配
Widget的空安全適配
對(duì)于自定的Widget無(wú)論是頁(yè)面的某控件還是整個(gè)頁(yè)面风宁,通常都會(huì)為Widget定義一些屬性洁墙。在進(jìn)行空安全適配時(shí)要對(duì)屬性進(jìn)行一下分類(lèi):
- 可空的屬性:通過(guò)
?
進(jìn)行修飾 - 不可空的屬性:在構(gòu)造函數(shù)中設(shè)置默認(rèn)值或者通過(guò)
required
進(jìn)行修飾
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})
...
提示:如果構(gòu)造方法中使用了
@required
那么需要改成required
。
State的空安全適配
State的空安全適配主要是根據(jù)它的成員變量是否可空進(jìn)行分類(lèi):
- 可空的變量:通過(guò)
?
進(jìn)行修飾 - 不可空的變量:可采用以下兩種方式進(jìn)行適配
- 定義時(shí)初始化
- 使用
late
修飾為延時(shí)變量
四戒财、數(shù)據(jù)模型(Model)空安全適配技巧
數(shù)據(jù)模型(Model)空安全適配主要以下兩種情況:
- 含有命令構(gòu)造函數(shù)的模型
- 含有命名工廠構(gòu)造函數(shù)的模型
含有命令構(gòu)造函數(shù)的模型
含有命令構(gòu)造函數(shù)的模型的空安全適配技巧:
適配前:
///旅拍頁(yè)模型
class TravelItemModel {
int totalCount;
List<TravelItem> resultList;
TravelItemModel.fromJson(Map<String, dynamic> json) {
totalCount = json['totalCount'];
if (json['resultList'] != null) {
resultList = new List<TravelItem>();
json['resultList'].forEach((v) {
resultList.add(new TravelItem.fromJson(v));
});
}
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['totalCount'] = this.totalCount;
if (this.resultList != null) {
data['resultList'] = this.resultList.map((v) => v.toJson()).toList();
}
return data;
}
}
適配之前首先要和服務(wù)端協(xié)商好热监,模型中那些字段可空,那些字段是一定會(huì)下發(fā)的饮寞。對(duì)于這個(gè)案例假如:totalCount字段是一定會(huì)下發(fā)的孝扛,resultList字段是不能保證一定會(huì)下發(fā),那么我們可以這樣來(lái)適配:
適配后:
///旅拍頁(yè)模型
class TravelItemModel {
late int totalCount;
List<TravelItem>? resultList;
//命名構(gòu)造方法
TravelItemModel.fromJson(Map<String, dynamic> json) {
totalCount = json['totalCount'];
if (json['resultList'] != null) {
resultList = new List<TravelItem>.empty(growable: true);
json['resultList'].forEach((v) {
resultList!.add(new TravelItem.fromJson(v));
});
}
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['totalCount'] = this.totalCount;
data['resultList'] = this.resultList!.map((v) => v.toJson()).toList();
return data;
}
}
- 對(duì)于一定會(huì)下發(fā)的字段我們通過(guò)
late
來(lái)修飾為延遲初始化的字段以方便訪問(wèn) - 對(duì)于不能保證一定會(huì)下發(fā)的字段幽崩,我們通過(guò)
?
將其修飾為可空的變量
含有命名工廠構(gòu)造函數(shù)的模型
命名工廠構(gòu)造函的數(shù)據(jù)模型也是比較常見(jiàn)的數(shù)據(jù)模型之一苦始,公共數(shù)據(jù)模型為例來(lái)分享含有命名工廠構(gòu)造函的數(shù)據(jù)模型的空安全適配技巧:
適配前:
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<String, dynamic> json) {
return CommonModel(
icon: json['icon'],
title: json['title'],
url: json['url'],
statusBarColor: json['statusBarColor'],
hideAppBar: json['hideAppBar']
);
}
}
含有命名工廠構(gòu)造函數(shù)的模型通常需要有自己的構(gòu)造函數(shù),構(gòu)造函數(shù)通常采用可選參數(shù)歉铝,所以在進(jìn)行適配時(shí)首先要明確哪些字段一定不為空盈简,哪些字段可空,確認(rèn)好之后就可以進(jìn)行下面適配了:
適配后:
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});
//命名工廠構(gòu)造函數(shù)必須要有返回值太示,類(lèi)似static 函數(shù)無(wú)法訪問(wèn)成員變量和方法
factory CommonModel.fromJson(Map<String, dynamic> json) {
return CommonModel(
icon: json['icon'],
title: json['title'],
url: json['url'],
statusBarColor: json['statusBarColor'],
hideAppBar: json['hideAppBar']
);
}
}
- 對(duì)于可空的字段通過(guò)
?
進(jìn)行修飾 - 對(duì)于不可空的字段柠贤,需要在構(gòu)造方法中在對(duì)應(yīng)的字段前面添加
required
修飾符來(lái)表示這個(gè)參數(shù)是必傳參數(shù)
五、單例空安全適配技巧
單例是Flutter開(kāi)發(fā)中使用最廣的一種設(shè)計(jì)模式类缤,那么單例該如何適配空安全呢臼勉?
接下來(lái)就以[Flutter高級(jí)進(jìn)階實(shí)戰(zhàn)中緩存模塊]單例的空安全適配技巧
適配前:
///緩存管理類(lèi)
class HiCache {
SharedPreferences prefs;
static HiCache _instance;
HiCache._() {
init();
}
HiCache._pre(SharedPreferences prefs) {
this.prefs = prefs;
}
static Future<HiCache> 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<String> value) {
prefs.setStringList(key, value);
}
T get<T>(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<HiCache> 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<String> value) {
prefs?.setStringList(key, value);
}
remove(String key) {
prefs?.remove(key);
}
T? get<T>(String key) {
var result = prefs?.get(key);
if (result != null) {
return result as T;
}
return null;
}
}
核心適配的地方主要有兩點(diǎn):
- 因?yàn)槭菓袧h模式的單例,所以單例instance設(shè)置成可空
- getInstance中因?yàn)闀?huì)有null時(shí)創(chuàng)建單例餐弱,所以返回instance時(shí)將其轉(zhuǎn)換成非空
六宴霸、插件的空安全適配問(wèn)題
三方插件的空安全適配問(wèn)題
目前在Dart的官方插件平臺(tái)上的主流插件都陸續(xù)進(jìn)行了空安全支持,如果你的項(xiàng)目開(kāi)啟了空安全那么所有使用的插件也必須是要支持空安全的膏蚓,否則會(huì)導(dǎo)致無(wú)法編譯:
Xcode's output:
?
Error: Cannot run with sound null safety, because the following dependencies
don't support null safety:
- package:flutter_splash_screen
遇到這個(gè)問(wèn)題后可以到Dart的官方插件平臺(tái)查看這個(gè)flutter_splash_screen插件是否有支持了空安全的版本瓢谢。如果插件支持了空安全插件平臺(tái)會(huì)為其打上空安全的標(biāo):
如果你所使用的某個(gè)插件還不支持空安全,而且你又必須要使用這個(gè)插件驮瞧,那么可以通過(guò)上文所講的方式來(lái)關(guān)閉空安全檢查氓扛。
我的插件該如何適配空安全?
通過(guò)Flutter進(jìn)階拓展:開(kāi)發(fā)包和插件開(kāi)發(fā)的學(xué)習(xí),有不少小伙伴已經(jīng)開(kāi)發(fā)并發(fā)布了一些插件采郎,那么該如何為你所開(kāi)發(fā)的插件適配空安全呢千所?
回顧我對(duì)一些插件的適配的整個(gè)過(guò)程來(lái)講,可以分為三個(gè)關(guān)鍵步驟:
- 開(kāi)啟空安全
- 代碼適配:進(jìn)行編譯蒜埋,對(duì)編譯的報(bào)錯(cuò)進(jìn)行空安全適配
- 發(fā)布:將適配后的代碼發(fā)布到插件市場(chǎng)