新手用不起,高手看不上系列之--MVVMHabit

goldze發表於2018-10-18

基於谷歌最新AAC架構,MVVM設計模式的一套快速開發庫,整合Okhttp+RxJava+Retrofit+Glide等主流模組,滿足日常開發需求。使用該框架可以快速開發一個健壯、易維護的Android應用。

更新日誌

v3.0.0:2018年10月8日

  • 全面升級AAC,引入谷歌lifecycle元件;
  • 修改Base基類,滿足新一套模式;
  • 升級第三方依賴庫;
  • 修改例子程式;
  • 修改文件說明。

v2.0.6:2018年7月19日

  • 優化框架效能、基類邏輯,新增繫結命令;
  • 補充例子程式及註釋;
  • 升級/修改第三方依賴庫;
  • 補充文件說明。

v2.0.0:2018年4月10日

  • 全面升級RxJava2;
  • 優化繫結回撥方式;
  • 升級第三方依賴庫;
  • 微調例子程式。

注:1.x-廢棄版(最後版本:1.2.6.1)2.x-順手版(最後版本:2.0.10)已停止維護,建議使用當前3.x-健壯版(最後版本:3.0.3)

原文地址: github.com/goldze/MVVM…

MVVMHabit-Family

MVVMHabit

目前,android流行的MVC、MVP模式的開發框架很多,然而一款基於MVVM模式開發框架卻很少。MVVMHabit是以谷歌DataBinding+LiveData+ViewModel框架為基礎,整合Okhttp+RxJava+Retrofit+Glide等流行模組,加上各種原生控制元件自定義的BindingAdapter,讓事件與資料來源完美繫結的一款容易上癮的實用性MVVM快速開發框架。從此告別findViewById(),告別setText(),告別setOnClickListener()...

框架流程

新手用不起,高手看不上系列之--MVVMHabit

框架特點

  • 快速開發

    只需要寫專案的業務邏輯,不用再去關心網路請求、許可權申請、View的生命週期等問題,擼起袖子就是幹。

  • 維護方便

    MVVM開發模式,低耦合,邏輯分明。Model層負責將請求的資料交給ViewModel;ViewModel層負責將請求到的資料做業務邏輯處理,最後交給View層去展示,與View一一對應;View層只負責介面繪製重新整理,不處理業務邏輯,非常適合分配獨立模組開發。

  • 流行框架

    retrofit+okhttp+rxJava負責網路請求;gson負責解析json資料;glide負責載入圖片;rxlifecycle負責管理view的生命週期;與網路請求共存亡;rxbinding結合databinding擴充套件UI事件;rxpermissions負責Android 6.0許可權申請;material-dialogs一個漂亮的、流暢的、可定製的material design風格的對話方塊。

  • 資料繫結

    滿足google目前控制元件支援的databinding雙向繫結,並擴充套件原控制元件一些不支援的資料繫結。例如將圖片的url路徑繫結到ImageView控制元件中,在BindingAdapter方法裡面則使用Glide載入圖片;View的OnClick事件在BindingAdapter中方法使用RxView防重複點選,再把事件回撥到ViewModel層,實現xml與ViewModel之間資料和事件的繫結(框架裡面部分擴充套件控制元件和回撥命令使用的是@kelin原創的)。

  • 基類封裝

    專門針對MVVM模式打造的BaseActivity、BaseFragment、BaseViewModel,在View層中不再需要定義ViewDataBinding和ViewModel,直接在BaseActivity、BaseFragment上限定泛型即可使用。普通介面只需要編寫Fragment,然後使用ContainerActivity盛裝(代理),這樣就不需要每個介面都在AndroidManifest中註冊一遍。

  • 全域性操作

    1. 全域性的Activity堆疊式管理,在程式任何地方可以開啟、結束指定的Activity,一鍵退出應用程式。
    2. LoggingInterceptor全域性攔截網路請求,列印Request和Response,格式化json、xml資料顯示,方便與後臺除錯介面。
    3. 全域性Cookie,支援SharedPreferences和記憶體兩種管理模式。
    4. 通用的網路請求異常監聽,根據不同的狀態碼或異常設定相應的message。
    5. 全域性的異常捕獲,程式發生異常時不會崩潰,可跳入異常介面重啟應用。
    6. 全域性事件回撥,提供RxBus、Messenger兩種回撥方式。
    7. 全域性任意位置一行程式碼實現檔案下載。

1、準備工作

網上的很多有關MVVM的資料,在此就不再闡述什麼是MVVM了,不清楚的朋友可以先去了解一下。todo-mvvm-live

