概述
透過todo-mvp來說明MVP中的Model
todo-mvp 是 Android 官方用來說明 MVP Pattern的範例,參考 https://github.com/googlesamples/android-architecture
todo-mvp 裡的 Model 為TaskRepository,TaskRepository繼承TasksDataSource。
TaskDataSource實際上是一個interface,其中2個內部介面LoadTasksCallback和GetTasksCallback用來作callback使用,在內部介面的onTasksLoaded方法用來當取得task成功之後把task傳回呼叫點的用途,而onDataNotAvailable方法用來當取得task失敗後的後續處理。
其餘在TasksDataSource介面的方法都是存取資料的共用方法,只要是Model都要實作這些方法。
TasksDataSource.java
public interface TasksDataSource { interface LoadTasksCallback { void onTasksLoaded(List<Task> tasks); void onDataNotAvailable(); } interface GetTaskCallback { void onTaskLoaded(Task task); void onDataNotAvailable(); } void getTasks(@NonNull LoadTasksCallback callback); void getTask(@NonNull String taskId, @NonNull GetTaskCallback callback); void saveTask(@NonNull Task task); void completeTask(@NonNull Task task); void completeTask(@NonNull String taskId); void activateTask(@NonNull Task task); void activateTask(@NonNull String taskId); void clearCompletedTasks(); void refreshTasks(); void deleteAllTasks(); void deleteTask(@NonNull String taskId); }
接著看Presenter如何關聯 Model以及使用Model。把焦點放在AddEditTaskPresenter。
1.AddEditTaskPresenter(Presenter)本身不會持有任何資料,資料放在Model中。
2.Presenter會通知Model去改變資料。
3.Presenter會持有Model和View的變數並在建構式初始化他們。
4.Presenter會在建構式初始化Model,接著在需要改變資料的位置去操縱Model改變資料,Model改變資料後Presenter再通知View重新載入資料。
AddEditTaskPresenter.java
public class AddEditTaskPresenter implements AddEditTaskContract.Presenter, TasksDataSource.GetTaskCallback { @NonNull private final TasksDataSource mTasksRepository; @NonNull private final AddEditTaskContract.View mAddTaskView; @Nullable private String mTaskId; private boolean mIsDataMissing; /** * Creates a presenter for the add/edit view. * * @param taskId ID of the task to edit or null for a new task * @param tasksRepository a repository of data for tasks * @param addTaskView the add/edit view * @param shouldLoadDataFromRepo whether data needs to be loaded or not (for config changes) */ public AddEditTaskPresenter(@Nullable String taskId, @NonNull TasksDataSource tasksRepository, @NonNull AddEditTaskContract.View addTaskView, boolean shouldLoadDataFromRepo) { mTaskId = taskId; mTasksRepository = checkNotNull(tasksRepository); mAddTaskView = checkNotNull(addTaskView); mIsDataMissing = shouldLoadDataFromRepo; mAddTaskView.setPresenter(this); } @Override public void start() { if (!isNewTask() && mIsDataMissing) { populateTask(); } } @Override public void saveTask(String title, String description) { if (isNewTask()) { createTask(title, description); } else { updateTask(title, description); } } @Override public void populateTask() { if (isNewTask()) { throw new RuntimeException("populateTask() was called but task is new."); } mTasksRepository.getTask(mTaskId, this); } @Override public void onTaskLoaded(Task task) { // The view may not be able to handle UI updates anymore if (mAddTaskView.isActive()) { mAddTaskView.setTitle(task.getTitle()); mAddTaskView.setDescription(task.getDescription()); } mIsDataMissing = false; } @Override public void onDataNotAvailable() { // The view may not be able to handle UI updates anymore if (mAddTaskView.isActive()) { mAddTaskView.showEmptyTaskError(); } } @Override public boolean isDataMissing() { return mIsDataMissing; } private boolean isNewTask() { return mTaskId == null; } private void createTask(String title, String description) { Task newTask = new Task(title, description); if (newTask.isEmpty()) { mAddTaskView.showEmptyTaskError(); } else { mTasksRepository.saveTask(newTask); mAddTaskView.showTasksList(); } } private void updateTask(String title, String description) { if (isNewTask()) { throw new RuntimeException("updateTask() was called but task is new."); } mTasksRepository.saveTask(new Task(title, description, mTaskId)); mAddTaskView.showTasksList(); // After an edit, go back to the list. } }
注意雖然Model的變數型態為TasksDataSource(interface),但在Presenter建構式傳入的其實是TaskRepository(繼承自TasksDataSource)。
AddEditTaskActivity.java
... mPresenter = new AddEditTaskPresenter( taskId, Injection.provideTasksRepository(getApplicationContext()), addEditTaskView); ...
Injection.java
public class Injection { public static TasksRepository provideTasksRepository(@NonNull Context context) { checkNotNull(context); return TasksRepository.getInstance(FakeTasksRemoteDataSource.getInstance(), TasksLocalDataSource.getInstance(context)); } }
因此我們需要看的是TasksRepository的內容。
TasksRepository.java
public class TasksRepository implements TasksDataSource { private static TasksRepository INSTANCE = null; private final TasksDataSource mTasksRemoteDataSource; private final TasksDataSource mTasksLocalDataSource; /** * This variable has package local visibility so it can be accessed from tests. */ Map<String, Task> mCachedTasks; /** * Marks the cache as invalid, to force an update the next time data is requested. This variable * has package local visibility so it can be accessed from tests. */ boolean mCacheIsDirty = false; // Prevent direct instantiation. private TasksRepository(@NonNull TasksDataSource tasksRemoteDataSource, @NonNull TasksDataSource tasksLocalDataSource) { mTasksRemoteDataSource = checkNotNull(tasksRemoteDataSource); mTasksLocalDataSource = checkNotNull(tasksLocalDataSource); } /** * Returns the single instance of this class, creating it if necessary. * * @param tasksRemoteDataSource the backend data source * @param tasksLocalDataSource the device storage data source * @return the {@link TasksRepository} instance */ public static TasksRepository getInstance(TasksDataSource tasksRemoteDataSource, TasksDataSource tasksLocalDataSource) { if (INSTANCE == null) { INSTANCE = new TasksRepository(tasksRemoteDataSource, tasksLocalDataSource); } return INSTANCE; } /** * Used to force {@link #getInstance(TasksDataSource, TasksDataSource)} to create a new instance * next time it's called. */ public static void destroyInstance() { INSTANCE = null; } /** * Gets tasks from cache, local data source (SQLite) or remote data source, whichever is * available first. * <p> * Note: {@link LoadTasksCallback#onDataNotAvailable()} is fired if all data sources fail to * get the data. */ @Override public void getTasks(@NonNull final LoadTasksCallback callback) { checkNotNull(callback); // Respond immediately with cache if available and not dirty if (mCachedTasks != null && !mCacheIsDirty) { callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values())); return; } if (mCacheIsDirty) { // If the cache is dirty we need to fetch new data from the network. getTasksFromRemoteDataSource(callback); } else { // Query the local storage if available. If not, query the network. mTasksLocalDataSource.getTasks(new LoadTasksCallback() { @Override public void onTasksLoaded(List<Task> tasks) { refreshCache(tasks); callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values())); } @Override public void onDataNotAvailable() { getTasksFromRemoteDataSource(callback); } }); } } @Override public void saveTask(@NonNull Task task) { checkNotNull(task); mTasksRemoteDataSource.saveTask(task); mTasksLocalDataSource.saveTask(task); // Do in memory cache update to keep the app UI up to date if (mCachedTasks == null) { mCachedTasks = new LinkedHashMap<>(); } mCachedTasks.put(task.getId(), task); } @Override public void completeTask(@NonNull Task task) { checkNotNull(task); mTasksRemoteDataSource.completeTask(task); mTasksLocalDataSource.completeTask(task); Task completedTask = new Task(task.getTitle(), task.getDescription(), task.getId(), true); // Do in memory cache update to keep the app UI up to date if (mCachedTasks == null) { mCachedTasks = new LinkedHashMap<>(); } mCachedTasks.put(task.getId(), completedTask); } @Override public void completeTask(@NonNull String taskId) { checkNotNull(taskId); completeTask(getTaskWithId(taskId)); } @Override public void activateTask(@NonNull Task task) { checkNotNull(task); mTasksRemoteDataSource.activateTask(task); mTasksLocalDataSource.activateTask(task); Task activeTask = new Task(task.getTitle(), task.getDescription(), task.getId()); // Do in memory cache update to keep the app UI up to date if (mCachedTasks == null) { mCachedTasks = new LinkedHashMap<>(); } mCachedTasks.put(task.getId(), activeTask); } @Override public void activateTask(@NonNull String taskId) { checkNotNull(taskId); activateTask(getTaskWithId(taskId)); } @Override public void clearCompletedTasks() { mTasksRemoteDataSource.clearCompletedTasks(); mTasksLocalDataSource.clearCompletedTasks(); // Do in memory cache update to keep the app UI up to date if (mCachedTasks == null) { mCachedTasks = new LinkedHashMap<>(); } Iterator<Map.Entry<String, Task>> it = mCachedTasks.entrySet().iterator(); while (it.hasNext()) { Map.Entry<String, Task> entry = it.next(); if (entry.getValue().isCompleted()) { it.remove(); } } } /** * Gets tasks from local data source (sqlite) unless the table is new or empty. In that case it * uses the network data source. This is done to simplify the sample. * <p> * Note: {@link GetTaskCallback#onDataNotAvailable()} is fired if both data sources fail to * get the data. */ @Override public void getTask(@NonNull final String taskId, @NonNull final GetTaskCallback callback) { checkNotNull(taskId); checkNotNull(callback); Task cachedTask = getTaskWithId(taskId); // Respond immediately with cache if available if (cachedTask != null) { callback.onTaskLoaded(cachedTask); return; } // Load from server/persisted if needed. // Is the task in the local data source? If not, query the network. mTasksLocalDataSource.getTask(taskId, new GetTaskCallback() { @Override public void onTaskLoaded(Task task) { // Do in memory cache update to keep the app UI up to date if (mCachedTasks == null) { mCachedTasks = new LinkedHashMap<>(); } mCachedTasks.put(task.getId(), task); callback.onTaskLoaded(task); } @Override public void onDataNotAvailable() { mTasksRemoteDataSource.getTask(taskId, new GetTaskCallback() { @Override public void onTaskLoaded(Task task) { // Do in memory cache update to keep the app UI up to date if (mCachedTasks == null) { mCachedTasks = new LinkedHashMap<>(); } mCachedTasks.put(task.getId(), task); callback.onTaskLoaded(task); } @Override public void onDataNotAvailable() { callback.onDataNotAvailable(); } }); } }); } @Override public void refreshTasks() { mCacheIsDirty = true; } @Override public void deleteAllTasks() { mTasksRemoteDataSource.deleteAllTasks(); mTasksLocalDataSource.deleteAllTasks(); if (mCachedTasks == null) { mCachedTasks = new LinkedHashMap<>(); } mCachedTasks.clear(); } @Override public void deleteTask(@NonNull String taskId) { mTasksRemoteDataSource.deleteTask(checkNotNull(taskId)); mTasksLocalDataSource.deleteTask(checkNotNull(taskId)); mCachedTasks.remove(taskId); } private void getTasksFromRemoteDataSource(@NonNull final LoadTasksCallback callback) { mTasksRemoteDataSource.getTasks(new LoadTasksCallback() { @Override public void onTasksLoaded(List<Task> tasks) { refreshCache(tasks); refreshLocalDataSource(tasks); callback.onTasksLoaded(new ArrayList<>(mCachedTasks.values())); } @Override public void onDataNotAvailable() { callback.onDataNotAvailable(); } }); } private void refreshCache(List<Task> tasks) { if (mCachedTasks == null) { mCachedTasks = new LinkedHashMap<>(); } mCachedTasks.clear(); for (Task task : tasks) { mCachedTasks.put(task.getId(), task); } mCacheIsDirty = false; } private void refreshLocalDataSource(List<Task> tasks) { mTasksLocalDataSource.deleteAllTasks(); for (Task task : tasks) { mTasksLocalDataSource.saveTask(task); } } @Nullable private Task getTaskWithId(@NonNull String id) { checkNotNull(id); if (mCachedTasks == null || mCachedTasks.isEmpty()) { return null; } else { return mCachedTasks.get(id); } } }
TasksRepository實現3層緩存,首先第1層緩存為記憶體也就是Map<String, Task> mCachedTasks;
第2層緩存為本地端資料來源,也就是private final TasksDataSource mTasksLocalDataSource;
因為該變數的型態也是TasksDataSource為interface,因此不會被資料來源的實現綁住,也就是說若想更換不同的資料庫,也只要新增TasksDataSource的子類別繼承TasksDataSource即可(OCP)。
第3層緩存為遠端資料來源,為 private final TasksDataSource mTasksRemoteDataSource;
變數型態也是TasksDataSource,也可以簡單替換遠端來源的實現(OCP),如volley, okhttp, retrofit等等。
若以儲存資料來說,在順序性來說沒有分別,這3層都會儲存資料,如下面的TasksRepository.saveTask方法的實作內容
@Override public void saveTask(@NonNull Task task) { checkNotNull(task); mTasksRemoteDataSource.saveTask(task); mTasksLocalDataSource.saveTask(task); // Do in memory cache update to keep the app UI up to date if (mCachedTasks == null) { mCachedTasks = new LinkedHashMap<>(); } mCachedTasks.put(task.getId(), task); }
若是讀取資料,則會先從第1層緩存記憶體(mCachedTasks)去讀取資料,若資料存在就直接回傳,若資料不存在,則從第2層緩存本地端資料庫(mTasksLocalDataSource)去讀取資料,若有資料則把該資料加到記憶體(mCachedTasks)後再回傳資料。
若還是沒有資料則從第3層緩存遠端網路(mTasksRemoteDataSource)去讀取資料,若有資料則把該資料加到記憶體(mCachedTasks)後再回傳資料,若資料不存在則顯示該資料不存在訊息。
如下方的TasksRepository.getTask方法內容
@Override public void getTask(@NonNull final String taskId, @NonNull final GetTaskCallback callback) { checkNotNull(taskId); checkNotNull(callback); Task cachedTask = getTaskWithId(taskId); // Respond immediately with cache if available if (cachedTask != null) { callback.onTaskLoaded(cachedTask); return; } // Load from server/persisted if needed. // Is the task in the local data source? If not, query the network. mTasksLocalDataSource.getTask(taskId, new GetTaskCallback() { @Override public void onTaskLoaded(Task task) { // Do in memory cache update to keep the app UI up to date if (mCachedTasks == null) { mCachedTasks = new LinkedHashMap<>(); } mCachedTasks.put(task.getId(), task); callback.onTaskLoaded(task); } @Override public void onDataNotAvailable() { mTasksRemoteDataSource.getTask(taskId, new GetTaskCallback() { @Override public void onTaskLoaded(Task task) { // Do in memory cache update to keep the app UI up to date if (mCachedTasks == null) { mCachedTasks = new LinkedHashMap<>(); } mCachedTasks.put(task.getId(), task); callback.onTaskLoaded(task); } @Override public void onDataNotAvailable() { callback.onDataNotAvailable(); } }); } }); }
接著來看看在TasksRepository建構式,存取權限為私有,代表只能透過該纇別內部呼叫。
// Prevent direct instantiation. private TasksRepository(@NonNull TasksDataSource tasksRemoteDataSource, @NonNull TasksDataSource tasksLocalDataSource) { mTasksRemoteDataSource = checkNotNull(tasksRemoteDataSource); mTasksLocalDataSource = checkNotNull(tasksLocalDataSource); }
呼叫該建構式的位置為getInstance()方法,會透過其方法的參數設定tasksRemoteDataSource和tasksLocalDataSource。
public static TasksRepository getInstance(TasksDataSource tasksRemoteDataSource, TasksDataSource tasksLocalDataSource) { if (INSTANCE == null) { INSTANCE = new TasksRepository(tasksRemoteDataSource, tasksLocalDataSource); } return INSTANCE; }
而getInstance方法的呼叫者為Injection類別的ProvideTasksRepository方法。
public class Injection { public static TasksRepository provideTasksRepository(@NonNull Context context) { checkNotNull(context); ToDoDatabase database = ToDoDatabase.getInstance(context); return TasksRepository.getInstance(FakeTasksRemoteDataSource.getInstance(), TasksLocalDataSource.getInstance(new AppExecutors(), database.taskDao())); } }
可以看到provideTasksRepository方法內即為FakeTasksRemoteDataSource.getIntance()和TasksLocalDataSource.getInstance()分別代表遠端資料來源和本地端資料來源。
接著看看FakeTasksRemoteDataSource類別。其內容相當簡單,儲存資料的方式是透過一個Map<String, Task> TASKS_SERVICE_DATA 來儲存資料。
FakeTasksRemoteDataSource.java
public class FakeTasksRemoteDataSource implements TasksDataSource { private static FakeTasksRemoteDataSource INSTANCE; private static final Map<String, Task> TASKS_SERVICE_DATA = new LinkedHashMap<>(); // Prevent direct instantiation. private FakeTasksRemoteDataSource() {} public static FakeTasksRemoteDataSource getInstance() { if (INSTANCE == null) { INSTANCE = new FakeTasksRemoteDataSource(); } return INSTANCE; } @Override public void getTasks(@NonNull LoadTasksCallback callback) { callback.onTasksLoaded(Lists.newArrayList(TASKS_SERVICE_DATA.values())); } @Override public void getTask(@NonNull String taskId, @NonNull GetTaskCallback callback) { Task task = TASKS_SERVICE_DATA.get(taskId); callback.onTaskLoaded(task); } @Override public void saveTask(@NonNull Task task) { TASKS_SERVICE_DATA.put(task.getId(), task); } @Override public void completeTask(@NonNull Task task) { Task completedTask = new Task(task.getTitle(), task.getDescription(), task.getId(), true); TASKS_SERVICE_DATA.put(task.getId(), completedTask); } @Override public void completeTask(@NonNull String taskId) { // Not required for the remote data source. } @Override public void activateTask(@NonNull Task task) { Task activeTask = new Task(task.getTitle(), task.getDescription(), task.getId()); TASKS_SERVICE_DATA.put(task.getId(), activeTask); } @Override public void activateTask(@NonNull String taskId) { // Not required for the remote data source. } @Override public void clearCompletedTasks() { Iterator<Map.Entry<String, Task>> it = TASKS_SERVICE_DATA.entrySet().iterator(); while (it.hasNext()) { Map.Entry<String, Task> entry = it.next(); if (entry.getValue().isCompleted()) { it.remove(); } } } public void refreshTasks() { // Not required because the {@link TasksRepository} handles the logic of refreshing the // tasks from all the available data sources. } @Override public void deleteTask(@NonNull String taskId) { TASKS_SERVICE_DATA.remove(taskId); } @Override public void deleteAllTasks() { TASKS_SERVICE_DATA.clear(); } @VisibleForTesting public void addTasks(Task... tasks) { for (Task task : tasks) { TASKS_SERVICE_DATA.put(task.getId(), task); } } }
最後看看 TasksLocalDataSource 類別。
該類別有TaskDao以及AppExecutors 變數,其中TaskDao提供存取Task的介面,為使用Room的寫法。關於Room可以參考這篇。
而AppExecutors主要負責Executor的執行。
TasksLocalDataSource.java
public class TasksLocalDataSource implements TasksDataSource { private static volatile TasksLocalDataSource INSTANCE; private TasksDao mTasksDao; private AppExecutors mAppExecutors; // Prevent direct instantiation. private TasksLocalDataSource(@NonNull AppExecutors appExecutors, @NonNull TasksDao tasksDao) { mAppExecutors = appExecutors; mTasksDao = tasksDao; } public static TasksLocalDataSource getInstance(@NonNull AppExecutors appExecutors, @NonNull TasksDao tasksDao) { if (INSTANCE == null) { synchronized (TasksLocalDataSource.class) { if (INSTANCE == null) { INSTANCE = new TasksLocalDataSource(appExecutors, tasksDao); } } } return INSTANCE; } /** * Note: {@link LoadTasksCallback#onDataNotAvailable()} is fired if the database doesn't exist * or the table is empty. */ @Override public void getTasks(@NonNull final LoadTasksCallback callback) { Runnable runnable = new Runnable() { @Override public void run() { final List<Task> tasks = mTasksDao.getTasks(); mAppExecutors.mainThread().execute(new Runnable() { @Override public void run() { if (tasks.isEmpty()) { // This will be called if the table is new or just empty. callback.onDataNotAvailable(); } else { callback.onTasksLoaded(tasks); } } }); } }; mAppExecutors.diskIO().execute(runnable); } /** * Note: {@link GetTaskCallback#onDataNotAvailable()} is fired if the {@link Task} isn't * found. */ @Override public void getTask(@NonNull final String taskId, @NonNull final GetTaskCallback callback) { Runnable runnable = new Runnable() { @Override public void run() { final Task task = mTasksDao.getTaskById(taskId); mAppExecutors.mainThread().execute(new Runnable() { @Override public void run() { if (task != null) { callback.onTaskLoaded(task); } else { callback.onDataNotAvailable(); } } }); } }; mAppExecutors.diskIO().execute(runnable); } @Override public void saveTask(@NonNull final Task task) { checkNotNull(task); Runnable saveRunnable = new Runnable() { @Override public void run() { mTasksDao.insertTask(task); } }; mAppExecutors.diskIO().execute(saveRunnable); } @Override public void completeTask(@NonNull final Task task) { Runnable completeRunnable = new Runnable() { @Override public void run() { mTasksDao.updateCompleted(task.getId(), true); } }; mAppExecutors.diskIO().execute(completeRunnable); } @Override public void completeTask(@NonNull String taskId) { // Not required for the local data source because the {@link TasksRepository} handles // converting from a {@code taskId} to a {@link task} using its cached data. } @Override public void activateTask(@NonNull final Task task) { Runnable activateRunnable = new Runnable() { @Override public void run() { mTasksDao.updateCompleted(task.getId(), false); } }; mAppExecutors.diskIO().execute(activateRunnable); } @Override public void activateTask(@NonNull String taskId) { // Not required for the local data source because the {@link TasksRepository} handles // converting from a {@code taskId} to a {@link task} using its cached data. } @Override public void clearCompletedTasks() { Runnable clearTasksRunnable = new Runnable() { @Override public void run() { mTasksDao.deleteCompletedTasks(); } }; mAppExecutors.diskIO().execute(clearTasksRunnable); } @Override public void refreshTasks() { // Not required because the {@link TasksRepository} handles the logic of refreshing the // tasks from all the available data sources. } @Override public void deleteAllTasks() { Runnable deleteRunnable = new Runnable() { @Override public void run() { mTasksDao.deleteTasks(); } }; mAppExecutors.diskIO().execute(deleteRunnable); } @Override public void deleteTask(@NonNull final String taskId) { Runnable deleteRunnable = new Runnable() { @Override public void run() { mTasksDao.deleteTaskById(taskId); } }; mAppExecutors.diskIO().execute(deleteRunnable); } @VisibleForTesting static void clearInstance() { INSTANCE = null; } }
整體來說因為Model提供了三層緩存,複雜度反而比Presenter和View還高。
從裡面的設計也可以看到設計原則(OCP)等等。
以上便是 todo-mvp 的 Model 。
以下為MVP相關內容
http://34.80.81.192/?p=5987
Article Comments