[譯]使用MVI打造響應(yīng)式APP(四):獨(dú)立性UI組件

原文:REACTIVE APPS WITH MODEL-VIEW-INTENT - PART4 - INDEPENDENT UI COMPONENTS
作者:Hannes Dorfmann
譯者:卻把清梅嗅

這篇博客中,我們將針對(duì)如何 如何構(gòu)建獨(dú)立組件 進(jìn)行探討,我將闡述為什么在我看來 父子關(guān)系會(huì)導(dǎo)致壞味道的代碼,以及為何這種關(guān)系是沒有意義的唉锌。

有這樣一個(gè)問題時(shí)不時(shí)涌現(xiàn)在我的腦海中—— MVI闯两、MVP秽浇、MVVM這些架構(gòu)設(shè)計(jì)模式中召廷,多個(gè)Presenter(或者ViewModel)彼此之間是如何進(jìn)行通訊的揖闸?更直白點(diǎn)說吧句灌,Child-Presenter是如何與Parent-Presenter通訊的夷陋?

對(duì)我來說,這種 父子關(guān)系 會(huì)產(chǎn)生壞味道的代碼胰锌,因?yàn)檫@直接 導(dǎo)致了父子層級(jí)之間的耦合骗绕,使得代碼難以閱讀和維護(hù)

這種情況下资昧,需求的更改會(huì)影響很多的組件(對(duì)于大型系統(tǒng)來說爹谭,這種情況下實(shí)現(xiàn)需求的變動(dòng)簡直難如登天);并非僅此而已榛搔,同時(shí)诺凡,這也 引入了難以預(yù)測的共享的狀態(tài),其導(dǎo)致的問題甚至難以重現(xiàn)和調(diào)試践惑。

其實(shí)這也沒那么不堪腹泌,但我實(shí)在不理解為何信息必須從Presenter A流向Presenter B呢?或者Presenter如何與另一個(gè)Presenter進(jìn)行通信尔觉?

根本沒必要凉袱! 什么情況下Presenter才會(huì)需要和Presenter進(jìn)行直接的通訊,是什么事件發(fā)生了嗎侦铜?Presenter根本不需要和其它的Presenter直接通訊专甩,它們都觀察了同一個(gè)Model(或者說是業(yè)務(wù)邏輯的相同部分),這就是它們?nèi)绾潍@得變化的通知:通過底層。

image

當(dāng)一些事件發(fā)生時(shí)(比如用戶點(diǎn)擊了View1按鈕)钉稍,Presenter將信息下沉到業(yè)務(wù)邏輯涤躲。因?yàn)槠渌?code>Presenter觀察了相同的業(yè)務(wù)邏輯,因此它們從業(yè)務(wù)邏輯中接收到了同樣變化的通知(Model被更新了)贡未。

image

關(guān)于這一點(diǎn)种樱,我們已經(jīng)在 第一章節(jié) 討論了 單向數(shù)據(jù)流 的原理的重要性。

讓我們通過一個(gè)真實(shí)的案例實(shí)現(xiàn)它:在我們的購物App中俊卤,我們能夠?qū)⑸唐芳尤胭徫镘嚹奂罚送猓羞@樣一個(gè)頁面消恍,我們可以看到購物車商品的內(nèi)容岂昭,并且能夠一次選擇或者刪除多個(gè)商品條目:

我們?nèi)绻軌驅(qū)⑦@樣一個(gè)復(fù)雜的界面分割成更多 精巧、獨(dú)立且可復(fù)用的UI組件 的話就太棒了狠怨。以Toolbar為例约啊,它展示了被選中條目的數(shù)量邑遏,以及RecyclerView展示了購物車?yán)飾l目的列表。

<LinearLayout>
  <com.hannesdorfmann.SelectedCountToolbar
      android:id="@+id/selectedCountToolbar"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      />

  <com.hannesdorfmann.ShoppingBasketRecyclerView
      android:id="@+id/shoppingBasketRecyclerView"
      android:layout_width="match_parent"
      android:layout_height="0dp"
      android:layout_weight="1"
      />
</LinearLayout>

但是這些組件之間如何保持相互的通訊呢棍苹?很明顯每個(gè)組件都有它自己的Presenter:SelectedCountPresenterShoppingBasketPresenter。這屬于父子關(guān)系嗎茵汰?不枢里,它們僅僅是觀察了同一個(gè)Model,該Model根據(jù)在的邏輯代碼中進(jìn)行更新:

