IntentService

IntentService是什麼?


IntentService是Service的一種,使用上較為方便簡單。其背景執行方式是透過 HandleThread,因此單個IntentService上的任務是循序執行的,可以保證執行緒安全。不同的IntentService在執行上也不會互相干擾。

IntentService適合用於客戶端不須和Service互動的情況下,基本上由客戶端呼叫startService並指定啟動的IntentService。




簡單的IntentService實作


public class SimpleIntentService extends IntentService {  

public SimpleIntentService() {
super("SimpleIntentService");
}

@Override
protected void onHandleIntent(Intent intent) {
}

}

onHandleIntent方法會執行在新的執行緒上,也是放置耗時操作的位置。其參數intent就是從客戶端傳遞過來的intent。

另外需要在AndroidManifest.xml宣告該IntentService
<service  
android:exported="false"
android:name=".simpleintentservice.SimpleIntentService">
</service>

android:exported="false"代表是否可以由其他的App啟動。




啟動IntentService


客戶端透過呼叫startService方法來啟動IntentService
public class SimpleIntentServiceActivity extends AppCompatActivitye {  

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

Intent launchSimpleIntentService = new Intent(this, SimpleIntentService.class);
startService(launchSimpleIntentService);
}
}






客戶端傳遞資料到IntentService


從客戶端透過intent放置資料便可傳遞到IntentService
Intent launchSimpleIntentService = new Intent(this, SimpleIntentService.class);  
launchSimpleIntentService.putExtra("data", "data from activity");
startService(launchSimpleIntentService);


透過IntentService的onHandleIntent的參數取出資料
@Override  
protected void onHandleIntent(Intent intent) {
String data = intent.getStringExtra("data");
Log.d(TAG, "data:" + data);
}

output:
D: data:data from activity  





IntentService的生命週期


複寫IntentService的生命週期方法觀察呼叫順序
public class SimpleIntentService extends IntentService {  

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

public SimpleIntentService() {
super("SimpleIntentService");
Log.d(TAG, "SimpleIntentService: ");
}

@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "onCreate: ");
}

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

@Override
public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
Log.d(TAG, "onStartCommand: ");
return super.onStartCommand(intent, flags, startId);
}

@Override
protected void onHandleIntent(Intent intent) {
Log.d(TAG, "onHandleIntent: ");
String data = intent.getStringExtra("data");
Log.d(TAG, "data:" + data);
}

}


Client call
Intent launchSimpleIntentService = new Intent(this, SimpleIntentService.class);  
launchSimpleIntentService.putExtra("data", "data from activity");
startService(launchSimpleIntentService);
Log.d(TAG, "startService");


Output
D/SimpleIntentServiceActivity: startService  
D/SimpleIntentService: SimpleIntentService:
D/SimpleIntentService: onCreate:
D/SimpleIntentService: onStartCommand:
D/SimpleIntentService: onHandleIntent:
D/SimpleIntentService: data:data from activity
D/SimpleIntentService: onDestroy:

從Output觀察IntentService生命週期的順序為
建構式 -> onCreate -> onStartCommand -> onHandleIntent -> onDestroy




客戶端停止IntentService


透過呼叫stopService()方法來停止IntentService。
Intent stopSimpleIntentService = new Intent(this, SimpleIntentService.class);  
stopService(stopSimpleIntentService);
Log.d(TAG, "stopService");

傳入的Intent必須指定要停止的IntentService。

呼叫StopService之後,IntentService便會呼叫onDestroy方法銷毀自己。

需要注意的是因為onHandleIntent方法內部是由新的執行緒來執行,因此即使是客戶端呼叫了stopService,但是onHandleIntent方法不會結束。

為了測試這點在onHandleIntent方法加入計時10秒的操作
@Override  
protected void onHandleIntent(Intent intent) {
Log.d(TAG, "onHandleIntent: ");
String data = intent.getStringExtra("data");
Log.d(TAG, "data:" + data);
sleep(10);
}

private void sleep(int sleepTimeSeconds) {
for (int i = 0; i < sleepTimeSeconds; ++i) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.d(TAG, "sleep: "+i);
}
}


客戶端並在第3秒呼叫stopService

Output如下
D/SimpleIntentServiceActivity: startService  
D/SimpleIntentService: SimpleIntentService:
D/SimpleIntentService: onCreate:
D/SimpleIntentService: onStartCommand:
D/SimpleIntentService: onHandleIntent:
D/SimpleIntentService: data:data from activity
D/SimpleIntentService: sleep: 0
D/SimpleIntentService: sleep: 1
D/SimpleIntentService: sleep: 2
D/SimpleIntentService: sleep: 3
D/SimpleIntentServiceActivity: stopService
D/SimpleIntentService: onDestroy:
D/SimpleIntentService: sleep: 4
D/SimpleIntentService: sleep: 5
D/SimpleIntentService: sleep: 6
D/SimpleIntentService: sleep: 7
D/SimpleIntentService: sleep: 8
D/SimpleIntentService: sleep: 9

