分類
Android

Activity 生命週期紀錄

概要:

生命週期主要包含 6 種狀態: onCreate, onStart, onResume, onPause, onStop, onDestroy。
各種狀態意義為
onCreate 代表 Activity 的建立:
當系統初次建立 Activity 時 onCreate 方法會被呼叫,此時會進入 Created 狀態,應該在這個方法中實作首次啟動的邏輯,如綁定資料到清單中,關聯ViewModel和Activity,初始化變數。
onCreate方法會傳入 bundle 參數,bundle參數會包含上一次Activity儲存的資料,若Activity之前還未存在過,則bundle參數為null。
當onCreate方法結束後,Activity 會進入 Started 狀態,系統將會呼叫onStart方法。
onStart 代表 Activity 的啟動
當Activity進入Started狀態時,系統將會呼叫 onStart 方法。onStart方法會讓 Activity 變得可見,但還無法和使用者互動。onStart會很快的執行完成,完成後Activity會進入 Resumed 狀態。
onResume 代表 Activity 的恢復
當 Activity 進入 Resumed 狀態,系統將會呼叫onResume方法,onResume方法會讓Activity聚焦並讓使用者可以和Activity互動。App將會停留在這個狀態直到有其它事件取得焦點。
當發生中斷事件時,Activity就會進入Paused狀態,系統便會呼叫onPause方法。
當Activity從 Paused 狀態回到 Resumed 狀態時,系統便會呼叫 onResume 方法
onPause 代表 Activity 的暫停
當Activity失焦(變的部分可見或是透明)系統便會呼叫 onPause方法,此時Activity無法和使用者互動。在onPause中可以考慮釋放一些不需要用到的資源,但在多窗口模式中,Activity依然是完全可見的,釋放資源有可能對Activity造成影響。
若有這種狀況發生,可以考慮在onStop中再釋放資源。
onPause的執行時間很短,不一定有足夠的時間來執行操作。所以不應該在onPause方法中保存資料,進行網路操作,資料庫存取。應該在onStop中進行這些耗時操作。
當 onPause方法結束後並不代表Activity會離開Paused狀態。相反的,Activity會保持在此狀態,直到Activity再次聚焦或完全不可見。
若Activity聚焦則系統會呼叫onResume方法讓Activity回到Resumed狀態,相反的,若Activity變的完全不可見則系統就會呼叫onStop方法讓Activity進入Stopped狀態。
onStop 代表 Activity 的停止
當Activity變得完全不可見(進入後台),系統便會叫onStop方法,Activity進入Stopped狀態。
在onStop方法中應該進行資源的調整或釋放或資料庫的儲存。
當Activity進入Stopped狀態,Activity實體保存在記憶體中,但不會連結到window manager,此時必須小心當系統的記憶體不足時將會銷毀 process。
在Activity為Stopped 狀態時,可以進入2種狀態,當Activity恢復時系統將會呼叫onRestart,若Activity結束時系統將會呼叫onDestroy。
onDestroy 代表 Activity 的銷毀
當Activity銷毀時系統將會呼叫onDestroy,呼叫的原因有2種:
1.當Actiivty結束(使用者關閉Activity或呼叫Activity的finish方法)
2.系統配置發生變化(旋轉螢幕)
在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)則另外需要另外紀錄。

分類
Android

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)

 

分類
Android

在裝置上產生 2 個相同的啟動圖示

情境:
安裝 App 後在裝置上產生 2 個該 App 的啟動圖示
 
原因:
通常是在 AndroidManifest.xml 中的 Activity,同時存在 2 個如下的 intent-filter 內容

      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>

如下面的 SplashActivity 和 MainActivity 都有相同的 intent-filter

...
<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="2 launcher icon"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity android:name=".splash.SplashActivity">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
    <activity android:name=".MainActivity">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
  </application>

 
解法:
移除其中一個相同的 intent-filter

分類
Android

Android Studio 在專案中引用 AAR

