Model in todo-mvp

概述


透過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。

AddEditTaskPresenter本身不會持有任何資料,資料放在Model中。

Presenter會通知Model去改變資料。

Presenter會持有Model和View的變數並在建構式初始化他們。

 

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)。

 

因此我們需要看的是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即可。

第3層緩存為遠端資料來源,private final TasksDataSource mTasksRemoteDataSource;
變數型態也是TasksDataSource,也可以簡單替換遠端來源的實現,如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;
}
}

以上便是 todo-mvp 的 Model 內容。

Orignal From: Model in todo-mvp

如何從 adb 啟動 App 並帶參數

概述


如何從adb啟動App並帶參數

做法


以todo-app為例,使用adb啟動App使用的指令為
adb shell am start -n [PACKAGE-NAME]/[ACTIVITY-NAME]

因此需要先找到PACKAGE-NAME 和 ACTIVITY-NAME

1.找PACKAGE-NAME


先安裝 todo App 到裝置上

1.1輸入以下指令便會列出 App 上所有已安裝的 PACKAGE-NAME
adb shell pm list packages -f

若連接多台裝置則使用 -s 指定裝置號碼如下
adb -s DeviceNumber shell pm listpackages -f

如何取得裝置號碼則使用 adb devices

輸入adb shell pm list packages -f 之後回傳的內容可能太長,因此可以在指令的最後加上 > D:\testlog\get_packages.txt 將顯示內容輸出到D:\testlog\get_packages.txt

因此輸入
adb -s DeviceNumber shell pm list packages -f > D:\testlog\get_packages.txt

在d:\testlog\get_packages.txt尋找todo關鍵字,只找到一項如下
package:/data/app/com.example.android.architecture.blueprints.todomvp.mock-1/base.apk=com.example.android.architecture.blueprints.todomvp.mock

 

2.找ACTIVITY-NAME


輸入 adb shell dumpsys activity 便可列出正在 App 上執行的所有 Activity
注意,App必須正在執行中才會顯示 activity name

2.1先在裝置上啟動 todo-app

2.2 輸入
adb -s DeviceNumber shell dumpsys activity -f > D:\testlog\get_activities.txt

2.3在D:\testlog\get_activities.txt 尋找 todo關鍵字,找到其中一個區塊如下

TaskRecord{cc64a79 #6942 A=com.example.android.architecture.blueprints.todomvp.mock U=0 StackId=1 sz=1}
Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.example.android.architecture.blueprints.todomvp.mock/com.example.android.architecture.blueprints.todoapp.tasks.TasksActivity bnds=[18,1035][268,1320] (has extras) }
Hist #0: ActivityRecord{199dedc u0 com.example.android.architecture.blueprints.todomvp.mock/com.example.android.architecture.blueprints.todoapp.tasks.TasksActivity t6942}
Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.example.android.architecture.blueprints.todomvp.mock/com.example.android.architecture.blueprints.todoapp.tasks.TasksActivity bnds=[18,1035][268,1320] (has extras) }
ProcessRecord{66be5e 5459:com.example.android.architecture.blueprints.todomvp.mock/u0a566}

