一個實用的android框架(一)——架構
這是這篇文章的第一個章節,描述為了開發一個可擴充套件,可維護以及可測試的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
- Model
- 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效果的話,可以進行參考,我也會翻譯出來,方便大家查詢。