編輯:Android資訊
關於Android程序的構架, 當前(2016.10)最流行的模式即為MVP模式, Google官方提供了Sample代碼來展示這種模式的用法.
Repo地址: android-architecture.
本文為閱讀官方sample代碼的閱讀筆記和分析.
官方Android Architecture Blueprints [beta]:
Android在如何組織和構架一個app方面提供了很大的靈活性, 但是同時這種自由也可能會導致app在測試, 維護, 擴展方面變得困難.
Android Architecture Blueprints展示了可能的解決方案. 在這個項目裡, 我們用各種不同的構架概念和工具實現了同一個應用(To Do App). 主要的關注點在於代碼結構, 構架, 測試和維護性.
但是請記住, 用這些模式構架app的方式有很多種, 要根據你的需要, 不要把這些當做絕對的典范.
之前有一個MVC模式: Model-View-Controller.
MVC模式 有兩個主要的缺點: 首先, View持有Controller和Model的引用; 第二, 它沒有把對UI邏輯的操作限制在單一的類裡, 這個職能被Controller和View或者Model共享.
所以後來提出了MVP模式來克服這些缺點.
MVP(Model-View-Presenter)模式:
app中有四個功能:
每個功能都有:
Contract
接口;Presenter基類:
public interface BasePresenter { void start(); }
例子中這個start()
方法都在Fragment的onResume()
中調用.
View基類:
public interface BaseView<T> { void setPresenter(T presenter); }
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
. 這樣方便測試.
MPAndroidChart是一款基於Android的開源圖表庫,MPAndroidChart不僅可以在Android設備上繪制各種統計圖表,而且可以對圖表進行拖
對於我這樣一個Android初級開發者來說,自定義View一直是一個遙不可及的東西,每次看到別人做的特別漂亮的控件,自己心裡那個癢癢啊,可是又生性懶惰,自己不肯努
在這次的工作開發項目中,涉及到一個視頻縮略圖的視頻列表;這個在大家看來,制作視頻縮略圖就是兩行代碼就搞定的事。確實是這樣的,百度一下,每個 帖子都知道制作視頻縮略
有時候我們需要錄制Android手機的屏幕,比如寫了一個Demo應用,需要發布到博客和微博上。 如下是我錄制轉GIF的效果圖 &amp;amp;