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

島上碼農發表於2021-08-15

這是我參與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 入門與實戰的專欄文章。

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

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

?:評論交流,互相進步!

相關文章