高效開發 MVVM 和 databinding 你需要使用的工具

WeaponZhi發表於2017-11-06

喜歡小之的文章的可以關注公眾號「WeaponZhi」持續關注動態


相信不少同學已經開始使用MVVM作為自己 Android 開發架構了,但實際上,我在使用過程中查閱資料發現,網上有關 MVVM 的資料並不是很多,這主要是因為 MVVM 還是有一定使用門檻的,並且 MVVM 不一定會幫助你提高開發效率,可能你需要寫的程式碼更多了,或者說為了你為了讓程式碼保持 Databinding 的雙向繫結特性,而需要考慮很多業務以外的設計邏輯。我們使用一個架構或者設計模式,當然是為了更好的開發體驗嘛,所以我將給大家介紹幾個實用的第三方庫和工具,來幫助大家解決這些問題。

MVVMLight

「MVVMLight」這個第三方庫實際上是對 Databinding 工具庫的一些擴充套件,並且通過ReplyCommandResponseCommand來對所有的 View 的事件進行統一封裝,這是我認為 MVVMLight 最大的用處

MVVMLight的官方介紹部落格
MVVMLight的原始碼地址

我們來看一下ReplyCommand怎麼用。我們用常見的下拉重新整理控制元件PullToRefreshLayout來舉例子。

我們知道如果你想自定義一個控制元件的事件,你需要使用@BindingAdapter註解,比如ImageView通過URL屬性直接根據地址下載圖片並顯示可以這樣寫:

@BindingAdapter("bind:urlImage")  
public static void getInternetImage(ImageView iv, String userface) {  
    Picasso.with(iv.getContext()).load(userface).into(iv);  
}複製程式碼
<ImageView  
    android:id="@+id/iv"  
    android:layout_width="wrap_content"  
    android:layout_height="wrap_content"  
    app:urlImage="@{user.urlImage}"/>複製程式碼

這種情況往往是比較簡單的,因為只是操作一個屬性,但我們要自定義某一個事件該怎麼辦呢,比如我們要自定義onClick事件,那可能就得寫介面了:

@BindingAdapter("setImageOnClick")
    public static void setImageOnClick(ImageView imageView, final ImageOnClickListener listener){
        if (listener != null) {
            imageView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    listener.onClick(v);
                }
            });
        }
    }

interface ImageOnClickListener{
    void onClick(View v);
}複製程式碼

使用的時候呢,你得在 VM 中定義一個ImageOnClickListener的成員變數listener,在裡面寫具體的onClick實現方法,然後在 xml 中通過app:setImageOnClick="viewModel.listener"來繫結這個事件。

當然,你可以直接通過android:onClick來進行繫結,這裡只是例項。

看起來好像也不是很麻煩,但是你可能每一個這樣的事件,就得定義一個特殊的介面,我們能不能封裝一下呢?

這就是 MVVMLight 中 ReplyCommand 和 ResponseCommand 做的事了。通過這兩個類封裝了各種請求引數數量和返回值引數數量的回撥方法,在使用的時候,只要在泛型裡具體指名請求引數和返回值的型別即可,可以說很方便了。

例項,PullToRefreshLayout 是一個重新整理列表控制元件,我們通過使用ReplyCommand監聽下拉重新整理和上拉載入的監聽器是這樣寫的:

@BindView(R.id.refresh_listview)
PullToRefreshLayout pullToRefreshLayout;

...

@BindingAdapter (value = {"onRefreshCommand", "onLoadCommand"}, requireAll = false)
public static void onRefreshLoadCommand(
    final PullToRefreshLayout pullToRefreshLayout, final ReplyCommand onRefreshCommand, final ReplyCommand onLoadCommand) {

    pullToRefreshLayout.setOnRefreshListener(new PullToRefreshLayout.OnRefreshListener() {
        @Override
        public void onRefresh(PullToRefreshLayout pullToRefreshLayout) {
            if (onRefreshCommand != null) {
                onRefreshCommand.execute();
            }
        }

        @Override
        public void onLoadMore(PullToRefreshLayout pullToRefreshLayout) {
            if (onLoadCommand != null) {
                onLoadCommand.execute();
            }
        }
    });
}複製程式碼

我們使用統一的ReplyCommand來處理控制元件的各種事件,這裡使用的是無參無返回值的最簡單的情況,我們在 ViewModel 和 xml 中的寫法是和之前的介面差不多的:

public final ReplyCommand onRefreshCommand = new ReplyCommand(() -> getPostData(true));

public final ReplyCommand onLoadCommand = new ReplyCommand(()->getPostData(true));複製程式碼
<com.weapon.joker.lib.view.pullrefreshload.PullToRefreshLayout
    android:id="@+id/pull_refresh_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:onRefreshCommand="@{viewModel.onRefreshCommand}"
    app:onLoadCommand="@{viewModel.onLoadCommand}"/>複製程式碼

