MVVM模式的意思是Model-View-ViewModel,这是另一种可以将View层与逻辑层代码分开的软件开发模式,尤其在Android上,可以将Android代码和其他代码分开。
Model的职责是业务逻辑和数据的实体模型,应该做到易于替换,易于测试,并且独立于用户界面。
ViewModel作为一个中间人,职责是将数据和请求从Model层转换成可以成View层可以直接显示的东西。
View层的职责就是布局文件。
其实就是MVP中的Presenter 变成了ViewModel
相对于MVP来说,其实就是Presenter变成了ViewModel,使用观察者提供给View层异步的结果。
以下是两种实现MVVM的主要方式:
- 使用RxJava Observables
- 使用Android Data Binding
我更喜欢第一种。这里我会尽量仔细的解释如何使用RxJava Observables实现MVVM。
##初始代码
下面的Activity包含了:
调用了Google Books API
将数据展示在ListView上,用Adapter的形式。
public class MainActivity extends AppCompatActivity {
EditText editText;
ImageButton imageButton;
BooksAdapter adapter;
ListView listView;
TextView textNoDataFound;
GoogleBooksService service;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Configure Retrofit
Retrofit retrofit = new Retrofit.Builder()
// Base URL can change for endpoints (dev, staging, live..)
.baseUrl("https://www.googleapis.com")
// Takes care of converting the JSON response into java objects
.addConverterFactory(GsonConverterFactory.create())
.build();
// Create the Google Book API Service
service = retrofit.create(GoogleBooksService.class);
editText = (EditText) findViewById(R.id.editText);
imageButton = (ImageButton) findViewById(R.id.imageButton);
textNoDataFound = (TextView) findViewById(R.id.text_no_data_found);
adapter = new BooksAdapter(this, -1);
listView = (ListView) findViewById(R.id.listView);
listView.setAdapter(adapter);
imageButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
performSearch();
}
});
}
private void performSearch() {
String formatUserInput = getUserInput().trim().replaceAll("\\s+", "+");
// Just call the method on the GoogleBooksService
service.search("search+" + formatUserInput)
// enqueue runs the request on a separate thread
.enqueue(new Callback<BookSearchResult>() {
// We receive a Response with the content we expect already parsed
@Override
public void onResponse(Call<BookSearchResult> call, Response<BookSearchResult> books) {
updateUi(books.body().getBooks());
}
// In case of error, this method gets called
@Override
public void onFailure(Call<BookSearchResult> call, Throwable t) {
t.printStackTrace();
}
});
}
private void updateUi(List<Book> books) {
if (books.isEmpty()) {
// if no books found, show a message
textNoDataFound.setVisibility(View.VISIBLE);
} else {
textNoDataFound.setVisibility(View.GONE);
}
adapter.clear();
adapter.addAll(books);
}
private String getUserInput() {
return editText.getText().toString();
}
}
这是一个典型的里面实现了很多东西的MainActivity。
MainActivity里面东西太多了
我们可以注意到:
- MainActivity知道太多接口实现的东西。ps:在这里我们网络请求使用的是Retrofit,用一个回调来获取数据的。
- 这种写法违背了单一职责原理:MainActivity不仅负责获取数据,还要负责展示数据。
- 测试不同的Google Book数据将会意味着你需要修改MainActivity的代码,注释掉不需要没用的代码等。如果把它搞成一个对象那就会方便很多了。
实现MVVM的目标:
- 将Retrofit的回调切换成RxJava的Observable。
- 将Retrofit的逻辑代码移到Model里,这样子MainAcitivity可以尽量的少去实现接口和将连接API的职责交给Model就可以了。
- 把View 订阅给ViewModel的结果。
##切换RxJava
在你将MainActivity里的逻辑迁移出去之前,你需要先将Retrofit切换成RxJava。这个步骤要求你先在你的项目里引入RxJava的库。
不是所有人都喜欢RxJava,但是我喜欢
要知道RxJava被很多人定义为“怪物”,因为他是一个巨大的库(超过5k的方法数量),你的leader可能会不高兴。相反,MVP不需要你添加更多的library到你的项目里。
添加下面这段到你的build.gradle里:
//In app/build.gradle
compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0'
compile 'io.reactivex:rxandroid:1.2.1'
compile 'io.reactivex:rxjava:1.2.1'
现在是时候迁移逻辑了。在Retrofit的服务层将GoogleBooksService 改成Observable:
Call<BookSearchResult> search(@Query("q") String search);
// change it to
Observable<BookSearchResult> search(@Query("q") String search);
注意:我们引用的是 rx.Observable而不是java.util.Observable。
现在我们就要使用Observable来改写MainActivity了。
旧代码:
service.search("search+" + formatUserInput)
.enqueue(new Callback<BookSearchResult>() {
@Override
public void onResponse(Call<BookSearchResult> call,
Response<BookSearchResult> books) {
updateUi(books.body().getBooks());
}
@Override
public void onFailure(Call<BookSearchResult> call,
Throwable t) {
t.printStackTrace();
}
});
改变成
subscription = service.search("search+" + formatUserInput)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<BookSearchResult>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
e.printStackTrace();
}
@Override
public void onNext(BookSearchResult bookSearchResult) {
updateUi(bookSearchResult.getBooks());
}
});
这段代码看上去类似回调函数,但是是不同的:
- 搜索(字符串)返回一个会返回结果的Observable。
- 你需要“订阅”Observable,所以你将会收到一个包括BookSearchResult的“下一步”事件。
- 如果出现错误,您将收到一个“错误”事件。
- 指定Rxjava我们想要使用的IO线程执行网络呼叫subscribeon(不是主线程)。
- 指定要处理Rxjava Android的主线程接收我们的事件observeon(UI)。
- 保持“订阅”的地方,因为你会想退订离开Activity时。
这是一个观察者模式:当你做的时候订阅退订的事件。
##创建Model
我们的模型将是一个java的接口,可以访问Google Books API提供了一种方法。
首先创建一个接口,将为我们提供数据。然后创建一个该接口的实现。将所有的改造代码都移动到它。他界面是这里的重要组成部分。它只包含模型中可用的方法的定义,并且应尽可能保持简单。
public interface BooksInteractor {
Observable<BookSearchResult> search(String search);
}
public class BooksInteractorImpl implements BooksInteractor {
private GoogleBooksService service;
public BooksInteractorImpl() {
// Configure Retrofit
Retrofit retrofit = new Retrofit.Builder()
// Base URL can change for endpoints (dev, staging, live..)
.baseUrl("https://www.googleapis.com")
// Takes care of converting the JSON response into java objects
.addConverterFactory(GsonConverterFactory.create())
// Retrofit Call to RxJava Observable
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build();
// Create the Google Book API Service
service = retrofit.create(GoogleBooksService.class);
}
@Override
public Observable<BookSearchResult> search(String search) {
return service.search("search+" + search).subscribeOn(Schedulers.io());
}
}
重点:
- 将重构配置代码移入构造函数。
- 搜索方法返回一个可观察booksearchresults。
- 我把“搜索+”部分的交互实现。
- Observable已经订阅了他。
- 指定调用适配器工厂能够使用Rxjava。
##创建ViewModel
现在讲的就是跟MVP最大的区别了,我们的ViewModel非常简单,它只做一件事情,确保我们使用的是正确的线程。
如果格式化的数据,这是做它的地方,你可以用Rxjava map的方法转化到另一个物体,还有一个巨大的名单Rxjava运营商可以使用所有可能的行动。
BooksViewModel的代码:
public class BooksViewModel {
private BooksInteractor interactor;
private Scheduler scheduler;
public BooksViewModel(BooksInteractor interactor, Scheduler scheduler) {
this.interactor = interactor;
this.scheduler = scheduler;
}
public Observable<BookSearchResult> search(String search) {
return interactor.search(search).observeOn(scheduler);
}
}
这个问题需要两个参数:关联和主线程调度器。因为AndroidSchedulers是一个Android的依赖,我决定搬出来,将它作为参数传递。
“只是想关联的搜索方法,确保项目在正确的线程观察。
##更新View
public class MainActivity extends AppCompatActivity {
EditText editText;
ImageButton imageButton;
BooksAdapter adapter;
ListView listView;
TextView textNoDataFound;
private CompositeSubscription subscriptions = new CompositeSubscription();
private BooksViewModel booksViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
booksViewModel = new BooksViewModel(new BooksInteractorImpl(),
AndroidSchedulers.mainThread());
editText = (EditText) findViewById(R.id.editText);
imageButton = (ImageButton) findViewById(R.id.imageButton);
textNoDataFound = (TextView) findViewById(R.id.text_no_data_found);
adapter = new BooksAdapter(this, -1);
listView = (ListView) findViewById(R.id.listView);
listView.setAdapter(adapter);
imageButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
performSearch();
}
});
}
@Override
protected void onDestroy() {
subscriptions.unsubscribe();
super.onDestroy();
}
private void performSearch() {
String formatUserInput = getUserInput().trim().replaceAll("\\s+", "+");
subscription = booksViewModel.search(formatUserInput)
.subscribe(new Observer<BookSearchResult>() {
@Override
public void onCompleted() {
}
@Override
public void onError(Throwable e) {
e.printStackTrace();
}
@Override
public void onNext(BookSearchResult bookSearchResult) {
updateUi(bookSearchResult.getBooks());
}
});
}
private void updateUi(List<Book> books) {
if (books.isEmpty()) {
// if no books found, show a message
textNoDataFound.setVisibility(View.VISIBLE);
} else {
textNoDataFound.setVisibility(View.GONE);
}
adapter.clear();
adapter.addAll(books);
}
private String getUserInput() {
return editText.getText().toString();
}
}
重要的几点:
- 使用compositesubscription保持所有的订阅,并在onDestroy方法的时候调用unsubscribe()。
- subscribeon和observeon方法现在是Model和ViewModel的一部分。
- ViewModel是在OnCreate初始化(就像MVP的Presenter)。
- 通过androidschedulers,mainthread()将作为ViewModel的一个依赖。
##so哪里更优秀了呢?
在原有的MainActivity里没有太多的业务逻辑,因此很难很快看到更好的了。
其中一个最大的改进就是测试。如果我们要考验我们的用户提供自己的图书列表,我们只需要创建一个booksinteractor:
public class BooksInteractorMock implements BooksInteractor {
@Override
public Observable<BookSearchResult> search(String search) {
return Observable.just(getMockedBookSearchResult());
}
private BookSearchResult getMockedBookSearchResult() {
BookSearchResult bookSearchResult = new BookSearchResult();
// bookSearchResult.setBooks(myListOfBooks);
return bookSearchResult;
}
}
与Observable的just()方法相比,我们可以创建一个可将自己的图书列表发出我们自己的booksearchresult。
所以在MainActivity我们改变它使用模拟:
booksViewModel = new BooksViewModel(new BooksInteractorMock(), AndroidSchedulers.mainThread());
更MVP相比,类跟接口变少了
对MVP的最大改进是,我们不需要一个视图界面了。“不知道看什么。这使得代码小于MVP和尺度更好当你有多个视图和ViewModels,频繁的在别人,以及,如果视图是无效的或不在主持人你不需要检查。
其次,但不是那么重要,你可以有多个视图模型视图。有时问题与MVP这帽子的主持人成为神的对象太多的责任,与MVVM可以有多个视图模型,每一个执行你的UX的一部分。
对MVP的最大缺点,除了要求rxjava,是MainActivity仍然持有一些职责:处理订阅和错误。
在我看来,MVP或MVVM之间的选择归结为这些不同的因素:
- 重构为MVP模式使遗留代码更容易
- MVP不需要RxJava
- MVVM能更好的服务于更大的项目
- 你可以有多个ViewModel服务于Model,而在MVP的关系是一对一