1.1、啟用databinding

在主工程app的build.gradle的android {}中加入:

dataBinding {
    enabled true
}
複製程式碼

1.2、依賴Library

從遠端依賴:

在根目錄的build.gradle中加入

allprojects {
    repositories {
		...
        google()
        jcenter()
        maven { url 'https://jitpack.io' }
    }
}
複製程式碼

在主專案app的build.gradle中依賴

dependencies {
    ...
    implementation 'com.github.goldze:MVVMHabit:3.0.3'
}
複製程式碼

下載例子程式,在主專案app的build.gradle中依賴例子程式中的mvvmhabit

dependencies {	
    ...
    implementation project(':mvvmhabit')
}
複製程式碼

1.3、配置config.gradle

如果不是遠端依賴,而是下載的例子程式,那麼還需要將例子程式中的config.gradle放入你的主專案根目錄中,然後在根目錄build.gradle的第一行加入:

apply from: "config.gradle"
複製程式碼

注意: config.gradle中的

android = [] 是你的開發相關版本配置,可自行修改

support = [] 是你的support相關配置,可自行修改

dependencies = [] 是依賴第三方庫的配置,可以加新庫,但不要去修改原有第三方庫的版本號,不然可能會編譯不過

1.4、配置AndroidManifest

新增許可權:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
複製程式碼

配置Application:

繼承mvvmhabit中的BaseApplication,在你的自己AppApplication中配置

//是否開啟日誌列印
KLog.init(true);
//配置全域性異常崩潰操作
CaocConfig.Builder.create()
    .backgroundMode(CaocConfig.BACKGROUND_MODE_SILENT) //背景模式,開啟沉浸式
    .enabled(true) //是否啟動全域性異常捕獲
    .showErrorDetails(true) //是否顯示錯誤詳細資訊
    .showRestartButton(true) //是否顯示重啟按鈕
    .trackActivities(true) //是否跟蹤Activity
    .minTimeBetweenCrashesMs(2000) //崩潰的間隔時間(毫秒)
    .errorDrawable(R.mipmap.ic_launcher) //錯誤圖示
    .restartActivity(LoginActivity.class) //重新啟動後的activity
    //.errorActivity(YourCustomErrorActivity.class) //崩潰後的錯誤activity
    //.eventListener(new YourCustomEventListener()) //崩潰後的錯誤監聽
    .apply();
複製程式碼

2、快速上手

2.1、第一個Activity

以大家都熟悉的登入操作為例:三個檔案LoginActivty.javaLoginViewModel.javaactivity_login.xml

2.1.1、關聯ViewModel

在activity_login.xml中關聯LoginViewModel。

<layout>
    <data>
        <variable
            type="com.goldze.mvvmhabit.ui.vm.LoginViewModel"
            name="viewModel"
        />
    </data>
    .....

</layout>
複製程式碼

variable - type:類的全路徑
variable - name:變數名

2.1.2、繼承Base

LoginActivity繼承BaseActivity


public class LoginActivity extends BaseActivity<ActivityLoginBinding, LoginViewModel> {
    //ActivityLoginBinding類是databinding框架自定生成的,對activity_login.xml
    @Override
    public int initContentView(Bundle savedInstanceState) {
        return R.layout.activity_login;
    }

    @Override
    public int initVariableId() {
        return BR.viewModel;
    }

    @Override
    public LoginViewModel initViewModel() {
        //View持有ViewModel的引用,如果沒有特殊業務處理,這個方法可以不重寫
        return ViewModelProviders.of(this).get(LoginViewModel.class);
    }
}
複製程式碼

儲存activity_login.xml後databinding會生成一個ActivityLoginBinding類。(如果沒有生成,試著點選Build->Clean Project)

BaseActivity是一個抽象類,有兩個泛型引數,一個是ViewDataBinding,另一個是BaseViewModel,上面的ActivityLoginBinding則是繼承的ViewDataBinding作為第一個泛型約束,LoginViewModel繼承BaseViewModel作為第二個泛型約束。

重寫BaseActivity的二個抽象方法

initContentView() 返回介面layout的id
initVariableId() 返回變數的id,對應activity_login中name="viewModel",就像一個控制元件的id,可以使用R.id.xxx,這裡的BR跟R檔案一樣,由系統生成,使用BR.xxx找到這個ViewModel的id。

選擇性重寫initViewModel()方法,返回ViewModel物件

