一.走進空安全
什么是空安全
從Flutter 2開始,Flutter便在配置中默認啟用了空安全,通過將空檢查合并到型別系統中,可以在開發程序中捕獲這些錯誤,從而防止再生產環境導致的崩潰,
引入空安全的好處
- 可以將原本運行時的控制參考錯誤變為編輯時的分析錯誤
- 增強程式的健壯性,有效避免由Null 而導致的崩潰
- 跟隨Dart 和Flutter 的發展趨勢,為程式的后續迭代不留坑
空安全的最小必備知識
- 空安全百度原則
- 引入空安全前后Dart 型別系統的變化
- 可空(?)型別的使用
- 延遲初始化(late)的使用
- 空值斷言運算子(!)的使用
空安全的原則
Dart的空安全支持基于以下三條核心原則
- 默認不可空:除非您將變數顯示的宣告為可空,否則他一定是非空的型別;
- 漸進遷移:您可以自由的選擇合適進行遷移,多少代碼會進行遷移;
- 安全可靠:Dart的空安全是非常可靠的 ,意味著編譯期間包含了很多優化
如果型別系統推斷出某個變數不為空,那么他永遠不為空,但您將整個專案和其 依賴完全遷移至空安全后,您將享有健壯性帶來的所有優勢----更少的Bug 、更小的二進制檔案以及更快的執行速度,
引入空安全前后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修飾的變數,可以讓開發者選擇初始化的時機,并且在使用這個變數時可以不用?.,
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<SpeakPage>
with SingleTickerProviderStateMixin {
String speakTips = '長按說話';
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();
}
});
}
...
空值斷言運算子(!)的使用
當我們排除變數或引數的可空的可能后,可以通過!來告訴編譯器這個可空的變數或引數不可空,這對我們進行方法傳參或將可空引數傳遞給一個不可空的入參時特別有用:
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!)),
],
述代碼是根據gridNavModel與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 如何做空安全適配
1: 啟用空安全
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"
2.自定義Widget的空安全適配技巧
自定義Widget的空安全適配分兩種情況:
- Widget的空安全適配
- State的空安全適配
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的空安全適配
State的空安全適配主要是根據它的成員變數是否可空進行分類:
- 可空的變數:通過
?進行修飾 - 不可空的變數:可采用以下兩種方式進行適配
- 定義時初始化
- 使用
late修飾為延時變數
class _TravelPageState extends State<TravelPage> with TickerProviderStateMixin {
late TabController _controller; //延時初始
List<TravelTab> tabs = []; //定義時初始化
...
@override
void initState() {
super.initState();
_controller = TabController(length: 0, vsync: this);
...
3.資料模型(Model)空安全適配技巧
資料模型(Model)空安全適配主要以下兩種情況:
- 含有命名建構式的模型
- 含有命名工廠建構式的模型
含有命名建構式的模型
修改前
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;
}
}
適配之前首先要和服務端協商好,模型中那些欄位可空,那些欄位是一定會下發的,對于這個案例假如:totalCount欄位是一定會下發的,resultList欄位是不能保證一定會下發,那么我們可以這樣來適配:
適配后
class TravelItemModel {
late int totalCount;
List<TravelItem>? resultList;
//命名構造方法
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;
}
}
- 對于一定會下發的欄位我們通過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<String, dynamic> 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<String, dynamic> json) {
return CommonModel(
icon: json['icon'],
title: json['title'],
url: json['url'],
statusBarColor: json['statusBarColor'],
hideAppBar: json['hideAppBar']
);
}
}
- 對于可空的欄位通過
?進行修飾 - 對于不可空的欄位,需要在構造方法中在對應的欄位前面添加
required修飾符來表示這個引數是必傳引數
4.單例空安全適配技巧
單例是Flutter開發中使用最廣的一種設計模式,那么單例該如何適配空安全呢?
///快取管理類
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;
}
}
核心適配的地方主要有兩點:
- 因為是懶漢模式的單例,所以單例instance設定成可空
- getInstance中因為會有null時創建單例,所以回傳instance時將其轉換成非空
5.三方插件的空安全適配問題
目前在Dart的官方插件平臺上的主流插件都陸續進行了空安全支持,如果你的專案開啟了空安全那么所有使用的插件也必須是要支持空安全的,否則會導致無法編譯:
Error: Cannot run with sound null safety, because the following dependencies
don't support null safety:
- package:flutter_splash_screen
遇到這個問題后可以到Dart的官方插件平臺查看這個flutter_splash_screen插件是否有支持了空安全的版本,如果插件支持了空安全插件平臺會為其打上空安全的標

如果你所使用的某個插件還不支持空安全,而且你又必須要使用這個插件,那么可以通過上文所講的方式來關閉空安全檢查,
我的插件該如何適配空安全?
可以分為三個關鍵步驟:
- 開啟空安全 代碼適配:
- 進行編譯,對編譯的報錯進行空安全適配
- 發布:將適配后的代碼發布到插件市場
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/294127.html
標籤:其他
