本文篇幅會相對長些。
請耐心看完,必有收獲.
目的:
通過一個完整的 原理簡單 但 結構稍微復雜 的 例子,
深入了解 Android Room 與 架構組件的使用。
以后可以基于這個樣例做很多拓展。
完成這個Demo后,你會發現,整個架構體系思想和設計非常優美的!
層層封裝、接口隔離的思想,職責單一的設計原則!
樣例采用自底向上的構建方式:
(1) Room(SQL TABLE / DAO/RoomDatabase)
(2) 存儲庫Repository
(3) ViewModel/LiveData
(4) Activity
一. 基礎介紹
1. Android Room + 架構組件
<Android Room +架構組件 架構圖>:
各部分作用后續會逐一介紹.
在完成Demo后理解會更深刻,值得反復研究這個架構圖!
2. 詞典 樣例介紹
官網文檔:
https://developer.android.com/codelabs/android-room-with-a-view-kotlin#0
源碼GitHub 地址會在文章的最后附上.
注:本文基于Kotlin 如需Java版本,參考以下(建議學習Kotlin,趨勢):
https://developer.android.com/codelabs/android-room-with-a-view#0 (JAVA)
實現功能
(1). 詞典
(2). 使用RecyclerView 顯示
(3). 顯示所有詞
(4). 提供添加入口,保存在數據庫
<應用效果圖>:
<Demo架構圖>:
可以看出,Demo架構圖是根據<Android Room + 架構組件> 實現的,
每個方框代表要創建的一個類(SQLite除外)
二、樣例創建過程(關鍵步驟)
Android studio 版本: Atctic Fox | 2020.3.1
Gradle 插件版本: gradle-6.8.3
build tool插件:com.android.tools.build:gradle:4.2.1
kotlin 插件:org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21
1. 配置依賴
1.1 app/build.gradle
(1) 添加 kapt 注解處理器 并且 jvmTarget 設置為 1.8:
plugins {
//...
id 'kotlin-kapt'
}
android {
//...
kotlinOptions {
jvmTarget = '1.8'
}
}
(2)依賴項depenencies (其中version設置在project/build.gralde):
dependencies {
implementation "androidx.appcompat:appcompat:$rootProject.appCompatVersion"
implementation "androidx.activity:activity-ktx:$rootProject.activityVersion"
// Dependencies for working with Architecture components
// You'll probably have to update the version numbers in build.gradle (Project)
// Room components
implementation "androidx.room:room-ktx:$rootProject.roomVersion"
kapt "androidx.room:room-compiler:$rootProject.roomVersion"
androidTestImplementation "androidx.room:room-testing:$rootProject.roomVersion"
// Lifecycle components
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$rootProject.lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-common-java8:$rootProject.lifecycleVersion"
// 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"
// UI
implementation "androidx.constraintlayout:constraintlayout:$rootProject.constraintLayoutVersion"
implementation "com.google.android.material:material:$rootProject.materialVersion"
// Testing
testImplementation "junit:junit:$rootProject.junitVersion"
androidTestImplementation "androidx.arch.core:core-testing:$rootProject.coreTestingVersion"
androidTestImplementation ("androidx.test.espresso:espresso-core:$rootProject.espressoVersion", {
exclude group: 'com.android.support', module: 'support-annotations'
})
androidTestImplementation "androidx.test.ext:junit:$rootProject.androidxJunitVersion"
}
1.2 project/build.gradle 設置上面所需的版本號
ext {
activityVersion = '1.1.0'
appCompatVersion = '1.2.0'
constraintLayoutVersion = '2.0.2'
coreTestingVersion = '2.1.0'
coroutines = '1.3.9'
lifecycleVersion = '2.2.0'
materialVersion = '1.2.1'
roomVersion = '2.2.5'
// testing
junitVersion = '4.13.1'
espressoVersion = '3.1.0'
androidxJunitVersion = '1.1.2'
}
2. 創建實體(Entity)
數據庫中的表,每一項的數據即是 實體Entity
<數據表圖>
Room 允許通過實體創建表
2.1 創建單詞的 類 Word
data class Word(val word: String)
類中的 每個屬性 代表 表中的一列
Room 最終會使用這些屬性來創建表并將數據庫行中的對象實例化。
2.2 添加注解,類與數據庫(表) 建立聯系。
@Entity(tableName = "word_table")
class Word(@PrimaryKey @ColumnInfo(name = "word") val word: String)
數據庫則會根據這個信息自動生成代碼。
注解的作用:
(1) @Entity 表明是一張SQLite 表。 可以指定表名,與類名區分,如word_table
(2) @PrimaryKey 表示主鍵
(3) @ColumnInfo(name = "word") 表示列名為word
*Room 的詳細注解可參考: https://developer.android.com/reference/kotlin/androidx/room/package-summary.html
*使用注解聲明類: https://developer.android.com/training/data-storage/room/defining-data.html
3. 創建DAO (data access object)
3.1 DAO 重要概念
這個Object 很好的反應了Java 中一切皆對象的概念.
作用是將方法 和 SQL 查詢 關聯,方便在代碼中調用
編譯器會檢查SQL 語法 并且會 根據注解生成 非常便捷的 查方法, 例如 @Insert。
DAO 必須是接口 或者 抽象類。(因為要使用@Dao注解生成它的實現類!!)
一般情況,所有查詢都必須在分離的線程里執行。
Room 在Kotlin 協程里支持。因此,可以使用suspend注解并在協程里調用,或者在其它掛起函數里調用。
3.2 實現DAO 的功能:
(1) 根據字母順序獲取所有單詞
(2) 插入一個單詞
(3) 刪除所有單詞
3.3 實現DAO的步驟:
創建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()
}
注意:
(1)WordDao 是一個接口。 DAOs 必須是接口 或者 抽象類。
(2)@Dao 注解說明它是一個用于Room的DAO類
(3)suspend fun 表示是一個 掛起函數
(4)@Insert DAO特有的注解,不需要SQL 查詢表達式 (@Delete 刪除,@Update 更新行)
(5)onConflict = OnConflictStrategy.IGNORE 表示相同的單詞會忽略
(6)@Query 需要提供SQL 查詢表達式
使用DAOs 訪問數據參考:
https://developer.android.com/training/data-storage/room/accessing-data.html
4. 觀察數據庫的變化
當數據庫變化時,需要更新到UI.
這就要求監聽數據庫。
可以使用 Flow 異步序列 (kotlinx-coroutines庫)
因此,WordDao獲取所有單詞的方法,可以改成這樣:
@Query("SELECT * FROM word_table ORDER BY word ASC")
fun getAlphabetizedWords(): Flow<List<Word>>
在后面,我們會把Flow 轉換成LiveData,保存在ViewModel中。
*協程里的Flow 介紹可以參考:
https://kotlinlang.org/docs/reference/coroutines/flow.html
5. 增加一個Room 數據庫(RoomDatabase)
5.1 RoomDatabase 是什么?
(1) 在數據庫層中,是位于 SQLite 數據庫之上的。
(2) 負責和 SQLiteOpenHelper 一樣的單調乏味的任務
(3) 使用 DAO 去執行 查詢 它的數據庫
(4) 在后臺線程運行異步操作 (如查詢返回Flow)
(5) 提供 SQLite 語句的編譯時檢查
5.2 實現 Room 數據庫
必須是抽象類 并且 繼承自 RoomDatabase。
通常僅需要一個 Room 數據庫 實例。
創建 WordRoomDatabase 類, 代碼:
// 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 {
// 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"
).build()
INSTANCE = instance
// return instance
instance
}
}
}
}
代碼分析:
(1) Room 數據庫類必須是抽象類 并且 繼承自 RoomDatabase
(2) @Database 將該類注解為 Room 數據庫;
注解參數 聲明 實體 及 版本號;
每個實體 對應 一個 將在數據庫中創建的表;
exportSchema=false 表示不做遷移,實際上應該考慮.
(3) 通過抽象方法 公開 DAO (WordDao)
abstract fun wordDao(): WordDao
(4) 該類是單例, 通過 getDatabase 返回。
實例使用 建造者 模式 創建, 即 Room.databaseBuilder
數據庫名設置為 : word_database
后面根據需要,可以靈活地添加配置.
6. 創建存儲庫 (Repository)
6.1 什么是存儲庫?
它并非 架構組件庫 的一部分,但它是推薦為 代碼分離和架構采用的最佳做法。
存儲庫類會將多個數據源(DAO或者網絡數據)的訪問權限 抽象化。
存儲庫類會提供一個整潔的 API,用于獲取對應用其余部分的數據訪問權限。
6.2 為什么使用存儲庫?
存儲庫可 管理查詢,且允許使用多個后端。
(參考架構圖,處理ViewModel 與 RoomDatabase 之間,
封裝來自與DAO/網絡的數據,只需要和DAO交互,不必知道具體的數據庫 )
6.3 實現存儲庫
創建類: WordRepository
// 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 Flow will notify the observer when the data has changed.
val allWords: Flow<List<Word>> = wordDao.getAlphabetizedWords()
// By default Room runs suspend queries off the main thread, therefore, we don't need to
// implement anything else to ensure we're not doing long running database work
// off the main thread.
@Suppress("RedundantSuspendModifier")
@WorkerThread
suspend fun insert(word: Word) {
wordDao.insert(word)
}
}
分析代碼:
(1) DAO 作為構造函數的參數傳遞。
DAO 包含數據庫的所有讀取/寫入方法,
因此存儲庫只需訪問DAO, 無需 獲取整個數據庫。
(2) 單詞列表(allWords) 具有公開屬性。
getAlphabetizedWords 返回的是 Flow 的方式.
(3) suspend 修飾符會告知編譯器需要從協程或其他掛起函數進行調用。
(4) Room 在主線程之外執行掛起查詢。 (@WorkderThread)
注意:
存儲庫的用途是在不同的數據源之間進行協調。
在這個簡單示例中,數據源只有一個,因此該存儲庫并未執行多少操作。
如需了解更復雜的實現,請參閱 BasicSample。
7. 創建ViewModel
7.1 什么是 ViewModel?
ViewModel 的作用是向界面提供數據,不受配置變化的影響。
ViewModel 充當存儲庫和界面之間的通信中心。
ViewModel 是 Lifecycle 庫的一部分。
7.2 為什么使用 ViewModel?
ViewModel 以一種可以感知生命周期的方式保存應用的界面數據,不受配置變化的影響.
更好地遵循單一責任原則:activity 和 fragment 負責將數據繪制到屏幕上,ViewModel 則負責保存并處理界面所需的所有數據。
7.3 LiveData 和 ViewModel
LiveData 是一種可觀察的數據存儲器,每當數據發生變化時,都會收到通知。
與 Flow 不同,LiveData 具有生命周期感知能力,即遵循其他應用組件(如 activity 或 fragment)的生命周期。
LiveData 會根據負責監聽變化的組件的生命周期自動停止或恢復觀察。因此,LiveData 適用于界面使用或顯示的可變數據。
ViewModel 會將存儲庫中的數據從 Flow 轉換為 LiveData,并將字詞列表作為 LiveData 傳遞給界面。
7.4 viewModelScope
在 Kotlin 中,所有協程都在 CoroutineScope
中運行。
AndroidX lifecycle-viewmodel-ktx 庫將 viewModelScope 添加為ViewModel 類的擴展函數
7.5 實現 ViewModel
創建為 WordViewModel, 代碼:
class WordViewModel(private val repository: WordRepository) : ViewModel() {
// Using LiveData and caching what allWords 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>> = repository.allWords.asLiveData()
/**
* Launching a new coroutine to insert the data in a non-blocking way
*/
fun insert(word: Word) = viewModelScope.launch {
repository.insert(word)
}
}
class WordViewModelFactory(private val repository: WordRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(WordViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return WordViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
分析代碼:
(1) 構造函數使用 WordRepository作為參數.
(2) LiveData 成員變量 以 緩存字詞列表
val allWords: LiveData<List<Word>> = repository.allWords.asLiveData().
并且將 Flow數據使用asLiveData轉成 LiveData數據
(3) 封裝存儲庫的insert方法
啟動新協程并調用存儲庫的掛起函數 insert
viewModelScope.launch{...}
(4) 實現 ViewModelProvider.Factory 并創建 WordViewModel
警告:請勿保留對生命周期短于 ViewModel 的 Context 的引用!例如:activity fragment view
保留引用可能會導致內存泄漏,例如 ViewModel 對已銷毀的 activity 的引用
重要提示:操作系統需要更多資源時,ViewModel 不會保留已在后臺終止的應用進程中。 可以參考SavedStateHandle
實測, 這個SavedStateHandle在設備中也不起效的(Github上也有人提出來)
8. 添加 XML 布局
這部分并非本文的重點.
8.1 樣式資源
values/styles.xml
<resources>
<!-- 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_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>
8.2 尺寸資源
values/dimens.xml
<dimen name="big_padding">16dp</dimen>
8.3 RecyclerView 每個條目的布局
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>
8.3 修改主Activity 布局
layout/activity_main.xml
<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"/>
</androidx.constraintlayout.widget.ConstraintLayout>
其中 FloatingActionButton 是一懸浮操作按鈕(FAB).
我們可以為它修改成 “+” 的符號,表示 添加 新的 單詞.
因此,新的矢量資源:
依次選擇 File > New > Vector Asset。
點擊 Clip Art: 字段中的 Android 機器人圖標
搜索“add”,然后選擇“+”資源。點擊 OK。
在 Asset Studio 窗口中,點擊 Next。
確認圖標的路徑為 main > drawable,然后點擊 Finish 以添加資源。
然后在 fab 按鈕上添加屬性:
android:src="@drawable/ic_add_black_24dp"
9. 添加 RecyclerView
這部分并非本文的重點.
需要熟悉 RecyclerView / ViewHolder / Adapter 等原理.
9.1 創建WordListAdapter
class WordListAdapter : ListAdapter<Word, WordViewHolder>(WordsComparator()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WordViewHolder {
return WordViewHolder.create(parent)
}
override fun onBindViewHolder(holder: WordViewHolder, position: Int) {
val current = getItem(position)
holder.bind(current.word)
}
class WordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val wordItemView: TextView = itemView.findViewById(R.id.textView)
fun bind(text: String?) {
wordItemView.text = text
}
companion object {
fun create(parent: ViewGroup): WordViewHolder {
val view: View = LayoutInflater.from(parent.context)
.inflate(R.layout.recyclerview_item, parent, false)
return WordViewHolder(view)
}
}
}
class WordsComparator : DiffUtil.ItemCallback<Word>() {
override fun areItemsTheSame(oldItem: Word, newItem: Word): Boolean {
return oldItem === newItem
}
override fun areContentsTheSame(oldItem: Word, newItem: Word): Boolean {
return oldItem.word == newItem.word
}
}
}
代碼相對簡單,無非就是做適配器的功能,綁定到VIEW上
9.2 添加RecyclerView
在 MainActivity 的 onCreate()中添加:
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
val adapter = WordListAdapter()
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
此時,編譯運行后沒有數據,因此是顯示空的.
10. 存儲庫(Repository)和 數據庫(RoomDatabase) 實例化
這里設計 數據庫和存儲庫只有一個實例。
因此,作為 Application 類的成員進行創建。
然后,在需要時只需從應用檢索,而不是每次都進行構建。
10.1 創建 WordsApplication
class WordsApplication : Application() {
// Using by lazy so the database and the repository are only created when they're needed
// rather than when the application starts
val database by lazy { WordRoomDatabase.getDatabase(this) }
val repository by lazy { WordRepository(database.wordDao()) }
}
分析代碼:
(1) 創建了 數據庫實例 database
(2) 創建了 *存儲庫實例 repository , 基于數據庫的DAO
同時,需要更新 AndroidManifest 配置文件
<application
android:name=".WordsApplication"
(3) 使用懶加載 lazy 的方式.
11. 填充數據庫
該樣例 添加數據的方式 有兩種:
(1) 在創建數據庫時添加一些數據
(2) 用于提供手動添加子詞的 Activity
先實現(1), 這就需要在 創建數據庫 后有回調,然后再添加數據.
RoomDatabase.Callback 正是提供的回調接口, 并覆寫它的onCreate()函數.
注意: 數據庫的操作不能在主線程上操作,需啟動協程.
要啟動協程,則可以使用 CoroutineScope.
這就需要使用 應用的 applicationScope。
11.1 修改創建數據庫時傳遞applicationScope
數據庫是在WordsApplication 上創建的,因此需要修改:
class WordsApplication : Application() {
// No need to cancel this scope as it'll be torn down with the process
val applicationScope = CoroutineScope(SupervisorJob())
// Using by lazy so the database and the repository are only created when they're needed
// rather than when the application starts
val database by lazy { WordRoomDatabase.getDatabase(this, applicationScope) }
val repository by lazy { WordRepository(database.wordDao()) }
}
applicationScope 是用于數據庫創建成功后, callBack回調onCreate時,執行插入數據的操作.
11.2 實現 RoomDatabase.Callback()
在 WordRoomDatabase 類中創建回調所用的代碼:
private class WordDatabaseCallback(
private val scope: CoroutineScope
) : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(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!
}
}
11.3 將回調添加到數據庫構建序列
.addCallback(WordDatabaseCallback(scope))
然后在 Room.databaseBuilder() 上調用 .build()
11.4 WordRoomDatabase 完整代碼:
@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 onCreate(db: SupportSQLiteDatabase) {
super.onCreate(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
}
}
}
}
12. 添加 NewWordActivity - 提供手動添加子詞
這里邏輯也很簡單。
先添加資源:
(1) 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>
<string name="add_word">Add word</string>
(2) values/dimens.xml 尺寸資源
<dimen name="min_height">48dp</dimen>
(3) 新建 NewWordActivity
注意要添加到 AndroidManifest中.
修改布局資源為:
<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>
更新 activity 的代碼:
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"
}
}
代碼說明:
在 “save” button 被點擊后,如果輸入有單詞,
則把單詞保存在 EXTRA_REPLY (setResult則返回結果)
13. 與數據建立關聯
最后一步是將界面連接到數據庫,方法是保存用戶輸入的新字詞,并在 RecyclerView 中顯示當前字詞數據庫的內容。
13.1 MainActivity 創建 ViewModel
private val wordViewModel: WordViewModel by viewModels {
WordViewModelFactory((application as WordsApplication).repository)
}
使用了 viewModels 委托,并傳入了 WordViewModelFactory 的實例.
基于從 WordsApplication 中檢索的存儲庫構建而成.
13.2 為所有字詞添加觀察者
wordViewModel.allWords.observe(this, Observer { words ->
// Update the cached copy of the words in the adapter.
words?.let { adapter.submitList(it) }
})
13.3 響應點擊 添加(FAB) 按鈕,進入NewWordActivity
val fab = findViewById<FloatingActionButton>(R.id.fab)
fab.setOnClickListener {
val intent = Intent(this@MainActivity, NewWordActivity::class.java)
startActivityForResult(intent, newWordActivityRequestCode)
}
在 NewWordActivity 完成后,處理返回的結果:
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()
}
}
如果 activity 返回 RESULT_OK,請通過調用 WordViewModel 的 insert() 方法將返回的字詞插入到數據庫中
13.4 MainActivity 完整代碼
class MainActivity : AppCompatActivity() {
private val newWordActivityRequestCode = 1
private val wordViewModel: WordViewModel by viewModels {
WordViewModelFactory((application as WordsApplication).repository)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
val adapter = WordListAdapter()
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
// Add an observer on the LiveData returned by getAlphabetizedWords.
// The onChanged() method fires when the observed data changes and the activity is
// in the foreground.
wordViewModel.allWords.observe(owner = this) { words ->
// Update the cached copy of the words in the adapter.
words.let { adapter.submitList(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, intentData: Intent?) {
super.onActivityResult(requestCode, resultCode, intentData)
if (requestCode == newWordActivityRequestCode && resultCode == Activity.RESULT_OK) {
intentData?.getStringExtra(NewWordActivity.EXTRA_REPLY)?.let { reply ->
val word = Word(reply)
wordViewModel.insert(word)
}
} else {
Toast.makeText(
applicationContext,
R.string.empty_not_saved,
Toast.LENGTH_LONG
).show()
}
}
}
至此, 所有代碼已結束。
運行后即可以體驗!!!
14. 總結
讓我們再看一遍Demo的架構圖:
應用的組件為:
(1) MainActivity:使用 RecyclerView 和 WordListAdapter 顯示列表中的字詞。MainActivity 中有一個 Observer,可觀察數據庫中的字詞,且可在字詞發生變化時接收通知。
(2) NewWordActivity: 可將新字詞添加到列表中。
(3) WordViewModel:提供訪問數據層所用的方法,并返回 LiveData,以便 MainActivity 可以設置觀察者關系。*
(4) LiveData<List<Word>>:讓界面組件的自動更新得以實現。您可以通過調用 flow.toLiveData() 從 Flow 轉換為 LiveData。
(5) Repository: 可管理一個或多個數據源。Repository 用于提供 ViewModel 與底層數據提供程序交互的方法。在此應用中,后端是一個 Room 數據庫。
(6) Room:是一個封裝容器,用于實現 SQLite 數據庫。Room 可完成許多以前由您自己完成的工作。
(7) DAO:將方法調用映射到數據庫查詢,以便在存儲庫調用 getAlphabetizedWords() 等方法時,Room 可以執行 SELECT * FROM word_table ORDER BY word ASC。
如果您希望在數據庫發生變化時接收通知,DAO 可以提供適用于單發請求的 suspend 查詢以及 Flow 查詢。
(8) Word:包含單個字詞的實體類。
(9) Views 和 Activities(以及 Fragments)僅通過 ViewModel 與數據進行交互。因此,數據的來源并不重要。
用于界面(反應式界面)自動更新的數據流
(1) 由于您使用了 LiveData,因此可以實現自動更新。MainActivity 中有一個 Observer,可用于觀察數據庫中的字詞 LiveData,并在發生變化時接收通知。如果字詞發生變化,則系統會執行觀察者的 onChange() 方法來更新 WordListAdapter 中的 mWords。
(2) 數據可以被觀察到的原因在于它是 LiveData。被觀察到的數據是由 WordViewModel allWords 屬性返回的 LiveData<List<Word>>。
(3) WordViewModel 會隱藏界面層后端的一切信息。WordViewModel 提供用于訪問數據層的方法,并返回 LiveData,以便 MainActivity 設置觀察者關系。Views 和 Activities(以及 Fragments)僅通過 ViewModel 與數據進行交互。因此,數據的來源并不重要。
(4) 在本例中,數據來自 Repository。ViewModel 無需知道存儲庫的交互對象。只需知道如何與 Repository 交互(通過 Repository 提供的方法)。
存儲庫可管理一個或多個數據源。在 WordListSample 應用中,后端是一個 Room 數據庫。Room 是一個封裝容器,用于實現 SQLite 數據庫。Room 可完成許多以前由您自己完成的工作。例如,Room 會執行您以前使用 SQLiteOpenHelper 類執行的所有操作。
(5) DAO:將方法調用映射到數據庫查詢,以便在存儲庫調用 getAllWords() 等方法時,Room 可以執行 SELECT * FROM word_table ORDER BY word ASC。
由于在從查詢返回的結果中觀察到了 LiveData,因此每當 Room 中的數據發生變化時,系統都會執行 Observer 接口的 onChanged() 方法并更新界面。
15. 附錄
官方GitHub 源碼地址:
(1) Kotlin
https://github.com/googlecodelabs/android-room-with-a-view/tree/kotlin
(1) Java:
https://github.com/googlecodelabs/android-room-with-a-view
本人源碼地址: TODO