架构组件的用途是提供关于应用架构的指南,其中包括生命周期管理和数据持久性处理等常见任务的库。架构组件可帮助您以稳健、可测试和易维护的方式组织应用结构,且所需样板代码较少。架构组件库是 Android Jetpack 的一部分。
本 Codelab 为 Kotlin 版本。如需查看 Java 编程语言的版本,请点击此处。
如果在此 Codelab 操作期间遇到任何问题(代码错误、语法错误、措辞含义不明等),都可以通过 Codelab 左下角的报告错误链接报告相应问题。
前提条件
您需要熟悉 Kotlin、面向对象的设计概念以及 Android 开发方面的基础知识,尤其是:
RecyclerView
和适配器- SQLite 数据库及 SQLite 查询语言
- 基本协程(如果您不熟悉协程,可以参阅在 Android 应用中使用 Kotlin 协程)。
此外,熟悉一下将数据与界面分离的软件架构模式(例如 MVP 或 MVC)也能有所帮助。本 Codelab 将实现应用架构指南中定义的架构。
本 Codelab 主要介绍 Android 架构组件。与主题无关的其他概念和代码主要是供您直接复制和粘贴的。
如果您不熟悉 Kotlin,可在此处查看 Java 编程语言版本的 Codelab。
您应执行的操作
在此 Codelab 中,您将学习如何使用架构组件 Room、ViewModel 和 LiveData 来设计和构建应用,以及如何构建执行以下操作的应用:
- 使用 Android 架构组件来实现我们的推荐架构。
- 与数据库配合使用以获取和保存数据,并用一些字词预填充数据库。
- 显示
MainActivity
中的RecyclerView
中的所有字词。 - 在用户点按“+”按钮时打开一个新 activity。当用户输入某个字词时,该字词就会添加到数据库和列表中。
该应用很简单,但其复杂程度也足够您将其作为构建模板。预览如下:
您需要满足的条件
- 熟悉 Android Studio 3.0 或更高版本及其使用方法。确保更新 Android Studio 以及您的 SDK 和 Gradle。
- 一台 Android 设备或模拟器。
本 Codelab 提供了构建完整应用所需的所有代码。
使用架构组件和实现推荐架构的步骤有很多。最重要的是,创建一个构思,构思整个过程,理解各个部分是如何组合在一起的,以及数据是如何传输的。您在学习本 Codelab 时,不要只是复制和粘贴代码,而应尝试深入了解架构。
推荐使用的架构组件有哪些?
下面将简要介绍架构组件及其协作方式。请注意,本 Codelab 将重点介绍部分组件,即 LiveData、ViewModel 和 Room,我们会在使用过程中详细介绍每个组件。
下图所示为架构的基本组成部分:
SQLite 数据库:设备上的存储空间。Room 持久性库会为您创建和维护此数据库。
DAO:数据访问对象。从 SQL 查询到函数的映射。在使用 DAO 时,您需要调用相应方法,其余操作均由 Room 处理。
Room 数据库:可简化数据库工作,并充当 SQLite 底层数据库的接入点(隐藏 SQLiteOpenHelper)
。它使用 DAO 向 SQLite 数据库发出查询请求。
存储库:您创建的类,主要用于管理多个数据源。
ViewModel:充当存储库(数据)和界面之间的通信中心。对于界面而言,数据来源不再是一个需要关注的问题。ViewModel 实例在重新创建 activity/fragment 后仍然存在。
LiveData:一种可观察的数据存储器类。务必保存/缓存最新版本的数据,并在数据发生变化时通知其监测者。LiveData
具有生命周期感知能力。界面组件只是监测相关数据,不会停止或恢复监测。LiveData 将自动管理所有这些操作,因为它在监测时可以感知相关的生命周期状态变化。
RoomWordSample 架构概览
下图所示为应用的所有组成部分。每个框(SQLite 数据库除外)均代表要创建的类。
- 打开 Android Studio,然后点击 Start a new Android Studio project。
- 在“Create New Project”窗口中,选择 Empty Activity,然后点击 Next。
- 在下一个屏幕上,将应用命名为 RoomWordSample,然后点击 Finish。
接下来,您必须将组件库添加到 Gradle 文件中。
- 在 Android Studio 中,点击“Projects”标签页,然后展开“Gradle Scripts”文件夹。
打开 build.gradle
(Module: app)。
- 应用
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'
}
此应用的数据为字词,您需要使用一个简单的表来保存这些值:
Room 允许您通过实体创建表,现在就开始创建吧。
- 创建一个名为
Word
的新 Kotlin 类文件,其中包含Word
数据类。
这个类用于描述字词的实体(代表 SQLite 表)。类中的每个属性代表表中的一列。Room 最终将使用这些属性来创建表并将数据库中的行对象实例化。
代码如下:
data class Word(val word: String)
为了使 Word
类对 Room 数据库有意义,您需要为其添加注解。注解可以标识该类的各个部分与数据库中的条目之间的关系。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"
)
如果您希望此表中列的名称与成员变量的名称不同,可以指定表中列的名称。此处的列名为“word”。- 存储在数据库中的每个属性均需公开,这是 Kotlin 的默认设置。
您可以在 Room 软件包摘要参考中找到完整的注解列表。
什么是 DAO?
在 DAO(数据访问对象)中,您可以指定 SQL 查询并将其与方法调用相关联。编译器会检查 SQL 并根据常见查询的方便的注解(如 @Insert
)生成查询。Room 使用 DAO 为代码创建整洁的 API。
DAO 必须是一个接口或抽象类。
默认情况下,所有查询都必须在单独的线程上执行。
Room 支持协程,可让查询使用 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 方法注解,使用 DAO 方法注解时,您无需提供任何 SQL!(还有用于删除和更新行的@Delete
和@Update
注解,但您未在此应用中使用这些注解。)onConflict = OnConflictStrategy.IGNORE
:所选 onConflict 策略将忽略与列表中的现有字词完全相同的新字词。如需详细了解可用的冲突策略,请参阅相关文档。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()
方法签名,以便使用 LiveData
封装返回的 List<Word>
。
@Query("SELECT * from word_table ORDER BY word ASC")
fun getAlphabetizedWords(): LiveData<List<Word>>
在此 Codelab 的后面部分,您将通过 MainActivity
中的 Observer
跟踪数据更改。
什么是 Room 数据库?
- Room 是 SQLite 数据库之上的一个数据库层。
- Room 负责您平常使用
SQLiteOpenHelper
处理的单调乏味的任务。 - Room 使用 DAO 向其数据库发出查询请求。
- 为避免界面性能不佳,默认情况下,Room 不允许在主线程上发出查询请求。当 Room 查询返回
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
}
}
}
}
我们来了解一下此代码:
- Room 数据库类必须是
abstract
且扩展RoomDatabase
- 您可以通过
@Database
将该类注解为 Room 数据库,并使用注解参数声明数据库中的实体以及设置版本号。每个实体都对应一个将在数据库中创建的表。数据库迁移不在本 Codelab 的范围内,因此我们将此处的exportSchema
设置为 false 以免显示构建警告。在真实的应用中,您应考虑为 Room 设置一个用于导出架构的目录,以便将当前架构签入版本控制系统。 - 该数据库通过每个 @Dao 的抽象“getter”方法公开 DAO。
- 我们定义了一个单例
WordRoomDatabase,
,以防出现同时打开数据库的多个实例的情况。 getDatabase
将返回单例。首次访问时,它会使用 Room 的数据库构建器在WordRoomDatabase
类的应用上下文中创建一个RoomDatabase
对象,并将其命名为"word_database"
。
什么是存储库?
存储库类会将多个数据源的访问权限抽象化。存储库并非架构组件库的一部分,但它是推荐为代码分离和架构采用的最佳做法。存储库类会提供一个整洁的 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,无需向存储库公开整个数据库。
- 字词列表具有公开属性。它通过从 Room 获取
LiveData
字词列表来进行初始化,之所以能做到这一点,是因为我们在getAlphabetizedWords
The LiveData 类中定义getAlphabetizedWords
方法以返回LiveData
。Room 将在单独的线程上执行所有查询。然后,观察到LiveData
会在数据发生更改时在主线程上通知观察器。 suspend
修饰符会告知编译器需要从协程或其他挂起函数进行调用。
什么是 ViewModel?
ViewModel
的作用是向界面提供数据,不受配置变化的影响。ViewModel
充当存储库和界面之间的通信中心。您还可以使用 ViewModel
在 fragment 之间共享数据。ViewModel 是生命周期库的一部分。
如需查看有关此主题的入门指南,请参阅 ViewModel Overview
或 ViewModel:简单示例博文。
为什么使用 ViewModel?
ViewModel
以一种可以感知生命周期的方式保存应用的界面数据,不受配置变化的影响。它会将应用的界面数据与 Activity
和 Fragment
类区分开,让您更好地遵循单一责任原则:activity 和 fragment 负责将数据绘制到屏幕上,ViewModel
则负责保存并处理界面所需的所有数据。
在 ViewModel
中,对于界面将使用或显示的可变数据,请使用 LiveData
。使用 LiveData
有多项优势:
- 您可以在数据上放置一个观察器(而不是轮询更改),并且仅在数据实际发生更改时才更新界面。
- 代码库和界面完全由
ViewModel
分隔。 - 没有来自
ViewModel
的数据库调用(这全部都在代码库中进行处理),从而使代码更易于测试。
viewModelScope
在 Kotlin 中,所有协程都在 CoroutineScope
中运行。范围用于控制协程在整个作业过程中的生命周期。如果取消某个作用域的作业,则该作用域内启动的所有协程也将取消。
AndroidX lifecycle-viewmodel-ktx
库将 viewModelScope
添加为 ViewModel
类的扩展函数,使您能够使用范围。
如需详细了解如何在 ViewModel 中使用协程,请参阅在 Android 应用中使用 Kotlin 协程 Codelab 的第 5 步或 Android 中的简易协程: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。 - 创建了一个可调用存储库的
insert()
方法的封装容器insert()
方法。这样一来,便可从界面封装insert()
的实现。我们不希望插入阻塞主线程,所以会启动一个新的协程并调用存储库的插入,后者是一个挂起函数。如上所述,ViewModel 的协程作用域基于其名为viewModelScope
的生命周期(我们在这里使用)。
接下来,您需要为列表和项添加 XML 布局。
本 Codelab 假定您已熟悉如何在 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 的外观应与可用操作相对应,因此,我们需要将该图标替换为 '+' 符号。
首先,我们需要添加一个新的矢量资源:
- 依次选择 File > New > Vector Asset。
- 点击 Clip Art: 字段中的 Android 机器人图标。
- 搜索“添加”,然后选择“'+素材资源”。点击确定
。 - 然后,点击 Next。
- 确认图标的路径为
main > drawable
,然后点击 Finish 以添加资源。 - 仍是在
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
中抛出数据略好。本 Codelab 假定您了解 RecyclerView
、RecyclerView.LayoutManager
、RecyclerView.ViewHolder
和 RecyclerView.Adapter
的工作原理。
请注意,适配器中的 words
变量会缓存数据。在下一个任务中,您将添加自动更新数据的代码。
为 WordListAdapter
创建一个扩展 RecyclerView.Adapter
的 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()
。由于您无法在界面线程上执行 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>
创建一个新的尺寸资源文件:
- 点击 Project 窗口中的应用模块。
- 依次选择 File > New > Android Resource File
- 在 Available Qualifiers 中,选择 Dimension
- 将文件名设置为“dimens”
在 values/dimens.xml
中添加以下尺寸资源:
<dimen name="small_padding">8dp</dimen>
<dimen name="big_padding">16dp</dimen>
使用“Empty Activity”模板创建一个空的新 Android Activity
:
- 依次选择 File > New > Activity > Empty Activity
- 输入
NewWordActivity
以将其用作 activity 的名称。 - 验证是否已将新的 activity 添加到 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>
更新 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"
}
}
最后一步是将界面连接到数据库,方法是保存用户输入的新字词,并在 RecyclerView
中显示当前字词数据库的内容。
如需显示数据库的当前内容,请添加可观察 ViewModel
中的 LiveData
的观察者。
每当数据发生变化时,系统都会调用 onChanged()
回调,此操作会调用适配器的 setWords()
方法来更新此适配器的缓存数据并刷新显示的列表。
在 MainActivity
中,为 ViewModel
创建一个成员变量:
private lateinit var wordViewModel: WordViewModel
使用 ViewModelProvider
将 ViewModel
与 Activity
相关联。
首次启动 Activity
时,ViewModelProviders
会创建 ViewModel
。当 Activity 被销毁时(例如通过配置更改),ViewModel
会保留。重新创建 Activity 后,ViewModelProviders
会返回现有的 ViewModel
。如需了解详情,请参阅 ViewModel
。
在 onCreate()
中的 RecyclerView
代码块下方,从 ViewModelProvider
获取 ViewModel
:
wordViewModel = ViewModelProvider(this).get(WordViewModel::class.java)
同样在 onCreate()
中,为 WordViewModel
中所有字词的 LiveData
属性添加观察者。
当观察到数据发生变化且 activity 位于前台时,将触发 onChanged()
方法(lambda 的默认方法):
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()
代码。
如果 activity 返回 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()
}
}
在 MainActivity,
中,在用户点按 FAB 后启动 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
用于提供 ViewModel 与底层数据提供程序交互的方法。在此应用中,后端是一个 Room 数据库。Room
:是一个封装容器,用于实现 SQLite 数据库。Room 可完成许多以前由您自己完成的工作。- DAO:将方法调用映射到数据库查询,以便在存储库调用
getAlphabetizedWords()
等方法时,Room 可以执行SELECT * from word_table ORDER BY word ASC
。 Word
:包含单个字词的实体类。
* Views
和 Activities
(以及 Fragments
)仅通过 ViewModel
与数据进行交互。因此,数据来自哪里并不重要。
用于界面(反应式界面)自动更新的数据流
界面能自动更新的原因是我们使用了 LiveData。MainActivity
中有一个 Observer
,可用于观察数据库中的字词 LiveData,并在发生变化时接收通知。如果字词发生变化,则系统会执行观察者的 onChange()
方法来更新 WordListAdapter
中的 mWords
。
数据可以被观察到的原因在于它是 LiveData
。被观察到的数据是由 WordViewModel
allWords
属性返回的 LiveData<List<Word>>
。
WordViewModel
会隐藏界面层后端的一切信息。WordViewModel 提供用于访问数据层的方法,并返回 LiveData
,以便 MainActivity
设置观察者关系。Views
和 Activities
(以及 Fragments
)仅通过 ViewModel
与数据进行交互。因此,数据来自哪里并不重要。
在本例中,数据来自 Repository
。ViewModel
无需知道存储库的交互对象。它只需要知道如何与 Repository
交互(通过 Repository
提供的方法)。
存储库可管理一个或多个数据源。在 WordListSample
应用中,后端是一个 Room 数据库。Room 是一个封装容器,用于实现 SQLite 数据库。Room 可完成许多以前由您自己完成的工作。例如,Room 会执行您以前使用 SQLiteOpenHelper
类执行的所有操作。
DAO:将方法调用映射到数据库查询,以便在存储库调用 getAllWords()
等方法时,Room 可以执行 SELECT * from word_table ORDER BY word ASC
。
由于从查询返回的结果观察到 LiveData
,因此每当 Room 中的数据发生变化时,系统都会执行 Observer
接口的 onChanged()
方法并更新界面。
[可选] 下载解决方案代码
如果您尚未查看解决方案代码,可以查看本 Codelab 的解决方案代码。您可以访问 GitHub 代码库或在此处下载代码:
解压下载的 ZIP 文件。此操作将解压根文件夹 android-room-with-a-view-kotlin
,其中包含完整的应用。