Tina Tang's Blog

在哪裡跌倒了,就在哪裡躺下來

0%

Android筆記(44)-專案:Forage應用程式

建構一款名為「Forage」的 app。本程式碼研究室將引導您逐步完成 Forage app project,包括在 Android Studio 中設定和測試 project。

完成後的 Forage app 可讓使用者追蹤他們在自然界中搜尋到的物品,例如食物。通過使用 Room,可使這些資料在會話間持久保存(persisted)下來。運用您掌握的 Room 知識以及對 database 進行讀取、寫入、更新和刪除,以在 Forage app 中實現資料的持久保留(persisted)

建構項目
實作 entityDAOViewModeldatabase class,以便透過 Room 在 app 中加入持續性機制(persistence)


完成的應用程式總覽

完成後的 Forage app 可讓使用者追蹤他們在自然界中覓得的 items,例如食物(food)。在使用 Room 的工作階段之間會保留(persisted)此資料。您將運用所掌握的 Room 知識,以及對在 database 執行讀取、寫入、更新及刪除作業的瞭解,在 Forage app 中實現持續性(persistence)。下文說明完成後的 app 及其功能。

App 初次啟動時,使用者會看到空白畫面,其中包含顯示 foraged items 的 recycler view,還有右下角可用來新增 items 的 floating button。

新增 item 時,使用者可以指定名稱(name)找到項目的地點(location),並加上其他附註(notes)。你也可以有 checkbox,以指定 food item 是否屬於當季。

新增的 item 將顯示在第一個畫面的 recycler view 中。

輕觸某個 item 就會開啟詳細資料(detail)畫面,並顯示名稱(name)、地點(location)和附註(notes)。

浮動式按鈕(floating button)也會從加號變更為編輯 icon。輕觸此按鈕即可開啟畫面,以便編輯名稱(name)地點(location)附註(notes)當季(in season) checkbox。輕觸 delete button 則可從 database 中移除 item。

雖然系統已導入此 app 的 UI 部分,但您的任務是運用對 Room 的瞭解來實現持續性(persistence),讓 app 能夠讀取、寫入、更新及刪除 database 中的 items。


開始操作

下載專案程式碼

請注意,資料夾名稱是 android-basics-kotlin-forage-app。在 Android Studio 中開啟專案時,請選取這個資料夾。


設定專案以使用 Room

定義 Forageable entity

專案已有 Forageable class,可定義 app 的資料 (model.Forageable.kt)。此 class 有多個屬性:idnameaddressinSeasonnotes

1
2
3
4
5
6
7
data class Forageable(
val id: Long = 0,
val name: String,
val address: String,
val inSeason: Boolean,
val notes: String?
)

不過,若要使用此 class 儲存持續性資料,就需要將 class 轉換為 Room entity。

  1. 使用這個 table 名稱為 "forageable_database"@Entity 為 class 加上註解。
1
2
3
@Entity(tableName = "forageable_database")
data class Forageable(
)
  1. id 屬性設定為主鍵(primary key)。主鍵應由系統自動產生。
1
2
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
  1. inSeason 屬性的 column name 設定為 "in_season"
1
2
@ColumnInfo(name = "in_season")
val inSeason: Boolean,

實作 DAO

您將透過 view model 存取 database,而 ForageableDao (data.ForageableDao.kt) 可用來定義從 database 讀取及寫入 database 的方法,就如同您的猜像。由於 DAO 只是您定義的 interface,因此無需撰寫任何程式碼即可導入這些方法。您應該改用 Room 註解,視需要指定 SQL 查詢。

ForageableDao interface 中,您必須新增五個方法。

  1. getForageables() 方法會對 database 中的所有 row 傳回 Flow<List<Forageable>>
1
2
@Query("SELECT * FROM forageable_database")
fun getForageables(): Flow<List<Forageable>>
  1. getForageable(id: Long) 方法會傳回符合指定 idFlow<Forageable>
