關於Android程序的構架, 當前(2016.10)最流行的模式即爲MVP模式, Google官方提供了Sample代碼來展現這種模式的用法.
Repo地址: android-architecture.
本文爲閱讀官方sample代碼的閱讀筆記和分析.html
官方Android Architecture Blueprints [beta]:
Android在如何組織和構架一個app方面提供了很大的靈活性, 可是同時這種自由也可能會致使app在測試, 維護, 擴展方面變得困難.java
Android Architecture Blueprints展現了可能的解決方案. 在這個項目裏, 咱們用各類不一樣的構架概念和工具實現了同一個應用(To Do App). 主要的關注點在於代碼結構, 構架, 測試和維護性.
可是請記住, 用這些模式構架app的方式有不少種, 要根據你的須要, 不要把這些當作絕對的典範.android
以前有一個MVC模式: Model-View-Controller.
MVC模式 有兩個主要的缺點: 首先, View持有Controller和Model的引用; 第二, 它沒有把對UI邏輯的操做限制在單一的類裏, 這個職能被Controller和View或者Model共享.
因此後來提出了MVP模式來克服這些缺點.git
MVP(Model-View-Presenter)模式:github
app中有四個功能:sql
每一個功能都有:數據庫
Contract
接口;Presenter基類:緩存
public interface BasePresenter { void start(); }
例子中這個start()
方法都在Fragment的onResume()
中調用.安全
View基類:網絡
public interface BaseView<T> { void setPresenter(T presenter); }
View接口中定義的方法多爲showXXX()
方法.
@Override public boolean isActive() { return isAdded(); }
在Presenter中數據回調的方法中, 先檢查View.isActive()是否爲true, 來保證對Fragment的操做安全.
start()
方法在onResume()
的時候調用, 這時候取初始數據; 其餘方法均對應於用戶在UI上的交互操做.onCreate()
裏作的: 先添加了Fragment(View), 而後把它做爲參數傳給了Presenter. 這裏並無存Presenter的引用.checkNotNull()
setPresenter()
方法把Presenter傳回View中引用.TasksRepository
. 它仍是一個單例. 由於在這個應用的例子中, 咱們操做的數據就這一份.它由手動實現的注入類Injection
類提供:
public class Injection { public static TasksRepository provideTasksRepository(@NonNull Context context) { checkNotNull(context); return TasksRepository.getInstance(FakeTasksRemoteDataSource.getInstance(), TasksLocalDataSource.getInstance(context)); } }
構造以下:
private TasksRepository(@NonNull TasksDataSource tasksRemoteDataSource, @NonNull TasksDataSource tasksLocalDataSource) { mTasksRemoteDataSource = checkNotNull(tasksRemoteDataSource); mTasksLocalDataSource = checkNotNull(tasksLocalDataSource); }
TasksDataSource
是一個接口. 接口中定義了Presenter查詢數據的回調接口, 還有一些增刪改查的方法.MVP模式的主要優點就是便於爲業務邏輯加上單元測試.
本例子中的單元測試是給TasksRepository
和四個feature的Presenter加的.
Presenter的單元測試, Mock了View和Model, 測試調用邏輯, 如:
public class AddEditTaskPresenterTest { @Mock private TasksRepository mTasksRepository; @Mock private AddEditTaskContract.View mAddEditTaskView; private AddEditTaskPresenter mAddEditTaskPresenter; @Before public void setupMocksAndView() { MockitoAnnotations.initMocks(this); when(mAddEditTaskView.isActive()).thenReturn(true); } @Test public void saveNewTaskToRepository_showsSuccessMessageUi() { mAddEditTaskPresenter = new AddEditTaskPresenter("1", mTasksRepository, mAddEditTaskView); mAddEditTaskPresenter.saveTask("New Task Title", "Some Task Description"); verify(mTasksRepository).saveTask(any(Task.class)); // saved to the model verify(mAddEditTaskView).showTasksList(); // shown in the UI } ... }
基於上一個例子todo-mvp, 只不過這裏改成用Loader來從Repository獲得數據.
使用Loader的優點:
既然是基於todo-mvp, 那麼以前說過的那些就再也不重複, 咱們來看一下都有什麼改動:
git difftool -d todo-mvp
添加了兩個類:
TaskLoader
和TasksLoader
.
在Activity中new Loader類, 而後傳入Presenter的構造方法.
Contract
中View接口刪掉了isActive()
方法, Presenter刪掉了populateTask()
方法.
添加的兩個新類是TaskLoader
和TasksLoader
, 都繼承於AsyncTaskLoader
, 只不過數據的類型一個是單數, 一個是複數.
AsyncTaskLoader
是基於ModernAsyncTask
, 相似於AsyncTask
,
把load數據的操做放在loadInBackground()
裏便可, deliverResult()
方法會將結果返回到主線程, 咱們在listener的onLoadFinished()
裏面就能夠接到返回的數據了, (在這個例子中是幾個Presenter實現了這個接口).
TasksDataSource
接口的這兩個方法:
List<Task> getTasks(); Task getTask(@NonNull String taskId);
都變成了同步方法, 由於它們是在loadInBackground()
方法裏被調用.
Presenter中保存了Loader
和LoaderManager
, 在start()
方法裏initLoader
, 而後onCreateLoader
返回構造傳入的那個loader.
onLoadFinished()
裏面調用View的方法. 此時Presenter實現LoaderManager.LoaderCallbacks
.
TasksRepository
類中定義了observer的接口, 保存了一個listener的list:
private List<TasksRepositoryObserver> mObservers = new ArrayList<TasksRepositoryObserver>(); public interface TasksRepositoryObserver { void onTasksChanged(); }
每次有數據改動須要刷新UI時就調用:
private void notifyContentObserver() { for (TasksRepositoryObserver observer : mObservers) { observer.onTasksChanged(); } }
在兩個Loader裏註冊和註銷本身爲TasksRepository
的listener: 在onStartLoading()
裏add, onReset()
裏面remove方法.
這樣每次TasksRepository
有數據變化, 做爲listener的兩個Loader都會收到通知, 而後force load:
@Override public void onTasksChanged() { if (isStarted()) { forceLoad(); } }
這樣onLoadFinished()
方法就會被調用.
基於todo-mvp, 使用Data Binding library來顯示數據, 把UI和動做綁定起來.
說到ViewModel, 還有一種模式叫MVVM(Model-View-ViewModel)模式.
這個例子並無嚴格地遵循Model-View-ViewModel
模式或者Model-View-Presenter
模式, 由於它既用了ViewModel又用了Presenter.
Data Binding Library讓UI元素和數據模型綁定:
添加了幾個類:
StatisticsViewModel
;SwipeRefreshLayoutDataBinding
;TasksItemActionHandler
;TasksViewModel
;從幾個View的接口能夠看出方法數減小了, 原來須要多個showXXX()方法, 如今只須要一兩個方法就能夠了.
以TasksDetailFragment
爲例:
之前在todo-mvp裏須要這樣:
public void onCreateView(...) { ... mDetailDescription = (TextView) root.findViewById(R.id.task_detail_description); } @Override public void showDescription(String description) { mDetailDescription.setVisibility(View.VISIBLE); mDetailDescription.setText(description); }
如今只須要這樣:
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.taskdetail_frag, container, false); mViewDataBinding = TaskdetailFragBinding.bind(view); ... } @Override public void showTask(Task task) { mViewDataBinding.setTask(task); }
由於全部數據綁定的操做都寫在了xml裏:
<TextView android:id="@+id/task_detail_description" ... android:text="@{task.description}" />
數據綁定省去了findViewById()
和setText()
, 事件綁定則是省去了setOnClickListener()
.
好比taskdetail_frag.xml
中的
<CheckBox android:id="@+id/task_detail_complete" ... android:checked="@{task.completed}" android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}" />
其中Presenter是這時候傳入的:
@Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); mViewDataBinding.setPresenter(mPresenter); }
在顯示List數據的界面TasksFragment
, 僅須要知道數據是否爲空, 因此它使用了TasksViewModel
來給layout提供信息, 當尺寸設定的時候, 只有一些相關的屬性被通知, 和這些屬性綁定的UI元素被更新.
public void setTaskListSize(int taskListSize) { mTaskListSize = taskListSize; notifyPropertyChanged(BR.noTaskIconRes); notifyPropertyChanged(BR.noTasksLabel); notifyPropertyChanged(BR.currentFilteringLabel); notifyPropertyChanged(BR.notEmpty); notifyPropertyChanged(BR.tasksAddViewVisible); }
TasksFragment
中的TasksAdapter
.@Override public View getView(int i, View view, ViewGroup viewGroup) { Task task = getItem(i); TaskItemBinding binding; if (view == null) { // Inflate LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext()); // Create the binding binding = TaskItemBinding.inflate(inflater, viewGroup, false); } else { binding = DataBindingUtil.getBinding(view); } // We might be recycling the binding for another task, so update it. // Create the action handler for the view TasksItemActionHandler itemActionHandler = new TasksItemActionHandler(mUserActionsListener); binding.setActionHandler(itemActionHandler); binding.setTask(task); binding.executePendingBindings(); return binding.getRoot(); }
TasksItemActionHandler
.StatisticsViewModel
.SwipeRefreshLayoutDataBinding
類定義的onRefresh()
動做綁定.這個例子是基於Clean Architecture的原則:
The Clean Architecture.
關於Clean Architecture, 還能夠看這個Sample App: Android-CleanArchitecture.
這個例子在todo-mvp的基礎上, 加了一層domain層, 把應用分爲了三層:
Domain: 盛放了業務邏輯, domain層包含use cases或者interactors, 被應用的presenters使用. 這些use cases表明了全部從presentation層可能進行的行爲.
關鍵概念
和基本的mvp sample最大的不一樣就是domain層和use cases. 從presenters中抽離出來的domain層有助於避免presenter中的代碼重複.
Use cases定義了app須要的操做, 這樣增長了代碼的可讀性, 由於類名反映了目的.
Use cases對於操做的複用來講也很好. 好比CompleteTask
在兩個Presenter中都用到了.
Use cases的執行是在後臺線程, 使用command pattern. 這樣domain層對於Android SDK和其餘第三方庫來講都是徹底解耦的.
每個feature的包下都新增了domain層, 裏面包含了子目錄model和usecase等.
UseCase
是一個抽象類, 定義了domain層的基礎接口點.
UseCaseHandler
用於執行use cases, 是一個單例, 實現了command pattern.
UseCaseThreadPoolScheduler
實現了UseCaseScheduler
接口, 定義了use cases執行的線程池, 在後臺線程異步執行, 最後把結果返回給主線程.
UseCaseScheduler
經過構造傳給UseCaseHandler
.
測試中用了UseCaseScheduler
的另外一個實現TestUseCaseScheduler
, 全部的執行變爲同步的.
Injection
類中提供了多個Use cases的依賴注入, 還有UseCaseHandler
用來執行use cases.
Presenter的實現中, 多個use cases和UsseCaseHandler
都由構造傳入, 執行動做, 好比更新一個task:
private void updateTask(String title, String description) { if (mTaskId == null) { throw new RuntimeException("updateTask() was called but task is new."); } Task newTask = new Task(title, description, mTaskId); mUseCaseHandler.execute(mSaveTask, new SaveTask.RequestValues(newTask), new UseCase.UseCaseCallback<SaveTask.ResponseValue>() { @Override public void onSuccess(SaveTask.ResponseValue response) { // After an edit, go back to the list. mAddTaskView.showTasksList(); } @Override public void onError() { showSaveError(); } }); }
關鍵概念:
dagger2 是一個靜態的編譯期依賴注入框架.
這個例子中改用dagger2實現依賴注入. 這樣作的主要好處就是在測試的時候咱們能夠用替代的modules. 這在編譯期間經過flavors就能夠完成, 或者在運行期間使用一些調試面板來設置.
Injection
類被刪除了.
添加了5個Component, 四個feature各有一個, 另外數據對應一個: TasksRepositoryComponent
, 這個Component被保存在Application裏.
數據的module: TasksRepositoryModule
在mock
和prod
目錄下各有一個.
對於每個feature的Presenter的注入是這樣實現的:
首先, 把Presenter的構造函數標記爲@Inject, 而後在Activity中構造component並注入到字段:
@Inject AddEditTaskPresenter mAddEditTasksPresenter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.addtask_act); ..... // Create the presenter DaggerAddEditTaskComponent.builder() .addEditTaskPresenterModule( new AddEditTaskPresenterModule(addEditTaskFragment, taskId)) .tasksRepositoryComponent( ((ToDoApplication) getApplication()).getTasksRepositoryComponent()).build() .inject(this); }
這個module裏provide了view和taskId:
@Module public class AddEditTaskPresenterModule { private final AddEditTaskContract.View mView; private String mTaskId; public AddEditTaskPresenterModule(AddEditTaskContract.View view, @Nullable String taskId) { mView = view; mTaskId = taskId; } @Provides AddEditTaskContract.View provideAddEditTaskContractView() { return mView; } @Provides @Nullable String provideTaskId() { return mTaskId; } }
注意原來構造方法裏調用的setPresenter方法改成用方法注入實現:
/** * Method injection is used here to safely reference {@code this} after the object is created. * For more information, see Java Concurrency in Practice. */ @Inject void setupListeners() { mAddTaskView.setPresenter(this); }
這個例子是基於todo-mvp-loaders的, 用content provider來獲取repository中的數據.
使用Content Provider的優點是:
注意這個例子是惟一一個不基於最基本的todo-mvp, 而是基於todo-mvp-loaders. (可是我以爲也能夠認爲是直接從todo-mvp轉化的.)
看diff: git difftool -d todo-mvp-loaders
.
去掉了TaskLoader
和TasksLoader
. (迴歸到了基本的todo-mvp).
TasksRepository
中的方法不是同步方法, 而是異步加callback的形式. (迴歸到了基本的todo-mvp).
TasksLocalDataSource
中的讀方法都變成了空實現, 由於Presenter如今能夠自動收到數據更新.
新增LoaderProvider
用來建立Cursor Loaders, 有兩個方法:
// 返回特定fiter下或所有的數據 public Loader<Cursor> createFilteredTasksLoader(TaskFilter taskFilter) // 返回特定id的數據 public Loader<Cursor> createTaskLoader(String taskId)
其中第一個方法的參數TaskFilter
, 用來指定過濾的selection條件, 也是新增類.
LoaderManager
和LoaderProvider
都是由構造傳入Presenter, 在回調onTaskLoaded()
和onTasksLoaded()
中init loader.
在TasksPresenter
中還作了判斷, 是init loader仍是restart loader:
@Override public void onTasksLoaded(List<Task> tasks) { // we don't care about the result since the CursorLoader will load the data for us if (mLoaderManager.getLoader(TASKS_LOADER) == null) { mLoaderManager.initLoader(TASKS_LOADER, mCurrentFiltering.getFilterExtras(), this); } else { mLoaderManager.restartLoader(TASKS_LOADER, mCurrentFiltering.getFilterExtras(), this); } }
其中initLoader()和restartLoader()時傳入的第二個參數是一個bundle, 用來指明過濾類型, 便是帶selection條件的數據庫查詢.
一樣是在onLoadFinshed()的時候作View處理, 以TaskDetailPresenter
爲例:
@Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { if (data != null) { if (data.moveToLast()) { onDataLoaded(data); } else { onDataEmpty(); } } else { onDataNotAvailable(); } }
數據類Task中新增了靜態方法從Cursor轉爲Task, 這個方法在Presenter的onLoadFinished()
和測試中都用到了.
public static Task from(Cursor cursor) { String entryId = cursor.getString(cursor.getColumnIndexOrThrow( TasksPersistenceContract.TaskEntry.COLUMN_NAME_ENTRY_ID)); String title = cursor.getString(cursor.getColumnIndexOrThrow( TasksPersistenceContract.TaskEntry.COLUMN_NAME_TITLE)); String description = cursor.getString(cursor.getColumnIndexOrThrow( TasksPersistenceContract.TaskEntry.COLUMN_NAME_DESCRIPTION)); boolean completed = cursor.getInt(cursor.getColumnIndexOrThrow( TasksPersistenceContract.TaskEntry.COLUMN_NAME_COMPLETED)) == 1; return new Task(title, description, entryId, completed); }
另一些細節:
數據庫中的內存cache被刪了.
Adapter改成繼承於CursorAdapter
.
新增了MockCursorProvider
類, 用於在單元測試中提供數據.
其內部類TaskMockCursor
mock了Cursor數據.
Presenter的測試中仍然mock了全部構造傳入的參數, 而後準備了mock數據, 測試的邏輯主要仍是拿到數據後的view操做, 好比:
@Test public void loadAllTasksFromRepositoryAndLoadIntoView() { // When the loader finishes with tasks and filter is set to all when(mBundle.getSerializable(TaskFilter.KEY_TASK_FILTER)).thenReturn(TasksFilterType.ALL_TASKS); TaskFilter taskFilter = new TaskFilter(mBundle); mTasksPresenter.setFiltering(taskFilter); mTasksPresenter.onLoadFinished(mock(Loader.class), mAllTasksCursor); // Then progress indicator is hidden and all tasks are shown in UI verify(mTasksView).setLoadingIndicator(false); verify(mTasksView).showTasks(mShowTasksArgumentCaptor.capture()); }
關於這個例子, 以前看過做者的文章: Android Architecture Patterns Part 2:
Model-View-Presenter,
這個文章上過Android Weekly Issue #226.
這個例子也是基於todo-mvp, 使用RxJava處理了presenter和數據層之間的通訊.
BasePresenter接口改成:
public interface BasePresenter { void subscribe(); void unsubscribe(); }
View在onResume()
的時候調用Presenter的subscribe()
; 在onPause()的時候調用presenter的unsubscribe()
.
若是View接口的實現不是Fragment或Activity, 而是Android的自定義View, 那麼在Android View的onAttachedToWindow()
和onDetachedFromWindow()
方法裏分別調用這兩個方法.
Presenter中保存了:
private CompositeSubscription mSubscriptions;
在subscribe()
的時候, mSubscriptions.add(subscription);
;
在unsubscribe()
的時候, mSubscriptions.clear();
.
數據層暴露了RxJava的Observable
流做爲獲取數據的方式, TasksDataSource
接口中的方法變成了這樣:
Observable<List<Task>> getTasks(); Observable<Task> getTask(@NonNull String taskId);
callback接口被刪了, 由於不須要了.
TasksLocalDataSource
中的實現用了SqlBrite, 從數據庫中查詢出來的結果很容易地變成了流:
@Override public Observable<List<Task>> getTasks() { ... return mDatabaseHelper.createQuery(TaskEntry.TABLE_NAME, sql) .mapToList(mTaskMapperFunction); }
TasksRepository
中整合了local和remote的data, 最後把Observable
返回給消費者(Presenters和Unit Tests). 這裏用了.concat()
和.first()
操做符.
Presenter訂閱TasksRepository的Observable, 而後決定View的操做, 並且Presenter也負責線程的調度.
簡單的好比AddEditTaskPresenter
中:
@Override public void populateTask() { if (mTaskId == null) { throw new RuntimeException("populateTask() was called but task is new."); } Subscription subscription = mTasksRepository .getTask(mTaskId) .subscribeOn(mSchedulerProvider.computation()) .observeOn(mSchedulerProvider.ui()) .subscribe(new Observer<Task>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { if (mAddTaskView.isActive()) { mAddTaskView.showEmptyTaskError(); } } @Override public void onNext(Task task) { if (mAddTaskView.isActive()) { mAddTaskView.setTitle(task.getTitle()); mAddTaskView.setDescription(task.getDescription()); } } }); mSubscriptions.add(subscription); }
StatisticsPresenter
負責統計數據的顯示, TasksPresenter
負責過濾顯示全部數據, 裏面的RxJava操做符運用比較多, 能夠看到鏈式操做的特色.
關於線程調度, 定義了BaseSchedulerProvider
接口, 經過構造函數傳給Presenter, 而後實現用SchedulerProvider
, 測試用ImmediateSchedulerProvider
. 這樣方便測試.