使用 Android Studio 並在專案中引用 AAR,需要 2 個步驟
1.把要使用的 AAR 檔拷貝到 app/libs 目錄,完成後如下圖
2.右鍵點擊專案 -> Open Module Settings
2-1.選擇左方的 Modules 的 app -> 點擊左上方的 + 號,如下圖

2-2. 在 Create New Module 視窗選擇 Import .JAR/.AAR Package -> 點擊 next

2-3.在 import Module from Library 視窗的 File Name: 選擇第1步驟的 AAR 檔案位置 -> 點擊 OK -> Subproject name: 不必修改 -> 點擊 Finish -> Gradle 會自動更新,更新完成後點擊 OK
2-4.右鍵點擊專案 -> Open Module Settings -> 左方 Modules 選擇 app -> 點擊右上方 Dependencies -> 點擊最右上方的 + 號 -> 選擇 3 Module dependency

2-5.在 Choose Modules 視窗選擇 CommonTool-debug -> 點擊 OK -> 在 Project Structure 視窗點擊 OK -> 完成
 

分類
Android

在 Activity 中判斷滑動手勢

在 Activity 判斷滑動手勢主要是透過 View.OnTouchListener 和 GestureDetector.OnGestureListener
透過讓 Activity 繼承 GestureDetector.OnGestureListener 並實作其中的 onFling 方法,在 onFling 方法中判斷第 1 個按下點的 X 座標和第 2 個按下點的 X 座標是否超過標準值來決定是否為滑動手勢。

import android.support.constraint.ConstraintLayout;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
public class FlingGestureExampleMainActivity extends AppCompatActivity implements View.OnTouchListener,
    GestureDetector.OnGestureListener{
  private static final String TAG = FlingGestureExampleMainActivity.class.getSimpleName();
  //main ui
  private LinearLayout mBasicLayout;
  private GestureDetector mGesetureDetector;
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.fling_gesture_example_main_activity);
    mBasicLayout = findViewById(R.id.basic_layout_activity);
    mBasicLayout.setOnTouchListener(this);
    mBasicLayout.setLongClickable(true);
    mGesetureDetector = new GestureDetector((GestureDetector.OnGestureListener) this);
  }
  @Override
  public boolean onDown(MotionEvent e) {
    return false;
  }
  @Override
  public void onShowPress(MotionEvent e) {
  }
  @Override
  public boolean onSingleTapUp(MotionEvent e) {
    return false;
  }
  @Override
  public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
    return false;
  }
  @Override
  public void onLongPress(MotionEvent e) {
  }
  @Override
  public boolean onFling(MotionEvent firstMotion, MotionEvent secondMotion, float velocityX, float velocityY) {
    final int FLING_MIN_DISTANCE = 100;
    final int FLING_MIN_VELOCITY = 200;
    boolean isFlingToLeft = firstMotion.getX() - secondMotion.getX() > FLING_MIN_DISTANCE && Math.abs(velocityX) > FLING_MIN_VELOCITY;
    if (isFlingToLeft) {
      Log.d(TAG, "to left");
    }
    boolean isFlingToRight = firstMotion.getX() - secondMotion.getX() < FLING_MIN_DISTANCE && Math.abs(velocityX) < FLING_MIN_VELOCITY;
    if (isFlingToRight) {
      Log.d(TAG, "to right");
    }
    return false;
  }
  @Override
  public boolean onTouch(View view, MotionEvent motionEvent) {
    return mGesetureDetector.onTouchEvent(motionEvent);
  }
}

主要的重點為讓 Activity實作 onDown, onShowPress, on SingleTapUp, onScroll, onLongPress, onFling, onTouch 方法,
並在 onFling 方法判斷滑動的邏輯。
以及第23~26行的手勢和 layout 物件的連結
layout檔(fling_gesture_example_main_activity.xml)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:id="@+id/basic_layout_activity"
  tools:context=".FlingGestureExampleMainActivity">
  <ListView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    .../>
</LinearLayout>