@Override
public LoginViewModel initViewModel() {
    //View持有ViewModel的引用,如果沒有特殊業務處理,這個方法可以不重寫
    return ViewModelProviders.of(this).get(LoginViewModel.class);
}
複製程式碼

注意: 不重寫initViewModel(),預設會建立LoginActivity中第二個泛型約束的LoginViewModel,如果沒有指定第二個泛型,則會建立BaseViewModel

LoginViewModel繼承BaseViewModel

public class LoginViewModel extends BaseViewModel {
    public LoginViewModel(@NonNull Application application) {
        super(application);
    }
    ....
}
複製程式碼

BaseViewModel與BaseActivity通過LiveData來處理常用UI邏輯,即可在ViewModel中使用父類的showDialog()、startActivity()等方法。在這個LoginViewModel中就可以盡情的寫你的邏輯了!

BaseFragment的使用和BaseActivity一樣,詳情參考Demo。

2.2、資料繫結

擁有databinding框架自帶的雙向繫結,也有擴充套件

2.2.1、傳統繫結

繫結使用者名稱:

在LoginViewModel中定義

//使用者名稱的繫結
public ObservableField<String> userName = new ObservableField<>("");
複製程式碼

在使用者名稱EditText標籤中繫結

android:text="@={viewModel.userName}"
複製程式碼

這樣一來,輸入框中輸入了什麼,userName.get()的內容就是什麼,userName.set("")設定什麼,輸入框中就顯示什麼。 注意: @符號後面需要加=號才能達到雙向繫結效果;userName需要是public的,不然viewModel無法找到它。

點選事件繫結:

在LoginViewModel中定義

//登入按鈕的點選事件
public View.OnClickListener loginOnClick = new View.OnClickListener() {
    @Override
    public void onClick(View v) {
            
    }
};
複製程式碼

在登入按鈕標籤中繫結

android:onClick="@{viewModel.loginOnClick}"
複製程式碼

這樣一來,使用者的點選事件直接被回撥到ViewModel層了,更好的維護了業務邏輯

這就是強大的databinding框架雙向繫結的特性,不用再給控制元件定義id,setText(),setOnClickListener()。

但是,光有這些,完全滿足不了我們複雜業務的需求啊!MVVMHabit閃亮登場:它有一套自定義的繫結規則,可以滿足大部分的場景需求,請繼續往下看。

2.2.2、自定義繫結

還拿點選事件說吧,不用傳統的繫結方式,使用自定義的點選事件繫結。

在LoginViewModel中定義

//登入按鈕的點選事件
public BindingCommand loginOnClickCommand = new BindingCommand(new BindingAction() {
    @Override
    public void call() {
            
    }
});
複製程式碼

在activity_login中定義名稱空間

xmlns:binding="http://schemas.android.com/apk/res-auto"
複製程式碼

在登入按鈕標籤中繫結

binding:onClickCommand="@{viewModel.loginOnClickCommand}"
複製程式碼

這和原本傳統的繫結不是一樣嗎?不,這其實是有差別的。使用這種形式的繫結,在原本事件繫結的基礎之上,帶有防重複點選的功能,1秒內多次點選也只會執行一次操作。如果不需要防重複點選,可以加入這條屬性

binding:isThrottleFirst="@{Boolean.TRUE}"
複製程式碼

那這功能是在哪裡做的呢?答案在下面的程式碼中。

//防重複點選間隔(秒)
public static final int CLICK_INTERVAL = 1;

/**
* requireAll 是意思是是否需要繫結全部引數, false為否
* View的onClick事件繫結
* onClickCommand 繫結的命令,
* isThrottleFirst 是否開啟防止過快點選
*/
@BindingAdapter(value = {"onClickCommand", "isThrottleFirst"}, requireAll = false)
public static void onClickCommand(View view, final BindingCommand clickCommand, final boolean isThrottleFirst) {
    if (isThrottleFirst) {
        RxView.clicks(view)
        .subscribe(new Consumer<Object>() {
            @Override
            public void accept(Object object) throws Exception {
                if (clickCommand != null) {
                    clickCommand.execute();
                }
            }
        });
    } else {
        RxView.clicks(view)
        .throttleFirst(CLICK_INTERVAL, TimeUnit.SECONDS)//1秒鐘內只允許點選1次
        .subscribe(new Consumer<Object>() {
            @Override
            public void accept(Object object) throws Exception {
                if (clickCommand != null) {
                    clickCommand.execute();
                }
            }
        });
    }
}
複製程式碼