可以看到即使IntentService已經銷毀了,但計時仍然繼續。

因此若要呼叫stopService也一併停止onHandleIntent的內容,可以建立成員變數來控制是否要停止計時,如下
private boolean mIsDestroy;  

@Override
public void onDestroy() {
mIsDestroy = true;
super.onDestroy();
Log.d(TAG, "onDestroy: ");
}

private void sleep(int sleepTimeSeconds) {
for (int i = 0; i < sleepTimeSeconds && !mIsDestroy; ++i) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.d(TAG, "sleep: "+i);
}
}






IntentService的循序


IntentService在單一執行緒上執行任務,若客戶端呼叫多次startService方法,IntentService也會保持等待當前的任務完成後再執行下一個任務。

修改印出耗時資訊時也印出IntentService toString
  private void sleep(int sleepTimeSeconds) {  
for (int i = 0; i < sleepTimeSeconds && !mIsDestroy; ++i) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.d(TAG, "sleep: "+i+" name:"+toString());
}
}

連續啟動3次IntentService,Outout如下
D/SimpleIntentService: SimpleIntentService:   
D/SimpleIntentService: onCreate:
D/SimpleIntentService: onStartCommand:
D/SimpleIntentService: onHandleIntent:
D/SimpleIntentService: data:data from activity
D/SimpleIntentService: onStartCommand:
D/SimpleIntentService: onStartCommand:
D/SimpleIntentService: sleep: 0 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 1 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 2 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 3 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 4 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 5 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 6 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 7 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 8 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 9 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: onHandleIntent:
D/SimpleIntentService: data:data from activity
D/SimpleIntentService: sleep: 0 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 1 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 2 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 3 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 4 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 5 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 6 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 7 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 8 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 9 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: onHandleIntent:
D/SimpleIntentService: data:data from activity
D/SimpleIntentService: sleep: 0 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 1 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 2 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 3 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 4 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 5 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 6 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 7 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 8 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: sleep: 9 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@369cf9c
D/SimpleIntentService: onDestroy:

可以看到每個任務都必須等到上一個任務完成後才執行。




不同IntentService執行


觀察不同的IntentService執行的情況
新增另一個IntentService為AnotherSimpleIntentService如下
public class AnotherSimpleIntentService extends IntentService {  

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

private boolean mIsDestroy;

public AnotherSimpleIntentService() {
super("AnotherSimpleIntentService");
Log.d(TAG, "AnotherSimpleIntentService: ");
}

@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "onCreate: ");
}

@Override
public void onDestroy() {
mIsDestroy = true;
super.onDestroy();
Log.d(TAG, "onDestroy: ");
}

@Override
public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
Log.d(TAG, "onStartCommand: ");
return super.onStartCommand(intent, flags, startId);
}

@Override
protected void onHandleIntent(Intent intent) {
Log.d(TAG, "onHandleIntent: ");
String data = intent.getStringExtra("data");
Log.d(TAG, "data:" + data);
sleep(10);
}

private void sleep(int sleepTimeSeconds) {
for (int i = 0; i < sleepTimeSeconds && !mIsDestroy; ++i) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.d(TAG, "sleep: "+i+" name:"+toString());
}
}

}

AndroidManifest.xml宣告AnotherSimpleIntentService
    <service  
android:exported="false"
android:name=".simpleintentservice.AnotherSimpleIntentService">
</service>

在客戶端依序啟動SimpleIntentService和AnotherSimpleIntentService
  public void onClick(View view) {  
int uiID = view.getId();
switch (uiID) {
case R.id.start_intent_service:
startSimpleIntentService();
startAnotherSimpleIntentService();
break;
}
}

private void startAnotherSimpleIntentService() {
Intent launchAnotherSimpleIntentService = new Intent(this, AnotherSimpleIntentService.class);
launchAnotherSimpleIntentService.putExtra("data", "data from activity");
startService(launchAnotherSimpleIntentService);
Log.d(TAG, "startService");
}

private void startSimpleIntentService() {
Intent launchSimpleIntentService = new Intent(this, SimpleIntentService.class);
launchSimpleIntentService.putExtra("data", "data from activity");
startService(launchSimpleIntentService);
Log.d(TAG, "startService");
}

