Flutter — 使用 Sealed class 讓你的類別更強大

Andy Lu
10 min readMar 17, 2024
Image by Simon from Pixabay

記得之前在寫 Kotlin 的時候,對於 Kotlin 所提供的 Sealed class 的功能感到喜愛、驚訝,我還給 sealed class 封上 enum 2.0 的封號,它擁有 class 的特性,將狀態封裝起來,使用 when 語法時,還可以詳盡列出所有的子項目, 而在 Flutter 中,其實也有 sealed class 可以使用,在 Dart 3.0 中,已經將 sealed class 加入了 Dart 的武器庫內。

enum

假設要實作一個收音機,我們可以使用 enum 宣告其狀態,如下

enum Status{  
init, playing, paused, stopped
}

若需要列印出現在的狀態,我們可能會這樣寫:利用一個函式,傳入 enum,在這個函式內去判斷該顯示什麼文字。

void displayStatus(Status status){  
switch(status){
case Status.init:
print('The radio is initializing');
break;
case Status.playing:
print('The radio is playing');
break;
case Status.paused:
print('The radio is paused');
break;
case Status.stopped:
print('The radio is stopped');
break;
}
}

但是,使用 enum 配合函式的方法會讓我們定義的內容散佈到各處,換句話說,我們使用這種方式時,宣告要列印的內容是在函式內,而不是在 enum 內。如此就很容易造成 bug。

Sealed class

在這邊我們可以使用 sealed class 來改寫,如下: 在 class 關鍵字前方加上 sealed 之後,該類別就成為了 sealed class(密封類別)

sealed class SealedStatus {  
void display();
}
class Init extends SealedStatus {  
@override
void display() {
print('The radio is initializing');
}
}
class Playing extends SealedStatus {
@override
void display() {
print('The radio is playing');
}
}
class Paused extends SealedStatus {
@override
void display() {
print('The radio is paused');
}
}
class Stopped extends SealedStatus {
@override
void display() {
print('The radio is stopped');
}
}
displaySealedStatus(SealedStatus status) => status.display();

如此一來,我們就可以將函式變得更精簡。

別忘了 sealed class 支援 switch 詳盡列舉,我們也是可以用原本的方式來實作

sealed class SealedStatus {}  
class Init extends SealedStatus {}  class Playing extends SealedStatus {}  class Paused extends SealedStatus {}  class Stopped extends SealedStatus {}

displaySealedStatus2() 就像是 Kotlin 的 lambda 表達式,可以直接回傳一個函式。 因為 sealed class 的特性,在 compile-time 的時候,可以知道所有的子類別,所以函式的使用是安全的。

displaySealedStatus2(SealedStatus status) => switch (status) {  
Init _ => print('The radio is initializing'),
Playing _ => print('The radio is playing'),
Paused _ => print('The radio is paused'),
Stopped _ => print('The radio is stopped')
};

使用 sealed class 能夠讓編譯器在compile-time 時就知道有哪些子類別,所以也就能在開發時抓出在 switch 中遺漏的狀態。

Sealed class + BLoC

BLoC 是 Flutter 中一項狀態管理的工具,使用 BLoC 時,需要定義狀態(state)、事件(event),若是 Cubit(類似 BLoC,但更為精簡) ,雖然不需要定義事件(因為使用函式取代了事件),不過還是需要定義狀態。 通常,在定義 BLoC、Cubit 的狀態時,會將基本狀態使用 abstract class 來宣告,之後所有的狀態都將會繼承該基本狀態型別。

如同上面的範例,我們將收音機狀態改成可以在 BLoC 使用的狀態。

abstract class RadioState extends Equatable {  
const RadioState();
}
class RadioInitial extends RadioState {  
@override
List<Object> get props => [];
}
class RadioPlaying extends RadioState {
@override
List<Object> get props => [];
}
class RadioPaused extends RadioState {
@override
List<Object> get props => [];
}
class RadioStopped extends RadioState {
@override
List<Object> get props => [];
}

在 Widget 內使用 BLoC 時,可以使用 BlocBuilder 來根據不同的狀態來產生不同的 Widget,如下:

BlocBuilder<RadioBloc, RadioState>(  
builder: (BuildContext context, state) {
if (state is RadioInitial) {
return const Text("Initial");
}
if (state is RadioPlaying) {
return const Text("Loading");
}
if (state is RadioPaused) {
return const Text("Paused");
}
if (state is RadioStopped) {
return const Text("Stopped");
}
return const CircularProgressIndicator();
},
),

使用 if 判斷 state 是否為特定狀態,再依不同狀態產生不同的 Widget ,這種是很常見判斷 BLoC 狀態的方法,這邊我們可以改用 switch 改寫。

BlocBuilder<RadioBloc, RadioState>(  
builder: (BuildContext context, state) {
switch (state) {
case RadioInitial():
return const Text("Initial");
case RadioPlaying():
return const Text("Playing");
case RadioPaused():
return const Text("Paused");
case RadioStopped():
return const Text("Stopped");
}
return const CircularProgressIndicator();
},
),

前面有提到,一般的類別是不支援詳盡列出 switch 所有可能的類別(因為編譯器在 compile-time 還不知道有哪些子類別),假如我們在這邊犯了一個錯,少加了一個狀態,那麼,compile 不會報錯,而我們可能直到程式執行時才會發現問題(或者沒發現)。

BlocBuilder<RadioBloc, RadioState>(  
builder: (BuildContext context, state) {
switch (state) {
case RadioInitial():
return const Text("Initial");
case RadioPlaying():
return const Text("Playing");
// case RadioPaused(): //<- 少了 Paused 還是可以正常編譯,不會發生錯誤
// return const Text("Paused");
case RadioStopped():
return const Text("Stopped");
}
return const CircularProgressIndicator();
},
),

如果我們將至基本狀態改為使用 sealed class 宣告時,這個問題就可以在開發時被揭露出來。 將 abstract class RadioState{} 改成 sealed class RadioState {}

sealed class RadioState extends Equatable {  
const RadioState();
}

可以發現 switch 就會出現 compile-time error,我們就必須在 switch 填入所有的狀態,才可以正常執行。

  • 加上缺少的狀態後,就不會發生 compile-time error。

在套件 bloc 套件中,4.0.0 版本已經自動將基本狀態的型別改為 sealed class。

結論

Sealed class 是一項很好用的工具,將型別使用 sealed class 宣告,就可以讓我們在開發的時候避免踩到難以發現的雷(如同上方的範例,使用 BLoC 時,若在 BlocBuilder 內有狀態忘記加,就有可能造成誤動作,而使用 sealed class 就可避免我們的粗心導致不容易發現的錯誤,讓我們可以及早發現及早除蟲(無誤)。

--

--

Andy Lu

Android/Flutter developer, Kotlin Expert, like to learn and share.