onClickCommand方法是自定義的,使用@BindingAdapter註解來標明這是一個繫結方法。在方法中使用了RxView來增強view的clicks事件,.throttleFirst()限制訂閱者在指定的時間內重複執行,最後通過BindingCommand將事件回撥出去,就好比有一種攔截器,在點選時先做一下判斷,然後再把事件沿著他原有的方向傳遞。

是不是覺得有點意思,好戲還在後頭呢!

2.2.3、自定義ImageView圖片載入

繫結圖片路徑:

在ViewModel中定義

public String imgUrl = "http://img0.imgtn.bdimg.com/it/u=2183314203,562241301&fm=26&gp=0.jpg";
複製程式碼

在ImageView標籤中

binding:url="@{viewModel.imgUrl}"
複製程式碼

url是圖片路徑,這樣繫結後,這個ImageView就會去顯示這張圖片,不限網路圖片還是本地圖片。

如果需要給一個預設載入中的圖片,可以加這一句

binding:placeholderRes="@{R.mipmap.ic_launcher_round}"
複製程式碼

R檔案需要在data標籤中匯入使用,如:<import type="com.goldze.mvvmhabit.R" />

BindingAdapter中的實現

@BindingAdapter(value = {"url", "placeholderRes"}, requireAll = false)
public static void setImageUri(ImageView imageView, String url, int placeholderRes) {
    if (!TextUtils.isEmpty(url)) {
        //使用Glide框架載入圖片
        Glide.with(imageView.getContext())
            .load(url)
            .placeholder(placeholderRes)
            .into(imageView);
    }
}
複製程式碼

很簡單就自定義了一個ImageView圖片載入的繫結,學會這種方式,可自定義擴充套件。

如果你對這些感興趣,可以下載原始碼,在binding包中可以看到各類控制元件的繫結實現方式

2.2.4、RecyclerView繫結

RecyclerView也是很常用的一種控制元件,傳統的方式需要針對各種業務要寫各種Adapter,如果你使用了mvvmhabit,則可大大簡化這種工作量,從此告別setAdapter()。

在ViewModel中定義:

//給RecyclerView新增items
public final ObservableList<NetWorkItemViewModel> observableList = new ObservableArrayList<>();
//給RecyclerView新增ItemBinding
public final ItemBinding<NetWorkItemViewModel> itemBinding = ItemBinding.of(BR.viewModel, R.layout.item_network);
複製程式碼

ObservableList<>和ItemBinding<>的泛型是Item佈局所對應的ItemViewModel

在xml中繫結

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

layoutManager控制是線性(包含水平和垂直)排列還是網格排列,lineManager是設定分割線

網格佈局的寫法:binding:layoutManager="@{LayoutManagers.grid(3)}
水平佈局的寫法:binding:layoutManager="@{LayoutManagers.linear(LinearLayoutManager.HORIZONTAL,Boolean.FALSE)}"

使用到相關類,則需要匯入該類才能使用,和匯入Java類相似

<import type="me.tatarka.bindingcollectionadapter2.LayoutManagers" />
<import type="me.goldze.mvvmhabit.binding.viewadapter.recyclerview.LineManagers" />
<import type="android.support.v7.widget.LinearLayoutManager" />

這樣繫結後,在ViewModel中呼叫ObservableList的add()方法,新增一個ItemViewModel,介面上就會實時繪製出一個Item。在Item對應的ViewModel中,同樣可以以繫結的形式完成邏輯

可以在請求到資料後,迴圈新增observableList.add(new NetWorkItemViewModel(NetWorkViewModel.this, entity));詳細可以參考例子程式中NetWorkViewModel類。

注意: 在以前的版本中,ItemViewModel是繼承BaseViewModel,傳入Context,新版本3.x中可繼承ItemViewModel,傳入當前頁面的ViewModel

更多RecyclerView、ListView、ViewPager等繫結方式,請參考 github.com/evant/bindi…

2.3、網路請求

網路請求一直都是一個專案的核心,現在的專案基本都離不開網路,一個好用網路請求框架可以讓開發事半功倍。

2.3.1、Retrofit+Okhttp+RxJava

現今,這三個組合基本是網路請求的標配,如果你對這三個框架不瞭解,建議先去查閱相關資料。

square出品的框架,用起來確實非常方便。MVVMHabit中引入了

api "com.squareup.okhttp3:okhttp:3.10.0"
api "com.squareup.retrofit2:retrofit:2.4.0"
api "com.squareup.retrofit2:converter-gson:2.4.0"
api "com.squareup.retrofit2:adapter-rxjava2:2.4.0"
複製程式碼

構建Retrofit時加入