Running activities (most recent first):
TaskRecord{cc64a79 #6942 A=com.example.android.architecture.blueprints.todomvp.mock U=0 StackId=1 sz=1}
Run #0: ActivityRecord{199dedc u0 com.example.android.architecture.blueprints.todomvp.mock/com.example.android.architecture.blueprints.todoapp.tasks.TasksActivity t6942}

mResumedActivity: ActivityRecord{199dedc u0 com.example.android.architecture.blueprints.todomvp.mock/com.example.android.architecture.blueprints.todoapp.tasks.TasksActivity t6942}


在該區塊中尋找關鍵字cmp,可以找到以下內容
cmp=com.example.android.architecture.blueprints.todomvp.mock/com.example.android.architecture.blueprints.todoapp.tasks.TasksActivity

cmp=後方的內容其實就是PACKAGE-NAME/ACTIVITY-NAME

因此可以輸入下方指令來啟動todo App了
adb -s DeviceNumber shell am start -n com.example.android.architecture.blueprints.todomvp.mock/com.example.android.architecture.blueprints.todoapp.tasks.TasksActivity  


成功啟動便會回應
Starting: Intent { cmp=com.example.android.architecture.blueprints.todomvp.mock/com.example.android.architecture.blueprints.todoapp.tasks.TasksActivity }

並在裝置上啟動todo app。

3.啟動App附帶參數


啟動App附帶參數的方式為在指令的最後加入 --es key 'value' 或 --ez key 'value'或 –ei key 'value' 或 –ef key 'value'

其中 --es 參數為string, --ez 參數為boolean, --ei參數型態為int, --ef參數為float。

若要取得參數,則在該Activity的onCreate方法中使用 getIntent().getXXXExtra的方式,xxx則視傳入參數為甚麼型態而定,若現在想啟動todo app並帶參數為string型態,參數的key為test, 參數的value為 test parameter from adb

,則輸入指令如下
adb -s DeviceNumber shell am start -n com.example.android.architecture.blueprints.todomvp.mock/com.example.android.architecture.blueprints.todoapp.tasks.TasksActivity --es test 'test parameter from adb'

 

在TasksActivity的onCreate方法中加入
Log.d(TAG, "onCreate: "+getIntent().getStringExtra("test"));

觀察logcat輸出便可看到 onCreate: test parameter from adb,代表使用adb成功啟動App並附帶參數。

 

Orignal From: 如何從 adb 啟動 App 並帶參數

使用 Stetho 查看實體裝置的資料庫內容

概述


Stetho 為 Facebook 出品的開源 Android 調試工具(官網連結),主要功能有網路抓包,查看資料庫,查看視圖階層等等。本篇主要描述如何使用 Stetho 查看實體裝置的資料庫內容。

步驟


1.Dependencies


在 Module 的 build.gradle 加入以下內容
dependencies {  

    implementation 'com.facebook.stetho:stetho:1.5.0'

}

 

2.在 App 的 Source Code 初始化 Stetho


在 App 第一個啟動 Activity 的 onCreate 方法或新增一個類別繼承 Application 的 onCreate方法加入Stetho.initializeWithDefaults(this);
public class SingletonApplication extends Application {   
public void onCreate() {
super.onCreate();
Stetho.initializeWithDefaults(this);
}
}

 

3.啟動 Stetho 工具


開啟 chrome 瀏覽器並輸入 chrome://inspect 就會開啟 Stetho 工具,Stetho 工具的用途為提供管理所有可調試元件的介面。在紅框內即為連接的實體裝置

4.啟動 App


啟動 App 後可以在 Stetho 工具的 Remote Target 看到啟動的 app
以 todo-app 為例。


點擊 inspect 便會跳出 DevTools 網頁,DevTools 網頁為該 App 專屬的調試工具,點擊網頁上方的 Resources -> 左方 Web SQL 就會顯示該 App 所使用的 db 檔(資料庫),再展開 db 檔(資料庫),內容為即為該資料庫內的資料表。

 

以 todo-app 為例,下方為啟動 App 後一開始的資料庫和資料表範例。
Tasks.db 為資料庫,其中有 tasks 資料表,但因為目前還未新增資料因此資料表沒有內容。



接著在 App 中新增資料,完成後點擊 DevTools 下方有個刷新的圖示或是再次點擊 tasks,就會顯示剛剛新增資料的內容(如紅框)


以上為使用 Stetho 觀察實體裝置的資料庫內容方法。

Orignal From: 使用 Stetho 查看實體裝置的資料庫內容

Error Type 3 in Android Studio

描述


在 Android Studio 啟動 App 時出現有關 Error Type 3 的錯誤,可以參考https://stackoverflow.com/questions/20915266/error-type-3-error-activity-class-does-not-exist

解決


到 File -> Settings -> Build, Execution, Deployment -> Instant Run -> 關閉 Enable Instant Run to hot swap code/resource changes on deploy (default enabled)

Orignal From: Error Type 3 in Android Studio

MVP Pattern in Android

概述


(關於 MVC Pattern in Android 可以參考這篇,本篇是套用 MVP 於 Android 的描述和實作)

MVP 將架構分為 3 個部分,分別為 Model(模型層),View(視圖層),Presenter(展示層)

View(視圖層): 負責與使用者互動並顯示來自展示層的資料。

Presenter(展示層): 負責處理視圖邏輯和互動邏輯。

Model(模型層): 負責管理業務邏輯,提供網路和資料庫的監聽和修改介面。

設計理念


當 Model 傳輸資料到 Presenter 時,Presenter 會封裝視圖邏輯後再把資料傳遞給 View。和 MVC 最大的不同為 MVP 將視圖邏輯從 Model 移動到 Presenter。而 Model 只保留業務邏輯,從而分離視圖邏輯和業務邏輯,且 View 和 Model 完全分離不會有任何相依關係。

Contract 介面


在 Google 所提供 MVP 範例中可以看到 View 和 Presenter 的介面互相對應,為了描述其相對應的關係,每對 View 和 Presenter 的介面都會放置於其 Contract 介面。e.g.,
public interface AddEditTaskContract {  

interface View extends BaseView<Presenter> {

void showEmptyTaskError();

void showTasksList();

void setTitle(String title);

void setDescription(String description);

boolean isActive();
}

interface Presenter extends BasePresenter {

void saveTask(String title, String description);

void populateTask();
}
}

BaseView 和 BasePresetner 介面定義了 View 層和 Presenter 層的公共接口,在 BaseView 會提供 setPresenter 方法以及泛型來讓 View 指定對應的 Presenter。e.g.,
public interface BaseView<T> {  

void setPresenter(T presenter);

}

BasePresenter 則是提供公共的啟動方法 start。e.g.,
public interface BasePresenter {  

void start();

}

 

View


在 MVP 架構中,View 和 Presenter 互相對應,View 用來顯示 Presenter 的資料。先將使用者輸入事件轉發給 Presenter,當 Presenter 處理完邏輯後,再呼叫 View 顯示內容。

  1. View 和 Presenter 的交互都會使用其介面的方法來呼叫,而不會呼叫具體的方法。

  2. View和Presenter的互相關連是透過 setPresenter 方法,該方法會在 Presenter 的建構式呼叫。

  3. View 通常提供更新畫面的方法讓 Presenter 在執行完邏輯後呼叫。


在 Google 範例中 View 比較特別,它不是 Activity,而是一個自定義類別,該類別會使用自定義的 xml,xml 中會包含其它的 UI 元件。e.g.,

AddEditTaskView.java(View)
public class AddEditTaskView extends ScrollView implements AddEditTaskContract.View {  

private TextView mTitle;

private TextView mDescription;

private AddEditTaskContract.Presenter mPresenter;

private boolean mActive;

public AddEditTaskView(Context context) {
super(context);
init();
}

public AddEditTaskView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}

private void init() {
inflate(getContext(), R.layout.addtask_view_content, this);
mTitle = (TextView) findViewById(R.id.add_task_title);
mDescription = (TextView) findViewById(R.id.add_task_description);

mActive = true;
}

@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mActive = true;
}

@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mActive = false;
}

@Override
public void showEmptyTaskError() {
Snackbar.make(mTitle,
getResources().getString(R.string.empty_task_message), Snackbar.LENGTH_LONG).show();
}

@Override
public void showTasksList() {
Activity activity = getActivity(this);
activity.setResult(Activity.RESULT_OK);
activity.finish();
}

@Override
public void setTitle(String title) {
mTitle.setText(title);
}

@Override
public void setDescription(String description) {
mDescription.setText(description);
}

@Override
public boolean isActive() {
return mActive;
}

@Override
public void setPresenter(AddEditTaskContract.Presenter presenter) {
mPresenter = checkNotNull(presenter);
}

// TODO: This should be in the view contract
public String getTitle() {
return mTitle.getText().toString();
}

// TODO: This should be in the view contract
public String getDescription() {
return mDescription.getText().toString();
}
}

在 init 方法中 R.layout.addtask_view_content = addtask_view_content.xml e.g.
<?xml version="1.0" encoding="utf-8"?>  
<merge xmlns:android="http://schemas.android.com/apk/res/android">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin">

<EditText
android:id="@+id/add_task_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/title_hint"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.AppCompat.Title" />

<EditText
android:id="@+id/add_task_description"
android:layout_width="match_parent"
android:layout_height="350dp"
android:gravity="top"
android:hint="@string/description_hint" />

</LinearLayout>
</merge>


簡單的說 View 是 Activity 內的一個 UI 元件,會以變數的方式存在於 Activity中。該變數的初始化也會使用 findViewById 方式初始化,初始化的位置在onCreate 中,而 View 和 Presenter 的相互關聯在 Presenter 的建構式。e.g.,

AddEditTaskActivity.java(這是 Activity 不是 View)
public class AddEditTaskActivity extends AppCompatActivity {  

public static final int REQUEST_ADD_TASK = 1;

public static final String ARGUMENT_EDIT_TASK_ID = "EDIT_TASK_ID";

private AddEditTaskPresenter mPresenter;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.addtask_act);

