bloc+rxdart 專案實戰,flutter的mvc方案

俞億發表於2019-05-05

文章結構:

  • bloc+rxdart的幾大優點
  • 示例程式碼簡單實現呼叫網路介面,然後重新整理頁面顯示
  • bloc+rxdart實現原理
  • bloc+rxdart經典使用場景,解決了業務場景中的兩個問題。

1.bloc+rxdart的幾大優點

1).實現MVC模式

widget只做UI展示,bloc實現控制邏輯,model做資料封裝。 我在運營線的新需求開發中實際使用了bloc做了一個新的頁面,就是商機敗北頁面。 這個頁面有三級的敗北原因選擇,敗北原因是級聯的,還有每次選擇裝置後要重新請求庫存,提交敗北等操作。

bloc+rxdart 專案實戰,flutter的mvc方案

2).實現跨層級model訪問和方法呼叫

在同一個頁面中跨widget資料獲取資料和操作比較多,例如提交敗北的時候,需要獲取多個子widget中的數值,如果沒有使用bloc的話,就需要在子widget的構造方法中傳遞model或回撥方法。

如果頁面層級較多,這種model和回撥方法就需要逐級傳遞。 例如在下圖左邊,需要把submiModel或回撥方法從最上層一直傳遞到最底部,才能做到收集所有的子widget中的資訊,最終集合所有的widget中的引數到submitModel中,提交敗北請求。

下面右邊的程式碼就是這樣逐層傳遞引數的方法,如果層級很多的話會非常頭疼,不僅要重複的宣告變數,而且如果修改的話要每個地方都改。

bloc+rxdart 專案實戰,flutter的mvc方案

而使用了bloc的話,建構函式就特別乾淨,不需要逐層傳遞model或者回撥方法。

下面的兩塊程式碼都是頁面widget結構中最下層的兩個widget,不需要在構造方法中傳遞任何引數或回撥方法,但是同樣能呼叫拿到model和呼叫回撥方法。怎麼實現的繼續往下看。:)

bloc+rxdart 專案實戰,flutter的mvc方案

3).不直接使用setState方法(StreamController)

之前在專案中直接使用setState會產生一些問題,例如當前State是unmounted狀態,這時直接呼叫setState會拋異常。

而使用bloc的話重新整理頁面是使用如下方式。關鍵的程式碼是_requestController.add

bloc+rxdart 專案實戰,flutter的mvc方案

4). 更加簡潔的區域性重新整理(StreamBuilder)

我們現有專案中有一些非常複雜的頁面,但是隻使用了一個檔案,所有的業務邏輯和widget元件都在這個頁面裡面。

下面是一段這種混雜網路請求、widget顯示的程式碼示例。

bloc+rxdart 專案實戰,flutter的mvc方案
bloc+rxdart 專案實戰,flutter的mvc方案
有多個網路請求和對應的資料顯示widget,不論哪個請求返回或者使用者點選事件,只要呼叫一個setState就能重新整理頁面更新顯示。

這樣相比於多個子widget的方式就不用逐層傳遞資料和回撥方法。但是破壞了物件導向開發的基本原則:資訊隱藏,比如修改某個widget的一個小功能,因為在一個很大的檔案中修改,其中業務邏輯錯綜複雜,改一個小功能可能就會影響其他邏輯,產生意料不到的後果,這一塊的程式碼就變得非常難以維護。

同時直接呼叫setState重新整理整個頁面的效能也有問題,也會產生拋異常的問題。


而使用bloc的方式會更加自由,可以像第二點中的圖中一樣使用多個子widget,在子widget中進行重新整理。

也可以直接改造上圖的程式碼,在一個widget中實現區域性重新整理。

第四個優點其實和第三個是一併實現的,都是通過streamBuidler和streamController,這兩者是配套使用的。其實bloc就是streamBuilder+streamController+ancestorWidgetOfExactType,和web端的ajax比較類似,幾項現有技術整合出了新的東西

StreamBuilder要傳入一個stream物件才能實現重新整理,而這個stream要從streamController中獲取的。

當在上面的程式碼中呼叫_requestController.add(deviceModel)後,下面的StreamBuilder就會自動呼叫builder方法,實現區域性重新整理StreamBuilder,而不是重新整理外層的InkWell。

StreamBuilder是一個Widget,可以在任意地方插入,實現區域性重新整理非常簡單。

bloc+rxdart 專案實戰,flutter的mvc方案