Retrofit retrofit = new Retrofit.Builder()
    .addConverterFactory(GsonConverterFactory.create())
    .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
    .build();
複製程式碼

或者直接使用例子程式中封裝好的RetrofitClient

2.3.2、網路攔截器

LoggingInterceptor: 全域性攔截請求資訊,格式化列印Request、Response,可以清晰的看到與後臺介面對接的資料,

LoggingInterceptor mLoggingInterceptor = new LoggingInterceptor
    .Builder()//構建者模式
    .loggable(true) //是否開啟日誌列印
    .setLevel(Level.BODY) //列印的等級
    .log(Platform.INFO) // 列印型別
    .request("Request") // request的Tag
    .response("Response")// Response的Tag
    .addHeader("version", BuildConfig.VERSION_NAME)//列印版本
    .build()
複製程式碼

構建okhttp時加入

OkHttpClient okHttpClient = new OkHttpClient.Builder()
    .addInterceptor(mLoggingInterceptor)
    .build();
複製程式碼

CacheInterceptor: 快取攔截器,當沒有網路連線的時候自動讀取快取中的資料,快取存放時間預設為3天。
建立快取物件

//快取時間
int CACHE_TIMEOUT = 10 * 1024 * 1024
//快取存放的檔案
File httpCacheDirectory = new File(mContext.getCacheDir(), "goldze_cache");
//快取物件
Cache cache = new Cache(httpCacheDirectory, CACHE_TIMEOUT);
複製程式碼

構建okhttp時加入

OkHttpClient okHttpClient = new OkHttpClient.Builder()
    .cache(cache)
    .addInterceptor(new CacheInterceptor(mContext))
    .build();
複製程式碼

2.3.3、Cookie管理

MVVMHabit提供兩種CookieStore:PersistentCookieStore (SharedPreferences管理)和MemoryCookieStore (記憶體管理),可以根據自己的業務需求,在構建okhttp時加入相應的cookieJar

OkHttpClient okHttpClient = new OkHttpClient.Builder()
    .cookieJar(new CookieJarImpl(new PersistentCookieStore(mContext)))
    .build();
複製程式碼

或者

OkHttpClient okHttpClient = new OkHttpClient.Builder()
    .cookieJar(new CookieJarImpl(new MemoryCookieStore()))
    .build();
複製程式碼

2.3.4、繫結生命週期

請求在ViewModel層。預設在BaseActivity中注入了LifecycleProvider物件到ViewModel,用於繫結請求的生命週期,View與請求共存亡。

RetrofitClient.getInstance().create(DemoApiService.class)
    .demoGet()
    .compose(RxUtils.bindToLifecycle(getLifecycleProvider())) // 請求與View週期同步
    .compose(RxUtils.schedulersTransformer())  // 執行緒排程
    .compose(RxUtils.exceptionTransformer())   // 網路錯誤的異常轉換
    .subscribe(new Consumer<BaseResponse<DemoEntity>>() {
        @Override
        public void accept(BaseResponse<DemoEntity> response) throws Exception {
                       
        }
    }, new Consumer<ResponseThrowable>() {
        @Override
        public void accept(ResponseThrowable throwable) throws Exception {
                        
        }
    });

複製程式碼

在請求時關鍵需要加入組合操作符.compose(RxUtils.bindToLifecycle(getLifecycleProvider()))
注意: 由於BaseActivity/BaseFragment都實現了LifecycleProvider介面,並且預設注入到ViewModel中,所以在呼叫請求方法時可以直接呼叫getLifecycleProvider()拿到生命週期介面。如果你沒有使用 mvvmabit 裡面的BaseActivity或BaseFragment,使用自己定義的Base,那麼需要讓你自己的Activity繼承RxAppCompatActivity、Fragment繼承RxFragment才能用RxUtils.bindToLifecycle(lifecycle)方法。

2.3.5、網路異常處理

網路異常在網路請求中非常常見,比如請求超時、解析錯誤、資源不存在、伺服器內部錯誤等,在客戶端則需要做相應的處理(當然,你可以把一部分異常甩鍋給網路,比如當出現code 500時,提示:請求超時,請檢查網路連線,此時偷偷將異常資訊傳送至後臺(手動滑稽))。

在使用Retrofit請求時,加入組合操作符.compose(RxUtils.exceptionTransformer()),當發生網路異常時,回撥onError(ResponseThrowable)方法,可以拿到異常的code和message,做相應處理。

mvvmhabit中自定義了一個ExceptionHandle,已為你完成了大部分網路異常的判斷,也可自行根據專案的具體需求調整邏輯。

