Flutter使用Sealed Class让状态类更强大

Flutter使用Sealed Class让状态类更强大

记得之前在写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 + function的方法会让我们定义的内容散布到各处,换而言之,我们使用这种方式时,打印方法的实现是在一个函数里面,而非在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方法:

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')  
    };

displaySealedStatus2()就像是Kotlin的 lambda表达式,可以直接回传一个方法。因为 sealed class 的特性,在 compile-time 的时候,就可以知道所有的子类,所以,函数的使用是安全的。

使用 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 get props => [];
}
class RadioPlaying extends RadioState {  
  @override  
  List get props => [];
}
class RadioPaused extends RadioState {  
  @override  
  List get props => [];
}
class RadioStopped extends RadioState {  
  @override  
  List get props => [];
}

在 Widget 内使用 BLoC 事,可以使用 BlocBuilder 来根据不同的状态来产生不同的 Widget, 如下:

BlocBuilder(  
  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(  
  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();
  },  
),

前面有提到,一般的Class是不支持switch列出所有可能的Class(因为编译器在compile-time还无法知道有哪些子类),假如我们犯了一个错,稍加了一个状态,那么,编译不会报错,而我们可能要到程序执行的时候才发现问题(甚至压根就没发现)。

BlocBuilder(  
  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();  
}

在IDE中,我们就会发现 switch 会出现 compile-time error

sealed_class_not_enough_state.png

这时候,我们就必须要在 switch 中填入所有的状态,这样才可以IDE不报 compile-time error,并能够执行编译。

sealed_class_enough_state.png

Bloc插件 4.0.0 版本中已经自动将基本状态的Class改为了sealed class。

结论

Sealed class 是一个很好用的工具,将Class使用 sealed class 进行声明,就可以让我们在开发的时候避免踩到难以发现的雷。如同上面的示例,使用BLoC的时候,若在BlocBuilder里面有状态忘记加了,就有可能造成误操作,而使用 sealed class 就可以避免我们的粗心大意所导致的难发现的错误,让我们可以及早的发现和排雷。

翻译自

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


这是一个从 https://juejin.cn/post/7368660526395588617 下的原始话题分离的讨论话题