重點為 LinearLayout 建立 id,以便在程式碼中參考。
以上便是讓 Activity 加入滑動手勢的判斷,若是想讓該 Activity中
的 ListView 也可偵測滑動,作法相當簡單,直接讓該 ListView 呼叫 setOnTouchListener 和 setLongClickable 即可。

        mListView = getListView();
        mListViewAdapter = new ListViewAdapter();
        setListAdapter(mListViewAdapter);
        ...
        mPatrolPointListView.setOnTouchListener(this);
        mPatrolPointListView.setLongClickable(true);

 

分類
Android Uncategorized

android.util.Log 列印訊息內容太多無法完全顯示

使用 android.util.Log 列印訊息時,若訊息內容太多就會出現無法完全顯示訊息內容的情況。
解決的方法就是分段列印,如下

    private void logLongMessage(String TAG, String message) {
        int maxMessageSize = 2000;
        for (int i = 0; i <= message.length() / maxMessageSize; i++) {
            int start = i * maxMessageSize;
            int end = (i + 1) * maxMessageSize;
            end = end > message.length() ? message.length() : end;
            Log.d(TAG, message.substring(start, end));
        }
    }

 

分類
Android Uncategorized

使用 Room DAO 存取資料 (Room)

要使用 Room 存取資料,需要使用 DAO。
這組 DAO 物件形成了 Room 的主要組件,因為每個 DAO 都包含提供對資料庫的抽象訪問方法。
通過使用 DAO 類別而不是查詢構建器或直接查詢來訪問資料庫,使用者可以分離出資料庫架構的不同元件。
此外,DAO 可在測試應用程序時輕鬆模擬資料庫訪問。
注意:
在加入 DAO 類別之前,先在 app 的 build.gradle 加入相依性。
DAO 可以是 interface,也可以是抽象類別。如果是抽象類別,它可以選擇有一個構造函數,它將 RoomDatabase 作為唯一的參數。Room 在編譯時期建立每個 DAO 實作。
注意:
Room 並不支援在 Main thread 上存取資料庫,因為可能會長時間鎖定 UI。
如果要讓 Room 支援在其他執行緒存取資料庫則必須在建構時呼叫allowMainThreadQueries() 方法。
異步查詢 – 若是查詢會返回 LiveData 或 Flowable 實例 – 不受此規則的約束,因為它們在需要時在後台線程上異步運行查詢
 

Define methods for convenience

可以使用 DAO 類別表示多個便捷查詢。

Insert

當建立 DAO 方法並使用 @Insert 註釋時,Room 會生成一個實現,該實現在單一事務(single transaction)中將所有參數插入到資料庫中。

@Dao
public interface MyDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    public void insertUsers(User... users);
    @Insert
    public void insertBothUsers(User user1, User user2);
    @Insert
    public void insertUsersAndFriends(User user, List<User> friends);
}

如果 @Insert 方法只接收 1 個參數,則它可以返回 long,這是該插入項目的新 rowId。如果參數是陣列或集合,則應返回 long [] 或 List <Long>。
有關更多詳細信息,請參閱 @Insert 的文件,以及 SQLite documenation for rowid tables

Update

Update 修改資料庫中作為參數給出的一組實體。它使用每個實體的主鍵做查詢。

@Dao
public interface MyDao {
    @Update
    public void updateUsers(User... users);
}

雖然通常沒有必要,但可以讓此方法返回一個 int 值,表示資料庫中更新的行數。
 

Delete

Delete 從資料庫中刪除一組從參數傳入的實體。使用主鍵來查找要刪除的實體。

@Dao
public interface MyDao {
    @Delete
    public void deleteUsers(User... users);
}

雖然通常沒有必要,但可以讓此方法返回一個 int 值,表示從資料庫中刪除的行數。
 

Query for information

@Query 是 DAO 類別中使用的主要註釋。它可以對資料庫執行讀/寫操作。
每個 @Query 方法都在編譯時進行驗證,因此如果查詢出現問題,則會發生編譯錯誤而不是運行時失敗。
Room 也會驗證查詢的返回值,避免返回的物件中的屬性名稱與查詢的相應列名稱不相符,Room 使用以下兩個方法之一

  • 如果只有一些屬性名稱相符,它會發出警告。
  • 如果沒有屬性名稱相符,則會發出錯誤。

