这是一篇写给那些听过MVP但是不是很清楚怎么去选择正确的使用方法的朋友。我们将从一个实现了API接口的MainActivity开始,将MainActivity上的逻辑迁移到MVP模式中。
免责申明:MVP模式不仅仅是一个层次性很优秀的模式
正如很多人说的,MVP不仅仅是一个层次性很优秀的模式:他只是一种分层次去写代码的方式,MVP很优秀,所以不管是大型APP还是小的一个demo,我们都可以使用并且很容易上手。但是,如果你想要对整个APP的代码进行规划,根据功能模块来写代码,而不是根据MVP那种定死的框架来写代码的话,那就一起往下看吧。
##准备##
下面的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();
}
}
这是一个典型的,在Activity里面实现了所有东西而不是根据MVP来写的Activity类。
MainActivity知道太多接口实现逻辑
我们需要注意到以下几点:
- MainActivity知道太多接口实现的东西。ps:在这里我们网络请求使用的是Retrofit,用一个回调来获取数据的。
- 这种写法违背了单一职责原理:MainActivity不仅负责获取数据,还要负责展示数据。
- 测试不同的Google Book数据将会意味着你需要修改MainActivity的代码,注释掉不需要没用的代码等。如果把它搞成一个对象那就会方便很多了。
我们的目标是在这个Activity里实现MVP,先说下思路:
- 将Retrofit代码移到Model里面,这样子MainAcitivity可以尽量的少去实现接口和将连接API的职责交给Model就可以了。
- 将performSearch方法移动到Presenter里面,这样就可以把处理响应的责任交给Presenter了。
- 在Presenter里调updateUi,这样更新UI的责任就交给了Presenter而不是MainActivity了。
##创建Model##
我们的Model是个接口,提供了一个方法去获取Google Books API。
首先,创建一个能提供给我们数据的接口,然后去实现它,具体实现的方法就是上面的使用Retrofit的方法。
public interface BooksInteractor {
Call<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())
.build();
// Create the Google Book API Service
service = retrofit.create(GoogleBooksService.class);
}
@Override
public Call<BookSearchResult> search(String search) {
return service.search("search+" + search);
}
}
现在我们就有Model了,上面的BooksInteractorImpl就是我们的Model。
这里我把Retrofit的配置也放到Model里了。其实最合适的方法是使用Dagger注入进去。(没了解过的同学可以看这里)
现在我们就需要在MainActivity的onCreate方法里创建我们的BookInteractor 对象了。
//...
BooksInteractor interactor;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
interactor = new BooksInteractorImpl();
//...
##创建Presenter
根据上面的分析,Presenter需要以下三样东西:
- Model
- View
- performSearch方法
首先创建Presenter类:
public class BooksPresenter {
}
添加一个Model的引用和一个构造方法。
public class BooksPresenter {
private BooksInteractor interactor;
public BooksPresenter(BooksInteractor interactor) {
this.interactor = interactor;
}
}
现在我们需要一个View的引用,和方法去绑定和取消绑定。
public class BooksPresenter {
BooksView view;
private BooksInteractor interactor;
public BooksPresenter(BooksInteractor interactor) {
this.interactor = interactor;
}
public void bind(BooksView view) {
this.view = view;
}
public void unbind() {
view = null;
}
}
绑定和解除绑定View是MVP模式的一个关键点,因为View是Android类的关键部分。
有些开发者喜欢在onCreate方法里绑定,在onDestroy方法里取消绑定,有些开发者则喜欢分别在onResume和onPause方法里绑定和取消绑定,这取决于你的使用场景。但是一般都是在最后的时候去取消绑定的。
记得remove View的引用(设置为空)
当然也有一些开发者使用不同的命名约定,比如attach/detach或者set/unset等。
最后将performSearch方法移动到Presenter里面。
public class BooksPresenter {
BooksView view;
private BooksInteractor interactor;
public BooksPresenter(BooksInteractor interactor) {
this.interactor = interactor;
}
public void bind(BooksView view) {
this.view = view;
}
public void unbind() {
view = null;
}
public void performSearch(String userInput) {
String formatUserInput = userInput.trim().replaceAll("\\s+", "+");
// Just call the method on the GoogleBooksService
interactor.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) {
if (view != null)
view.updateUi(books.body().getBooks());
}
// In case of error, this method gets called
@Override
public void onFailure(Call<BookSearchResult> call, Throwable t) {
t.printStackTrace();
}
});
}
}
我们需要在performSearch方法里改变几个地方:
- 首先我添加了一个用户输入的参数,而不是使用getUserInput。
- 我在onResponse回调里也做了些改变,updateUi由BooksView实施,但是因为View层是分开的,所以我们要去判断下是否不为空。
注意判断Presenter里的View是否为空
##创建View
我们移动了很多代码到了Model和Presenter里面,所以我们现在的MainActivity就很清晰了,但是还没实现View层的交接。
BooksView是一个包含了updateUi方法的小接口,所以我们需要让MainActivity去实现BooksView接口。
public interface BooksView {
void updateUi(List<Book> books);
}
public class MainActivity extends AppCompatActivity implements BooksView {
现在updateUi方法就在MainActivity里被重写了:
@Override
public void updateUi(List<Book> books) {
为了让所有东西都连接起来,我们就需要在onCreate方法里创建Presenter对象,并且绑定View层。
BooksInteractor interactor = new BooksInteractorImpl();
presenter = new BooksPresenter(interactor);
presenter.bind(this);
现在imageButton的点击事件就是调用presenter的performSearch方法。
imageButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
presenter.performSearch(getUserInput());
}
});
附上MainActivity的全部代码:
public class MainActivity extends AppCompatActivity implements BooksView {
EditText editText;
ImageButton imageButton;
BooksAdapter adapter;
ListView listView;
TextView textNoDataFound;
private BooksPresenter presenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
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) {
presenter.performSearch(getUserInput());
}
});
// Note: Don't do this on production code, use Dependency Injection instead
// to provide the BooksInteractor and the BooksPresenter to the View
// Learn how to use Dagger 2 here:
// https://medium.com/@Miqubel/understanding-dagger-2-367ff1bd184f#.s2jza32df
BooksInteractor interactor = new BooksInteractorImpl();
presenter = new BooksPresenter(interactor);
presenter.bind(this);
}
@Override
protected void onDestroy() {
presenter.unbind();
super.onDestroy();
}
@Override
public 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();
}
}
不要在View层写代码的时候将Model和Presenter实例化,最好使用依赖注入的方式提供Presenter和Model
##总结
- MainActivity不关心Model里逻辑是怎么实现的,它不知道是使用Retrofit还是其他的。我们可以通过改变Presenter里的方法列表来使测试更加方便,各个方法模块之间也很独立
- MainActivity也不关心Presenter的结果是成功还是错误的
- 回调回来的逻辑放到Presenter里。如果有一天,你想把Retrofit的回调方法都改成用RxJava或者其他方式来实现,你根本不用动View层。
- 你现在可以对Model和Presenter进行分开测试了