image
public class SelectedCountPresenter
    extends MviBasePresenter<SelectedCountView, Integer> {

  private ShoppingCart shoppingCart;

  public SelectedCountPresenter(ShoppingCart shoppingCart) {
    this.shoppingCart = shoppingCart;
  }

  @Override protected void bindIntents() {
    subscribeViewState(shoppingCart.getSelectedItemsObservable(), SelectedCountView::render);
  }
}


class SelectedCountToolbar extends Toolbar implements SelectedCountView {

  ...

  @Override public void render(int selectedCount) {
   if (selectedCount == 0) {
     setVisibility(View.VISIBLE);
   } else {
       setVisibility(View.INVISIBLE);
   }
 }
}

ShoppingBasketRecyclerView 的代碼和上述代碼的實(shí)現(xiàn)非常類似蹂午,因此本文不對(duì)其進(jìn)行展示栏豺。然而,如果我們認(rèn)真去觀察這段代碼豆胸,你會(huì)發(fā)現(xiàn)SelectedCountPresenterShoppingCart有一定的耦合奥洼。

我們完全有可能會(huì)在其它的頁面去復(fù)用這個(gè)UI組件,因此我們需要移除這個(gè)依賴的關(guān)系以達(dá)到復(fù)用該組件的目的晚胡。重構(gòu)其實(shí)很簡單:presenter持有一個(gè) Observable<Integer> 作為Model代替之前構(gòu)造器中所需要的ShoppingCart

public class SelectedCountPresenter
    extends MviBasePresenter<SelectedCountView, Integer> {

  private Observable<Integer> selectedCountObservable;

  public SelectedCountPresenter(Observable<Integer> selectedCountObservable) {
    this.selectedCountObservable = selectedCountObservable;
  }

  @Override protected void bindIntents() {
    subscribeViewState(selectedCountObservable, SelectedCountToolbarView::render);
  }
}

There you go (原文為法語灵奖,大概意思是“就是這樣”),每當(dāng)我們需要顯示當(dāng)前選擇的條目數(shù)量時(shí)估盘,我們就可以使用SelectedCountToolbar組件——這可以代表ShoppingCart中的條目數(shù)瓷患,也可以表示在App中的完全不同的上下文環(huán)境和頁面中。

此外遣妥,此UI組件可以放入獨(dú)立的庫中擅编,并在另一個(gè)App(如相冊(cè)應(yīng)用程序)中使用,以顯示所選照片的??數(shù)量:

Observable<Integer> selectedCount = photoManager.getPhotos()
    .map(photos -> {
       int selected = 0;
       for (Photo item : photos) {
         if (item.isSelected()) selected++;
       }
       return selected;
    });

return new SelectedCountToolbarPresnter(selectedCount);

結(jié)語

本文的目的是證明通常情況下箫踩,代碼的設(shè)計(jì)中根本不需要 父子關(guān)系 爱态,它們僅需要通過簡單的對(duì)相同業(yè)務(wù)邏輯進(jìn)行觀察就能實(shí)現(xiàn)。

不需要EventBus境钟,不需要從上層的Activity或者Fragment中調(diào)用findViewById()锦担,不需要presenter.getParentPresenter()或者其它的解決方案。僅使用 觀察者模式 就夠了慨削。借助于RxJava——它本身也是基于觀察者模式思想的體現(xiàn)吆豹,我們就能夠輕而易舉構(gòu)建這樣響應(yīng)式的UI組件。

額外的思考

MVPMVVM相比理盆,MVI的實(shí)現(xiàn)過程中痘煤,我們被迫(通過積極的方式)使用業(yè)務(wù)邏輯驅(qū)動(dòng)某個(gè)組件的狀態(tài)。因此猿规,具有更多MVI經(jīng)驗(yàn)的開發(fā)人員可以得出以下結(jié)論:

如果View的狀態(tài)是另一個(gè)組件的Model怎么辦衷快?如果一個(gè)組件的ViewState的變更是另一個(gè)組件的Intent怎么辦?

舉個(gè)例子:

Observable<Integer> selectedItemCountObservable =
        shoppingBasketPresenter
           .getViewStateObservable()
           .map(items -> {
              int selected = 0;
              for (ShoppingCartItem item : items) {
                if (item.isSelected()) selected++;
              }
              return selected;
            });

Observable<Boolean> doSomethingBecauseOtherComponentReadyIntent =
        shoppingBasketPresenter
          .getViewStateObservable()
          .filter(state -> state.isShowingData())
          .map(state -> true);

return new SelectedCountToolbarPresenter(
              selectedItemCountObservable,
              doSomethingBecauseOtherComponentReadyIntent);

乍一看,這似乎是一種可行的方案姨俩,但它不是父子關(guān)系的變體嗎蘸拔?當(dāng)然不是师郑,這并非傳統(tǒng)分層的父子關(guān)系,也許將其比喻為洋蔥更為恰當(dāng)(洋蔥的內(nèi)層為外層提供了一種狀態(tài))调窍。

但是宝冕,這依然是一種耦合的關(guān)系,不是嗎邓萨?我還沒有下定決心地梨,但現(xiàn)在我認(rèn)為避免這種洋蔥般的關(guān)系更好。如果您有不同意見缔恳,請(qǐng)?jiān)谙旅媪粞员ζ剩液芷诖挠^點(diǎn)。


系列目錄

《使用MVI打造響應(yīng)式APP》原文

《使用MVI打造響應(yīng)式APP》譯文

《使用MVI打造響應(yīng)式APP》實(shí)戰(zhàn)


關(guān)于我

Hello歉甚,我是卻把清梅嗅万细,如果您覺得文章對(duì)您有價(jià)值,歡迎 ??纸泄,也歡迎關(guān)注我的博客或者Github赖钞。

如果您覺得文章還差了那么點(diǎn)東西,也請(qǐng)通過關(guān)注督促我寫出更好的文章——萬一哪天我進(jìn)步了呢聘裁?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末仁烹,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子咧虎,更是在濱河造成了極大的恐慌焙糟,老刑警劉巖嫡秕,帶你破解...
    沈念sama閱讀 217,826評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡础拨,警方通過查閱死者的電腦和手機(jī)欢摄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門国瓮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來抽米,“玉大人,你說我怎么就攤上這事理肺∩阏ⅲ” “怎么了?”我有些...
    開封第一講書人閱讀 164,234評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵妹萨,是天一觀的道長年枕。 經(jīng)常有香客問我,道長乎完,這世上最難降的妖魔是什么熏兄? 我笑而不...
    開封第一講書人閱讀 58,562評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上摩桶,老公的妹妹穿的比我還像新娘桥状。我一直安慰自己,他們只是感情好硝清,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,611評(píng)論 6 392
  • 文/花漫 我一把揭開白布辅斟。 她就那樣靜靜地躺著,像睡著了一般芦拿。 火紅的嫁衣襯著肌膚如雪士飒。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,482評(píng)論 1 302
  • 那天防嗡,我揣著相機(jī)與錄音变汪,去河邊找鬼侠坎。 笑死蚁趁,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的实胸。 我是一名探鬼主播他嫡,決...
    沈念sama閱讀 40,271評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼庐完!你這毒婦竟也來了钢属?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,166評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤门躯,失蹤者是張志新(化名)和其女友劉穎淆党,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體讶凉,經(jīng)...
    沈念sama閱讀 45,608評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡染乌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,814評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了懂讯。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片荷憋。...
    茶點(diǎn)故事閱讀 39,926評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖褐望,靈堂內(nèi)的尸體忽然破棺而出勒庄,到底是詐尸還是另有隱情,我是刑警寧澤瘫里,帶...
    沈念sama閱讀 35,644評(píng)論 5 346
  • 正文 年R本政府宣布实蔽,位于F島的核電站,受9級(jí)特大地震影響谨读,放射性物質(zhì)發(fā)生泄漏盐须。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,249評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望贼邓。 院中可真熱鬧阶冈,春花似錦、人聲如沸塑径。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽统舀。三九已至匆骗,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間誉简,已是汗流浹背碉就。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留闷串,地道東北人瓮钥。 一個(gè)月前我還...
    沈念sama閱讀 48,063評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像烹吵,于是被迫代替她去往敵國和親碉熄。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,871評(píng)論 2 354