Flutter的最佳實踐:mixins 與 基礎類

在我們開發多屏app的時候,我們更傾向于復用一些類的代碼去完成一些功能,讓代碼能夠“高復用”,比如:全局錯誤提示、頁面的公共視圖部分的復用、響應式編程(Bloc)里面的依賴邏輯等。使用抽象類abstract基本上就能完成這些功能,但是問題來了,如果頁面上的公共部分我不想用到所有頁面,只想在特定的頁面上用,那該怎么辦呢? 比如AppBar, 部分頁面不需要公共的,而需要自定義,因為一個class類只能是一個類的子類,而我們需要更靈活的類的組合,這就是我們為什么需要mixin

mixins 與 基礎類:介紹

Mixins 讓我們在脫離父類子類關系約束的條件下給類添加一些“新功能”,允許我們在同一個類中有一個父級類和多個mixin組件。然而,因為它不是我們類的父類,所以mixins不允許有任何構造函數聲明。你可以閱讀更多相關與mixin的文章 What are mixins?,或者查看官方文檔 documentation;
mixin到底是怎么用的?讓我們先舉一個例子,新建一個abstract class Person

abstract class Person {
  void think() {
    print("Hmm, I wonder what I can do today");
  }
}

我們可以用extend關鍵字把這個類作為父類來使用,比如:

class Mike extends Person {}

然后,我們初始化這個類,并且調用父類的方法 think()

void main() {
  var mike = Mike();
  mike.think(); // prints: "Hmm, I wonder what I can do today"
}

但是我們要這么給Mike添加一些其他“新功能”呢?比如Mike是一位 coder ,他有coder的一些特性,但不是所有人都有,該怎么辦呢? mixin就能解決這個問題。

首先,我們需要創建一個mixin類并添加一我們需要的新的方法:

mixin Coder {
  void code() {
    print("Coding intensifies");
  }
}

使用關鍵字with,我們能將這個“新功能”添加給Mike:

class Mike extends Person with Coder {}

并且,與父類一樣,我們可以調用在Coder中創建的所有函數:

void main() {
  var mike = Mike();
  mike.code(); // prints: "Coding intensifies"
}

現在,每一個使用 mixin coder的類都擁有coder的方法,然而這帶來了一個問題:這意味著,如果我們有一個帶有子級Squirrel的父類Animal,那么我們也可以擁有一個可以code()方法的Squirrel!為了防止這種情況,我們可以使用關鍵字onmixin的使用“鎖定”到一個類以及從該類繼承的所有類:

mixin Coder on Person{
  void code() {
    print("Coding intensifies");
  }
}

這相當于為我們提供了一個強大的工具:現在我們可以覆蓋重寫在Person類中設置的方法用來添加或擴展其功能。

mixin Coder on Person{
  //...

  @override
  void think() {
    super.think();
    print("I'm going to code today!");
  }
}

調用super.think()可確保我們仍然可以調用Person中定義的代碼,上面的代碼擴展了Mike類的think()方法將會輸出:

Hmm, I wonder what I can do today
I'm going to code today!

通過掌握基類abstract和mixin的概念,我們可以將它們靈活的應用于Flutter app中。

mixins 與 基礎類:一個實際的常用例子

先試著想一下這種情況我們在flutter app中應該怎么做:
在app中我們有兩個頁面是這樣的:

我們的app有幾個屏幕如上面顯示的那樣。我們想共用每個屏幕的AppBar和background,我們可以使用mixin解決問題。

在這種情況下,我們都定義了屏幕標題,我們將創建一個基類,該基類具有一種提供屏幕名稱的方法,該基類稱為BasePage。我們也將僅在StatefulWidgets中應用mixins,因為我們的類將維護并更改其狀態。這樣,我們創建了兩個用于頁面的類:BasePageBaseState <BasePage>分別繼承StatefulWidgetState <StatefulWidget>

abstract class BasePage extends StatefulWidget {
  BasePage({Key key}) : super(key: key);
}

// TODO: Page為命名泛型 繼承 BasePage,    BaseState作為抽象基類,也會被子類繼承,所以傳入泛型限制參數類型
abstract class BaseState<Page extends BasePage> extends State<Page> {
  String screenName();
}

我們現在創建一個自定義mixin BasicPageMixin,在其中定義頁面的背景和標題名稱。

// TODO: BasicPage 是一個mixin,作用于BaseState和其基類, 抽取渲染頁面公共部分
mixin BasicPage<Page extends BasePage> on BaseState<Page> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(screenName()),
        ),
        body: Container(
          child: body(),
          color: Colors.amber,
        ));
  }

  Widget body();
}

由于body()方法沒有實例,因此使用此mixin的每個類都必須實現它,以確保我們不會忘記在頁面中添加body()

在屏幕上面我們看到了FloatingActionButton,但是我們不是每個屏幕都需要展示它,該怎么辦呢?我們可以聲明一個新方法fab(),默認的渲染輸出一個空的Container。如果繼承的子類需要它,那么子類可以通過@override重寫fab()來添加一個FloatingActionButton

  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(screenName()),
        ),
        body: Container(
          child: body(),
          color: Colors.amber,
        ));
        floatingActionButton: fab(),
  }
   
  // NOTE: 這里沒有在基類中初始化是因為渲染wedget 盡量寫在子組件state中, 更好的狀態管理,更好的性能
  // TODO: body()子類必須實現
  Widget body();
  // TODO: fab()子類可選實現
  Widget fab() => Container();
}