注意: 這裡的網路異常code,並非是與服務端協議約定的code。網路異常可以分為兩部分,一部分是協議異常,即出現code = 404、500等,屬於HttpException,另一部分為請求異常,即出現:連線超時、解析錯誤、證照驗證失等。而與服務端約定的code規則,它不屬於網路異常,它是屬於一種業務異常。在請求中可以使用RxJava的filter(過濾器),也可以自定義BaseSubscriber統一處理網路請求的業務邏輯異常。由於每個公司的業務協議不一樣,所以具體需要你自己來處理該類異常。

3、輔助功能

一個完整的快速開發框架,當然也少不了常用的輔助類。下面來介紹一下MVVMabit中有哪些輔助功能。

3.1、事件匯流排

事件匯流排存在的優點想必大家都很清楚了,android自帶的廣播機制對於元件間的通訊而言,使用非常繁瑣,通訊元件彼此之間的訂閱和釋出的耦合也比較嚴重,特別是對於事件的定義,廣播機制侷限於序列化的類(通過Intent傳遞),不夠靈活。

3.3.1、RxBus

RxBus並不是一個庫,而是一種模式。相信大多數開發者都使用過EventBus,對RxBus也是很熟悉。由於MVVMabit中已經加入RxJava,所以採用了RxBus代替EventBus作為事件匯流排通訊,以減少庫的依賴。

使用方法:

在ViewModel中重寫registerRxBus()方法來註冊RxBus,重寫removeRxBus()方法來移除RxBus

//訂閱者
private Disposable mSubscription;
//註冊RxBus
@Override
public void registerRxBus() {
    super.registerRxBus();
    mSubscription = RxBus.getDefault().toObservable(String.class)
        .subscribe(new Consumer<String>() {
            @Override
            public void accept(String s) throws Exception {

            }
        });
    //將訂閱者加入管理站
    RxSubscriptions.add(mSubscription);
}

//移除RxBus
@Override
public void removeRxBus() {
    super.removeRxBus();
    //將訂閱者從管理站中移除
    RxSubscriptions.remove(mSubscription);
}
複製程式碼

在需要執行回撥的地方傳送

RxBus.getDefault().post(object);
複製程式碼

3.3.2、Messenger

Messenger是一個輕量級全域性的訊息通訊工具,在我們的複雜業務中,難免會出現一些交叉的業務,比如ViewModel與ViewModel之間需要有資料交換,這時候可以輕鬆地使用Messenger傳送一個實體或一個空訊息,將事件從一個ViewModel回撥到另一個ViewModel中。

使用方法:

定義一個靜態String型別的字串token

public static final String TOKEN_LOGINVIEWMODEL_REFRESH = "token_loginviewmodel_refresh";
複製程式碼

在ViewModel中註冊訊息監聽

//註冊一個空訊息監聽 
//引數1:接受人(上下文)
//引數2:定義的token
//引數3:執行的回撥監聽
Messenger.getDefault().register(this, LoginViewModel.TOKEN_LOGINVIEWMODEL_REFRESH, new BindingAction() {
    @Override
    public void call() {
	
    }
});

//註冊一個帶資料回撥的訊息監聽 
//引數1:接受人(上下文)
//引數2:定義的token
//引數3:實體的泛型約束
//引數4:執行的回撥監聽
Messenger.getDefault().register(this, LoginViewModel.TOKEN_LOGINVIEWMODEL_REFRESH, String.class, new Consumer<String>() {
    @Override
    public void accept(String s) throws Exception {
                
    }
});
複製程式碼

在需要回撥的地方使用token傳送訊息

//傳送一個空訊息
//引數1:定義的token
Messenger.getDefault().sendNoMsg(LoginViewModel.TOKEN_LOGINVIEWMODEL_REFRESH);

//傳送一個帶資料回撥訊息
//引數1:回撥的實體
//引數2:定義的token
Messenger.getDefault().send("refresh",LoginViewModel.TOKEN_LOGINVIEWMODEL_REFRESH);
複製程式碼

token最好不要重名,不然可能就會出現邏輯上的bug,為了更好的維護和清晰邏輯,建議以aa_bb_cc的格式來定義token。aa:TOKEN,bb:ViewModel的類名,cc:動作名(功能名)。

為了避免大量使用Messenger,建議只在ViewModel與ViewModel之間使用,View與ViewModel之間採用ObservableField去監聽UI上的邏輯,可在繼承了Base的Activity或Fragment中重寫initViewObservable()方法來初始化UI的監聽