// Set up the toolbar.
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
ActionBar actionBar = getSupportActionBar();
checkNotNull(actionBar, "actionBar cannot be null");
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setDisplayShowHomeEnabled(true);

final AddEditTaskView addEditTaskView =
(AddEditTaskView) findViewById(R.id.add_edit_task_view);
checkNotNull(addEditTaskView, "addEditTaskView not found in layout");

FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab_edit_task_done);
checkNotNull(fab, "fab not found in layout");
fab.setImageResource(R.drawable.ic_done);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
/*
* TODO:
* View listeners should simply report the event to the presenter.
* In this case: mPresenter.onSavePressed()
*/
mPresenter.saveTask(addEditTaskView.getTitle(), addEditTaskView.getDescription());
}
});

String taskId = null;

if (getIntent().hasExtra(ARGUMENT_EDIT_TASK_ID)) {
taskId = getIntent().getStringExtra(ARGUMENT_EDIT_TASK_ID);
actionBar.setTitle(R.string.edit_task);
} else {
actionBar.setTitle(R.string.add_task);
}

mPresenter = new AddEditTaskPresenter(
taskId,
Injection.provideTasksRepository(getApplicationContext()),
addEditTaskView);
}

@Override
protected void onResume() {
super.onResume();
mPresenter.start();
}

@Override
public boolean onSupportNavigateUp() {
onBackPressed();
return true;
}

@VisibleForTesting
public IdlingResource getCountingIdlingResource() {
return EspressoIdlingResource.getIdlingResource();
}
}

 

Presenter



  1. Presenter 其內部會持有 View 和 Model 的引用變數。

  2. View 和 Presenter 的交互都會使用其介面的方法來呼叫,而不會呼叫具體的方法。

  3. Presenter的建構式會傳入 View 和 Model 並進行初始化。

  4. Presenter不會依賴於 Android 的 UI 元件,也就是說不會出現 Button, TextView, Toast, Context 等等,只會處理業務邏輯,但可以依賴 RESULT_OK 之纇的整數常數。

  5. Presenter 會有一個 Model 的變數並從該變數來操作資料。

  6. Presenter 通常是處理完邏輯之後,再呼叫 View 的介面更新 UI。


e.g., 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;

/**
* 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
*/
public AddEditTaskPresenter(@Nullable String taskId, @NonNull TasksDataSource tasksRepository,
@NonNull AddEditTaskContract.View addTaskView) {
mTaskId = taskId;
mTasksRepository = checkNotNull(tasksRepository);
mAddTaskView = checkNotNull(addTaskView);

mAddTaskView.setPresenter(this);
}

@Override
public void start() {
if (!isNewTask()) {
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());
}
}

@Override
public void onDataNotAvailable() {
// The view may not be able to handle UI updates anymore
if (mAddTaskView.isActive()) {
mAddTaskView.showEmptyTaskError();
}
}

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.
}
}

TasksDataSource mTasksRepository 就是 Model。在建構式中傳入 View 和 Model,並呼叫 View 的 setPresenter 綁定 Presenter 自己。


從 updateTask 方法可以看到 先呼叫 Model 改變資料(mTasksRepository.saveTask)
接著再呼叫 View 顯示內容(mAddTaskView.showTasksList)


e.g.,

    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.
}

 

總結



  1. View 和 Model 不會有依賴

  2. View 和 Presenter 都是透過介面互相呼叫,而不是透過具體方法

  3. Presenter 不會依賴於 Android 的 UI 元件,也就是說不會出現 Button, Toast, EditText, Context 等等

  4. View 會提供 Presenter 處理完邏輯後顯示內容的方法



Orignal From: MVP Pattern in Android

MVC Pattern in Android

概述


(關於 MVC 在 POSA 的說明可以參考這篇,本篇是套用 MVC 於 Android 的描述和實作)

MVC 將架構分為 3 個部分,分別為 Model(模型層),View(視圖層),Controller(控制層)

View(視圖層):負責與使用者互動並顯示來自模型層的資料。

Controller(控制層):負責根據來自視圖層的互動訊息,指揮模型層更新資料或指揮視圖層重新載入模型層的資料。

Model(模型層):負責管理視圖邏輯和業務邏輯,提供網路和資料庫的監聽和修改介面。

設計理念


通常視圖邏輯發生修改的機會高於業務邏輯發生修改的機會,因此希望當業務邏輯發生變化時不會影響視圖邏輯。

在 MVC 架構中,View 依賴於 Model,Controller 依賴 View 和 Model,Model 完全獨立不依賴其它兩者。

View 接受使用者輸入事件並轉發給 Controller ,再根據 Controller 的指令顯示 Model 的資料。

Model 提供資料並更新資料,View 的顯示內容就是來自 Model 提供的資料,Controller 也會更新 Model 的資料。

Controller 根據來自 View 的使用者輸入事件,發出指令給 Model 更新資料並通知 View 重新載入 Model 的資料。Controller 負責更新 Model 的資料,根據更新的方式分為主動和被動。因此 MVC 也可分為主動模式(Active)和被動模式(Passive)。

被動(Passive)模式


在被動模式中 Controller 是唯一可以更改 Model 資料的角色,而 View 只能根據 Controller 的指令被動從 Model 取得資料並顯示內容。在被動模式中 Controller 控制整個流程的更新過程,根據輸入事件被動的通知 Model 更改資料並通知 View 顯示資料。

流程如下:
使用者觸發 View 的輸入事件,事件轉發給 Controller,Controller 通知 Model 更改資料。當 Model 資料更改完成後,Controller 再通知 View 取得 Model 的資料並顯示內容。

被動模式實作


以簡單的亂數產生器作為範例。

首先從 View 開始。

View 就是 Activity,Activity 只有 1 個 Button 用來產生亂數,以及 1 個 TextView 用來顯示亂數。

View 會持有 Model 和 Controller 的參考,Model 參考用來取得資料並顯示內容,而 Controller 參考用來轉發使用者輸入事件。
public class MVCPassiveActivity extends AppCompatActivity implements OnClickListener {  

private Button mRollIt;
private TextView mRollNumber;

private MVCPassiveController mController;
private MVCPassiveModel mModel;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.mvc_passive_activity);

initData();
initUI();
}

private void initData() {
mModel = new MVCPassiveModel();
mController = new MVCPassiveController(this, mModel);
}

private void initUI() {
mRollIt = findViewById(R.id.mvc_passive_roll_it);
mRollIt.setOnClickListener(this);
mRollNumber = findViewById(R.id.mvc_passive_show_roll_number);
}

@Override
public void onClick(View view) {
int uiID = view.getId();
switch (uiID) {
case R.id.mvc_passive_roll_it:
mController.rollIt();
break;
}
}