2.簡單示例程式碼

要實現一個BlocProvider,現有專案中已經整合,整個專案只需要一個BlocProvider,可以放在Utils目錄中。

實現Bloc其實就只用這一個類和flutter自帶的方法就行了,程式碼非常簡單,並不需要在yaml檔案中引入第三方庫。

bloc+rxdart 專案實戰,flutter的mvc方案

所有的bloc都需要繼承BlocBase,通常一個bloc對應一個會重新整理頁面的業務邏輯(如網路請求、切換tab)。

bloc+rxdart 專案實戰,flutter的mvc方案
bloc初始化可以在任意父頁面、爺爺頁面中,獲取只需要使用BlocProvider.of(context)。
bloc+rxdart 專案實戰,flutter的mvc方案
使用bloc程式碼如下:
bloc+rxdart 專案實戰,flutter的mvc方案
有三點需要注意的。 1.bloc要儲存在state中,不然會因為widget重新構建而丟失。 2.bloc需要呼叫dispose,在bloc中的dispose中會呼叫streamController的close方法,最好把bloc的dispose和create在同一個state中呼叫。 3.BlocProvider要在更外面一層建立。

下面是bloc建立和blocProvider的程式碼。

bloc+rxdart 專案實戰,flutter的mvc方案

3.bloc+rxdart實現原理

1).bloc實現原理

bloc觸發重新整理的方式就是使用StreamBuilder+StreamController,但是直接使用StreamController有一個很大的缺點就是隻能進行一對一的listen,且模式非常單一。所以需要使用到rxdart,下面會講到。

bloc+rxdart 專案實戰,flutter的mvc方案

bloc另一重要特性就是跨層級獲取bloc,在bloc可以獲取到model或者呼叫方法。

其中使用了ancestorWidgetOfExactType,因為flutter widget是樹狀結構的,結構層次儲存在BuildContext中,可以從子節點依次往父節點找符合型別的widget,所以所有使用了BlocProvider包裹的widget都可以通過of方法找到,再返回widget中的bloc。(bloc最終是儲存在外一層的state中)。

final type = _typeOf<BlocProvider<T>>();
BlocProvider<T> provider = context.ancestorWidgetOfExactType(type);
複製程式碼

在下圖中,最下層的widget呼叫of方法後,可以直接獲取到頂層頁面的submitBloc,因為flutter widget是樹狀結構的,結構層次儲存在BuildContext中,可以從子節點依次往父節點找符合型別的widget,所以所有使用了BlocProvider包裹的widget都可以通過of方法找到,再返回widget中的bloc。(bloc最終是儲存在外一層的state中)。

bloc+rxdart 專案實戰,flutter的mvc方案

2).rxdart實現原理

StreamController實現了觀察者模式,監聽者不是直接被呼叫,而是處於觀察狀態,當event加入streamController後,監聽者獲得非同步回撥。

bloc+rxdart 專案實戰,flutter的mvc方案
rxdart是對StreamController的擴充套件,提供了更多的模式。分為兩個部分Subject和Observable。
bloc+rxdart 專案實戰,flutter的mvc方案
其中Observable對stream封裝後提供多個處理方法,例如map、expand、merge、every、contact。

var obs = Observable(Stream.fromIterable([1,2,3,4,5]))
    .map((item)=>++item);
obs.listen(print);
輸出:2 3 4 5 6
複製程式碼

Subject是對StreamController的擴充套件,常用的有以下幾種。

PublishSubject:StreamController廣播版,streamController只能有一個listener,PublishSubject可以多次listen。下面的其他幾種Subject也都是廣播版。

BehaviorSubject: 快取最近一次的事件。如果先發生event,後listen,也能收到快取的event。

ReplaySubject: 快取所有的事件,之前加入的所有event,listen後,都會順序傳送過來。

4.經典使用場景

1).多個網路圖片元件共用一個http下載。

bloc+rxdart 專案實戰,flutter的mvc方案
開發zn_web_image這個網路圖片下載快取元件的時候,為了測試資料載入快取的請求,把圖片連結放在一個陣列中並多次重複。發現同樣的圖片連結在上面已經下載完成後,下面還會重複下載。

發現這是由於每次都建立新的bloc導致的,通過使用一個bloc list解決了這個問題。

static ZNImageBLocList _getInstance() {
  if (_instance == null) {
    _instance = new ZNImageBLocList._internal();
  }
  return _instance;
}

