原文出处:http://saulmm.github.io/2015/02/02/A%20useful%20stack%20on%20android%20%231,%20architecture/
原码github地址:https://github.com/saulmm/Material-Movies
作者:Saúl Molinero
译者注:这是最近接触到的一个关于安卓架构的项目,也是基于MVP的,分包上的想法和我比较契合。另外,该项目也是使用了Material Design,感觉比较新颖实用。因此,决定将该项目对应的blog翻译过来,供大家参考。
这是这篇文章的第一个章节,描述为了开发一个可扩展,可维护以及可测试的android项目,如何去搭建一个基础环境。在这个章节中,我会介绍一些使用到的模式和工具库。这些模式和库能够保证你的项目不会因为每天的开发而变失去控制。
我会将下面这个项目作为示例来进行讲解。这个项目是一个简单的电影分类app,支持某一电影进行多种方式的标记(已读和即将上映)。
电影的数据是从一个叫做themoviedb公共的API获取的。关于API的描述,你可以从Apiary获取到一个完整的文档。
这个项目是基于Model-View-Presenter模式的,同时也实现了Material Design的一些特性,如过渡效果,UI结构,动画,色彩等。
所有的代码都在Github上,可以随意下载或者阅读。同时,也有一个视频来展示app的效果。
这个架构的设计是基于MVP的。MVP是一个MVC架构的变换。
MVP架构试图将业务逻辑从展示层(Presenter)中抽离出来。在android中,这是一个很重要的过程。因为android本身的架构也在促进业务逻辑和展示部分的分离,这两者之间通过数据层面连接。几个典型的例子就是Adapter和CursorLoader。
这个架构可以使得对视图的修改变不需要影响到业务逻辑层和数据层。也使得负责转换多个数据源(数据库或者REST APIT)的domain和转换工具可以很轻松的被重用。
总体架构可以被分成三个部分 :
Presentation
Presentation层负责展示图形接口,并填充数据。
Model
Model层负责提供数据。Model层不会知道任何关于Domain和Presentation的数据。它可以用来实现和数据源(数据库,REST API或者其他源)的连接或者接口。
这个层面同时也实现了整个app所需要的实体类,例如用来表示电影或者分类的类。
Domain
Domain层相对于Presentation层完全独立,它会实现app的业务逻辑。(译者注:这里所谓的业务逻辑可能会于Presenter的功能概念上有点混淆。打个比方,假如usecase接收到的是一个json串,里面包含电影的列表,那么把这个json串转换成json以及包装成一个ArrayList,这个应当是由usecase来完成。而假如ArrayList的size为0,即列表为空,需要显示缺省图,这个判断和控制应当是由presenter完成的。)
Domain和Model层分别放在两个java模块中(译者注:意味着不会有android依赖),Presentation层则为默认的app模块,也就是所谓的android应用。同时,我增加了一个通用的模块,用来在各个模块直接共享支持库和工具类。
Domain模块
Domain模块存放了一些Usecase以及它们的实现。这些都是应用业务逻辑的实现。这个模块相对于android框架来说完全独立。它的依赖只是来源于model模块和通用模块。
一个usecase可以用来获取各个电影分类的总评分,从而用来获取最最热门的分类。要实现这个功能,usecase可能需要获取数据并进行计算。而数据则是由model模块提供的。
dependencies {
compile project (‘:common‘)
compile project (‘:model‘)
}
Model模块
Model模块是负责管理数据的,例如获取,保存,删除等操作。在第一个版本中,我只是通过REST API来实现对电影数据的管理。
同时,这个模块也实现了一些实体类。例如TvMovie
就是用来表示一个电影的。
它的依赖仅仅是通用模块和用来发起网络请求的支持库。关于这一个功能,我使用了Square开发的Retrofit。我会在下一篇博客中介绍一下Retrofit。
dependencies {
compile project(‘:common‘)
compile ‘com.squareup.retrofit:retrofit:1.9.0‘
}
Presentation模块
这个模块就是android应用本身,包括他的resource,asset以及逻辑等。它也同时通过运行usecase来和domain层进行交互。例如:获取一段时间范围内的电影列表,请求一个电影的具体数据。
这个模块包含了presenter和view。每一个Activity
,Fragment
和Dialog
都实现了一个MVP中的View接口。这个接口指定了一个View需要支持的操作,包括显示,隐藏以及展示数据等。例如:PopularMoviesView
定义了接口来展示当前的电影列表,而具体的实现是由MoviesActivity
完成的。
public interface PopularMoviesView extends MVPView {
void showMovies (List<TvMovie> movieList);
void showLoading ();
void hideLoading ();
void showError (String error);
void hideError ();
}
MVP模式的设计初衷就是:View应当越简单越好,View的行为应当是由Presenter决定的,而不是View本身。
public class MoviesActivity extends ActionBarActivity implements
PopularMoviesView, ... {
...
private PopularShowsPresenter popularShowsPresenter;
private RecyclerView popularMoviesRecycler;
private ProgressBar loadingProgressBar;
private MoviesAdapter moviesAdapter;
private TextView errorTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
popularShowsPresenter = new PopularShowsPresenterImpl(this);
popularShowsPresenter.onCreate();
}
@Override
protected void onStop() {
super.onStop();
popularShowsPresenter.onStop();
}
@Override
public Context getContext() {
return this;
}
@Override
public void showMovies(List<TvMovie> movieList) {
moviesAdapter = new MoviesAdapter(movieList);
popularMoviesRecycler.setAdapter(moviesAdapter);
}
@Override
public void showLoading() {
loadingProgressBar.setVisibility(View.VISIBLE);
}
@Override
public void hideLoading() {
loadingProgressBar.setVisibility(View.GONE);
}
@Override
public void showError(String error) {
errorTextView.setVisibility(View.VISIBLE);
errorTextView.setText(error);
}
@Override
public void hideError() {
errorTextView.setVisibility(View.GONE);
}
...
}
Usecase会在presenter中被执行,presenter会接受到返回的数据,并根据数据来控制view的行为。
public class PopularShowsPresenterImpl implements PopularShowsPresenter {
private final PopularMoviesView popularMoviesView;
public PopularShowsPresenterImpl(PopularMoviesView popularMoviesView) {
this.popularMoviesView = popularMoviesView;
}
@Override
public void onCreate() {
...
popularMoviesView.showLoading();
Usecase getPopularShows = new GetMoviesUsecaseController(GetMoviesUsecase.TV_MOVIES);
getPopularShows.execute();
}
@Override
public void onStop() {
...
}
@Override
public void onPopularMoviesReceived(PopularMoviesApiResponse popularMovies) {
popularMoviesView.hideLoading();
popularMoviesView.showMovies(popularMovies.getResults());
}
}
在这个项目中,我选择了消息总线(MessageBus)系统来。这个系统十分有利于发送广播事件或者在各个模块中建立通信。而这正是我们所需要的。简单来说,事件会被送到总线(Bus)上,而需要处理这个事件的类就必须向总线订阅事件。使用这个系统可以大大降低模块之间的耦合。
为了实现这个系统的总线,我使用了Square开发的库Otto。我声明了两个总线,一个(REST_BUS)用来实现usecase和REST API直接的通信,另一个(UI_BUS)则发送事件到展示(presentation)层中。其中,REST_BUS将使用任何可用的线程来处理事件,而UI_BUS只会将事件发送到默认的线程上去,即UI主线程。
public class BusProvider {
private static final Bus REST_BUS = new Bus(ThreadEnforcer.ANY);
private static final Bus UI_BUS = new Bus();
private BusProvider() {};
public static Bus getRestBusInstance() {
return REST_BUS;
}
public static Bus getUIBusInstance () {
return UI_BUS;
}
}
这个类会由通用模块来管理,因为所有的模块都有这个模块的依赖,也可以通过这个模块来操作总线。
dependencies {
compile ‘com.squareup:otto:1.3.5‘
}
最后,来思考这样一种情况:当用户打开了应用,最热门的电影首先被展示。
当onCreate
方法被调用的时候,presenter将订阅UI_BUS来监听事件。然后,presenter就会执行GetMoviesUseCase
来发起请求。presenter会在onStop
被调用的时候取消订阅。
@Override
public void onCreate() {
BusProvider.getUIBusInstance().register(this);
Usecase getPopularShows = new GetMoviesUsecaseController(GetMoviesUsecase.TV_MOVIES);
getPopularShows.execute();
}
...
@Override
public void onStop() {
BusProvider.getUIBusInstance().unregister(this);
}
为了接收到事件,presenter必须实现一个方法。这个方法的参数必须和送入总线的事件的参数一致。并且这个方法必须用注解@Subsribe
进行标注
@Subscribe
@Override
public void onPopularMoviesReceived(PopularMoviesApiResponse popularMovies) {
popularMoviesView.hideLoading();
popularMoviesView.showMovies(popularMovies.getResults());
}
这个项目的作者也是参考了几个著名的安卓架构后,总结出来的一套模板。在架构和分包上,都和我的想法Android架构实战(一)—— 核心思想比较契合,比一套完整的流程精简了不少,也更加适合小型团队(1-3个人)开发。
不同的是,这个项目采用了事件总线的方式来实现模块间通信。关于事件总线和RxJava的对比,个人觉得事件总线属于比较简单易懂的。但是事件总线表现出来的缺陷就是依赖关系较弱,即你没办法轻易的找到一个事件到底是由谁发起的。这个效果,一方面可以理解成低耦合,一方面也可能造成维护的时候”跟丢“的现象。因此,我还是更偏向于RxJava的设计模式。
关于此项目,后续还有两个章节,主要是关于Material Design和其兼容性的一些问题。讲得不是特别深,不过如果需要相同的UI效果的话,可以进行参考,我也会翻译出来,方便大家查找。
原文地址:http://blog.csdn.net/hwz2311245/article/details/48123863