Simple queries

@Dao
public interface MyDao {
    @Query("SELECT * FROM user")
    public User[] loadAllUsers();
}

以上是一個非常簡單的查詢,可以取得所有用戶。在編譯時,Room 知道正在查詢用戶表中的所有列。如果查詢包含語法錯誤,或者資料庫中不存在該資料表,則在應用程序編譯時,Room 會顯示包含相應消息的錯誤。

Passing parameters into the query

大多數情況下,需要將參數傳遞給查詢以執行過濾操作,例如僅顯示年齡超過特定年齡的用戶。
要傳入參數,請在 Room 註釋中使用方法參數,如以下代碼段所示

@Dao
public interface MyDao {
    @Query("SELECT * FROM user WHERE age > :minAge")
    public User[] loadAllUsersOlderThan(int minAge);
}

在編譯時處理此查詢時,Room 會將:minAge 綁定參數與 minAge 方法的參數。
Room 使用參數名稱檢查是否相符。如果存在不相符,則在應用編譯時會發生錯誤。
還可以在查詢中多次傳遞多個參數或引用它們,如以下代碼段所示:

@Dao
public interface MyDao {
    @Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
    public User[] loadAllUsersBetweenAges(int minAge, int maxAge);
    @Query("SELECT * FROM user WHERE first_name LIKE :search " +
           "OR last_name LIKE :search")
    public List<User> findUserWithName(String search);
}

 

Returning subsets of columns

大多數情況下,只需要獲得實體的幾個屬性。
例如,UI 可能只顯示用戶的名字和姓氏,而不是用戶的每一個訊息。
通過僅提取 UI 顯示的列,可以節省寶貴的資源,並且讓查詢可以更快地完成。
Room 允許從查詢中返回任何基於 Java 的對象,只要結果可以映射到返回的對象即可。
例如,可以建立以下(POJO)來獲取用戶的名字和姓氏

public class NameTuple {
    @ColumnInfo(name = "first_name")
    public String firstName;
    @ColumnInfo(name = "last_name")
    public String lastName;
}

現在,可以在查詢方法中使用此 POJO:

@Dao
public interface MyDao {
    @Query("SELECT first_name, last_name FROM user")
    public List<NameTuple> loadFullName();
}

Room 了解查詢返回 first_name 和 last_name 列的值,並且這些值可以映射到 NameTuple 類別的屬性中。因此,Room 可以生成正確的代碼。
如果查詢返回太多列,或者有 NameTuple 類別中不存在的列,則 Room 會顯示警告。
注意:
這些 POJO 也可以使用 @Embedded 註釋。
 

Passing a collection of arguments

某些查詢可能要求傳入可變數量的參數,並且在運行時之前不知道參數的確切數量。
例如,可能希望從區域子集中檢索有關所有用戶的信息。
Room 了解參數何時表示集合,並根據提供的參數數量在運行時自動擴展它。

@Dao
public interface MyDao {
    @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
    public List<NameTuple> loadUsersFromRegions(List<String> regions);
}

 

Observable queries

執行查詢時,通常希望應用程序的UI在資料更改時自動更新。
要實現此目的,請在查詢方法描述中使用 LiveData 類型的返回值。
Room 會產生所有必要的代碼,以便在更新資料庫時更新 LiveData。

@Dao
public interface MyDao {
    @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
    public LiveData<List<User>> loadUsersFromRegionsSync(List<String> regions);
}

注意:
從版本 1.0 開始,Room 使用查詢中訪問的表列表來決定是否更新 LiveData 的實例。
 

Reactive queries with RxJava

Room 為 RxJava2 類型的返回值提供以下支持:

  • @Query methods: Room supports return values of type Publisher, Flowable, and Observable
  • @Insert, @Update, and @Delete methods: Room 2.1.0 and higher supports return values of type Completable, Single<T>, and Maybe<T>

