Flutter 入門與實戰(五十二):昇級踩坑,聊聊 Dart 的 null safety

島上碼農 2021-08-15 13:36:57 阅读数:574

本文一共[544]字,预计阅读时长:1分钟~
flutter 五十二 十二 聊聊 dart

這是我參與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 字段的時候,當_cookienull 的時候導致出現空异常了。這時候我們要檢查一下,如果_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 聲明的場合,目前 Dartdynamic 聲明的變量、屬性是不做空校驗的,這會導致這樣聲明的出現空异常,例如上面說到的 RequestOptions optionsheaders,就是一個 Map<String, dynamic>對象,結果使用 null 賦值的時候就會拋出异常。對於這種情况最好是盡量少用 dynamic 聲明,同時調用第三方的時候,如果發現有這種情况,需要檢查一下是否允許賦值 null


我是島上碼農,微信公眾號同名,這是Flutter 入門與實戰的專欄文章。

:覺得有收獲請點個贊鼓勵一下!

:收藏文章,方便回看哦!

:評論交流,互相進步!

版权声明:本文为[島上碼農]所创,转载请带上原文链接,感谢。 https://gsmany.com/2021/08/20210815133652305A.html