public void update() {
mRollNumber.setText("" + mModel.getRandomNumber());
}
}

需要注意的是在 initData 方法中初始化 Model 以及 Controller。而在 MVCPassiveController 的建構式中會去關連 View 和 Model。

在 onClick 方法中會去轉發使用者輸入事件給 Controller,而 Controller 的 rollIt 方法就是去通知 Model 更改資料。

最後在 update 方法就是從 Model 取得資料並顯示內容。

接著是 Controller ,Controller 會持有 View 和 Model 的參考並在建構式中關連起來。
public class MVCPassiveController {  

private MVCPassiveActivity mView;
private MVCPassiveModel mModel;

public MVCPassiveController(MVCPassiveActivity view, MVCPassiveModel model) {
mView = view;
mModel = model;
}

public void rollIt() {
mModel.rollOnce();
mView.update();
}

}

在 rollIt 方法首先通知 Model 更改資料,接著再通知 View 取得 Model 的資料並顯示內容。

最後是 Model,Model 完全獨立不依賴於其它層,唯一需要注意的是實際上的 Model 應該是 repository 其中會有取得本地端和遠端的資料的元件,本篇的範例相對簡單的多。
public class MVCPassiveModel {  

private int mRandomNumber;

public void rollOnce() {
mRandomNumber = new Random().nextInt(100);
}

public int getRandomNumber() {
return mRandomNumber;
}

}

 

主動(Active)模式


主動模式會應用觀察者模式(Observer Pattern) 來控制整個流程。首先 Model 就是主題(Subject),而 View 和 Controller 則都是觀察者(Observer)。

當 Model 發生變化,其註冊的觀察者(View & Controller)都會自動收到通知,Controller 仍然是通知 Model 資料變化的角色,和被動模式的差別是 Controller 不再需要通知 View 更新資料,因為當 Model 的資料發生變化時,View 會自動收到通知。

流程如下:
使用者觸發 View 的輸入事件,事件轉發給 Controller,Controller 通知 Model 更改資料。

當 Model 資料更改完成後,觀察者們(View & Controller)都會自動收到通知並進行相關處理。

主動模式實作


也是以簡單的亂數產生器作為範例。

首先建立觀察者介面,觀察者必須繼承該介面,並實作介面中的 update 方法,該方法的參數就是更新資料後的 Model。
public interface IObserver {  

public void update(MVCActiveModel mvcActiveModel);

}

接著實作 Model,Model 就是主題,會持有 1 個觀察者們的 list。對該 Model 有興趣的觀察者都會註冊成為該主題的觀察者。
public class MVCActiveModel {  

private ArrayList<IObserver> mObservers = new ArrayList<>();
private int mRandomNumber;

public void rollOnce() {
mRandomNumber = new Random().nextInt(100);
notifyObservers();
}

public int getRandomNumber() {
return mRandomNumber;
}

public void addObserver(IObserver observer) {
mObservers.add(observer);
}

public void removeObserver(IObserver observer) {
if (mObservers.contains(observer)) {
mObservers.remove(observer);
}
}

private void notifyObservers() {
int sizeOfObservers = mObservers.size();
for (int i = 0; i < sizeOfObservers; ++i) {
mObservers.get(i).update(this);
}
}
}

notifyObservers 方法就是通知所有的觀察者,主題(Model)的資料已經發生變化,觀察者會收到通知,並自行決定如何處理已變化的資料。
而 rollOnce 方法就是改變資料並通知觀察者們的觸發點。

接著是 Controller,Controller 也是觀察者之一,因此需要繼承 IObserver。

而在建構式中會將自己註冊到 Model 的 observer list 中。
public class MVCActiveController implements IObserver {  

private MVCActiveActivity mView;
private MVCActiveModel mModel;

public MVCActiveController(MVCActiveActivity view, MVCActiveModel model) {
mView = view;
mModel = model;
mModel.addObserver(this);
}

public void rollIt() {
mModel.rollOnce();
}

@Override
public void update(MVCActiveModel model) {

}
}

最後是 View,View 也是觀察者所以需要繼承 IObserver,並實作 update 方法,在該方法中取得更新後的 Model 並顯示其資料內容。

而在 initData 方法內會將自己註冊為 Model 的觀察者。
public class MVCActiveActivity extends AppCompatActivity implements OnClickListener, IObserver {  

//Main UI
private Button mRollIt;
private TextView mRollNumber;

private MVCActiveController mController;
private MVCActiveModel mModel;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.mvc_active_activity);

initData();
initUI();
}

private void initData() {
mModel = new MVCActiveModel();
mModel.addObserver(this);
mController = new MVCActiveController(this, mModel);
}

private void initUI() {
mRollIt = findViewById(R.id.mvc_active_roll_it);
mRollIt.setOnClickListener(this);
mRollNumber = findViewById(R.id.mvc_active_show_roll_number);
}

@Override
public void onClick(View view) {
int uiID = view.getId();
switch (uiID) {
case R.id.mvc_active_roll_it:
mController.rollIt();
break;
}
}

@Override
public void update(MVCActiveModel model) {
mRollNumber.setText("" + model.getRandomNumber());
}
}

 

 

Orignal From: MVC Pattern in Android

Activity 的啟動模式(LaunchMode)紀錄

概述


Activity 的啟動模式主要包含 4 種:
Standard(標準模式,預設),SingleTop(棧頂重用),SingleTask(棧內重用),
SingleInstance(單例模式)。

Standard 每次啟動都會建立新的 Activity 實體,其它 3 種會根據情況選擇新增或
是重用 Activity 實體。

建立新實體的生命週期為 onCreate -> onStart -> onResume
重用舊實體的生命週期為 onNewIntent -> onResume

如何使用啟動模式A.


在 AndroidManifest.xml 中可以透過設定 launchMode 設定不同的啟動模式
使用 taskAffinity 並加入套件名稱便可以設定不同的栈,預設棧為目前套件名
稱,taskAffinity 只能用於 SingleTop, SingleTask, SingleInstance

1.Standard(標準):


啟動 Activity 的預設模式,當使用 Standard 每次都會產生新的實體。

2.SingleTop(棧頂重用):


當要啟動的 Activity 在棧頂,則重用該實體。若不在棧頂則建立新的實體(建立完成後該實體位於棧頂)。
啟動的生命週期:
D: onCreate
D: onStart
D: onResume
重用的生命週期
D: onPause
D: onNewIntent
D: onResume

3.SingleTask(棧內重用)


A.沒有設定 taskAffinity (情況較為簡單):

1.當要啟動的 Activity 位於棧頂時,重用該實體,不會建立新的實體。