要使用此功能,請在 app 的 build.gradle 文件中包含最新版本的 rxjava2:

dependencies {
    implementation 'androidx.room:room-rxjava2:2.1.0-alpha02'
}

以下代碼示範如何使用這些返回類型:

@Dao
public interface MyDao {
    @Query("SELECT * from user where id = :id LIMIT 1")
    public Flowable<User> loadUserById(int id);
    // Emits the number of users added to the database.
    @Insert
    public Maybe<Integer> insertLargeNumberOfUsers(List<User> users);
    // Makes sure that the operation finishes successfully.
    @Insert
    public Completable insertLargeNumberOfUsers(User... users);
    /* Emits the number of users removed from the database. Always emits at
       least one user. */
    @Delete
    public Single<Integer> deleteUsers(List<User> users);
}

有關更多詳細信息,請參閱 Google Developers Room and RxJava
 

Direct cursor access

如果需要直接存取返回的行,則可以從查詢中返回 Cursor 物件,如以下代碼段所示:

@Dao
public interface MyDao {
    @Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
    public Cursor loadRawUsersOlderThan(int minAge);
}

警告:
非常不建議使用 Cursor API,因為它不能保證行是否存在或行包含的值。
 

Query multiple tables

某些查詢可能需要訪問多個表來計算結果。Room 允許編寫任何查詢,因此也可以查詢多個資料表。
此外,如果響應是可觀察的數據類型(如 Flowable 或 LiveData),則會監視查詢中引用的所有資料表以檢查是否無效。
以下代碼段顯示如何查詢不同的資料表:

@Dao
public interface MyDao {
    @Query("SELECT * FROM book " +
           "INNER JOIN loan ON loan.book_id = book.id " +
           "INNER JOIN user ON user.id = loan.user_id " +
           "WHERE user.name LIKE :userName")
   public List<Book> findBooksBorrowedByNameSync(String userName);
}

還可以從這些查詢中返回 POJO。

@Dao
public interface MyDao {
   @Query("SELECT user.name AS userName, pet.name AS petName " +
          "FROM user, pet " +
          "WHERE user.id = pet.user_id")
   public LiveData<List<UserPet>> loadUserAndPetNames();
   // You can also define this class in a separate file, as long as you add the
   // "public" access modifier.
   static class UserPet {
       public String userName;
       public String petName;
   }
}

 

分類
Android Uncategorized

在資料庫中建立 View (Room)

Room 2.1.0 及更高版本提供對 SQLite 資料庫的 View 功能,允許使用者將查詢封裝到類別中。Room 將這些查詢支持的類別稱為 View,作用和使用 DAO 的簡單資料物件相同。
注意:
與實體(Entitiy)一樣,可以針對 View 運行 SELECT 語句。但是無法對 View 進行 INSERT, UPDATE 或 DELETE 語句。
 

Create a view

要建立 View,請將 @DatabaseView 註釋加入類別。將註釋的值設置為該類別應表示的查詢。以下代碼段提供了一個 View 範例:

@DatabaseView("SELECT user.id, user.name, user.departmentId," +
              "department.name AS departmentName FROM user " +
              "INNER JOIN department ON user.departmentId = department.id")
public class UserDetail {
    public long id;
    public String name;
    public long departmentId;
    public String departmentName;
}

 
 

Associate a view with your database

要將該 View 作為資料庫的一部分,請在 @Database 註釋中加入 views 屬性

@Database(entities = {User.class}, views = {UserDetail.class},
          version = 1)
public abstract class AppDatabase extends RoomDatabase {
    public abstract UserDao userDao();
}

 

分類
Android Uncategorized

使用 Room entities 定義資料 (Room)

使用 Room 時,可以將相關的屬性定義為實體(entities)。
對於每個實體,將會創建一個資料表來保存項目在 Database 物件中,必須透過 Database 類別中的 entities 陣列來引用實體類別。
以下示範如何定義一個實體(entity)