1
2
@Query("SELECT * FROM forageable_database WHERE id = :id")
fun getForageable(id: Long): Flow<Forageable>
  1. insert(forageable: Forageable) 方法會將新的 Forageable 插入至 database 中。
1
2
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(forageable: Forageable)
  1. update(forageable: Forageable) 方法會採用現有 Forageable 作為參數,並據此更新 row。
1
2
@Update
suspend fun update(forageable: Forageable)
  1. delete(forageable: Forageable) 方法會採用 Forageable 作為參數,然後從 database 中將其刪除。
1
2
@Delete
suspend fun delete(forageable: Forageable)

實作 view model

ForageableViewModel (ui.viewmodel.ForageableViewModel.kt) 已部分導入,但您必須新增可存取 DAO 方法的功能,才能實際讀取及寫入資料。請按照下列步驟導入 ForageableViewModel

  1. 傳遞 ForageableDao instance 時,應以 class constructor 中參數的形式傳遞。
1
2
3
4
// 將 ForageableDao object 做為參數傳遞至預設 constructor
class ForageableViewModel(
private val forageableDao: ForageableDao
): ViewModel() {
  1. 建立 LiveData<List<Forageable>> class 的變數,此變數會使用 DAO 取得完整的 Forageable entity list,並將結果轉換為 LiveData
1
val forageables: LiveData<List<Forageable>> = forageableDao.getForageables().asLiveData()
  1. 建立採用 id (Long type) 做為參數的方法,此方法會在 DAO 上呼叫 getForageable() 方法,並將結果轉換為 LiveData,藉此傳回 LiveData<Forageable>
1
2
3
fun retrieveForageable(id: Long): LiveData<Forageable> {
return forageableDao.getForageable(id).asLiveData()
}
  1. addForageable() 方法中,使用 viewModelScope 啟動 coroutine,並使用 DAO 將 Forageable instance 插入至 database 中。
1
2
3
viewModelScope.launch {
forageableDao.insert(forageable)
}
  1. updateForageable() 方法中,使用 DAO 更新 Forageable entity。
1
2
3
viewModelScope.launch(Dispatchers.IO) {
forageableDao.update(forageable)
}
  1. deleteForageable() 方法中,使用 DAO 更新 Forageable entity。
1
2
3
viewModelScope.launch(Dispatchers.IO) {
forageableDao.delete(forageable)
}
  1. 建立可透過 ForageableDao constructor 參數建立 ForageableViewModel instance 的 ViewModelFactory
1
2
3
4
5
6
7
8
9
10
11
12
class ForageableViewModelFactory(private val forageableDao: ForageableDao): ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
// 檢查 modelClass 是否和 ForageableViewModel class 相同
if (modelClass.isAssignableFrom(ForageableViewModel::class.java)) {

// return 一個 instance
return ForageableViewModel(forageableDao) as T
}
// throw exception
throw IllegalArgumentException("Unknown ViewModel class")
}
}

實作 Database class

ForageDatabase (data.ForageDatabase.kt) class 實際上是將 entitiesDAO 公開給 Room。按照說明導入 ForageDatabase class。

  1. Entities:Forageable
  2. Version:1
  3. exportSchema:false
1
2
@Database(entities = [Forageable::class], version = 1, exportSchema = false)
abstract class ForageDatabase : RoomDatabase()
  1. ForageDatabase class 中,加入能夠傳回 ForageableDao 的抽象函式(abstract function)