重用的生命週期
D: onPause
D: onNewIntent
D: onResume

2.當要啟動的 Activity 位於棧內時,會重用該實體並清除該實體上方的所有其它Activity。(此時該啟動的 Activity 位於棧頂)。

重用的生命週期
D: onPause
D: onNewIntent
D: onResume

B.有設定 taskAffinity:(注意 taskAffinity 只能用於 SingleTop, SingleTask, SingleInstance)

當設定 taskAffinity 時有幾個重點:
1.被啟動的 Activity 會跟著啟動的 Activity 處於同一個棧中。
2.有一些操作會切換棧,如按下 home key,再點擊 app icon 會重用預設栈
3.當從棧退出 Activity,會優先退出同一個棧的 Activity,而當退完同一個棧的所有 Activity,會切換回桌面,不會啟動另一個棧。

以範例說明:

FirstActivity 啟動 SecondActivity,SecondActivity 啟動 ThirdActivity(SingleTask, 不同棧,棧名為 another.stack))啟動 FourthActivity (預設)

此時 FirstActivity 和 SecondActivity 位於預設棧,而 ThirdActivity 和 FourthActivity 位於 another.stack 棧。顯示在螢幕最上方的為 FourthActivity。

2個走向:

1.按下 home key 再點擊 app icon,此時會啟動預設棧,因此顯示在螢幕上的為 SecondActivity,點擊 back key(SecondActivity 銷毀),顯示 FirstActivity,點擊 back key(FirstActivity 銷毀),回到桌面。
(注意此時 ThirdActivity 和 FourthActivity 還未銷毀,保存在記憶體中)

2.按下 back key(FourthActivity 銷毀),顯示 ThirdActivity,按下 back key(ThirdActivity 銷毀),顯示 SecondActivity,按下 back key( SecondActivity 銷毀),顯示 FirstActivity,按下 back key(FirstActivity 銷毀),回到桌面。

重點:


1.若設定新的任務棧,有些操作會啟動不同的任務棧,這些操作可能會讓使用者感覺混淆。

2.當啟動新的 Activity 而產生新的任務棧時,畫面切換的動畫會不同。

4.SingleInstance(單例實體)


啟動設定為單例實體的 Activity 時,會建立單獨的栈且該棧無法加入其它的 Activiity,之後都會重用該實體。

以範例說明:

FirstActivity 啟動 SecondActivity,SecondActivity 啟動 FifthActivity(單例實體),FifthActivity 啟動 FourthActivity。

(此時有 2 個棧,預設棧有 FirstActivity,SecondActivity,FourthActivity。另一個棧只有 FifthActivity)

2個走向:

1.按下 back key(FourthActivity 銷毀),顯示 SecondActivity
(注意這裡因為出棧的規則是同一個棧的 Activity 會先出完,因此顯示的是 SecondActivity,而不是 FifthActivity),按下 back key(SecondActivity 銷毀),顯示 FirstActivity,按下 back key(FirstActivity 銷毀),顯示 FifthActivity,按下 back  key(FifthActivity 銷毀),回到桌面。

2.FourthActivity 啟動 FifthActivity,FifthActivity 啟動 FourthActivity,按下 back key(FourthActivity 銷毀),顯示 FourthActivity,按下 back key(FourthActivity 銷毀),顯示 SecondActivity,按下 back key(SecondActivity 銷毀),顯示FirstActivity,按下 back key(FirstActivity 銷毀),顯示 FifthActivity,按下back key(FifthActivity 銷毀),回到桌面。

重點:


1.設定為單例實體之後一定都會重用實體。

2.單例實體的 Activity 啟動另一個 Activity 後,被啟動的 Activity 不會加入到單例實體的棧中,而會加到預設棧。

3.出棧規則也是先出完同一個棧,待同一個棧的所有的 Activity 出完之後再切換到另一個棧。

 

如何使用啟動模式B.


呼叫 startActivity 方法時使用參數 intent 設定啟動標幟(flag),如下
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

常用的標幟如下:

1.FLAG_ACTIVITY_SINGLE_TOP: 和棧頂重用(SingleTop)相同。

2.FLAG_ACTIVITY_NEW_TASK: 和棧內重用(SingleTask)相同,但不會清除該 Activity以上的其它 Activity。

3.FLAG_ACTIVITY_CLEAR_TOP:如果是棧內重用的啟動模式,則會清除棧上其它的 Activity 並重用實體且呼叫 onNewIntent 方法。
如果是預設的啟動模式則會清除自己和其它的實體,再重新建立,呼叫 onCreate

在 AndroidManifest.xml 設定和在 startActivity 的 intent 設定的不同點:


1.2 種方式選擇 1 個使用即可。如果同時使用,startActivity 的優先級較高會蓋過AndroidManifest.xml 的設定

2.startActivity 無法設定 SingleInstance 模式

3.AndroidManifest.xml 只有 SingleTask 具有清除頂部的功能,但 startActivity 可以透過設定 FLAG_ACTIVITY_CLEAR_TOP 清除頂部,在標準的啟動模式也可以使用

4.當使用 startActivityForResult 啟動 Activity 時不會重用實體,會重複建立多個實體,因此使用 startActivityForResult 時不建議同時使用會重用實體的啟動模式(SingleTop, SingleTask, SingleInstance)

 

總結:


1.SingleTop(棧頂重用)
當要啟動的 Activity 位於棧頂時重用該實體。如果不在棧頂則建立新的實體。

2.SingleTask(棧內重用)
當要啟動的 Activity 位於棧頂時重用該實體。如果不在棧頂則清除該 Activity 以上的其它 Activity。

3.SingleInstance(單例實體)
當要啟動的 Activity 不存在時直接建立新的任務棧並放入該 Activity,之後都會重用該 Activity。

4.任務棧的切換
SingleTop, SingleTask, SingleInstance 都可透過 taskAffinity 建立新的任務棧,當使用 back key退出 Activity 時會先把同一個棧的 Activity 全部退出。切換任務棧可能會搞混。

5.設定啟動模式的方法
有 2 種,可在 AndroidManifest.xml 以及 startActivity 的intent。startActivity 的優先性大於 AndroidManifest.xml。

6.startActivityForResult 建議不要和會重用實體的模式同時使用,因為該方法無法重用實體。

Orignal From: Activity 的啟動模式(LaunchMode)紀錄

Activity 生命週期紀錄

概要:


生命週期主要包含 6 種狀態: onCreat, onStart, onResume, onPause, onStop, onDestroy。

