在我們開發多屏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
!為了防止這種情況,我們可以使用關鍵字on
將mixin
的使用“鎖定”到一個類以及從該類繼承的所有類:
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
,因為我們的類將維護并更改其狀態。這樣,我們創建了兩個用于頁面的類:BasePage
和BaseState <BasePage>
分別繼承StatefulWidget
和State <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
狀態讓它用來顯示一個展示Snackbar
的ScaffoldWidget
:
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
的調用順序與方法覆蓋);
注:本文原文出自 原文地址;在原文基礎上加以補充理解與完善,完善了原文中沒有寫全的例子代碼;