本文翻译自 Android Room with a View - Kotlin codelab,较长。
目录
推荐的 Architecture Components(架构组件)是什么?
1. 介绍
Architecture Components 的目的是使用用于生命周期管理和数据持久性等常见任务的库,来提供有关应用程序架构的指导。
架构组件帮助你使用较少的样板代码,以健壮、可测试和可维护的方式构建 app。
这是codelab的 Kotlin 版本。 Java版本在这里。
推荐的 Architecture Components(架构组件)是什么?
以下是 Architecture Components 的简短介绍,以及它们如何协同工作的。 请注意,此 codelab 侧重于组件的子集,即 LiveData,ViewMode l和 Room。 每个组件在使用时都会有更多解释。
该图显示了这种架构的基本形式:
Entity: 在使用 Room 时,这是一个带有注解的类,用于描述数据库表。
SQLite 数据库: 在设备上,数据存储在 SQLite 数据库中。 Room 持久化库为你创建和维护此数据库。
DAO: 数据访问对象。 SQL 查询到函数的映射。 当你使用 DAO 时,你可以调用方法,而 Room 负责其余的操作。
Room 数据库: SQLite 数据库之上的数据库层,负责处理你以前使用 SQLiteOpenHelper 处理的普通任务。 数据库持有者,用作底层 SQLite 数据库的访问点。 Room 数据库使用 DAO 向 SQLite 数据库发出查询。
Repository: 你创建的类。 你使用 Repository 来管理多个数据源。
ViewModel: 向 UI 提供数据。 充当 Repository 和 UI 之间的通信中心。 隐藏数据源自 UI 的位置。 ViewModel 实例在 Activity / Fragment 重建时可以保持存活。
LiveData: 可观察的数据持有者类。 始终保存/缓存最新版本的数据。 数据发生变化时通知其观察者。 LiveData 可以感知生命周期。 UI组件只是观察相关数据,不会停止或恢复观察。 LiveData 自动管理所有这些,因为它在观察时可以感知到相关的生命周期状态变化。
寻找更多? 查看完整的应用程序架构指南。
你将构建什么
你将构建一个使用 Android Architecture Components(Android 架构组件) 的 app,并为这些组件实现来自应用程序架构指南的架构。 示例 app 在 Room 数据库中存储单词列表,并将其显示在 RecyclerView 中。 该应用程序是骨架,但足够复杂,你可以使用它作为模板来构建。
在此 codelab 中,你将构建一个执行以下操作的 app:
- 使用数据库来获取和保存数据,并使用一些单词预填充数据库。
- 显示 MainActivity 中 RecyclerView 里的所有单词。
- 当用户点击 + 按钮时打开第二个 activity。 当用户输入单词时,将单词添加到数据库和列表中。
RoomWordSample 架构概览
下图显示了 app 的所有部分。 每个封闭框(SQLite 数据库除外)代表你将创建的类。
你将学到什么
- 如何使用 Architecture Components Room 和 Lifecycles 库设计和构建 app。
使用架构组件和实现推荐的架构需要很多步骤。最重要的事情是创建一个关于正在发生的事情的心理模型,并理解这些部分如何组合在一起,以及数据如何流动。在你完成这个 codelab 时,不要只是复制和粘贴代码,而是尝试开始构建这种内部理解。
你需要什么
- Android Studio 3.0 或更高版本,以及如何使用它的知识。 确保 Android Studio 以及 SDK 和 Gradle已更新。 否则,你可能必须等到所有更新完成。
- Android 设备 或 模拟器。
你需要熟悉 Kotlin,面向对象设计概念和 Android 开发基础。特别是:
- RecyclerView 和适配器
- SQLite 数据库和 SQLite 查询语言
- 基本协同程序(如果你不熟悉协同程序,你可以在你的Android应用程序中使用 Kotlin 协同程序)
- 这有助于熟悉将数据与用户界面分离的软件架构模式,例如 MVP 或 MVC 。 此 codelab 实现了应用程序架构指南中定义的架构。
如果你不熟悉 Kotlin,这里提供了此 codelab 的 Java 版本。
此 codelab 专注于 Android 架构组件。 提供了非主题概念和代码,你只需复制和粘贴即可。
此 codelab 提供了构建完整 app 所需的所有代码。
2. 创建你的 app
打开 Android Studio 并按如下方式创建 app:
- 将 app 命名为 RoomWorldSample
- Target SDK 26+
- 勾选 include Kotlin support , 不勾选 include C++ support.
- 只勾选 Phone & Tablet form factor, 且 minimum SDK API 26
- 选择 Basic Activity
3. 更新 gradle 文件
你必须将组件库添加到 gradle 文件中。
在 build.gradle(Module:app)中进行以下更改:
通过在 build.gradle(Module: app)文件顶部定义的其他插件之后添加 kapt Kotlin 插件来应用它。
apply plugin: 'kotlin-kapt'
将以下代码添加到 dependencies
语句块。
-
// Room 组件
-
implementation "android.arch.persistence.room:runtime:$rootProject.roomVersion"
-
kapt "android.arch.persistence.room:compiler:$rootProject.roomVersion"
-
androidTestImplementation "android.arch.persistence.room:testing:$rootProject.roomVersion"
-
// Lifecycle 组件
-
implementation "android.arch.lifecycle:extensions:$rootProject.archLifecycleVersion"
-
kapt "android.arch.lifecycle:compiler:$rootProject.archLifecycleVersion"
-
// Coroutines
-
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$rootProject.coroutines"
-
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$rootProject.coroutines"
通过在build.gradle(Module: app)文件的末尾添加以下代码来启用协同程序支持。
-
kotlin {
-
experimental {
-
coroutines "enable"
-
}
-
}
在 build.gradle(Project: RoomWordsSample)文件中,将版本号添加到文件末尾,如下面的代码所示。
从 将组件添加到你的项目 页面中获取最新版本号。
-
ext {
-
roomVersion = '1.1.1'
-
archLifecycleVersion = '1.1.1'
-
coroutines = '0.26.1'
-
}
4. 创建实体
这个 app 的数据是单词,每个单词都是一个 Entity。创建描述单词 Entity 的名为 Word 的数据类。你需要列的公共值,因为 Room 就是这样知道如何实例化对象的。
这是代码:
data class Word(val word: String)
要使 Word 类对 Room 数据库有意义,你需要对其进行注解。 注解标识此类的每个部分如何与数据库中的条目相关。 Room 使用此信息生成代码。
@Entity(tableName =
"word_table"
)
每个 @Entity 类表示表中的一个实体。注解类声明以表明它是一个实体。如果希望表的名称与类的名称不同,请指定表的名称。@PrimaryKey
每个实体都需要一个主键。 为了简单起见,每个单词都充当自己的主键。@ColumnInfo(name =
"word"
)
如果希望它与成员变量的名称不同,请在表中指定列的名称。- 存储在数据库中的每个字段都必须是公共的或具有 “getter” 方法。
你可以在 Room包摘要参考 中找到完整的注解列表。
用注解更新 Word 类,如代码所示。如果键入注解,Android Studio 将自动导入。
-
@Entity(tableName = "word_table")
-
class Word(@PrimaryKey @ColumnInfo(name = "word") val word: String)
在复制粘贴代码时,如果出现注解错误,可能需要手动导入注解。
import android.arch.persistence.room.ColumnInfo
import android.arch.persistence.room.Entity
import android.arch.persistence.room.PrimaryKey
参见使用Room实体定义数据。
5. 创建 DAO
什么是DAO
在 DAO (数据访问对象)中,指定 SQL 查询并将它们与方法调用相关联。 编译器检查 SQL 并从便捷注解生成查询以查找常见查询,例如 @Insert。
DAO 必须是接口或抽象类。
默认情况下,所有查询都必须在单独的线程上执行。
Room 使用 DAO 为你的代码创建一个干净的 API。
实现DAO
这个 codelab 的 DAO 是基本的,它提供获取所有单词、插入单词和删除所有单词的查询。
- 创建一个新的接口并将其命名为WordDao
- 使用 @Dao 注解类,以将其标识为 Room 的 DAO 类。
- 声明插入一个单词的方法:fun insert( word: Word )
- 使用 @Insert 注解方法。 你不必提供任何 SQL! (还有 @Delete 和 @Update 注解用于删除和更新行,但你没有在此 app 中使用它们。)
- 声明一个删除所有单词的方法:fun deleteAll( )。
- 删除多个实体没有便利注解,因此使用泛型 @Query 注解该方法。
- 将 SQL 查询作为字符串参数提供给 @Query 注解。
@Query("DELETE FROM word_table")
- 创建一个方法来获取所有单词并让它返回一个单词列表。
fun getAllWords(): List<Word>
- 使用 SQL 查询注解方法:
@Query(
"SELECT * from word_table ORDER BY word ASC"
)
以下是完整的代码:
-
@Dao
-
interface WordDao {
-
@Query("SELECT * from word_table ORDER BY word ASC")
-
fun getAllWords(): List<Word>
-
@Insert
-
fun insert(word: Word)
-
@Query("DELETE FROM word_table")
-
fun deleteAll()
-
}
提示:对于这个 app,给单词排序并不是绝对必要的。但是,在默认情况下,顺序是不能保证的,并且排序使测试变得简单。
.提示:当插入数据时,你可以提供冲突策略。
在这个 codelab 中,你不需要冲突策略,因为该单词是你的主键,并且默认的 SQL 行为是 ABORT,因此你无法将具有相同主键的两个项插入到数据库中。
如果表有多个列,则可以使用
@Insert(onConflict = OnConflictStrategy.REPLACE)
替换一行。 要了解有关可用冲突策略的更多信息,请查看文档。
了解有关 Room DAOs 的更多信息。
6. LiveData 类
数据更改时,你通常需要执行某些操作,例如在 UI 中显示更新的数据。 这意味着你必须观察数据,以便在更改时,你可以做出反应。 根据数据的存储方式,这可能很棘手。 观察应用程序的多个组件之间的数据更改可以在组件之间创建明确,严格的依赖关系路径。 这使测试和调试变得尤为困难。
LiveData
是一个用于数据观察的生命周期库类,可以解决这个问题。 在方法描述中使用 LiveData
类型的返回值,Room 会在更新数据库时生成更新 LiveData 所需的所有代码。
如果你独立于 Room 使用 LiveData,则必须管理更新数据。 LiveData 没有公开的方法来更新存储的数据。
如果你(开发人员)想要更新存储的数据,则必须使用
MutableLiveData
而不是 LiveData。 MutableLiveData 类有两个公共方法,允许你设置 LiveData 对象的值,setValue(T)
和postValue(T)
。 通常,在ViewModel
中使用 MutableLiveData,然后 ViewModel 仅向观察者公开不可变的 LiveData 对象。
在 WordDao中,更改 getAllWords() 方法签名,以便用 LiveData 包装返回的 List<Word>。
-
@Query("SELECT * from word_table ORDER BY word ASC")
-
fun getAllWords(): LiveData<List<Word>>
稍后在此 codelab 中,你将在 MainActivity 的 onCreate()方法中创建数据的 Observer。 覆盖观察者的 onChanged( ) 方法可以在 onChanged( ) 方法中接收LiveData 更改时的通知。 然后,你将更新适配器中的缓存数据,适配器将更新用户看到的内容。
请参阅 LiveData 文档以了解有关使用 LiveData 的其他方法的更多信息,或观看这个 Architecture Components: LifeData and Lifecycle 视频。
7. 添加 Room 数据库
什么是 Room 数据库?
- Room 是在 SQLite 数据库之上的数据库层。
- Room 负责处理你过去使用
SQLiteOpenHelper
处理的普通任务。 - .Room 使用 DAO 向其数据库发出查询。
- 默认情况下,为了避免糟糕的UI性能,Room 不允许在主线程上发出查询。当 Room 查询返回
LiveData
时,查询将在后台线程上自动同步运行。 - Room 提供 SQLite 语句的编译时检查。
实现 Room 数据库
你的 Room 数据库类必须是抽象的,并且继承 RoomDatabase。通常,整个应用程序只需要 Room 数据库的一个实例。
1.创建一个继承 RoomDatabase 的公共抽象类,并将其命名为 WordRoomDatabase。public abstract class
WordRoomDatabase
:
RoomDatabase() {}
2.将类注解为 Room 数据库,声明属于数据库的实体并设置版本号。 列出实体将在数据库中创建表。@Database(entities = {Word.
class
}, version = 1)
3.定义使用数据库的 DAO。 为每个 @Dao 提供抽象的 “getter” 方法。abstract
fun wordDao(): WordDao
这是代码:
-
@Database(entities = [Word::class], version = 1)
-
public abstract class WordRoomDatabase : RoomDatabase() {
-
abstract fun wordDao(): WordDao
-
}
4.使 WordRoomDatabase 成为单例,以防止同时打开多个数据库实例。
这是代码:
-
companion object {
-
@Volatile
-
private var INSTANCE: WordRoomDatabase? = null
-
fun getDatabase(context: Context): WordRoomDatabase {
-
return INSTANCE ?: synchronized(this) {
-
// 在此创建数据库
-
val instance = // TODO
-
INSTANCE = instance
-
instance
-
}
-
}
-
}
5.此代码使用 Room 的数据库 builder 在来自 WordRoomDatabase 类的应用程序上下文中创建 RoomDatabase
对象,并将其命名为 “word_database”。
-
// 在此创建数据库
-
val instance = Room.databaseBuilder(
-
context.applicationContext,
-
WordRoomDatabase::class.java,
-
"Word_database"
-
).build()
这是完整代码:
-
@Database(entities = arrayOf(Word::class), version = 1)
-
public abstract class WordRoomDatabase : RoomDatabase() {
-
abstract fun wordDao(): WordDao
-
companion object {
-
@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
-
}
-
}
-
}
-
}
修改数据库模式时,你需要更新版本号并定义迁移策略。
对于示例,销毁和重新创建策略就足够了。但是,对于一个真正的 app,你必须实现迁移策略。参见了解使用 Room 进行迁移。
在Android Studio中,如果在粘贴代码时或在构建过程中出现错误,请选择 Build >Clean Project。然后选择 Build > Rebuild Project,然后再次 build。 如果使用提供的代码,则无论何时你指示构建应用程序,都应该没有错误。
8. 创建Repository(
存储库)
什么是 Repository
?
Repository 类抽象对多个数据源的访问。Repository
不是 Architecture Components 库的一部分,而是用于代码分离和架构的建议的最佳实践。Repository 类为应用其余部分的数据访问提供了一个干净的 API。
为什么使用 Repository?
.Repository 管理查询并允许你使用多个后端。 在最常见的示例中,Repository 实现了用于决定是从网络获取数据还是使用在本地数据库中缓存的结果的逻辑。
实现 Repository
1.创建一个名为 WordRepository
的公共类。
2.将 DAO 声明为构造函数中的私有属性。
3.将单词列表添加为公共属性并对其进行初始化。 Room 在单独的线程上执行所有查询。 被观察的 LiveData 将在数据发生变化时通知观察者。
-
class WordRepository(private val wordDao: WordDao) {
-
}
4.添加作为公共属性的单词列表并初始化它。Room 在单独的线程上执行所有查询。被观察的 LiveData 将在数据发生变化时通知观察者。
val allWords: LiveData<List<Word>> = wordDao.getAllWords()
5.为 insert( ) 方法添加一个包装器。 你必须在非 UI 线程上调用此方法,否则你的 app 将崩溃。 Room 确保你不在主线程上执行任何长时间运行的阻塞 UI 的操作。 添加 @WorkerThread 注解,以标记需要从非UI线程调用此方法。 添加 suspendmodifier 告诉编译器需要从协同程序或其他挂起函数调用它。
-
@WorkerThread
-
suspend fun insert(word: Word) {
-
wordDao.insert(word)
-
}
这是完整的代码:
-
class WordRepository(private val wordDao: WordDao) {
-
val allWords: LiveData<List<Word>> = wordDao.getAllWords()
-
@WorkerThread
-
suspend fun insert(word: Word) {
-
wordDao.insert(word)
-
}
-
}
对于这个简单的例子,Repository没有做太多。 有关更复杂的实现,请参阅 BasicSample。
9. 创建 ViewModel
什么是 ViewModel?
ViewMode 的作用是向 UI 提供数据并在配置更改后继续存活。 ViewModel 充当 Repository 和 UI 之间的通信中心。 你还可以使用 ViewModel 在 fragments 之间共享数据。 ViewModel 是生命周期库的一部分。
有关此主题的介绍性指南,请参阅 ViewModel
。
为什么使用 ViewModel?
ViewModel 以生命周期意识的方式保存你的 app 的 UI 数据,以便在配置更改后继续存活。 将应用程序的 UI 数据与Activity 和 Fragment 类分开可以让你更好地遵循单一责任原则:你的 acvtivities 和 fragments 负责将数据绘制到屏幕,而ViewModel 可以负责保存和处理 UI 所需的所有数据。
在ViewModel中,使用 LiveData 获取 UI 将使用或显示的可更改数据。 使用 LiveData 有几个好处:
- 你可以将观察者放在数据上(而不是轮询更改)并仅更新数据实际更改时的UI。
- .Repository 和 UI 完全由 ViewModel 分隔。 ViewModel 没有数据库调用,使代码更易于测试。
实现 ViewModel
1.创建一个名为WordViewModel 的类,它将 Application 作为参数获取并继承 AndroidViewModel。
class WordViewModel(application: Application) : AndroidViewModel(application)
2.添加私有成员变量以持有对 repository 的引用。
private val repository: WordRepository
3.添加私有 LiveData 成员变量以缓存单词列表。
val allWords: LiveData<List<Word>>
4.创建一个 init 块,从 WordRoomDatabase 获取对 WordDao 的引用,并基于它构建 WordRepository。
-
init {
-
val wordsDao = WordRoomDatabase.getDatabase(application).wordDao()
-
repository = WordRepository(wordsDao)
-
}
5.在init块中,使用来自 repository 的数据初始化allWords 属性:
allWords = repository.allWords
6.定义 parentJob 和 coroutineContext。 默认情况下,coroutineContext 使用parentJob和主调度程序基于 coroutineContext 创建 CoroutineScope 的新实例。
-
private var parentJob = Job()
-
private val coroutineContext: CoroutineContext
-
get() = parentJob + Dispatchers.Main
-
private val scope = CoroutineScope(coroutineContext)
7.重写 onCleared 方法并取消 parentJob。 当ViewModel 不再使用并且将被销毁时调用 onCleared,现在是时候取消parentJob完成的任何长时间运行的作业了。
-
override fun onCleared() {
-
super.onCleared()
-
parentJob.cancel()
-
}
8.创建一个调用 Repository 的 insert( ) 方法的包装器 insert( ) 方法。 通过这种方式,insert( ) 的实现对 UI 是完全隐藏的。 我们希望从主线程调用 insert( ) 方法,因此我们基于之前定义的协同程序范围启动一个新的协同程序。 因为我们正在进行数据库操作,所以我们正在使用 IO Dispatcher。
-
fun insert(word: Word) = scope.launch(Dispatchers.IO) {
-
repository.insert(word)
-
}
这是 WordViewModel
的完整代码:
-
class WordViewModel(application: Application) : AndroidViewModel(application) {
-
private var parentJob = Job()
-
private val coroutineContext: CoroutineContext
-
get() = parentJob + Dispatchers.Main
-
private val scope = CoroutineScope(coroutineContext)
-
private val repository: WordRepository
-
val allWords: LiveData<List<Word>>
-
init {
-
val wordsDao = WordRoomDatabase.getDatabase(application).wordDao()
-
repository = WordRepository(wordsDao)
-
allWords = repository.allWords
-
}
-
fun insert(word: Word) = scope.launch(Dispatchers.IO) {
-
repository.insert(word)
-
}
-
override fun onCleared() {
-
super.onCleared()
-
parentJob.cancel()
-
}
-
}
警告:如果你在 ViewModel 中保留对生命周期较短的上下文的引用,则可能发生内存泄漏:
- Activity
- Fragment
- View
所有这些对象都可能被操作系统销毁,并在配置更改时重新创建,这可能会在 ViewModel 的生命周期中多次发生。
例如,如果在 ViewModel 中保留 Activity 的引用,则最终可能会引用已销毁的 Activity。 这是内存泄漏。
如果你需要应用程序上下文,请使用 AndroidViewModel,如此 codelab 中所示。
重要提示:ViewModel 不是 onSaveInstanceState( ) 方法的替代品,因为 ViewModel 在进程关闭后无法存活。 在这里了解更多。
要了解有关 ViewModel 类的更多信息,请观看 Architecture Components: ViewModel 视频。
要了解有关协同程序的更多信息,请查看 Coroutines codelab。
10. 添加 XML 布局
接下来,你需要为列表和条目添加 XML 布局。
这个 codelab 假设你熟悉用 XML 创建布局,因此我们只向你提供代码。
在 values/styles.xml 中为列表条目添加样式:
-
<!-- RecyclerView 条目的默认字体太小。边距是单词之间的简单分隔符。 -->
-
<style name="word_title">
-
<item name="android:layout_width">match_parent</item>
-
<item name="android:layout_height">26dp</item>
-
<item name="android:textSize">24sp</item>
-
<item name="android:textStyle">bold</item>
-
<item name="android:layout_marginBottom">6dp</item>
-
<item name="android:paddingLeft">8dp</item>
-
</style>
添加 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 / content_main.xml 中,将 TextView 替换 为RecyclerView:
-
<android.support.v7.widget.RecyclerView
-
android:id="@+id/recyclerview"
-
android:layout_width="match_parent"
-
android:layout_height="match_parent"
-
android:background="@android:color/darker_gray"
-
tools:listitem="@layout/recyclerview_item" />
你的浮动操作按钮(FAB)应与可用操作对应。在 layout / activity_main.xml 文件中,为 FloatingActionButton 指定一个+符号图标:
- 在layout / activity_main.xml文件中,选择 File > New > Vector Asset。
- 在 Icon 中单击 Android 机器人图标:: field,然后选择
+
("添加") 资源。 - 更改布局文件代码如下。
android:src="@drawable/ic_add_black_24dp"
11. 添加 RecyclerView
你将在 RecyclerView 中显示数据,这比仅仅在 TextView 中抛出数据要好一些。 此 codelab 假设你了解 RecyclerView
,RecyclerView.LayoutManager
, RecyclerView.ViewHolder
和 RecyclerView.Adapter
的工作原理。
请注意,适配器中的 words 变量会缓存数据。 在下一个任务中,你将添加自动更新数据的代码。
添加一个继承 RecyclerView.Adapter 的类 WordListAdapter。 这是代码。
-
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
-
}
在 MainAcitvity 的 onCreate( ) 方法中添加 RecyclerView。
在 onCreate()方法中:
-
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
-
val adapter = WordListAdapter(this)
-
recyclerView.adapter = adapter
-
recyclerView.layoutManager = LinearLayoutManager(this)
运行你的 app 以确保一切正常。 没有条目,因为你尚未连接数据,因此 app 应显示灰色背景,没有任何列条目。
12. 填充数据库
数据库中没有数据。你将以两种方式添加数据:在打开数据库时添加一些数据,并添加一个用于添加单词的 Activity。
要在应用程序启动时删除所有内容并重新填充数据库,你需要创建 RoomDatabase.Callback
并重写 onOpen( )。 因为你无法在UI线程上执行 Room 数据库操作,所以 onOpen( ) 会在 IO Dispatcher 上启动协同程序。
为了启动协同程序,我们需要一个 CoroutineScope
。更新 RoomDatabase 类的 getDatabase 方法,以获得作为参数的协同程序 scope:
-
fun getDatabase(
-
context: Context,
-
scope: CoroutineScope
-
): WordRoomDatabase {
-
...
-
}
更新 WordViewModel 的 init 块中的数据库检索,以传递 scope
val wordsDao = WordRoomDatabase.getDatabase(application, scope).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(Dispatchers.IO) {
-
populateDatabase(database.wordDao())
-
}
-
}
-
}
-
}
下面是删除数据库内容的函数的代码,然后使用 “Hello” 和 “World” 这两个单词填充它。请随意添加更多的单词!
-
fun populateDatabase(wordDao: WordDao) {
-
wordDao.deleteAll()
-
var word = Word("Hello")
-
wordDao.insert(word)
-
word = Word("World!")
-
wordDao.insert(word)
-
}
最后,在调用 .build( ) 之前将回调添加到数据库 build 序列。
.addCallback(WordDatabaseCallback(scope))
13. 添加 NewWordActivity
在 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">#d3d3d3</color>
在 values/dimens.xml 中添加这些尺寸资源:
-
<dimen name="small_padding">6dp</dimen>
-
<dimen name="big_padding">16dp</dimen>
使用 Empty Activity 模板创建新活动NewWordActivity。 验证 activity 是否已添加到 Android Manifest!<
activity android:name=".NewWordActivity"
></
activity
>
以下是 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"
-
}
-
}
使用以下代码更新 layout 文件夹中的 activity_new_word.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="match_parent">
-
<EditText
-
android:id="@+id/edit_word"
-
android:layout_width="match_parent"
-
android:layout_height="wrap_content"
-
android:fontFamily="sans-serif-light"
-
android:hint="@string/hint_word"
-
android:inputType="textAutoComplete"
-
android:padding="@dimen/small_padding"
-
android:layout_marginBottom="@dimen/big_padding"
-
android:layout_marginTop="@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:textColor="@color/buttonLabel" />
-
</LinearLayout>
14. 连接数据
最后一步是通过保存用户输入的新单词并在 RecyclerView 中显示单词数据库的当前内容,来将 UI 连接到数据库。
要显示数据库的当前内容,请添加一个观察者,该观察者在 ViewModel 中观察 LiveData。 每当数据发生更改时,都会调用 onChanged( ) 回调,调用适配器的 setWord( ) 方法来更新适配器的缓存数据,并刷新显示的列表。
在 MainActivity 中,为 ViewModel 创建一个成员变量:
private lateinit var wordViewModel: WordViewModel
使用 ViewModelProviders
将 ViewModel 与你的 Activity 相关联。 当你的 Activity 首次启动时,ViewModelProviders 将创建 ViewModel。 当 activity 被销毁时,例如通过配置更改,ViewModel 仍然存在。 重新创建 activity 时,ViewModelProviders 将返回现有的 ViewModel。 有关更多信息,请参阅 ViewModel
。
在 onCreate( ) 中,从 ViewModelProvider 获取 ViewModel。
wordViewModel = ViewModelProviders.of(this).get(WordViewModel::class.java)
同样在 onCreate( ) 中,为getAllWords( ) 返回的 LiveData 添加一个观察者。
当观察到的数据发生变化且 activity 位于前台时,onChanged( ) 方法将触发。
-
wordViewModel.allWords.observe(this, Observer { words ->
-
// 更新适配器中单词的缓存副本。
-
words?.let { adapter.setWords(it) }
-
})
将请求代码定义为 MainActivity 的成员:
-
companion object {
-
const 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?.let {
-
val word = Word(it.getStringExtra(NewWordActivity.EXTRA_REPLY))
-
wordViewModel.insert(word)
-
}
-
} else {
-
Toast.makeText(
-
applicationContext,
-
R.string.empty_not_saved,
-
Toast.LENGTH_LONG).show()
-
}
-
}
在 MainActivity 中,当用户点击 FAB 时启动 NewWordActivity。 使用以下代码替换 FAB 的 onClick( ) 单击处理程序中的代码:
-
fab.setOnClickListener {
-
val intent = Intent(this@MainActivity, NewWordActivity::class.java)
-
startActivityForResult(intent, newWordActivityRequestCode)
-
}
运行你的APP!!!
在 NewWordActivity 中向数据库添加单词时,UI 将自动更新。
15. 总结
现在,你已经有了一个可用的 app,让我们回顾一下你构建的内容。下面是应用程序结构:
该应用程序的组件是:
MainActivity
: 使用 RecyclerView 和 WordListAdapter 在列表中显示单词。在 MainActivity 中,有一个 Observer,它从数据库中观察单词 LiveData,并在它们发生变化时得到通知。NewWordActivity:
在列表中添加一个新单词。WordViewModel
(*): 提供访问数据层的方法,并返回 LiveData,以便 MainActivity 可以建立观察者关系。LiveData<List<Word>>
: 使 UI 组件中的自动更新成为可能。 在 MainActivity 中,有一个 Observer,它从数据库中观察单词 LiveData,并在它们发生变化时得到通知。Repository:
管理一个或多个数据源。 Repository 公开了 ViewModel 与底层数据提供者交互的方法。 在这个 app 中,该后端是一个 Room 数据库。Room
: 是围绕并实现 SQLite 数据库的包装器。Room 为你做了很多你以前必须自己做的工作。- DAO: 将方法调用映射到数据库查询,以便当 Repository 调用 getAllWords( ) 之类的方法时,Room 可以执行
SELECT * from word_table ORDER BY word ASC
。 Word
: 是包含单个工作的实体类。
(*)Views 和 Activities(和 Fragments)仅通过 ViewModel 与数据交互。 因此,数据的来源无关紧要。
16. 进一步探索
解决方案代码包括 Room 数据库的单元测试。 测试超出了此 codelab 的范围。 如果你有兴趣,请查看代码。
如果你需要迁移应用程序,请在成功完成此代码框后查看 7 Steps to Room。 请注意,删除
SQLiteOpenHelper
类和许多其他代码是非常令人满意的。
当你有大量数据时,考虑使用 paging library。
- 应用程序架构指南
- Android 架构概述(视频)
- Android 持久化 codelab(
LiveData
, Room, DAO)- Android 生命周期感知组件 codelab(ViewModel,LiveData,LifecycleOwner,LifecycleRegistryOwner)
- 架构组件代码示例
17. 解决方案代码
[可选]下载解决方案代码
单击下面的链接来下载此 codelab 的解决方案代码:
解压下载的 zip 文件。 这将解压根文件夹,android-room-with-a-view-kotlin,其中包含完整的应用程序。