1
abstract fun forageableDao(): ForageableDao
  1. ForageDatabase class 中,用名為 INSTANCE 的 private 變數和傳回 ForageDatabase 單例模式(singleton)getDatabase() 函式定義 companion object
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 定義 companion object (可使用 class name 做為限定詞,建立或取得 database)
companion object {

private var INSTANCE: ForageDatabase? = null

// 使用 database builder 所需的 Context 參數定義 getDatabase() method
fun getDatabase(context: Context): ForageDatabase {
return INSTANCE ?: synchronized(this) { // synchronized區塊中,一次只能執行一個 thread
// 使用 database builder 取得 database instance
// 將 application context、database class 以及 database name item_database 傳遞給 database builder
val instance = Room.databaseBuilder(
context.applicationContext,
ForageDatabase::class.java,
"forageable_database")
.fallbackToDestructiveMigration() // 將遷移策略新增至 builder
.build()
INSTANCE = instance // 將 INSTANCE 設為剛才建立好的 instance
return instance
}
}
}
  1. BaseApplication class 中,建立使用延遲(lazy)初始化功能傳回 ForageDatabase instance 的 database 屬性。
1
2
3
4
5
class BaseApplication : Application() {

// TODO: provide a ForageDatabase value by lazy here
val database: ForageDatabase by lazy { ForageDatabase.getDatabase(this) }
}

保留及讀取 fragment 中的資料

設定 entityDAOview model,以及定義要在 Room 公開的 database class 後,您只需要修改 Fragments,即可存取 view model。您必須在三個檔案中進行變更,各檔案用於 app 中的各畫面。

Forageable list

Forageable list 畫面只需要兩個 item:

  • view model 的 reference。
  • 對完整 Forageable list 的存取權限。
    ui.ForageableListFragment.kt 中執行以下工作。
  1. class 中已有 viewModel 屬性。但不使用您在上一個步驟中定義的 Factory function。您必須先重構此宣告,才能使用 ForageableViewModelFactory
1
2
3
4
5
private val viewModel: ForageableViewModel by activityViewModels {
ForageableViewModelFactory(
(activity?.application as BaseApplication).database.foragableDao()
)
}
  1. 然後在 onViewCreated() 中,從 viewModel 觀察(observe) allForageables 屬性,並視需要在 adapter 上呼叫 submitList() 來填入 list。
1
2
3
4
5
viewModel.forageables.observe(this.viewLifecycleOwner) { forageables ->
forageables.let {
adapter.submitList(it)
}
}

Forageable detail 畫面

對於 ui/ForageableDetailFragment.kt 中的 detail list,您需要執行的動作幾乎完全相同。

  1. 轉換 viewModel 屬性以正確初始化 ForageableViewModelFactory
1
2
3
4
5
private val viewModel: ForageableViewModel by activityViewModels {
ForageableViewModelFactory(
(activity?.application as BaseApplication).database.forageableDao()
)
}
  1. onViewCreated() 中,呼叫 view model 上的 getForageable(),並傳入 id 以取得 Forageable entity。觀察(observe) LiveData 並將結果設定為 forageable 屬性,然後呼叫 bindForageable() 來更新 UI。
1
2
3
4
viewModel.getForageable(id).observe(this.viewLifecycleOwner) { selectedForageable ->
forageable = selectedForageable
bindForageable()
}

新增及編輯 Forageable 畫面

最後,您需要在 ui.AddForageableFragment.kt 中執行類似操作。請注意,此畫面也會負責更新及刪除 entity。不過,系統已從 view model 的正確位置呼叫這些方法。您只需在此檔案中進行兩項變更即可。

  1. 再次重構 viewModel 屬性以使用 ForageableViewModelFactory
1
2
3
4
5
private val viewModel: ForageableViewModel by activityViewModels {
ForageableViewModelFactory(
(activity?.application as BaseApplication).database.forageableDao()
)
}
  1. 在您調整 delete button 的顯示設定之前,請在 onViewCreated()if statement 區塊中,呼叫 view model 上的 getForageable(),並傳入 id,然後將結果設定為 forageable 屬性。
1
2
3
4
viewModel.getForageable(id).observe(this.viewLifecycleOwner) { selectedForageable ->
forageable = selectedForageable
bindForageable(forageable)
}

這就是您需要在 fragment 中執行的所有動作。您現在可以執行 app,藉此查看動作中的所有持續性(persistence)功能。


執行 App


執行自動化測試

執行結果: