「架構元件」目的在於提供應用程式架構的相關指引,並提供程式庫,說明生命週期管理和資料持續性等常見工作。架構元件可協助您建立應用程式架構,讓應用程式具備穩固、測試及維護,能夠減少樣板程式碼。架構元件程式庫是 Android Jetpack 的一部分。
這是程式碼研究室的 Kotlin 版本。如需 Java 程式語言的版本,請按這裡。
使用本程式碼研究室時,如果遇到任何問題 (程式碼錯誤、文法錯誤、措辭不明確等),請透過程式碼研究室左下角的 [回報錯誤] 連結回報問題。
事前準備
您必須熟悉 Kotlin、物件導向設計概念和 Android 開發基礎知識,尤其是:
RecyclerView
和轉接器- SQLite 資料庫和 SQLite 查詢語言
- 基本協同程式 (如果您不熟悉協同程式,可以參閱在 Android 應用程式中使用 Kotlin 協同程式)。
而且,也有助於瞭解軟體架構模式與使用者介面不同的資料,例如 MVP 或 MVC。本程式碼研究室將實作應用程式架構指南中定義的架構。
本程式碼研究室著重於 Android 架構元件。我們提供離題和程式碼,方便您直接複製並貼上。
如果您不熟悉 Kotlin,請參閱這裡的 Java 程式設計語言版本。
要執行的步驟
在本程式碼研究室中,您將瞭解如何使用架構元件會議室、ViewModel 和 LiveData 設計及建構應用程式,以及如何建構會執行下列作業的應用程式:
- 使用 Android 架構元件實作建議的架構。
- 使用資料庫取得及儲存資料,並在資料庫中預先填入一些字詞。
- 顯示
MainActivity
中RecyclerView
中的所有字詞。 - 使用者輕觸 + 按鈕時,系統會開啟第二個活動。使用者輸入字詞時,將字詞加入資料庫和清單。
這款應用程式十分簡單,但功能相當複雜,你可以將其當做範本建構。以下是預覽畫面:
軟硬體需求
- Android Studio 3.0 以上版本及其使用方法。確認 Android Studio 已更新,以及您的 SDK 和 Gradle。
- Android 裝置或模擬器。
這個程式碼研究室會提供建構完整應用程式的一切必要程式碼。
使用架構元件及實作建議的架構有許多步驟。最重要的一件事就是建立寫作模型,瞭解這些元件的結合方式以及資料流動方式。編輯這個程式碼研究室時,請不要直接複製及貼上程式碼,而是開始著手培養對內心的瞭解。
建議的架構元件有哪些?
為了介紹這項術語,以下將簡單介紹一下「架構元件」及其共同的運作方式。請注意,本程式碼研究室著重在部分元件,也就是 LiveData、ViewModel 和 Room。每個元件在您使用時都會更清楚說明。
這個架構的基本架構如下:
SQLite 資料庫:裝置上的儲存空間。「會議室持續性程式庫」會為您建立及維護這個資料庫。
DAO:資料存取物件。SQL 查詢和函式的對應。使用 DAO 時,您須呼叫方法,其餘的任務則由 Room 處理。
會議室資料庫:簡化資料庫工作,並做為基礎 SQLite 資料庫的存取點 (隱藏 SQLiteOpenHelper)
)。Room 資料庫使用 DAO 對 SQLite 資料庫發出查詢。
存放區:您建立的類別,主要用於管理多個資料來源。
ViewModel:做為存放區 (資料) 和 UI 之間的通訊中心。並不需要擔心資料的來源。ViewModel 執行個體在「活動」/「片段」娛樂中存活。
LiveData:可以觀察的帳戶持有人類別。一律保留/快取最新版本的資料,並在資料變更時通知觀察器。「LiveData
」可辨識生命週期。UI 元件只會觀察相關資料,而且不會停止或繼續觀測。LiveData 會觀察相關的生命週期狀態變化,因此在處理過程中自動管理上述所有事項。
RoomWordSample 架構總覽
下圖顯示應用程式的所有部分。每個封閉框 (SQLite 資料庫除外) 代表您要建立的類別。
- 開啟 Android Studio,然後按一下 [Start a new Android Studio project] (建立新的 Android Studio 專案)。
- 在「建立新專案」視窗中,選擇 [空白活動],然後按一下 [下一步]。
- 在下一個畫面中,輸入名為 RoomWordSample 的應用程式,然後按一下 [完成]。
接下來,您必須將元件程式庫加入 Gradle 檔案。
- 在 Android Studio 中,按一下 [Projects] (專案) 標籤並展開 Gradle Scripts 資料夾。
開啟 build.gradle
(模組:應用程式)。
- 套用
kapt
註解處理工具 Kotlin 外掛程式,將這個檔案新增至build.gradle
(模組:應用程式) 檔案頂端的其他外掛程式之後。
apply plugin: 'kotlin-kapt'
- 在
android
區塊中加上packagingOptions
區塊,即可將不可分割函式模組從套件中排除,以免造成警告。
android {
// other configuration (buildTypes, defaultConfig, etc.)
packagingOptions {
exclude 'META-INF/atomicfu.kotlin_module'
}
}
- 在
dependencies
區塊的結尾處加入下列程式碼。
// Room components
implementation "androidx.room:room-runtime:$rootProject.roomVersion"
kapt "androidx.room:room-compiler:$rootProject.roomVersion"
androidTestImplementation "androidx.room:room-testing:$rootProject.roomVersion"
// Lifecycle components
implementation "androidx.lifecycle:lifecycle-extensions:$rootProject.archLifecycleVersion"
kapt "androidx.lifecycle:lifecycle-compiler:$rootProject.archLifecycleVersion"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.archLifecycleVersion"
// Kotlin components
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$rootProject.coroutines"
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$rootProject.coroutines"
// Material design
implementation "com.google.android.material:material:$rootProject.materialVersion"
// Testing
testImplementation 'junit:junit:4.12'
androidTestImplementation "androidx.arch.core:core-testing:$rootProject.coreTestingVersion"
- 在您的
build.gradle
(Project: RoomWordsSample) 檔案中,將程式碼的版本號碼加到檔案結尾處,如下所示。
ext {
roomVersion = '2.2.5'
archLifecycleVersion = '2.2.0'
coreTestingVersion = '2.1.0'
materialVersion = '1.1.0'
coroutines = '1.3.4'
}
這個應用程式的資料為文字,您必須建立簡單的表格才能存放這些值:
聊天室可讓您透過實體建立資料表。現在,讓我們開始吧。
- 建立名為
Word
的新 Kotlin 類別檔案,其中包含Word
資料類別。
本課程將說明您實體的「實體」(代表 SQLite 表格)。類別中的每個屬性都代表表格中的一個資料欄。會議室最終會使用這些屬性來建立資料表,並將資料庫中的物件執行個體化。
程式碼如下:
data class Word(val word: String)
如要讓 Word
類別對 Room 資料庫具有意義,您必須加上註解。註解可識別這個類別的各個部分與資料庫項目之間的關係。聊天室會使用這項資訊產生驗證碼。
如果您是自行輸入註解 (而不是貼上),Android Studio 會自動匯入註解類別。
- 按照以下程式碼所示的註解來更新
Word
類別:
@Entity(tableName = "word_table")
class Word(@PrimaryKey @ColumnInfo(name = "word") val word: String)
讓我們看看這些註解的作用:
@Entity(tableName =
"word_table"
)
每個@Entity
類別都代表一個 SQLite 資料表。為類別宣告加上註解,表示這是實體。如果您希望表格名稱與類別的名稱不同,可以指定資料表名稱。這會把表格命名為「word_table」。@PrimaryKey
每個實體都需要一個主鍵。為了讓每個字簡潔,每個字詞都是獨立的主鍵。@ColumnInfo(name =
"word"
)
如果資料欄的名稱與成員變數名稱不同,請指定表格中的資料欄名稱。這會將這個資料欄命名為「wordt」。- 儲存在資料庫中的所有資源都必須具有公開顯示設定,這是 Kotlin 預設值。
如需註解的完整清單,請參閱會議室套件摘要參考資料。
什麼是 DAO?
在 DAO (資料存取物件) 中,您可以指定 SQL 查詢,並將這些查詢與方法呼叫建立關聯。編譯器會檢查 SQL,並從便利註解產生常用查詢的查詢 (如 @Insert
)。Room 會使用 DAO 為您的程式碼建立簡潔的 API。
DAO 必須是介面或抽象類別。
根據預設,所有查詢都必須在個別執行緒上執行。
會議室提供協同程式支援,可讓您使用 suspend
修飾符來為您的查詢加上註解,然後再透過協同程式或其他暫停函式呼叫。
實作 DAO
我們編寫 DAO 來提供下列查詢:
- 按字母順序排列所有字詞
- 插入字詞
- 刪除所有字詞
- 建立名為
WordDao
的新 Kotlin 類別檔案。 - 複製下列程式碼並貼到
WordDao
中,然後視需要修正匯入,以便進行編譯。
@Dao
interface WordDao {
@Query("SELECT * from word_table ORDER BY word ASC")
fun getAlphabetizedWords(): List<Word>
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(word: Word)
@Query("DELETE FROM word_table")
suspend fun deleteAll()
}
我們來逐步說明一下:
WordDao
是介面;DAO 必須是介面或抽象類別。@Dao
註解識別為 Room 的 DAO 類別。suspend fun insert(word: Word)
:宣告暫停函式來插入一個字詞。@Insert
註解是一種特殊的 DAO 方法註解,您不需要提供任何 SQL!(另外還有@Delete
和@Update
個註解可用來刪除及更新列,但您並未在此應用程式中使用這些註解)。onConflict = OnConflictStrategy.IGNORE
:如果所選「衝突」策略中的字詞與清單中現有的字詞完全相同,系統會忽略該字詞。如要進一步瞭解可用的衝突策略,請參閱說明文件。suspend fun deleteAll()
:宣告暫停函式,以刪除所有字詞。- 您沒有刪除多個實體的便利註解,因此使用一般
@Query
註解。 @Query
("DELETE FROM word_table")
:@Query
需要在註解中提供 SQL 查詢字串字串,以便執行複雜的讀取查詢和其他作業。fun getAlphabetizedWords(): List<Word>
:取得所有字詞,並傳回Words
的List
的方法。@Query(
"SELECT * from word_table ORDER BY word ASC"
)
:此查詢會傳回按遞增順序排序的字詞清單。
資料變更時,您通常會想要採取某些行動,例如在使用者介面中顯示更新後的資料。也就是說,您必須觀察資料,才能進行修改。
視資料儲存的方式而定,這可能並不容易。觀察應用程式中多個元件的資料變化時,可在元件之間建立明確且嚴格的相依性路徑。這會導致測試和偵錯等問題變得難以解決。
LiveData
是資料觀察的生命週期程式庫類別,能解決這個問題。使用方法說明中的 LiveData
類型傳回值時,Room 會在資料庫更新時產生所有必要的程式碼,以更新 LiveData
。
在 WordDao
中,變更 getAlphabetizedWords()
方法簽名,以便傳回的 List<Word>
納入 LiveData
。
@Query("SELECT * from word_table ORDER BY word ASC")
fun getAlphabetizedWords(): LiveData<List<Word>>
稍後在這個程式碼研究室中,您將透過 MainActivity
中的 Observer
追蹤資料變更。
什麼是聊天室資料庫?
- 聊天室是 SQLite 資料庫頂端的資料庫層。
- 聊天室會處理您透過
SQLiteOpenHelper
處理的日常工作。 - Room 會使用 DAO 對資料庫提出查詢。
- 根據預設,為避免使用者介面效能不佳,「聊天室」不允許在主執行緒上發出查詢。如果聊天室查詢傳回
LiveData
,查詢就會自動以非同步方式在背景執行緒上執行。 - Room 提供 SQLite 陳述式的編譯時間檢查。
實作 Room 資料庫
您的 Room 資料庫類別必須進行抽象並擴充 RoomDatabase
。一般來說,整個應用程式只需要一個「Room」資料庫執行個體。
現在我們就開始製作吧!
- 建立名為
WordRoomDatabase
的 Kotlin 類別檔案,然後將下列程式碼加入檔案:
// Annotates class to be a Room Database with a table (entity) of the Word class
@Database(entities = arrayOf(Word::class), version = 1, exportSchema = false)
public abstract class WordRoomDatabase : RoomDatabase() {
abstract fun wordDao(): WordDao
companion object {
// Singleton prevents multiple instances of database opening at the
// same time.
@Volatile
private var INSTANCE: WordRoomDatabase? = null
fun getDatabase(context: Context): WordRoomDatabase {
val tempInstance = INSTANCE
if (tempInstance != null) {
return tempInstance
}
synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
WordRoomDatabase::class.java,
"word_database"
).build()
INSTANCE = instance
return instance
}
}
}
}
我們來逐步瀏覽程式碼:
- 會議室的資料庫類別須為
abstract
並擴充RoomDatabase
- 您可以使用
@Database
將類別註解為 Room 資料庫,並使用註解參數宣告資料庫內的實體,並設定版本號碼。每個實體都會對應至會在資料庫中建立的資料表。資料庫遷移作業不在這個程式碼研究室的範圍內,因此將exportSchema
設為 false 以避免發生建構警告。在實際應用程式中,建議您為 Room 設定一個目錄,以便用來匯出架構,以便檢查目前的架構到版本控制系統中。 - 資料庫透過抽象「getter」方法公開 DAO,每個 @Dao 方法都會有相同的方法。
- 我們定義了 singleton (
WordRoomDatabase,
),以避免同時開啟多個資料庫執行個體。 getDatabase
會傳回單調。
什麼是存放區?
存放區類別可抽象存取多個資料來源。該存放區不屬於架構元件程式庫,但建議您選用程式碼區隔和架構的最佳做法。存放區類別提供簡潔的 API,可用來存取應用程式其餘部分的資料。
為什麼要使用存放區?
存放區可用來管理查詢,並讓您使用多個後端。在最常見的例子中,存放區會實作邏輯來決定要從網路擷取資料,還是使用本機資料庫中快取的結果。
實作存放區
建立名為 WordRepository
的 Kotlin 類別檔案,並將下列程式碼貼到該檔案中:
// Declares the DAO as a private property in the constructor. Pass in the DAO
// instead of the whole database, because you only need access to the DAO
class WordRepository(private val wordDao: WordDao) {
// Room executes all queries on a separate thread.
// Observed LiveData will notify the observer when the data has changed.
val allWords: LiveData<List<Word>> = wordDao.getAlphabetizedWords()
suspend fun insert(word: Word) {
wordDao.insert(word)
}
}
重點如下:
- DAO 會傳送至存放區建構函式,而不是整個資料庫。這是因為該 DAO 包含資料庫的所有讀取/寫入方法,因此只需要存取 DAO。完全不需將整個資料庫公開至存放區。
- 字詞清單是公開的屬性。其是透過初始化的
LiveData
字詞清單進行初始化;這是因為我們定義了在「LiveData 類別」步驟中傳回LiveData
方法以傳回LiveData
的方法。聊天室會分別在另一個執行緒上執行所有查詢。然後,觀察到LiveData
會在資料變更時通知主執行緒。 suspend
修飾符會通知編譯器,其需由協同程式或其他懸置函式呼叫。
什麼是 ViewModel?
ViewModel
的職責是將資料提供給使用者介面,並繼續沿用設定變更。ViewModel
是存放區和 UI 之間的通訊中心。您也可以使用 ViewModel
在片段之間共用資料。ViewModel 屬於生命週期程式庫的一部分。
如需這個主題的入門指南,請參閱 ViewModel Overview
或 ViewModels:簡易範例網誌文章。
為什麼要使用 ViewModel?
ViewModel
會沿用應用程式的 UI 資料,並採用可維持生命週期的永續做法。將應用程式的 UI 資料與 Activity
和 Fragment
類別區隔,您就能更妥善地遵守單一責任準則:您的活動和片段應負責將畫面繪製到畫面中,ViewModel
可負責處理及處理該 UI 所需的所有資料。
在 ViewModel
中,使用 LiveData
針對使用者介面將使用或顯示可變更的資料。使用 LiveData
有幾個好處:
- 您可以針對資料建立觀察項目 (而非輪詢變更),而且只有在資料實際變更時,
才更新使用者介面。 - 存放區與使用者介面完全由
ViewModel
區隔。 - 沒有任何來自
ViewModel
的資料庫呼叫 (全部在存放區中處理),因此可以測試更多程式碼。
viewModelScope
在 Kotlin 中,所有協同程式均會在 CoroutineScope
內執行。範圍可控制協同程式的生命週期。當您取消特定範圍的工作時,系統會取消在該範圍內啟動的所有協同程式。
AndroidX lifecycle-viewmodel-ktx
程式庫將 viewModelScope
新增為 ViewModel
類別的擴充功能函式,讓您可以處理範圍。
如要進一步瞭解如何在 ViewModel 中使用協同程式,請參閱在 Android 應用程式中使用 Kotlin Coroutine 程式碼研究室的步驟 5,或是參閱在 Android 中輕鬆使用 Coroutouts:viewModelScope 網誌文章一文。
導入 ViewModel
為 WordViewModel
建立 Kotlin 類別檔案並新增下列程式碼:
class WordViewModel(application: Application) : AndroidViewModel(application) {
private val repository: WordRepository
// Using LiveData and caching what getAlphabetizedWords returns has several benefits:
// - We can put an observer on the data (instead of polling for changes) and only update the
// the UI when the data actually changes.
// - Repository is completely separated from the UI through the ViewModel.
val allWords: LiveData<List<Word>>
init {
val wordsDao = WordRoomDatabase.getDatabase(application).wordDao()
repository = WordRepository(wordsDao)
allWords = repository.allWords
}
/**
* Launching a new coroutine to insert the data in a non-blocking way
*/
fun insert(word: Word) = viewModelScope.launch(Dispatchers.IO) {
repository.insert(word)
}
}
以下是我們:
- 建立名為
WordViewModel
的類別,該類別將Application
做為參數並延伸AndroidViewModel
。 - 已新增私人成員變數,用來存放存放區的參照。
- 新增公開的
LiveData
成員變數來快取字詞清單。 - 建立了
init
區塊,從WordRoomDatabase
取得WordDao
的參照。 - 在
init
區塊中,根據WordRoomDatabase
建構WordRepository
。 - 在
init
區塊中,使用存放區初始化allWords
LiveData。 - 建立會呼叫 Repository 的
insert()
方法的包裝函式insert()
方法。如此一來,使用者介面會納入insert()
的實作。我們不想插入插入阻斷主執行緒,所以我們正在發起一個新的協同程式,然後呼叫一個存儲備插入片段,它是一個懸浮函數。如前文所述,ViewModels 有使用之前稱為viewModelScope
的生命週期範圍,我們在這裡使用的是這個類別。
接下來,您必須為清單和項目新增 XML 版面配置。
本程式碼研究室假設您已熟悉在 XML 中建立版面配置,因此我們僅提供程式碼給您。
將 AppTheme
的家長設為 Theme.MaterialComponents.Light.DarkActionBar
,藉此製作應用程式主題內容。為「values/styles.xml
」中的清單項目新增樣式:
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.MaterialComponents.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<!-- The default font for RecyclerView items is too small.
The margin is a simple delimiter between the words. -->
<style name="word_title">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_marginBottom">8dp</item>
<item name="android:paddingLeft">8dp</item>
<item name="android:background">@android:color/holo_orange_light</item>
<item name="android:textAppearance">@android:style/TextAppearance.Large</item>
</style>
</resources>
新增 layout/recyclerview_item.xml
版面配置:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/textView"
style="@style/word_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/holo_orange_light" />
</LinearLayout>
在 layout/activity_main.xml
中,將 TextView
替換為 RecyclerView
,然後新增浮動動作按鈕 (FAB)。現在您的版面配置應該如下所示:
<androidx.constraintlayout.widget.ConstraintLayout
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"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="0dp"
android:layout_height="0dp"
tools:listitem="@layout/recyclerview_item"
android:padding="@dimen/big_padding"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="@string/add_word"/>
</androidx.constraintlayout.widget.ConstraintLayout>
您的 FAB 外貌似乎應該要配合可用的操作,所以我們將把它換成 &&33;+' 符號。
首先,我們必須新增 Vector 資產:
- 選取 [File > New > Vector Asset]。
- 按一下 [剪輯藝術品:] 欄位中的 Android 機器人圖示。
- 搜尋「add」,然後選取 '+' 資產。按一下 [確定]。
- 完成後,點選 [下一步]。
- 將圖示路徑確認為
main > drawable
,然後按一下 [完成] 即可新增資產。 - 在
layout/activity_main.xml
中更新 FAB,加入新的可繪項目:
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="@string/add_word"
android:src="@drawable/ic_add_black_24dp"/>
您將在 RecyclerView
中顯示資料,比將資料丟入 TextView
來得簡單。本程式碼研究室假設您瞭解 RecyclerView
、RecyclerView.LayoutManager
、RecyclerView.ViewHolder
和 RecyclerView.Adapter
如何運作。
請注意,轉接程式中的 words
變數可以快取資料。在下一項工作中,您會加入自動更新資料的程式碼。
為延伸至 RecyclerView.Adapter
的 WordListAdapter
建立 Kotlin 類別檔案。程式碼如下:
class WordListAdapter internal constructor(
context: Context
) : RecyclerView.Adapter<WordListAdapter.WordViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var words = emptyList<Word>() // Cached copy of words
inner class WordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val wordItemView: TextView = itemView.findViewById(R.id.textView)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WordViewHolder {
val itemView = inflater.inflate(R.layout.recyclerview_item, parent, false)
return WordViewHolder(itemView)
}
override fun onBindViewHolder(holder: WordViewHolder, position: Int) {
val current = words[position]
holder.wordItemView.text = current.word
}
internal fun setWords(words: List<Word>) {
this.words = words
notifyDataSetChanged()
}
override fun getItemCount() = words.size
}
在 MainActivity
的 onCreate()
方法中新增 RecyclerView
。
在 setContentView
之後的 onCreate()
方法中:
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
val adapter = WordListAdapter(this)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
執行應用程式,確保一切運作正常。沒有項目,因為您尚未連結資料。
資料庫中沒有任何資料。新增資料的方法有兩種:在資料庫開啟時新增一些資料,然後新增 Activity
來加入字詞。
如要刪除所有內容並在每次啟動應用程式時填入資料庫,請建立 RoomDatabase.Callback
並覆寫 onOpen()
。由於您無法對 UI 執行緒執行 Room 資料庫作業,onOpen()
會在 IO 調度員中啟動協同程式。
如要啟動協同程式,請提供 CoroutineScope
。更新 WordRoomDatabase
類別的 getDatabase
方法,也可取得參數範圍做為參數:
fun getDatabase(
context: Context,
scope: CoroutineScope
): WordRoomDatabase {
...
}
更新 WordViewModel
的 init
區塊中的資料庫擷取初始化工具,以傳遞範圍:
val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao()
在 WordRoomDatabase
中,我們建立了 RoomDatabase.Callback()
的自訂實作,這也會取得 CoroutineScope
做為建構函式參數。接著,我們會覆寫 onOpen
方法以填入資料庫。
以下是在 WordRoomDatabase
類別「中」建立回呼的程式碼:
private class WordDatabaseCallback(
private val scope: CoroutineScope
) : RoomDatabase.Callback() {
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
INSTANCE?.let { database ->
scope.launch {
populateDatabase(database.wordDao())
}
}
}
suspend fun populateDatabase(wordDao: WordDao) {
// Delete all content here.
wordDao.deleteAll()
// Add sample words.
var word = Word("Hello")
wordDao.insert(word)
word = Word("World!")
wordDao.insert(word)
// TODO: Add your own words!
}
}
最後,在 Room.databaseBuilder()
上呼叫 .build()
之前,立即將回呼新增至資料庫建構序列:
.addCallback(WordDatabaseCallback(scope))
最終程式碼看起來應該像這樣:
@Database(entities = arrayOf(Word::class), version = 1, exportSchema = false)
abstract class WordRoomDatabase : RoomDatabase() {
abstract fun wordDao(): WordDao
private class WordDatabaseCallback(
private val scope: CoroutineScope
) : RoomDatabase.Callback() {
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
INSTANCE?.let { database ->
scope.launch {
var wordDao = database.wordDao()
// Delete all content here.
wordDao.deleteAll()
// Add sample words.
var word = Word("Hello")
wordDao.insert(word)
word = Word("World!")
wordDao.insert(word)
// TODO: Add your own words!
word = Word("TODO!")
wordDao.insert(word)
}
}
}
}
companion object {
@Volatile
private var INSTANCE: WordRoomDatabase? = null
fun getDatabase(
context: Context,
scope: CoroutineScope
): WordRoomDatabase {
// if the INSTANCE is not null, then return it,
// if it is, then create the database
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
WordRoomDatabase::class.java,
"word_database"
)
.addCallback(WordDatabaseCallback(scope))
.build()
INSTANCE = instance
// return instance
instance
}
}
}
}
在 values/strings.xml
中加入這些字串資源:
<string name="hint_word">Word...</string>
<string name="button_save">Save</string>
<string name="empty_not_saved">Word not saved because it is empty.</string>
在 value/colors.xml
中加入這個顏色資源:
<color name="buttonLabel">#FFFFFF</color>
建立新的維度資源檔案:
- 在「專案」視窗中按一下應用程式模組。
- 選取 [File > New > Android 資源檔案]。
- 在「限定詞」中選取 [維度]。
- 設定檔案名稱:dimens
在 values/dimens.xml
中新增這些維度資源:
<dimen name="small_padding">8dp</dimen>
<dimen name="big_padding">16dp</dimen>
使用空白活動範本Activity
建立新的空白 Android 裝置:
- 選取 [File > New > Activity > Empty Activity] (檔案 > > 活動 & > 空白活動)
- 針對活動名稱輸入
NewWordActivity
。 - 確認新的活動已新增至 Android 資訊清單中。
<activity android:name=".NewWordActivity"></activity>
在版面配置資料夾中更新 activity_new_word.xml
檔案,然後加入下列程式碼:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/edit_word"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="@dimen/min_height"
android:fontFamily="sans-serif-light"
android:hint="@string/hint_word"
android:inputType="textAutoComplete"
android:layout_margin="@dimen/big_padding"
android:textSize="18sp" />
<Button
android:id="@+id/button_save"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
android:text="@string/button_save"
android:layout_margin="@dimen/big_padding"
android:textColor="@color/buttonLabel" />
</LinearLayout>
更新活動程式碼:
class NewWordActivity : AppCompatActivity() {
private lateinit var editWordView: EditText
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_new_word)
editWordView = findViewById(R.id.edit_word)
val button = findViewById<Button>(R.id.button_save)
button.setOnClickListener {
val replyIntent = Intent()
if (TextUtils.isEmpty(editWordView.text)) {
setResult(Activity.RESULT_CANCELED, replyIntent)
} else {
val word = editWordView.text.toString()
replyIntent.putExtra(EXTRA_REPLY, word)
setResult(Activity.RESULT_OK, replyIntent)
}
finish()
}
}
companion object {
const val EXTRA_REPLY = "com.example.android.wordlistsql.REPLY"
}
}
最後一個步驟是儲存使用者輸入的新字詞,並在 RecyclerView
中顯示字詞資料庫的目前內容,藉此將使用者介面連結到資料庫。
如要顯示資料庫目前的內容,請新增觀察 ViewModel
中 LiveData
的觀測器。
只要資料有所變更,就會叫用 onChanged()
回呼,以呼叫轉接程式的 setWords()
方法更新轉接程式的快取資料並重新整理顯示的清單。
在 MainActivity
中,為 ViewModel
建立成員變數:
private lateinit var wordViewModel: WordViewModel
使用「ViewModelProvider
」將「ViewModel
」與您的「Activity
」建立關聯。
首次使用 Activity
時,ViewModelProviders
將會建立 ViewModel
。如果活動遭到刪除 (例如透過設定變更),ViewModel
也會繼續保留該活動。重新建立活動時,ViewModelProviders
會傳回現有的 ViewModel
。詳情請參閱 ViewModel
。
在 RecyclerView
代碼區塊下方的 onCreate()
中,從 ViewModelProvider
取得 ViewModel
:
wordViewModel = ViewModelProvider(this).get(WordViewModel::class.java)
此外,在 onCreate()
中,針對 WordViewModel
中的所有 AdWords 資源 LiveData
新增觀察器。
onChanged()
方法 (Lumda 的預設方法) 會在觀察到的資料變更且活動於前景運作時啟動:
wordViewModel.allWords.observe(this, Observer { words ->
// Update the cached copy of the words in the adapter.
words?.let { adapter.setWords(it) }
})
我們想在輕觸 FAB 時開啟 NewWordActivity
,並在系統回到 MainActivity
時,在資料庫中插入新字詞或顯示 Toast
。為了達成這個目標,讓我們先定義要求代碼:
private val newWordActivityRequestCode = 1
在 MainActivity
中,新增 NewWordActivity
的 onActivityResult()
代碼。
如果活動傳回 RESULT_OK
,請呼叫 WordViewModel
的 insert()
方法,將傳回的字詞插入資料庫:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == newWordActivityRequestCode && resultCode == Activity.RESULT_OK) {
data?.getStringExtra(NewWordActivity.EXTRA_REPLY)?.let {
val word = Word(it)
wordViewModel.insert(word)
}
} else {
Toast.makeText(
applicationContext,
R.string.empty_not_saved,
Toast.LENGTH_LONG).show()
}
}
在使用者輕觸 FAB 時,在MainActivity,
啟動NewWordActivity
後啟動。在 MainActivity
onCreate
中找出 FAB,並加入以下代碼的 onClickListener
:
val fab = findViewById<FloatingActionButton>(R.id.fab)
fab.setOnClickListener {
val intent = Intent(this@MainActivity, NewWordActivity::class.java)
startActivityForResult(intent, newWordActivityRequestCode)
}
完成的程式碼應如下所示:
class MainActivity : AppCompatActivity() {
private const val newWordActivityRequestCode = 1
private lateinit var wordViewModel: WordViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
val adapter = WordListAdapter(this)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
wordViewModel = ViewModelProvider(this).get(WordViewModel::class.java)
wordViewModel.allWords.observe(this, Observer { words ->
// Update the cached copy of the words in the adapter.
words?.let { adapter.setWords(it) }
})
val fab = findViewById<FloatingActionButton>(R.id.fab)
fab.setOnClickListener {
val intent = Intent(this@MainActivity, NewWordActivity::class.java)
startActivityForResult(intent, newWordActivityRequestCode)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == newWordActivityRequestCode && resultCode == Activity.RESULT_OK) {
data?.getStringExtra(NewWordActivity.EXTRA_REPLY)?.let {
val word = Word(it)
wordViewModel.insert(word)
}
} else {
Toast.makeText(
applicationContext,
R.string.empty_not_saved,
Toast.LENGTH_LONG).show()
}
}
}
現在執行您的應用程式吧!當您在 NewWordActivity
的資料庫中新增字詞後,使用者介面會自動更新。
現在您已有運作中的應用程式,接著來回顧一下您建構的應用程式。以下是應用程式結構:
應用程式的元件如下:
MainActivity
:使用RecyclerView
與WordListAdapter
顯示清單中的字詞。MainActivity
中有一個Observer
會觀察資料庫中的 LiveData 字詞,並在變更時通知您。NewWordActivity:
會在清單中新增一個字詞。WordViewModel
:提供存取資料層的方法,並傳回 LiveData,以便 MainActivity 設定觀察者關係。*LiveData<List<Word>>
:可在使用者介面元件中自動更新。在MainActivity
中,有Observer
會從資料庫觀察 LiveData 文字,並在變更時收到通知。Repository:
會管理一或多個資料來源。Repository
會公開讓 ModelModel 與基礎資料供應商互動的方法。在這個應用程式中,該後端是 Room 資料庫。Room
:是包圍且實作 SQLite 資料庫的包裝函式。會議室需要您花費許多心力,- DAO:將方法呼叫對應至資料庫查詢,這樣當存放區呼叫
getAlphabetizedWords()
等方法時,Room 就可以執行SELECT * from word_table ORDER BY word ASC
。 Word
:是包含單一字詞的實體類別。
* Views
和 Activities
(和 Fragments
) 只會透過 ViewModel
與資料互動。因此,無論資料來源為何,都沒有影響。
資料自動更新自動更新介面 (回應式 UI)
由於我們使用 LiveData,所以自動更新可以實現。在 MainActivity
中,有 Observer
會從資料庫觀察 LiveData 文字,並在變更時收到通知。如有任何變更,觀察器的 onChange()
方法會執行,並在 WordListAdapter
中更新 mWords
。
由於資料是LiveData
,因此可以加以觀察。而觀察到的是 WordViewModel
a llWords
屬性傳回的 LiveData<List<Word>>
。
WordViewModel
會隱藏來自使用者介面圖層的所有後端。它提供存取資料層的方法,並傳回 LiveData
,以便 MainActivity
設定觀察器關係。Views
和 Activities
(和 Fragments
) 只會透過 ViewModel
與資料互動。因此,無論資料來源為何,都沒有影響。
在此案例中,資料來自 Repository
。「ViewModel
」不需要知道存放區的互動方式。其中只需知道如何與 Repository
互動,也就是透過 Repository
公開的方法。
存放區會管理一或多個資料來源。在 WordListSample
應用程式中,該後端是 Room 資料庫。Room 是一個環繞式且實作 SQLite 資料庫的包裝函式。會議室需要您花費許多心力,例如,Room 可執行您在 SQLiteOpenHelper
課程中執行的一切作業。
DAO 將方法呼叫傳送至資料庫查詢,這樣當存放區呼叫 getAllWords()
等方法時,Room 就可以執行 SELECT * from word_table ORDER BY word ASC
。
由於從查詢結果傳回的結果是LiveData
,因此每次變更會議室中的資料時,系統都會執行 Observer
介面的 onChanged()
方法,並更新使用者介面。
[選用] 下載解決方案程式碼
如果您尚未查看程式碼研究室的解決方案代碼,請先查看。您可以在 GitHub 存放區中查看程式碼,或是在這裡下載程式碼:
將下載的 ZIP 檔案解壓縮。這樣做會解壓縮根資料夾 android-room-with-a-view-kotlin
,其中包含完整應用程式。