這是我參與8月更文挑戰的第15天,活動詳情檢視:8月更文挑戰
重要提示,因為最新版本的 Flutter 需要 Xcode 升級,昨天折騰到半夜,升級系統到 Big Sur(11.5.2)和 Xcode升級到了12.4。Flutter已經升級到最新穩定版2.2.3(Dart 版本2.13.4),以後的示例將會以2.2.3版本為準(之前版本是2.0.6)。如果要多版本切換可以看上一篇:Flutter入門與實戰(五十一):Flutter多版本切換開發,狀態管理部分的
null safety
版本的程式碼已經提交:狀態管理2.2版本程式碼。
前言
由於升級了 Flutter 版本,升級完之後跑了一下之前的程式碼,也沒什麼問題。升級後最大的區別在於升級後的版本支援 Dart 的 null safety
版本了。關於 null safety
其實並不是什麼新鮮事了,很早的時候 Swift 就已經支援了,Dart是從2.12.2版本開始支援該特性的。本篇以官方文件為藍本,聊一下 Dart 的 null safety
特性。官方文件連結:Null Safety。
null safety
最大的特點就是預設宣告的物件是非空的,除非你明確該物件可以為空。
Nullable 和 non-nullable 型別
當你選擇使用 null satety
特性時,所有的型別預設是非空的。例如如果宣告瞭一個 String
型別的變數,那麼就意味著它一直包含字串值。如果你想要一個 String
物件能夠接收字串值或null
,那麼就需要在型別宣告後面加上?
標識,一個宣告為String?
型別的變數可以包含字串值或 null
。
String? str1;
String str2;
// OK
str1 = null;
// 報錯
str2 = null;
// OK
List<String?> strList1 = ['a', null, 'c'];
// 報錯
List<String> strList2 = ['a', null, 'c'];
複製程式碼
空斷言操作符!
如果確定一個物件或表示式返回值有值,那麼就可以使用空斷言操作符!強制轉為不為空物件,然後可以使用它賦值給非空物件,或訪問其屬性或方法,如 valiable!.xx
。這種情況下,如果對於nullable
物件不加!,編譯器就會報錯。但是,如果物件本身是null
,加!
操作符會導致異常。
int? couldReturnNullButDoesnt() => -3;
void main() {
int? nullableInt = 1;
List<int?> intListHasNull = [2, null, 4];
int a = nullableInt!;
int b = intListHasNull.first!;
int c = couldReturnNullButDoesnt()!.abs();
print('a is $a.');
print('b is $b.');
print('c is $c.');
}
複製程式碼
型別提升(Type promotion)
為了保證空安全特性,Dart 的流分析(flow analysis)已經考慮了空特性。如果一個 nullable 物件不可能有空值,那麼就會被當作非空物件處理,例如:
String? str;
if (str != null) {
print(str); //已經確保不為空,不會編譯出錯
}
複製程式碼
late 關鍵字
有些時候變數、類成員屬性或其他全域性變數應該是非空的,但是沒法在宣告的時候直接賦值,這個時候就需要在變數宣告的時候加上 late
關鍵字。當在變數宣告的時候加上late
關鍵字後,就是告訴 Dart 如下的內容:
- 目前還沒有給該變數賦值;
- 我們將在之後才給該變數賦值;
- 我們保證在使用該變數前肯定會對其賦值。
例如,下面的_description 宣告編譯器如果沒有 late 關鍵字會報錯。
class Meal {
late String _decription; //錯誤宣告
set description(String desc) {
_description = 'Meal Description: $desc';
}
String get description => _description;
}
void main() {
final myMeal = Meal();
myMeal.description = 'Feijoada';
print(myMeal.description);
}
複製程式碼
late
關鍵字對處理迴圈引用還十分有幫助,譬如我們有一個球隊和一個教練,球隊和教練就存在相互應用的情況。如果沒有 late
關鍵字,我們就只能宣告為nullable,那樣到時候使用到時候就很彆扭了——需要到處加空斷言操作符或者使用if
來判斷是否為空。
class Team {
late final Coach coach;
}
class Coach {
late final Team team;
}
void main() {
final myTeam = Team();
final myCoach = Coach();
myTeam.coach = myCoach;
myCoach.team = myTeam;
print('搞定!');
}
複製程式碼
升級修改
升級修改時,需要根據呼叫的方法引數、返回值或宣告的屬性做如下處理:
- 類屬性:非空類屬性預設需要由初始值,如果類屬性會在別的方法中初始化,那可以加上
late
關鍵字,表示該屬性稍後會被初始化,而且是非空的。如果屬性可能為空,那麼就加上?
空標識。這種可為空的屬性使用的時候需要特別注意,需要檢查是否為空才可以使用,或者使用variable?.xx
這種形式訪問,如果明確屬性有值,則需要使用!強制指定為非空,如variable!.xx
。 - 方法引數:根據需要設定引數是否是可為空或必傳引數,必傳的引數加上在引數宣告前加上
required
關鍵字,可為空的加上?
標識。 - 返回值:如果返回值可能為
null
,就在返回引數後加上?
標識。如果是集合物件中的某個物件為空,那麼需要在集合的型別後加上?標識,例如List<int?>
。
對於依賴,也需要修改 pubspec.yaml 檔案,包括如下修改:
- 將依賴最低的 Dart 版本修改為2.12.0
environment:
sdk: ">=2.12.0 <3.0.0"
複製程式碼
- 修改部分第三方外掛依賴,升級到支援null safety 版本,具體可以參考 pub 上的版本說明。
Dio 踩坑
升級完之後,Dio
請求報錯DioError [DioErrorType.other]: type 'Null' is not a subtype of type 'Object'
。上網搜了,在 issue
裡有提到過,但是說是已經解決了。然後按照 issue 裡的方法試也不行,後面想了一下,先直接請求百度網頁看看是不是 Dio
的問題,結果請求百度網頁正常,那就說明是程式碼自身的問題。最後再定位發現 是我們的 CookieManager
攔截器的請求 headers
設定 Cookie
欄位的時候,當_cookie
為 null
的時候導致出現空異常了。這時候我們要檢查一下,如果_cookie
不為空才設定 Cookie
。
// CookieManager 之前的程式碼,_cookie 可能為 null 導致 Dio 報異常
void onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) {
options.headers['Cookie'] = _cookie;
return super.onRequest(options, handler);
}
// 修改後
@override
void onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) {
// null safety後需要不為空才可以設定
if (_cookie != null) {
options.headers['Cookie'] = _cookie;
}
return super.onRequest(options, handler);
}
複製程式碼
總結
從編碼的角度來說,null safety
特性實際上增加了編碼的工作量。但是null safety
更像是一個強制的約定,要求介面或類明確引數或屬性的是否為空,從而可以簡化協作,提高程式碼的健壯性。
當然,對於第三方庫來說就需要特別小心,有些第三方庫使用的是dynamic 宣告的場合,目前 Dart
對 dynamic
宣告的變數、屬性是不做空校驗的,這會導致這樣宣告的出現空異常,例如上面說到的 RequestOptions options
的 headers
,就是一個 Map<String, dynamic>
物件,結果使用 null
賦值的時候就會丟擲異常。對於這種情況最好是儘量少用 dynamic
宣告,同時呼叫第三方的時候,如果發現有這種情況,需要檢查一下是否允許賦值 null
。
我是島上碼農,微信公眾號同名,這是Flutter 入門與實戰的專欄文章。
??:覺得有收穫請點個贊鼓勵一下!
?:收藏文章,方便回看哦!
?:評論交流,互相進步!