我們的mixin 已經創建好了, 我們來通過它新建一個頁面:

class MyMixinHomePage extends BasePage {
  MyMixinHomePage({Key key}) : super(key: key);
  @override
  _MyMixinHomePageState createState() => _MyMixinHomePageState();
}

class _MyMixinHomePageState extends BaseState<MyMixinHomePage> with BasicPage{
  @override
  String screenName() => "Home";

  @override
  Widget body() {
    return Center(child: Text("This is a basic usage of a mixin"));
  }
}

有了這個,我們現在只需要聲明一個body()和一個可能在屏幕中使用的fab()小部件,從而節省了幾十行代碼。very nice !

組合 mixins

擴展一項新功能,我們的某些頁面將請求服務器調用API,并且如果發生錯誤,我們需要以Snackbar的形式顯示錯誤消息。此外,我們決定使用BLoC(響應式編程)體系結構,在該體系結構中,我們需要在創建每個頁面時注入一個新的功能模塊(也就是錯誤提示)。這兩個問題將需要執行以下步驟:

注:關于響應式編程的知識請自行查閱,這里可以簡單的理解為異步的全局事件監聽機制。

  • 在我們的BasePage構造函數中注入BLoC功能模塊,以至于子類都有BLoC功能
  • BaseState新建全局狀態GlobalKey<ScaffoldState>
  • 創建一個新的mixin,使我們可以使用Snackbar在頁面中顯示BLoC發送的錯誤消息

我們新建BaseBloc,里面至提供數據的發送方法sink, 和數據的監聽方法stream:

abstract class BaseBloc {
  final StreamController<String> _errorSubject = StreamController<String>();

  Sink<String> get errorSink => _errorSubject.sink;

  Stream<String> get errorStream => _errorSubject.stream;
}

因為對于bloc我們暫時沒有其他的任何交互 ,我們的HomeBloc緊緊是繼承它:

// TODO: 抽象類只能被繼承,子類實例化后實現內部方法;
class HomeBloc extends BaseBloc {}

我們通過更改BasePage的構造函數讓它初始化包含bloc模塊。這將使我們也更改所有對其進行擴展的子類,以將bloc添加到子類構造函數中。 bloc參數用作泛型類型,以便擴展它的每個子類都可以聲明正確的bloc的類型。這可以確保在BaseState中調用它時,我們將獲得正確的bloc類型,從而允許我們訪問bloc其方法。

// TODO: 傳入Bloc 命名泛型, 以限制bloc的類型
abstract class BasePage<Bloc extends BaseBloc> extends StatefulWidget {
  Bloc bloc;
  BasePage({Key key, this.bloc}) : super(key: key);
}

之后在BaseState里面,我們聲明一個scaffoldKey狀態讓它用來顯示一個展示SnackbarScaffoldWidget

abstract class BaseState<Page extends BasePage> extends State<Page> {
  String screenName();
  GlobalKey<ScaffoldState> scaffoldKey = GlobalKey();
}

mixin的一個很奇特的特性是,如果將它們“鏈接”到一個類,則它們可以覆蓋其方法。這很有用,因為在StatefulWidget中,我們可以在initState方法中偵聽bloc發出來的數據流。這樣,為了顯示錯誤消息,我們可以創建一個mixin來覆蓋initState方法,并提供一些方法讓得到的消息顯示在Snackbar

mixin ErrorHandlingMixin<Page extends BasePage> on BaseState<Page> {
  @override
  void initState() {
    super.initState();
    widget.bloc.errorStream
        .listen((error) => showErrorSnackbar(error, scaffoldKey.currentState));
  }

  void showErrorSnackbar(String event, ScaffoldState context) {
    print('scaffoldKey ${scaffoldKey.currentWidget}');
    if (event != null) {
      context.showSnackBar(new SnackBar(content: new Text(event)));
    }
  }
}

最后我們把它在BasicPage添加之后添加在我們的 MyMixinHomePage中:

class MyMixinHomePage extends BasePage<HomeBloc> {
  // TODO: HomeBloc實例化基類 重新賦值給bloc;
  MyMixinHomePage({Key key}) : super(key: key, bloc: HomeBloc());

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends BaseState<MyMixinHomePage> with BasicPage, ErrorHandlingMixin {
  @override
  String screenName() => "Home";

  @override
  Widget fab() => FloatingActionButton(
        child: Icon(Icons.error),
        onPressed: (){
          widget.bloc.errorSink.add('A error message!');
        },
      );

  @override
  Widget body() {
    return Center(
      child: Text("This is a basic usage of a mixinss"),
    );
  }
}

結語

最后我們發現,我們用了兩個mixin和基礎類abstract class,共用了我們app中的很多代碼。

也許我們不需要為應用程序共用基本的UI,但是我們可以使用諸如ErrorHandlingMixin之類的mixin向用戶提供錯誤反饋、全局loading顯示、檢測用戶當前是否登錄的顯示等!

但是,同時創建基類和mixins是一個需要仔細考慮的過程,否則我們可能會在調用在基類和mixins中聲明的一個方法時,編譯器不知道選擇哪一個(注意mixin的調用順序與方法覆蓋);

注:本文原文出自 原文地址;在原文基礎上加以補充理解與完善,完善了原文中沒有寫全的例子代碼;

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容