註冊了監聽,當然也要解除它。在BaseActivity、BaseFragment的onDestroy()方法裡已經呼叫Messenger.getDefault().unregister(viewModel);解除註冊,所以不用擔心忘記解除導致的邏輯錯誤和記憶體洩漏。

3.2、檔案下載

檔案下載幾乎是每個app必備的功能,圖文的下載,軟體的升級等都要用到,mvvmhabit使用Retrofit+Okhttp+RxJava+RxBus實現一行程式碼監聽帶進度的檔案下載。

下載檔案

String loadUrl = "你的檔案下載路徑";
String destFileDir = context.getCacheDir().getPath();  //檔案存放的路徑
String destFileName = System.currentTimeMillis() + ".apk";//檔案存放的名稱
DownLoadManager.getInstance().load(loadUrl, new ProgressCallBack<ResponseBody>(destFileDir, destFileName) {
    @Override
    public void onStart() {
        //RxJava的onStart()
    }

    @Override
    public void onCompleted() {
        //RxJava的onCompleted()
    }

    @Override
    public void onSuccess(ResponseBody responseBody) {
        //下載成功的回撥
    }

    @Override
    public void progress(final long progress, final long total) {
        //下載中的回撥 progress:當前進度 ,total:檔案總大小
    }

    @Override
    public void onError(Throwable e) {
        //下載錯誤回撥
    }
});
複製程式碼

在ProgressResponseBody中使用了RxBus,傳送下載進度資訊到ProgressCallBack中,繼承ProgressCallBack就可以監聽到下載狀態。回撥方法全部執行在主執行緒,方便UI的更新,詳情請參考例子程式。

3.3、ContainerActivity

一個盛裝Fragment的一個容器(代理)Activity,普通介面只需要編寫Fragment,使用此Activity盛裝,這樣就不需要每個介面都在AndroidManifest中註冊一遍

使用方法:

在ViewModel中呼叫BaseViewModel的方法開一個Fragment

startContainerActivity(你的Fragment類名.class.getCanonicalName())
複製程式碼

在ViewModel中呼叫BaseViewModel的方法,攜帶一個序列化實體開啟一個Fragment

Bundle mBundle = new Bundle();
mBundle.putParcelable("entity", entity);
startContainerActivity(你的Fragment類名.class.getCanonicalName(), mBundle);
複製程式碼

在你的Fragment中取出實體

Bundle mBundle = getArguments();
if (mBundle != null) {
    entity = mBundle.getParcelable("entity");
}
複製程式碼

3.4、6.0許可權申請

對RxPermissions已經熟悉的朋友可以跳過。

使用方法:

例如請求相機許可權,在ViewModel中呼叫

//請求開啟相機許可權
RxPermissions rxPermissions = new RxPermissions((Activity) context);
rxPermissions.request(Manifest.permission.CAMERA)
    .subscribe(new Consumer<Boolean>() {
        @Override
        public void accept(Boolean aBoolean) throws Exception {
            if (aBoolean) {
                ToastUtils.showShort("許可權已經開啟,直接跳入相機");
            } else {
                ToastUtils.showShort("許可權被拒絕");
            }
        }
    });
複製程式碼

更多許可權申請方式請參考RxPermissions原專案地址

3.5、圖片壓縮

為了節約使用者流量和加快圖片上傳的速度,某些場景將圖片在本地壓縮後再傳給後臺,所以特此提供一個圖片壓縮的輔助功能。

使用方法:

RxJava的方式壓縮單張圖片,得到一個壓縮後的圖片檔案物件

String filePath = "mnt/sdcard/1.png";
ImageUtils.compressWithRx(filePath, new Consumer<File>() {
    @Override
    public void accept(File file) throws Exception {
        //將檔案放入RequestBody
        ...
    }
});
複製程式碼

RxJava的方式壓縮多張圖片,按集合順序每壓縮成功一張,都將在onNext方法中得到一個壓縮後的圖片檔案物件

List<String> filePaths = new ArrayList<>();
filePaths.add("mnt/sdcard/1.png");
filePaths.add("mnt/sdcard/2.png");
ImageUtils.compressWithRx(filePaths, new Subscriber() {
    @Override
    public void onCompleted() {
	
    }
	
    @Override
    public void onError(Throwable e) {
	
    }
	
    @Override
    public void onNext(File file) {

    }
});
複製程式碼

3.6、其他輔助類

ToastUtils: 吐司工具類

MaterialDialogUtils: Material風格對話方塊工具類

SPUtils: SharedPreferences工具類