@Entity
public class User {
    @PrimaryKey
    public int id;
    public String firstName;
    public String lastName;
}

為了保存屬性,Room 必須去存取它。可以透過將該屬性設定為 public 或提供 getter 和 setter 存取器。若是使用存取器必須注意該存取器必須符合 JavaBeans 的命名規則。
注意:
實體(Entity)可以有一個空構造函數(如果相應的 DAO 類別可以訪問每個持久化字段),或者一個構造函數,其參數包含與實體中的屬性匹配的類型和名稱。Room 也可以使用完整或部分構造函數,例如只接收某些屬性的構造函數。

Use a primary key

每個實體必須至少定義一個屬性作為主鍵。即使只有一個屬性,仍需要使用 @PrimaryKey 註釋來註釋該屬性。
此外,如果希望 Room 為實體分配自動 ID,可以設定 @PrimaryKey 的 autoGenerate 屬性。
如果實體具有複合主鍵,則可以使用 @Entity 註釋的 primaryKeys 屬性,如以下代碼段所示:

@Entity(primaryKeys = {"firstName", "lastName"})
public class User {
    public String firstName;
    public String lastName;
}

 
Room 預設使用類別名稱作為資料表名稱。如果希望資料表使用不同的名稱,可以設定  @Entity 註釋的 tableName 屬性,如以下代碼段所示:

@Entity(tableName = "users")
public class User {
    // ...
}

注意:
資料表名稱是區分大小寫的
 
類似於 tableName,Room 使用屬性名稱作為資料庫中的列名稱。如果希望列具有不同的名稱,請將 @ColumnInfo 註釋加到屬性中,如以下代碼段所示:

@Entity(tableName = "users")
public class User {
    @PrimaryKey
    public int id;
    @ColumnInfo(name = "first_name")
    public String firstName;
    @ColumnInfo(name = "last_name")
    public String lastName;
}

 

Ignore fileds

預設情況下,Room 會為實體中定義的每個屬性都創建一列。如果實體具有不想保存的屬性,則可以使用 @Ignore 對其進行註釋,如以下代碼段所示:

@Entity
public class User {
    @PrimaryKey
    public int id;
    public String firstName;
    public String lastName;
    @Ignore
    Bitmap picture;
}

 
如果實體從另一個實體(父實體)繼承屬性,則通常使用 @Entity 屬性的 ignoredColumns:

@Entity(ignoredColumns = "picture")
public class RemoteUser extends User {
    @PrimaryKey
    public int id;
    public boolean hasVpn;
}

 

Provide table search support

Room 支持多種類型的註釋,可以更輕鬆地搜索資料表中的詳細信息。除非應用程序的 minSdkVersion 小於 16,否則請使用全文搜索(full-text search)。

Support full-text search

如果應用程序需要通過全文搜索(FTS)快速訪問數據庫信息,就讓實體由使用 FTS3 或 FTS4 SQLite 擴展模塊的虛擬表支持。
要使用全文搜索,必須在 Room 2.1.0 及更高版本,請將 @ Fts3 或 @ Fts4 註釋添加到給定實體,如以下代碼段所示:

// Use `@Fts3` only if your app has strict disk space requirements or if you
// require compatibility with an older SQLite version.
@Fts4
@Entity(tableName = "users")
public class User {
    // Specifying a primary key for an FTS-table-backed entity is optional, but
    // if you include one, it must use this type and column name.
    @PrimaryKey
    @ColumnInfo(name = "rowid")
    public int id;
    @ColumnInfo(name = "first_name")
    public String firstName;
}

注意:啟用 FTS 的資料表必須使用 INTEGER 類型的主鍵和列名”rowid”。如果 FTS 資料表支持的實體定義主鍵,則它必須使用該類型和列名稱。
 
如果資料表支持多種語言的內容,請使用 languageId 選項指定存儲每行語言信息的列:

@Fts4(languageId = "lid")
@Entity(tableName = "users")
public class User {
    // ...
    @ColumnInfo(name = "lid")
    int languageId;
}

 
Room 提供了其他幾個用於定義FTS支持的實體選項,包括結果排序(result ordering),tokenizer 類型(tokenizer types)和作為外部內容管理的資料表。有關這些選項的更多詳細信息,請參閱 FtsOptions 參考。
 

Index specific columns

如果應用程序必須支持不允許使用 FTS3 或 FTS4 資料表支持實體的 SDK 版本,仍然可以索引數據庫中的某些列以加快查詢速度。
要向實體添加索引,請在 @Entity 註釋中包含 indices 屬性,列出要包含在索引或複合索引中的列的名稱。如下所示

@Entity(indices = {@Index("name"),
        @Index(value = {"last_name", "address"})})
public class User {
    @PrimaryKey
    public int id;
    public String firstName;
    public String address;
    @ColumnInfo(name = "last_name")
    public String lastName;
    @Ignore
    Bitmap picture;
}

有時,資料庫中的某些屬性或屬性組必須是唯一的。
可以通過將 @Index 註釋的 unique 屬性設置為 true 來強制實施此唯一性屬性。
以下代碼示例可防止表具有兩行,這些行包含 firstName 和 lastName 列的相同值集:

@Entity(indices = {@Index(value = {"first_name", "last_name"},
        unique = true)})
public class User {
    @PrimaryKey
    public int id;
    @ColumnInfo(name = "first_name")
    public String firstName;
    @ColumnInfo(name = "last_name")
    public String lastName;
    @Ignore
    Bitmap picture;
}

 

Include AutoValue-based objects

注意:
這個特性只用於 java-based entities,不支援 Kotlin。
在 Room 2.1.0 及更高版本中,可以使用基於 Java 的不可變值類(使用 @AutoValue 進行註釋)作為數據庫中的實體。
如果實體的兩個實例的列包含相同的值,則此支持特別有用。
使用帶 @AutoValue 註釋的類別作為實體時,可以使用 @PrimaryKey,@ColumnInfo,@ Embedded 和 @Relation 註釋類別的抽象方法。但是,在使用這些註釋時,每次都必須包含 @CopyAnnotations 註釋,以便 Room 可以正確解釋方法的自動生成實現。
以下代碼段顯示了一個使用 @AutoValue 註釋類別的示例,Room 將其識別為實體:

@AutoValue
@Entity
public abstract class User {
    // Supported annotations must include `@CopyAnnotations`.
    @CopyAnnotations
    @PrimaryKey
    public abstract long getId();
    public abstract String getFirstName();
    public abstract String getLastName();
    // Room uses this factory method to create User objects.
    public static User create(long id, String firstName, String lastName) {
        return new AutoValue_User(id, firstName, lastName);
    }
}

 

Define relationships between objects

由於 SQLite 是關係數據庫,因此可以指定物件之間的關係。儘管大多數對象關係映射庫允許實體對象相互引用,但 Room 明確禁止這樣做。即使不能使用直接關係,Room 仍允許在實體之間定義外鍵約束。
例如,如果有另一個名為 Book 的實體,可以使用 @ForeignKey 註釋定義其與 User 實體的關係,如以下代碼段所示:

@Entity(foreignKeys = @ForeignKey(entity = User.class,
                                  parentColumns = "id",
                                  childColumns = "user_id"))
public class Book {
    @PrimaryKey
    public int bookId;
    public String title;
    @ColumnInfo(name = "user_id")
    public int userId;
}

外鍵非常強大,因為它們允許指定更新引用實體時發生的情況。
例如,如果通過在 @ForeignKey 註釋中包含 onDelete = CASCADE 來刪除相應的 User 實例,則可以告訴 SQLite 刪除用戶的所有書籍。
注意:
SQLite 將 @Insert(onConflict = REPLACE)作為一組 REMOVE 和 REPLACE 操作處理,而不是單個 UPDATE 操作。這種替換衝突值的方法可能會影響外鍵約束。
 

Create nested objects

