现在用一个基于MVP模式的APP项目进一步分析MVP的实际应用。
原项目应该使用的是Android studio开发,笔者对项目进行了整理,广大Eclipser请猛点Github链接。
一、项目功能说明
APP获取好友列表后将数据展示在一个ListView中,点击Item会打开一个新页面展示好友详细信息。
二、项目结构
示例将代码分为四层,对应到MVP模式中:
- Mode:Entities
- Presenter:Use Cases+Presenters
- View:UI
为了保证每个层都能方便的进行单元测试并和其它层相对独立,将项目分为三个模块,Presentation、Domain和Data。
1)Presentation
Presenters和UI被划分到这一层,但Presenters在这里只是负责将Domain逻辑处理后的数据进行组装并调度UI显示,没有业务处理逻辑。这样将数据逻辑处理划分给Domain可以让Presenter更关注于UI显示的调度,从而避免Present逻辑的冗余。这也是我选择这个工程作为示例的原因。
2)Domain
这里对Data中的数据进行逻辑处理,为Present提供业务逻辑和数据支持。
3)Data
数据仓库。例如,当通过id获取用户数据时,首先会检测用户信息是否已经存储在本地,否则的话就会从服务器获取后在本地缓存。根据上篇博客提到的设计原则,外圆代码逻辑无需关心用户数据是从存储介质、内存还是服务器获取,只需拿到Domain处理好的最终数据进行展示和交互即可。
根据以上说明,笔者将APP分为三个工程
其中Domain和Data作为Library的形式供Presentation引入。
三、代码详解
通过获取用户详细信息这个功能分析各个层级之间的调度及数据传递方向和方式。
首先入口View层的UserDetaisFragment。
[java]
view plain
copy
- public class UserDetailsFragment extends BaseFragment implements UserDetailsView
[java]
view plain
copy
- private UserDetailsPresenter userDetailsPresenter;
[java]
view plain
copy
- @Override void initializePresenter() {
- // All these dependency initialization could have been avoided using a
- // dependency injection framework. But in this case are used this way for
- // LEARNING EXAMPLE PURPOSE.
- ThreadExecutor threadExecutor = JobExecutor.getInstance();
- PostExecutionThread postExecutionThread = UIThread.getInstance();
- JsonSerializer userCacheSerializer = new JsonSerializer();
- UserCache userCache = UserCacheImpl.getInstance(getActivity(), userCacheSerializer,
- FileManager.getInstance(), threadExecutor);
- UserDataStoreFactory userDataStoreFactory =
- new UserDataStoreFactory(this.getContext(), userCache);
- UserEntityDataMapper userEntityDataMapper = new UserEntityDataMapper();
- UserRepository userRepository = UserDataRepository.getInstance(userDataStoreFactory,
- userEntityDataMapper);
- GetUserDetailsUseCase getUserDetailsUseCase = new GetUserDetailsUseCaseImpl(userRepository,
- threadExecutor, postExecutionThread);
- UserModelDataMapper userModelDataMapper = new UserModelDataMapper();
- this.userDetailsPresenter =
- new UserDetailsPresenter(this, getUserDetailsUseCase, userModelDataMapper);
- }
UserDetailsFragment内部有个UserDetailsPresenter引用,下面我们看UserDetailsPresenter代码。
[java]
view plain
copy
- public class UserDetailsPresenter implements Presenter {
- /** id used to retrieve user details */
- private int userId;
- private final UserDetailsView viewDetailsView;
- private final GetUserDetailsUseCase getUserDetailsUseCase;
- private final UserModelDataMapper userModelDataMapper;
- public UserDetailsPresenter(UserDetailsView userDetailsView,
- GetUserDetailsUseCase getUserDetailsUseCase, UserModelDataMapper userModelDataMapper) {
- if (userDetailsView == null || getUserDetailsUseCase == null || userModelDataMapper == null) {
- throw new IllegalArgumentException("Constructor parameters cannot be null!!!");
- }
- this.viewDetailsView = userDetailsView;
- this.getUserDetailsUseCase = getUserDetailsUseCase;
- this.userModelDataMapper = userModelDataMapper;
- }
[java]
view plain
copy
- private void showViewLoading() {
- this.viewDetailsView.showLoading();
- }
- private void getUserDetails() {
- this.getUserDetailsUseCase.execute(this.userId, this.userDetailsCallback);
- }
- private final GetUserDetailsUseCase.Callback userDetailsCallback = new GetUserDetailsUseCase.Callback() {
- @Override public void onUserDataLoaded(User user) {
- UserDetailsPresenter.this.showUserDetailsInView(user);
- UserDetailsPresenter.this.hideViewLoading();
- }
- @Override public void onError(ErrorBundle errorBundle) {
- UserDetailsPresenter.this.hideViewLoading();
- UserDetailsPresenter.this.showErrorMessage(errorBundle);
- UserDetailsPresenter.this.showViewRetry();
- }
- };
- }
- private void hideViewLoading() {
- this.viewDetailsView.hideLoading();
- }
- private void showViewRetry() {
- this.viewDetailsView.showRetry();
- }
- private void hideViewRetry() {
- this.viewDetailsView.hideRetry();
- }
Presenter包涵一个GetUserDetailsUseCase引用,并在调用getUserDetails()方法时传入一个callBack作为数据处理结果的回调。可以看到Presenter中并没有复杂的逻辑处理,反而更多的是hideViewRetry(),showViewLoading()等对Fragemnt的显示调度方法。Fragemt实现了UserDetailsView接口协议并在实例化Presenter时当做参数传入,这样就搭建好了符合MVP模式的Presenter和View的交互方式。
[java]
view plain
copy
- public interface UserDetailsView extends LoadDataView {
- /**
- * Render a user in the UI.
- *
- * @param user The {@link UserModel} that will be shown.
- */
- void renderUser(UserModel user);
- }
[java]
view plain
copy
- public interface LoadDataView {
- /**
- * Show a view with a progress bar indicating a loading process.
- */
- void showLoading();
- /**
- * Hide a loading view.
- */
- void hideLoading();
- /**
- * Show a retry view in case of an error when retrieving data.
- */
- void showRetry();
- /**
- * Hide a retry view shown if there was an error when retrieving data.
- */
- void hideRetry();
- /**
- * Show an error message
- *
- * @param message A string representing an error.
- */
- void showError(String message);
- /**
- * Get a {@link android.content.Context}.
- */
- Context getContext();
- }
下面我们看GetUserDetailUseCase代码。GetUserDetailUseCase在Domain模块,负责加工处理Data模块的数据。
[java]
view plain
copy
- public class GetUserDetailsUseCaseImpl implements GetUserDetailsUseCase {
[java]
view plain
copy
- private final UserRepository userRepository;
[java]
view plain
copy
- @Override public void execute(int userId, Callback callback) {
- if (userId < 0 || callback == null) {
- throw new IllegalArgumentException("Invalid parameter!!!");
- }
- this.userId = userId;
- this.callback = callback;
- this.threadExecutor.execute(this);
- }
[java]
view plain
copy
- @Override public void run() {
- this.userRepository.getUserById(this.userId, this.repositoryCallback);
- }
[java]
view plain
copy
- private final UserRepository.UserDetailsCallback repositoryCallback =
- new UserRepository.UserDetailsCallback() {
- @Override public void onUserLoaded(User user) {
- notifyGetUserDetailsSuccessfully(user);
- }
- @Override public void onError(ErrorBundle errorBundle) {
- notifyError(errorBundle);
- }
- };
- private void notifyGetUserDetailsSuccessfully(final User user) {
- this.postExecutionThread.post(new Runnable() {
- @Override public void run() {
- callback.onUserDataLoaded(user);
- }
- });
- }
[java]
view plain
copy
- }
GetUserDetailUseCaseImpl又会调用Data模块的UserRepository获取用户数据。UserRepository会从本地存储或服务器获取用户数据,这里就不再跟进UserRepository的源码。
当GetUserDetailUseCaseImpl从UserRepository获取用户数据后通过UserRepository.UserDetailCallback回调给UserDetailPresenter,然后Presenter就会根据数据调度Fragment的显示。
下面总结下从用户点击Item到打开用户详情页的调用流程及数据流动方向
用户点击Fragment中ListView的Item,Fragment向Presentor询问用户详情信息,Presentor将命令传递给UseCase,UseCase从Data获取数据并加工后通过callBack将数据传递回Presentor,Presentor最终告诉Fragment打开新页面并展示数据。
大家下载源码后会发现甭管是Presentor和UI,还是UseCase和Data,都是通过接口协议进行交互,这也是MVP模式的特点之一。
MVP就分析到这里,我认识的也很有限,欢迎大家讨论指正。
参考资料:
MVC or MVP Pattern - Whats the difference?