這樣,我們所有事件的介面就統一了。ResponseCommand 和 ReplyCommand 的區別主要在,ResponseCommand 是用來定義那種有返回值的引數的,而 ReplyCommand 是沒有返回值的,具體的使用方法,大家可以參考上面的連結,作者自己講的最詳細。

binding-collection-adapter

「binding-collection-adapter」對所有需要adapter的控制元件進行了封裝,比如一些常用的:ListViewRecyclerViewViewPager等,通過使用這個庫,我們就不需要再寫 adapter 了,通過 databinding 的方式,在 xml 繫結一些屬性,並在 ViewModel 中對這些屬性進行處理即可完成這些控制元件的處理,邏輯清晰,程式碼簡單。

GitHub 地址

下面舉一個 RecyclerView 的例子。我們現在 xml 中定義一個 RecyclerView 控制元件。

<android.support.v7.widget.RecyclerView
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  app:layoutManager="@{LayoutManagers.linear()}"
  app:items="@{viewModel.items}"
  app:itemBinding="@{viewModel.itemBinding}"/>複製程式碼

我們看到有三個特殊的屬性:layoutManageritemsitemBinding,這裡的layoutManager大家都比較熟悉了,引數是在開頭的import匯入的,傳入相關的類名即可。

<data>
    <variable
        name="viewModel"
        type="com.weaponzhi.test.ViewModel"/>
    <import type="com.weaponzhi.test.LayoutManagers"/>
</data>複製程式碼

我們先來看一下itemBinding是幹什麼用的,我們知道有時候列表項是可能多佈局的,那麼這個itemBinding就是用來處理每種佈局和對應 item 的 ViewModel 的繫結關係的。上述程式碼的 ViewModel 中,定義了該itemBinding

public final OnItemBindClass<Object> itemBinding =
    new OnItemBindClass<>
    .map(NoDataViewModel.class,BR.noData,R.layout.listitem_no_data)
    .map(ItemViewModel.class,BR.itemVM,R.layout.listitem_page);複製程式碼

map方法中有三個引數,第一個引數是這個佈局的 ViewModel,第三個引數是這個佈局的 xml 檔案,第二個引數這個 xml 中引入的 ViewModel 的 BR 檔案 id。這樣我們就繫結好了這個列表控制元件的多佈局邏輯了。一個空資料時候的佈局,一個正常返回資料時候的佈局。

那麼我們的資料是如何重新整理的呢,這就要用到上面的items這個屬性了,在我們這個例子裡,它是這樣定義的:

public final ObservableList<Object> viewModels = new ObservableArrayList<>();複製程式碼

當我們網路請求返回的時候,我們在資料回撥裡,通過對資料型別的處理,進行ItemViewModel的構造,最後只需要將構造好的物件一個個新增到這個ObservableList資料結構中去,介面的重新整理工作都在對應的ItemViewModel裡中進行處理,我們剛剛設定的itemBinding在這時候就起作用了,當新增資料的時候,它會先判斷這個更新資料的ItemViewModel的資料型別,NoDataViewModel.class型別的,那麼就使用R.layout.listitem_no_dataItemViewModel.class型別的,就使用R.layout.listitem_page。當然,其他的資料更新和刪除操作,也會因為雙向繫結而同步重新整理。

我們完全從 Adapter 的繁瑣中解放出來了!

Databinding support

這是一個 Android Studio 外掛,我們寫 xml 中的一些 Databind 程式碼比如<layout><data><variable><import>等標籤的使用還是比較多的,而且寫起來也比較繁瑣,這個外掛就是可以幫助你解放雙手,只需要在適當的地方按⌥+⏎(Windows 是 Alt+Enter)即可,從官網盜幾張 Gif 圖給大家感受一下吧。

Wrap with <layout></layout>

Add <data> tag

Wrap with @{}

Wrap with @={}

Switch @{} and @={}

Add <import>

Add <variable>

MVVM 自動程式碼生成

MVVM 和 MVP 這種架構並不一定會讓我們程式碼量減少,每一個介面可能都要以一種固定的模式建立很多類,那我們為什麼不通過一種自動程式碼生成工具來通過簡單的配置就完成這些類的建立呢,Java 完全就可以實現這些功能。網上有很多用 Java 實現的自動生成程式碼的方式,但每個人實現的 MVP 和 MVVM 架構方式都不同,所以自動化程式碼也會不同,我來展示下我這邊使用的過程吧。

我使用的 MVVM 程式碼生成工具的主要思路是比較簡單粗暴的,通過一個 xml 檔案配置一些屬性,比如起一個名字,設定一下檔案輸出的路徑,然後在 Java 裡用字串拼接和檔案流讀取的方式來生成模板程式碼。


我現在維護的一個專案中,使用了 MVVMLight 和 binding-collection-adapter 大家可以參考下。
GitHub 連結


期待您關注我的公眾號:WeaponZhi

相關文章