有時候希望將實體或 POJO 表達為資料庫邏輯中的一個整體,即使該物件包含多個屬性。
在這些情況下,可以使用 @Embedded 註釋來表示要分解到資料表內子字屬性的物件。然後,可以像查找其他單個列一樣查詢嵌入屬性(Embedded filed)。
例如,User 類別可以包含 Address 類別的屬性,而 Address 也有其屬性為 street,city,state 和 postCode 屬性。
User.java

@Entity
public class User {
    @PrimaryKey
    public int id;
    public String firstName;
    @Embedded
    public Address address;
}

Address.java

public class Address {
    public String street;
    public String state;
    public String city;
    @ColumnInfo(name = "post_code")
    public int postCode;
}

然後,表示 User 物件的資料表包含具有以下名稱的列:id,firstName,street,state,city 和 post_code。
注意:
嵌入屬性(Embedded filed)可以再包含其它嵌入屬性(Embedded filed)
如果實體具有多個相同類型的嵌入屬性,則可以通過設置 prefix 屬性使每個列保持唯一。然後,Room 將提供的值添加到嵌入屬性中每個列名稱的開頭。

分類
Android Uncategorized

在本地端資料庫儲存資料 (Room)

在 Room 中有 3 個主要的元件

1.Database:

包含資料庫持有者(database holder),並作為應用程序的持久化數據基礎的主要訪問點。
使用 @Database 註釋的類別需要滿足以下條件:

  • 是一個抽象類別並繼承 RoomDatabase 類別
  • 在註釋加入與資料庫關聯的實體列表
  • 包含一個無參數的抽象方法,並返回使用 @Dao 註釋的類別

在程式運行時,可以通過呼叫 Room.databaseBuilder() 或 Room.inMemoryDatabaseBuilder()來獲取 Database 實體。

2.Entity:

表示為資料庫中的資料表。

3.DAO:

包含用於訪問資料庫的方法。
應用程序使用 Room 資料庫來獲取與該資料庫關聯的數據訪問對像(DAO)。接著,應用程序使用每個 DAO 從資料庫中獲取 Entity,並將對這些 Entity 的任何更改保存回資料庫。最後,應用程序使用 Entity 來獲取和設置與資料庫的表列互相對應的值。
如下圖所示

以下的範例為包含一個 entity 和一個 dao 的資料庫
User.java

@Entity
public class User {
    @PrimaryKey
    public int uid;
    @ColumnInfo(name = "first_name")
    public String firstName;
    @ColumnInfo(name = "last_name")
    public String lastName;
}

UserDao.java

@Dao
public interface UserDao {
    @Query("SELECT * FROM user")
    List<User> getAll();
    @Query("SELECT * FROM user WHERE uid IN (:userIds)")
    List<User> loadAllByIds(int[] userIds);
    @Query("SELECT * FROM user WHERE first_name LIKE :first AND " +
           "last_name LIKE :last LIMIT 1")
    User findByName(String first, String last);
    @Insert
    void insertAll(User... users);
    @Delete
    void delete(User user);
}

AppDatabase.java

@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
    public abstract UserDao userDao();
}

注意:
AppDatabase 即為上方第 1 點提到的 Database 並符合其 3 個條件,它是一個抽象類別並繼承 RoomDatabase,在註釋中加入使用的 Entity,提供無參數並返回 Dao 的抽象方法。
 
下面是建立並取得 database 的實體

AppDatabase db = Room.databaseBuilder(getApplicationContext(),
        AppDatabase.class, "database-name").build();

 
注意:
如果應用程序在單進程(single process)中執行,則在實例化 AppDatabase 物件時應該套用 Singleton Pattern。因為建立 RoomDatabase 實例相當昂貴,很少需要在單進程中訪問多個實例。
如果應用程序在多進程中運行,請在數據庫構建器調用中包含 enableMultiInstanceInvalidation(),當在每個進程中都有一個 AppDatabase 實例時,可在一個進程中使共享數據庫文件無效,並且讓此無效自動傳播到其他進程中的 AppDatabase 實例。