MVI 概念
MVI 是和 MVVM 一起出現(xiàn)的概念,是跟著 Rxjava 響應式思路衍生出來的一種想法
MVVM 我猜大家都熟悉,數(shù)據(jù)層傳遞 Livedata -> persenter -> 再到 UI 層去注冊監(jiān)聽,這是個單向的過程,從 [數(shù)據(jù) -> UI] 的過程:

一般 MVVM 我們都是這么寫,但是也有的人寫的更徹底,app 的交互是個雙向過程,先從 UI -> 數(shù)據(jù) 再到 數(shù)據(jù) -> UI,上面常見的方式我們只是實現(xiàn)了一邊,更徹底的響應式改造是連點擊事件都是響應式的,數(shù)據(jù)層注冊監(jiān)聽按鈕的點擊事件,通過上水管道接受數(shù)據(jù),遠程數(shù)據(jù)返回后再通過下水管道返回數(shù)據(jù):

一般沒寫這么麻煩的~
MVI 和雙向 MVVM 的思路相同,區(qū)別就是 MVI 進一步抽象了 UI 的動作,也就是 MVI 中的 I - Intent,MVI 中把任何一個 UI 事件都看成一個響應式數(shù)據(jù)源,P 層的任務就是綁定注冊 V 和 M 層的關系,就像熱水器一樣接通上下水管道:

MVI 中的這個 Intent 有自己的思想,Intent 把 UI 頁面的任何變化都看成一個整體,用一個數(shù)據(jù)類型來表示,比如有一個 ViewState 對象里面有 loading,netError,success 各種表示頁面狀態(tài)的標志位,數(shù)據(jù)層返回的是這個 ViewState 而不再直接是數(shù)據(jù)了,UI 層根據(jù) ViewState 的狀態(tài)來顯示不同的 UI 樣式,這樣 P 層就不用再寫一堆控制 UI 顯示狀態(tài)的方法了,真正實現(xiàn)了 MVP 的分層思想,誰的事誰關心,當然 MVVM 也可以做到,但是一般 MVVM 里面都是直接返回數(shù)據(jù)的,真的把頁面狀態(tài)也封裝進數(shù)據(jù)的沒幾個人
class NetViewState(
var loading: Boolean = false,
var success: Boolean = false,
var netError: Boolean = false,
var dataError: Boolean = false,
var dataNo: Boolean = false,
var message: String = "",
var data: BookResponse = BookResponse()) {
companion object Help {
@JvmStatic
fun loading(): NetViewState {
return NetViewState(loading = true)
}
@JvmStatic
fun success(data: BookResponse): NetViewState {
return NetViewState(success = true, data = data)
}
@JvmStatic
fun netError(message: String): NetViewState {
return NetViewState(netError = true, message = message)
}
@JvmStatic
fun dataError(message: String): NetViewState {
return NetViewState(dataError = true, message = message)
}
@JvmStatic
fun dataNo(message: String): NetViewState {
return NetViewState(dataNo = true,message = message)
}
}
}
代碼走起
MVI 我看了好多文章,都是借助 mosby 這個庫來實現(xiàn)的,mosby 帶來了大量的衍生類型,每個角色都有其基類,無形中大大增加了學習成本,MVI 本是 MVVM 思路的進一步而已,沒想到大伙做的反倒是越來越復雜,全完沒必要,簡簡單單的多好,還容易理解,容易閱讀
使用 rxjava 熱發(fā)射或是 Livedata 就可以簡單的實現(xiàn) MVI 了,我就不想用 mosby ,自己實現(xiàn)一個 MVI 出來,下面的 Demo 只是用來演示,更多的請自省封裝,優(yōu)化
Ui 層對外提供事件 Intent
protected void onCreate(Bundle savedInstanceState) {
btn_book.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
bookIntent.onNext(new BookRequest("人生", "", "0", "10"));
}
});
persenter.netActivity = this;
persenter.bingIntent(bookIntent);
}
void updata(NetViewState netViewState) {
if (netViewState.getLoading()) {
Log.d("AA", "loading ...");
return;
}
if (netViewState.getNetError()) {
Log.d("AA", " netError:" + netViewState.getMessage());
}
if (netViewState.getDataError()) {
Log.d("AA", " dataError:" + netViewState.getMessage());
}
if (netViewState.getSuccess()) {
List<BookResponse.Book> books = netViewState.getData().getBooks();
if (books != null && books.size() == 0) {
ToastComponent.Companion.getInstance().show("沒有數(shù)據(jù)", Toast.LENGTH_SHORT);
}
adapter.refreshData(books);
}
}
M 層關注上游 UI 層事件,提供下游數(shù)據(jù)層觀察者
public class BookRepositroy {
public static final String URL_BOOK_LIST = "book/search";
public PublishSubject<NetViewState> bookData = PublishSubject.create();
public void bingIntent(Observable<BookRequest> bookIntent) {
bookIntent.subscribe(new Consumer<BookRequest>() {
@Override
public void accept(BookRequest bookRequest) throws Exception {
getBookData(bookRequest);
}
});
}
private void getBookData(BookRequest bookRequest) {
Map<String, String> map = new HashMap<>();
map.put("q", bookRequest.getTitle());
map.put("tag", bookRequest.getTag());
map.put("start", bookRequest.getStartCount());
map.put("count", bookRequest.getWantCount());
HttpClient.Companion.getInstance().get(URL_BOOK_LIST, map)
.map(new Function<ResponseBody, NetViewState>() {
@Override
public NetViewState apply(ResponseBody responseBody) throws Exception {
BookResponse bookResponse = null;
try {
bookResponse = new Gson().fromJson(responseBody.string(), BookResponse.class);
} catch (Exception e) {
Observable.error(e);
}
return NetViewState.success(bookResponse);
}
})
.onErrorReturn(new Function<Throwable, NetViewState>() {
@Override
public NetViewState apply(Throwable throwable) throws Exception {
if (throwable instanceof HttpException) {
// HTTP錯誤
return NetViewState.netError("網絡錯誤");
} else if (throwable instanceof ConnectException
|| throwable instanceof UnknownHostException) {
// 連接錯誤
return NetViewState.netError("連接錯誤");
} else if (throwable instanceof InterruptedIOException) {
// 連接超時
return NetViewState.netError("連接超時");
} else if (throwable instanceof JsonParseException
|| throwable instanceof JSONException
|| throwable instanceof ParseException) {
return NetViewState.dataError("解析錯誤");
} else if (throwable instanceof ApiException) {
return NetViewState.netError(throwable.getMessage());
} else if (throwable instanceof IOException) {
return NetViewState.netError("網絡錯誤");
}
return NetViewState.netError("位置錯誤");
}
})
.startWith(NetViewState.loading())
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe(new Consumer<NetViewState>() {
@Override
public void accept(final NetViewState netViewState) throws Exception {
bookData.onNext(netViewState);
}
});
}
}
p 層綁定上下游(V 和 P)關系
public class NetPersenter {
public NetActivity netActivity;
BookRepositroy repositroy = new BookRepositroy();
public void bindBook(PublishSubject<BookRequest> bookIntent) {
repositroy.bingIntent(bookIntent);
repositroy.bookData.subscribe(new Consumer<NetViewState>() {
@Override
public void accept(NetViewState netViewState) throws Exception {
netActivity.updata(netViewState);
}
});
}
}
思考:
- P 層注冊下游數(shù)據(jù)而沒有交給 V 層,是因為在 P 層里面我們可能有業(yè)務邏輯要要先一部處理,不能直接交給 U層
- M 層使用 startWith 優(yōu)先發(fā)送一個 loading 的事件出來
- ViewState 這里可以進一步優(yōu)化的,大部分頁面的狀態(tài)都一樣,有必要抽象一個基類出來,而且使用 koltin 的密封類(sealed) 還可以做的更好,再說現(xiàn)在是 kotlin 的時代了,純 java 的代碼我都不應該貼出來,這還是因為這個頁面還是以前寫的在上面改的,要不我就 kotlin 了
創(chuàng)作不易喜歡的話記得點贊+關注哦