各種狀態意義為
onCreate 代表 Activity 的建立
onStart 代表 Activity 的啟動
onResume 代表 Activity 的恢復
onPause 代表 Activity 的暫停
onStop 代表 Activity 的停止
onDestroy 代表 Activity 的銷毀

這些狀態分別互相配對,構成 3 種生命週期,分別為完整的生命週期,可視的生命週期,前台的生命週期。
完整的生命週期包含可視的生命週期,可視的生命週期包含前台的生命週期。

完整的生命週期:

Activity 從建立到銷毀的全部過程。最外層生命週期,生命週期發生在 onCreate 到 onDestroy 之間。

可視的生命週期:

Activity 從使用者可視到離開使用者視線的過程。生命週期發生在 onStart 到 onStop 之間。注意可視包含該 Activity 被其它元件遮蓋只顯示一部分的情況。

前台的生命週期:

Activity 顯示在所有的元件之前並可與使用者互動。生命週期發生在 onResume 到 onPause 之間。

另外還有 4 種特殊的生命週期狀態,各為 onRestart, onSaveInstanceState, on RestoreInstanceState, onNewIntent。

onRestart 的呼叫點為從 onStop 到 onStart 之間,也就是 onStop -> onRestart ->  onStart。

onSaveInstanceState 用於儲存 Activity 的資料。

onNewIntent 只有在 LaunchMode 為 SingleTop 且啟動的 Activity 為符合重用該 Activity 的規則時才會被呼叫。(onNewIntent -> onResume)

為了實際了解生命週期的變化,建立 FirstActivity 並覆寫所有生命週期印出 Log
FirstActivity.java
public class FirstActivity extends AppCompatActivity implements OnClickListener {  

private static final String TAG = FirstActivity.class.getSimpleName();

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, TAG + " onCreate");
setContentView(R.layout.first_activity);
}

@Override
protected void onStart() {
super.onStart();
Log.d(TAG, TAG + " onStart");
}

@Override
protected void onResume() {
super.onResume();
Log.d(TAG, TAG + " onResume");
}

@Override
protected void onPause() {
super.onPause();
Log.d(TAG, TAG + " onPause");
}

@Override
protected void onStop() {
super.onStop();
Log.d(TAG, TAG + " onStop");
}

@Override
protected void onDestroy() {
super.onDestroy();
Log.d(TAG, TAG + " onDestroy");
}

@Override
protected void onRestart() {
super.onRestart();
Log.d(TAG, TAG + " onRestart");
}

@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
Log.d(TAG, TAG + " onSaveInstanceState");
}

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
Log.d(TAG, TAG + " onRestoreInstanceState");
}

@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
Log.d(TAG, TAG + " onNewIntent");
}

}

 

紀錄1


使用者點擊 back key, home key, menu key 所觸發的生命週期變化:


A.啟動 FirstActivity 後點擊 back key 關閉 FirstActivity
1.點擊 App icon 啟動 FirstActivity:
D: FirstActivity onCreate
D: FirstActivity onStart
D: FirstActivity onResume
2.點擊 back key:
D: FirstActivity onPause
D: FirstActivity onStop
D: FirstActivity onDestroy

B.啟動 FirstActivity 後點擊 home key
1.點擊App icon 啟動 FirstActivity:
D: FirstActivity onCreate
D: FirstActivity onStart
D: FirstActivity onResume
2.點擊 home key:
D: FirstActivity onPause
D: FirstActivity onSaveInstanceState
D: FirstActivity onStop

C.啟動 FirstActivity 後點擊 home key 再點擊 App icon
1.點擊 App icon 啟動 FirstActivity:
D: FirstActivity onCreate
D: FirstActivity onStart
D: FirstActivity onResume
2.點擊 home key:
D: FirstActivity onPause
D: FirstActivity onSaveInstanceState
D: FirstActivity onStop
3.點擊 App icon:
D: FirstActivity onRestart
D: FirstActivity onStart
D: FirstActivity onResume

D.啟動 FirstActivity 後點擊 home key 再點擊 menu key 再拖曳移除 App頁面
1.點擊 App icon 啟動 FirstActivity:
D: FirstActivity onCreate
D: FirstActivity onStart
D: FirstActivity onResume
2.點擊 home key:
D: FirstActivity onPause
D: FirstActivity onSaveInstanceState
D: FirstActivity onStop
3.點擊 menu key,拖曳移除 App:
D: FirstActivity onDestroy(注意有時候不會顯示)

E.啟動 FirstActivit y後點擊 home key 再點擊 menu key 再點擊 App 頁面
1.點擊 App icon 啟動 FirstActivity:
D: FirstActivity onCreate
D: FirstActivity onStart
D: FirstActivity onResume
2.點擊 home key:
D: FirstActivity onPause
D: FirstActivity onSaveInstanceState
D: FirstActivity onStop
3.點擊 menu key 並點擊 App 頁面:
D: FirstActivity onRestart
D: FirstActivity onStart
D: FirstActivity onResume

F.啟動 FirstActivity後點擊 menu key
1.點擊 App icon 啟動 FirstActivity:
D: FirstActivity onCreate
D: FirstActivity onStart
D: FirstActivity onResume
2.點擊 menu key:
D: FirstActivity onPause
D: FirstActivity onSaveInstanceState
D: FirstActivity onStop

G.啟動 FirstActivity 後點擊 menu key 再拖曳移除 App
1.點擊 App icon 啟動 FirstActivity:
D: FirstActivity onCreate
D: FirstActivity onStart
D: FirstActivity onResume
2.點擊 menu key:
D: FirstActivity onPause
D: FirstActivity onSaveInstanceState
D: FirstActivity onStop
3.拖曳移除 App:
沒有顯示Log , 推測應該呼叫onDestroy

H.啟動 FirstActivity 後點擊 menu key 再點擊 App頁面
1.點擊 App icon 啟動 FirstActivity:
D: FirstActivity onCreate
D: FirstActivity onStart
D: FirstActivity onResume
2.點擊 menu key:
D: FirstActivity onPause
D: FirstActivity onSaveInstanceState
D: FirstActivity onStop
3.點擊 App 頁面:
D: FirstActivity onRestart
D: FirstActivity onStart
D: FirstActivity onResume

重點:


1.按下 home key 和 menu key 都會觸發 onSaveInstanceState (在 onPause 之後),但按下 back key 不會觸發 onSaveInstanceState。

2.當 Activity 從後台回到前台都會觸發 onRestart -> onStart -> onResume

 

紀錄2


Activity之間切換的生命週期變化:


2.1修改 FirstActivity,加入啟動 SecondActivity 的功能。