List<ZNImageBloc> blocList;
 
ZNImageBloc getBloc(String url,ZNImageConfig config){
  for(ZNImageBloc bloc in blocList){
    if(bloc.imageUrl==url){
      return bloc;
    }
  }
  ZNImageBloc bloc = new ZNImageBloc(url, config);
  blocList.add(bloc);
  return bloc;
}
複製程式碼
class ZnWebImageState extends State<ZnWebImage> {
  ZNImageBloc bloc;

  @override
  void dispose() {
    // TODO: implement dispose
    bloc.dispose();
    super.dispose();
  }

  @override
  void initState() {
    // TODO: implement initState
    super.initState();

    bloc = ZNImageBLocList.instance.getBloc(widget.url, widget.config);
  }

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return BlocProvider<ZNImageBloc>(
      bloc: bloc,
      child: ZNImageContainer(),
    );
  }
}
複製程式碼

這樣多個zn_web_image的widget就能共用同一個bloc,在下載過程中同步顯示進度條,下載完成後同時收到通知顯示下載好的圖片。

2).tabBarView中多個頁面共用一個是否通過認證屬性。

bloc在徵信專案中也解決了一個很頭疼的問題,就是否通過認證的狀態,這個狀態全域性共用,且有多個變更入口,認證狀態變更後要求所有頁面更新顯示。

bloc+rxdart 專案實戰,flutter的mvc方案
用到個人認證的地方有多個入口:

1.第一個tab中的首頁頂部。

2.確認訂單頁面,可以從第一個tab首頁進入,也可以從第二個tab的訂單列表進入。

3.第四個tab的個人中心頁面。

最開始沒有使用bloc的時候,寫了多個網路請求獲取認證狀態,isAuth屬性也在每個頁面分別儲存。

1.首頁通過requestAuth從伺服器端獲取isAuth屬性。

2.確認訂單頁面也需要從requestAuth獲取isAuth屬性。雖然首頁通過請求獲取到了isAuth屬性,從首頁進入確認訂單頁面可以傳遞isAuth屬性。但是從訂單列表進入時沒有這個屬性,訂單列表是和首頁同時初始化的,還是需要網路請求。

3.個人中心頁面也需要從requestAuth獲取isAuth屬性。雖然首頁通過請求獲取到了isAuth屬性,但是個人中心頁面是和首頁同時在tabbarView中初始化的,不能通過構造方法傳遞isAuth屬性。

bloc+rxdart 專案實戰,flutter的mvc方案
第二個問題就是從其中一個頁面進入認證頁面完成認證後,其他所有頁面都需要重新整理認證狀態,沒有使用bloc時,在這些頁面的生命週期方法中寫了重新發起網路請求來重新整理isAuth狀態。

這樣非常的麻煩,而且工作量大很多。

使用bloc之後,isAuth這樣全域性共用的屬性,只需要在MaterialApp外面加一層BlocProvider就行了,任何一個子頁面都能直接獲取isAuth,並且能同步isAuth的狀態更新,重新整理UI顯示。

//個人認證bloc
CreditAuthBloc authBloc;

@override
void dispose() {
  authBloc.dispose();
  super.dispose();
}

@override
void initState() {
  super.initState();
  authBloc = CreditAuthBloc();
}


@override
Widget build(BuildContext context) {
  return BlocProvider<CreditAuthBloc>(bloc: authBloc,child: MaterialApp(
    title: '',
    initialRoute:'/',
    home: Scaffold(
        body: Column(
          children: <Widget>[
            Expanded(
              child: TabBarView(physics: NeverScrollableScrollPhysics(),controller: tabController, children: [
                ZNMarketPage(),//首頁
                ZNOrderList(),//訂單列表
                ZNBill(),
                ZNMyCenter(),//個人中心
              ]),
            ),
          ],
        )),
  ),);
}
複製程式碼

子頁面獲取bloc

  //使用bloc中的auth
//  int userAuthStatus = 0; //個人認證 0: 未認證, 1: 已認證
  CreditAuthBloc authBloc;
  
  @override
  void didChangeDependencies() {
    // TODO: implement didChangeDependencies
    super.didChangeDependencies();
    authBloc = BlocProvider.of<CreditAuthBloc>(context);
  }
複製程式碼

通過streamBuilder使用bloc

bloc+rxdart 專案實戰,flutter的mvc方案

相關文章