SDCardUtils: SD卡相關工具類

ConvertUtils: 轉換相關工具類

StringUtils: 字串相關工具類

RegexUtils: 正則相關工具類

KLog: 日誌列印,含json格式列印

4、附加

4.1、編譯錯誤解決方法

使用databinding其實有個缺點,就是會遇到一些編譯錯誤,而AS不能很好的定位到錯誤的位置,這對於剛開始使用databinding的開發者來說是一個比較鬱悶的事。那麼我在此把我自己在開發中遇到的各種編譯問題的解決方法分享給大家,希望這對你會有所幫助。

4.1.1、繫結錯誤

繫結錯誤是一個很常見的錯誤,基本都會犯。比如TextView的 android:text="" ,本來要繫結的是一個String型別,結果你不小心,可能綁了一個Boolean上去,或者變數名寫錯了,這時候編輯器不會報紅錯,而是在點編譯執行的時候,在AS的Messages中會出現錯誤提示,如下圖:

新手用不起,高手看不上系列之--MVVMHabit

解決方法:把錯誤提示拉到最下面 (上面的提示找不到BR類這個不要管它),看最後一個錯誤 ,這裡會提示是哪個xml出了錯,並且會定位到行數,按照提示找到對應位置,即可解決該編譯錯誤的問題。

注意: 行數要+1,意思是上面報出第33行錯誤,實際是第34行錯誤,AS定位的不準確 (這可能是它的一個bug)

4.1.2、xml導包錯誤

在xml中需要匯入ViewModel或者一些業務相關的類,假如在xml中導錯了類,那一行則會報紅,但是res/layout卻沒有錯誤提示,有一種場景,非常特殊,不容易找出錯誤位置。就是你寫了一個xml,匯入了一個類,比如XXXUtils,後來因為業務需求,把那個XXXUtils刪了,這時候res/layout下不會出現任何錯誤,而你在編譯執行的時候,才會出現錯誤日誌。苦逼的是,不會像上面那樣提示哪一個xml檔案,哪一行出錯了,最後一個錯誤只是一大片的報錯報告。如下圖:

新手用不起,高手看不上系列之--MVVMHabit

解決方法:同樣找到最後一個錯誤提示,找到Cannot resolve type for xxx這一句 (xxx是類名),然後使用全域性搜尋 (Ctrl+H) ,搜尋哪個xml引用了這個類,跟蹤點選進去,在xml就會出現一個紅錯,看到錯誤你就會明白了,這樣就可解決該編譯錯誤的問題。

4.1.3、build錯誤

構建多module工程時,如出現【4.1.1、繫結錯誤】,且你能確定這個繫結是沒有問題的,經過修改後出現下圖錯誤:

新手用不起,高手看不上系列之--MVVMHabit

解決方法: 這種是databinding比較大的坑,清理、重構和刪build都不起作用,網上很難找到方法。經過試驗,解決辦法是手動建立異常中提到的資料夾,或者拷貝上一個沒有報錯的版本中對應的資料夾,可以解決這個異常

4.1.4、自動生成類錯誤

有時候在寫完xml時,databinding沒有自動生成對應的Binding類及屬性。比如新建了一個activity_login.xml,按照databinding的寫法加入<layout> <variable>後,理論上會自動對應生成ActivityLoginBinding.java類和variable的屬性,可能是as對databding的支援還不夠吧,有時候偏偏就不生成,導致BR.xxx報紅等一些莫名的錯誤。

解決方法:其實確保自己的寫法沒有問題,是可以直接執行的,報紅不一定是你寫的有問題,也有可能是編譯器抽風了。或者使用下面的辦法
第一招:Build->Clean Project;
第二招:Build->Rebuild Project;
第三招:重啟大法。

混淆

例子程式中給出了最新的【MVVMHabit混淆規則】,包含MVVMHabit中依賴的所有第三方library,可以將規則直接拷貝到自己app的混淆規則中。在此基礎上你只需要關注自己業務程式碼以及自己引入第三方的混淆,【MVVMHabit混淆規則】請參考app目錄下的proguard-rules.pro檔案。

About

goldze: 本人喜歡嘗試新的技術,以後發現有好用的東西,我將會在企業專案中實戰,沒有問題了就會把它引入到MVVMHabit中,一直維護著這套框架,謝謝各位朋友的支援。如果覺得這套框架不錯的話,麻煩點個 star,你的支援則是我前進的動力!

QQ群:84692105

License

Copyright 2017 goldze(曾憲澤)

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
複製程式碼

相關文章