FirstActivity.java
public class FirstActivity extends AppCompatActivity implements OnClickListener {  

private Button mLaunchSecondBtn;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, TAG + " onCreate");
setContentView(R.layout.first_activity);
initUI();
}

private void initUI() {
mLaunchSecondBtn = findViewById(R.id.first_activity_launch);
mLaunchSecondBtn.setOnClickListener(this);
}

@Override
public void onClick(View v) {
int uiID = v.getId();
switch (uiID) {
case R.id.first_activity_launch_second_activity:
Intent intent = new Intent(this, SecondActivity.class);
startActivity(intent);
finish();
break;
}
}

2.2 新增 SecondActivity 和 FirstActivity 互相切換來觀察生命週期。

SecondActivity.java
public class SecondActivity extends AppCompatActivity implements OnClickListener {  

private static final String TAG = SecondActivity.class.getSimpleName();
private Button mLaunchFirstBtn;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, TAG + " onCreate");
setContentView(R.layout.second_activity);
initUI();
}

@Override
protected void onStart() {
super.onStart();
Log.d(TAG, TAG + " onStart");
}

@Override
protected void onResume() {
super.onResume();
Log.d(TAG, TAG + " onResume");
}

@Override
protected void onPause() {
super.onPause();
Log.d(TAG, TAG + " onPause");
}

@Override
protected void onStop() {
super.onStop();
Log.d(TAG, TAG + " onStop");
}

@Override
protected void onDestroy() {
super.onDestroy();
Log.d(TAG, TAG + " onDestroy");
}

@Override
protected void onRestart() {
super.onRestart();
Log.d(TAG, TAG + " onRestart");
}

@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
Log.d(TAG, TAG + " onSaveInstanceState");
}

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
Log.d(TAG, TAG + " onRestoreInstanceState");
}

@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
Log.d(TAG, TAG + " onNewIntent");
}

private void initUI() {
mLaunchFirstBtn = findViewById(R.id.second_activity_launch_first_activity);
mLaunchFirstBtn.setOnClickListener(this);
}

@Override
public void onClick(View v) {
int uiID = v.getId();
switch (uiID) {
case R.id.second_activity_launch_first_activity:
Intent intent = new Intent(this,FirstActivity.class);
startActivity(intent);
finish();
break;
}
}
}

A.FirstActivity 啟動 SecondActivity (FirstActivity 不會 finish),再關閉 SecondActivity,再關閉 FirstActivity
1.點擊 App icon 啟動 FirstActivity:
D: FirstActivity onCreate
D: FirstActivity onStart
D: FirstActivity onResume
2.點擊 FirstActivity 的按鈕啟動 SecondActivity。注意 FirstActivity 沒有 finish
D: FirstActivity onPause
D: SecondActivity onCreate
D: SecondActivity onStart
D: SecondActivity onResume
D: FirstActivity onSaveInstanceState
D: FirstActivity onStop
3.點擊 back key 關閉 SecondActivity,讓 FirstActivity 回到前台
D: SecondActivity onPause
D: FirstActivity onRestart
D: FirstActivity onStart
D: FirstActivity onResume
D: SecondActivity onStop
D: SecondActivity onDestroy
4.點擊 back key 關閉 FirstActivity
D: FirstActivity onPause
D: FirstActivity onStop
D: FirstActivity onDestroy

B.FirstActivity 啟動 SecondActivity(FirstActivity 會 finish),再關閉 SecondActivity
1.點擊 App icon 啟動 FristActivity:
D: FirstActivity onCreate
D: FirstActivity onStart
D: FirstActivity onResume
2.點擊 FirstActivity 的按鈕啟動 SecondActivity。注意 FristActivity 會 finish
D: FirstActivity onPause
D: SecondActivity onCreate
D: SecondActivity onStart
D: SecondActivity onResume
D: FirstActivity onStop
D: FirstActivity onDestroy
3.按下 back key 關閉 SecondActivity
D: SecondActivity onPause
D: SecondActivity onStop
D: SecondActivity onDestroy

C.FirstActivity 啟動 SecondActivity(FirstActivity 不會 finish), SecondActivity 再啟動 FirstActivity (SecondActivity不會finish)
1.點擊 App icon 啟動 FristActivity:
D: FirstActivity onCreate
D: FirstActivity onStart
D: FirstActivity onResume
2.點擊 FirstActivity 的按鈕啟動 SecondActivity。注意 FirstActivity 不會 finish
D: FirstActivity onPause
D: SecondActivity onCreate
D: SecondActivity onStart
D: SecondActivity onResume
D: FirstActivity onSaveInstanceState
D: FirstActivity onStop
3.點擊 SecondActivity 的按鈕啟動 FirstActivity。注意 SecondActivity 不會 finish
D: SecondActivity onPause
D: FirstActivity onCreate
D: FirstActivity onStart
D: FirstActivity onResume
D: SecondActivity onSaveInstanceState
D: SecondActivity onStop

D.FirstActivity 啟動 SecondActivity,注意 SecondActivity 使用透明主題(FirstActivity 不會 finish), SecondActivity在啟動 FirstActivity(SecondActivity 不會 finish)
1.點擊 App icon 啟動 FristActivity:
D: FirstActivity onCreate
D: FirstActivity onStart
D: FirstActivity onResume
2.點擊 FirstActivity 的按鈕啟動 SecondActivity(注意 SecondActivity 使用透明主題注意 FirstActivity不會 finish)
D: FirstActivity onPause
D: SecondActivity onCreate
D: SecondActivity onStart
D: SecondActivity onResume
D: FirstActivity onSaveInstanceState
3.點擊 SecondActivity 的按鈕啟動 FirstActivity。注意 SecondActivity 不會 finish
D: SecondActivity onPause
D: FirstActivity onStop
D: FirstActivity onCreate
D: FirstActivity onStart
D: FirstActivity onResume
D: SecondActivity onSaveInstanceState
D: SecondActivity onStop

重點:
1.當第 1 個 Activity 啟動第 2 個 Activity時,會讓第 1 個 Activity先呼叫完 onPause,再進行第 2 個 Activity 的初始化(onCreate -> onStart -> onResume)直到第 2 個 Activity 呼叫完 onResume 之後再進行第 1 個 Activity 的後續動作。

2.當第 1 個 Activity 啟動第 2 個 Activity 時,若第 1 個 Activity 會 finish,則 onSaveInstanceState 不會被呼叫。動作順序如下
(第 1 個 Activity 啟動第 2 個 Activity)
Activity1 onPause
Activity2 onCreate
Activity2 onStart
Activity2 onResume
Activity1 onStop
Activity1 onDestroy
反之,若第 1 個Activity 不會 finish,則 onSaveInstanceState 會在 onStop 之前被呼叫。
(第 1 個 Activity 啟動第 2 個 Activity)
Activity1 onPause
Activity2 onCreate
Activity2 onStart
Activity2 onResume
Activity1 onSaveInstanceState
Activity1 onStop