Output如下
D/SimpleIntentServiceActivity: startService  
D/SimpleIntentServiceActivity: startService
D/SimpleIntentService: SimpleIntentService:
D/SimpleIntentService: onCreate:
D/SimpleIntentService: onStartCommand:
D/SimpleIntentService: onHandleIntent:
D/SimpleIntentService: data:data from activity
D/AnotherSimpleIntentService: AnotherSimpleIntentService:
D/AnotherSimpleIntentService: onCreate:
D/AnotherSimpleIntentService: onStartCommand:
D/AnotherSimpleIntentService: onHandleIntent:
D/AnotherSimpleIntentService: data:data from activity
D/SimpleIntentService: sleep: 0 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@ff71da5
D/AnotherSimpleIntentService: sleep: 0 name:com.codefoxx.serviceexample.simpleintentservice.AnotherSimpleIntentService@9de157a
D/SimpleIntentService: sleep: 1 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@ff71da5
D/AnotherSimpleIntentService: sleep: 1 name:com.codefoxx.serviceexample.simpleintentservice.AnotherSimpleIntentService@9de157a
D/SimpleIntentService: sleep: 2 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@ff71da5
D/AnotherSimpleIntentService: sleep: 2 name:com.codefoxx.serviceexample.simpleintentservice.AnotherSimpleIntentService@9de157a
D/SimpleIntentService: sleep: 3 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@ff71da5
D/AnotherSimpleIntentService: sleep: 3 name:com.codefoxx.serviceexample.simpleintentservice.AnotherSimpleIntentService@9de157a
D/SimpleIntentService: sleep: 4 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@ff71da5
D/AnotherSimpleIntentService: sleep: 4 name:com.codefoxx.serviceexample.simpleintentservice.AnotherSimpleIntentService@9de157a
D/SimpleIntentService: sleep: 5 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@ff71da5
D/AnotherSimpleIntentService: sleep: 5 name:com.codefoxx.serviceexample.simpleintentservice.AnotherSimpleIntentService@9de157a
D/SimpleIntentService: sleep: 6 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@ff71da5
D/AnotherSimpleIntentService: sleep: 6 name:com.codefoxx.serviceexample.simpleintentservice.AnotherSimpleIntentService@9de157a
D/SimpleIntentService: sleep: 7 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@ff71da5
D/AnotherSimpleIntentService: sleep: 7 name:com.codefoxx.serviceexample.simpleintentservice.AnotherSimpleIntentService@9de157a
D/SimpleIntentService: sleep: 8 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@ff71da5
D/AnotherSimpleIntentService: sleep: 8 name:com.codefoxx.serviceexample.simpleintentservice.AnotherSimpleIntentService@9de157a
D/SimpleIntentService: sleep: 9 name:com.codefoxx.serviceexample.simpleintentservice.SimpleIntentService@ff71da5
D/SimpleIntentService: onDestroy:
D/AnotherSimpleIntentService: sleep: 9 name:com.codefoxx.serviceexample.simpleintentservice.AnotherSimpleIntentService@9de157a
D/AnotherSimpleIntentService: onDestroy:

可以看到不同的IntentService彼此不會互相影響,各自執行任務。

 

Orignal From: IntentService

Error: Default interface methods are only supported starting with Android N (--min-api 24)

這是在 Android Studio 中套用了 androidx.core 之後出現的問題,build project 出現以下錯誤:
Error: Default interface methods are only supported starting with Android N (--min-api 24): android.view.MenuItem androidx.core.internal.view.SupportMenuItem.setContentDescription(java.lang.CharSequence)

解決方法有 2 種:
1. 在 /app/build.gradle 修改 minSdkVersion 為 24
android {  
...
defaultConfig {
...
minSdkVersion 24
...
}
...
}

 

2.在 /app/build.gradle 加入以下內容
android {  
...
compileOptions {
sourceCompatibility = '1.8'
targetCompatibility = '1.8'
}
...
}

 

Orignal From: Error: Default interface methods are only supported starting with Android N (--min-api 24)

Google Cloud Source Repositories(GCSR) 如何將遠端的 Repo clone 到本地端

問題
想將 Google Cloud Source Repositories 已存在的遠端 repo clone 到本地端

方案
1.安裝 Google Cloud SDK(已安裝請忽略)
2.開啟 Google Cloud SDK Shell 並使用指令移動到想放置本地端 repo 的路徑
3.使用瀏覽器開啟 Cloud Source Repositories 的遠端 repo 頁面(以下為 hello-world 的示範頁面)

4.點擊右上方的"建立本機副本"並選擇 "Google Cloud SDK" 再複製"透過指令列複製存放區"的指令(這個指令就是把遠端 repo clone 到本地端的指令)5.將第4步驟複製的指令貼到第2步驟開啟的 Google Cloud SDK Shell 中並執行(如下畫面)

6.完成(在本地端應該可以看到從遠端 clone 下來的 repo)


 

Orignal From: Google Cloud Source Repositories(GCSR) 如何將遠端的 Repo clone 到本地端

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 查看實體裝置的資料庫內容

Twitter Delicious Facebook Digg Stumbleupon Favorites More

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