3.如果要啟動的 Activity(SecondActivity)是透明主題,則啟動者(FirstActivity)啟動 SecondActivity 之後不會呼叫 onStop,如下
(當 FirstActivity 啟動 SecondActivity 之後)
D: FirstActivity onPause
D: SecondActivity onCreate
D: SecondActivity onStart
D: SecondActivity onResume
D: FirstActivity onSaveInstanceState
而當 SecondActivity 啟動 FirstActivity之後才會呼叫 onStop 如下
D: SecondActivity onPause
D: FirstActivity onStop
D: FirstActivity onCreate
D: FirstActivity onStart
D: FirstActivity onResume
D: SecondActivity onSaveInstanceState
D: SecondActivity onStop

紀錄3.


系統配置改變所造成的生命週期變化


當系統配置發生改變,Activity 就會觸發重建過程,最常見的系統配置發生變化為旋轉螢幕。因為這種生命週期變化為系統控制,所以當觸發 onSaveInstanceState 時也會一併呼叫 onRestoreInstanceState 來恢復資料。

A.啟動 FirstActivity 後旋轉螢幕
1.點擊 App icon 啟動 FirstActivity:
D: FirstActivity onCreate
D: FirstActivity onStart
D: FirstActivity onResume
2.旋轉螢幕觸發從直屏到橫屏導致 FirstActivity 重建
D: FirstActivity onPause
D: FirstActivity onSaveInstanceState
D: FirstActivity onStop
D: FirstActivity onDestroy
D: FirstActivity onCreate
D: FirstActivity onStart
D: FirstActivity onRestoreInstanceState
D: FirstActivity onResume
3.再次旋轉螢幕從橫屏到直屏導致 FristActivity 重建
D: FirstActivity onPause
D: FirstActivity onSaveInstanceState
D: FirstActivity onStop
D: FirstActivity onDestroy
D: FirstActivity onCreate
D: FirstActivity onStart
D: FirstActivity onRestoreInstanceState
D: FirstActivity onResume

重點:


1.注意旋轉螢幕之後 onSaveInstanceState 會在 onPause 之後呼叫,而 onRestoreInstanceState 會在 onStart 之後呼叫。

2.若 Activity 要恢復資料可以選擇 onCreate 或 onRestoreInstanceState,差別為於在 onCreate 中需要判斷傳入的參數 Bundle 是否為空,如果是空則不需要恢復資料,而非空才需要進行恢復資料的動作。如下
  @Override  
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState)
if(savedInstanceState != null){
// restore data
}
}

 

3.若要支援旋轉螢幕,但不重建 Activity,可以在 AndroidManifest.xml 中對該 Activity 設定為
      android:configChanges="orientation|screenSize"

如此,旋轉螢幕之後 Activity 還是會切換直橫屏,但不會重建,資料也不會初始化,在這種情況下若想在旋轉螢幕時收到通知,可在 Activity 覆寫以下方法。(注意該方法只有在 android:configChanges="orientation|screenSize" 被設定時才呼叫)。如下
  @Override  
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
Log.d(TAG, "newConfig.toString():"+newConfig.toString());
}

測試生命週期如下
1.點擊 App icon 啟動 FirstActivity
D: FirstActivity onCreate
D: FirstActivity onStart
D: FirstActivity onResume
2.旋轉螢幕從直屏轉為橫屏
D: newConfig.toString():{0 1.0 ?mcc?mnc zh_TW ldltr sw360dp w640dp h335dp
320dpi nrml long land finger -keyb/v/h -nav/h s.47}
3.旋轉螢幕從橫屏轉為直屏
D: newConfig.toString():{0 1.0 ?mcc?mnc zh_TW ldltr sw360dp w360dp h615dp 320dpi nrml long port finger -keyb/v/h -nav/h s.48}

4.若想保持螢幕為直屏或橫屏,可在 AndroidManifest.xml 的 Activity 加入以下
      android:screenOrientation="portrait"

螢幕便會保持直屏不會旋轉。

5.注意 onRestoreInstanceState 僅用於系統配置變化時(如旋轉螢幕)導致的重建。如果是使用者導致的重建(點擊 back key or home key)則需要在 onCreate 恢復資料。

注意以上 Activity 的生命週期實驗都是以 LaunchMode (啟動模式)為 default,在其它的啟動模式(SingleTop, SingleTask, SingleInstance)則另外需要另外紀錄。

Orignal From: Activity 生命週期紀錄

OKHttp 使用注意

1.若想在 Callback 中修改畫面(包含產生 Dialog, Toast 等等),必須使用 runonUiThread 包含要更改的畫面的行為,如下為使用 OKHttp 包裝器的方法宣告,
  public static void sendPostWithJSONFormat(String URL, Map<String, String> requestParameter, Callback callback) {  
MediaType JSON = MediaType.parse("application/json; charset=utf-8");
RequestBody requestBody = RequestBody.create(JSON, new JSONObject(requestParameter).toString());
Request request = new Request.Builder().url(URL).post(requestBody).build();
Call call = sOKHttpClient.newCall(request);
call.enqueue(callback);
}

以下是使用該包裝器方法的範例
    OKHttpWrapper.sendPostWithJSONFormat(URL, httpParameter, new Callback() {  
@Override
public void onResponse(Call call, Response response) throws IOException {
//更改畫面行為
}

@Override
public void onFailure(Call call, IOException e) {
//更改畫面行為
}
});

重點在於第 4 行和第 9 行若有更改畫面的行為必須使用 runOnUiThread 去包含。否則會出現以下 Exception:
E/AndroidRuntime: FATAL EXCEPTION: OkHttp Dispatcher  
java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
at android.os.Handler.<init>(Handler.java:200)
at android.os.Handler.<init>(Handler.java:114)
at android.app.Dialog.<init>(Dialog.java:119)
at android.app.AlertDialog.<init>(AlertDialog.java:200)
at android.app.AlertDialog$Builder.create(AlertDialog.java:1086)
at okhttp3.RealCall$AsyncCall.execute(RealCall.java:153)
at okhttp3.internal.NamedRunnable.run(NamedRunnable.java:32)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
at java.lang.Thread.run(Thread.java:818)

 

Orignal From: OKHttp 使用注意

Twitter Delicious Facebook Digg Stumbleupon Favorites More

 
Design by Free WordPress Themes | Bloggerized by Lasantha - Premium Blogger Themes | Affiliate Network Reviews