带 View 的 Android Room - Kotlin

架构组件的用途是提供关于应用架构的指南,其中包括生命周期管理和数据持久性处理等常见任务的库。架构组件可帮助您以稳健、可测试和易维护的方式组织应用结构,且所需样板代码较少。架构组件库是 Android Jetpack 的一部分。

本 Codelab 为 Kotlin 版本。如需查看 Java 编程语言的版本,请点击此处

如果在此 Codelab 操作期间遇到任何问题(代码错误、语法错误、措辞含义不明等),都可以通过 Codelab 左下角的报告错误链接报告相应问题。

前提条件

您需要熟悉 Kotlin、面向对象的设计概念以及 Android 开发方面的基础知识,尤其是:

此外,熟悉一下将数据与界面分离的软件架构模式(例如 MVP 或 MVC)也能有所帮助。本 Codelab 将实现应用架构指南中定义的架构。

本 Codelab 主要介绍 Android 架构组件。与主题无关的其他概念和代码主要是供您直接复制和粘贴的。

如果您不熟悉 Kotlin,可在此处查看 Java 编程语言版本的 Codelab。

您应执行的操作

在此 Codelab 中,您将学习如何使用架构组件 Room、ViewModel 和 LiveData 来设计和构建应用,以及如何构建执行以下操作的应用:

  • 使用 Android 架构组件来实现我们的推荐架构
  • 与数据库配合使用以获取和保存数据,并用一些字词预填充数据库。
  • 显示 MainActivity 中的 RecyclerView 中的所有字词。
  • 在用户点按“+”按钮时打开一个新 activity。当用户输入某个字词时,该字词就会添加到数据库和列表中。

该应用很简单,但其复杂程度也足够您将其作为构建模板。预览如下:

您需要满足的条件

本 Codelab 提供了构建完整应用所需的所有代码。

使用架构组件和实现推荐架构的步骤有很多。最重要的是,创建一个构思,构思整个过程,理解各个部分是如何组合在一起的,以及数据是如何传输的。您在学习本 Codelab 时,不要只是复制和粘贴代码,而应尝试深入了解架构。

下面将简要介绍架构组件及其协作方式。请注意,本 Codelab 将重点介绍部分组件,即 LiveData、ViewModel 和 Room,我们会在使用过程中详细介绍每个组件。

下图所示为架构的基本组成部分:

实体:使用 Room 时用于描述数据库表的带注解的类。

SQLite 数据库:设备上的存储空间。Room 持久性库会为您创建和维护此数据库。

DAO:数据访问对象。从 SQL 查询到函数的映射。在使用 DAO 时,您需要调用相应方法,其余操作均由 Room 处理。

Room 数据库:可简化数据库工作,并充当 SQLite 底层数据库的接入点(隐藏 SQLiteOpenHelper)。它使用 DAO 向 SQLite 数据库发出查询请求。

存储库:您创建的类,主要用于管理多个数据源。

ViewModel:充当存储库(数据)和界面之间的通信中心。对于界面而言,数据来源不再是一个需要关注的问题。ViewModel 实例在重新创建 activity/fragment 后仍然存在。

LiveData:一种可观察的数据存储器类。务必保存/缓存最新版本的数据,并在数据发生变化时通知其监测者。LiveData 具有生命周期感知能力。界面组件只是监测相关数据,不会停止或恢复监测。LiveData 将自动管理所有这些操作,因为它在监测时可以感知相关的生命周期状态变化。

RoomWordSample 架构概览

下图所示为应用的所有组成部分。每个框(SQLite 数据库除外)均代表要创建的类。

  1. 打开 Android Studio,然后点击 Start a new Android Studio project
  2. 在“Create New Project”窗口中,选择 Empty Activity,然后点击 Next
  3. 在下一个屏幕上,将应用命名为 RoomWordSample,然后点击 Finish

接下来,您必须将组件库添加到 Gradle 文件中。

  1. 在 Android Studio 中,点击“Projects”标签页,然后展开“Gradle Scripts”文件夹。

打开 build.gradle (Module: app)。

  1. 应用 kapt 注解处理器 Kotlin 插件,方法是在 build.gradle模块:应用)文件顶部定义的其他插件之后添加此插件。
apply plugin: 'kotlin-kapt'
  1. android 代码块内添加 packagingOptions 代码块,以从软件包中排除原子函数模块并防止出现警告。
android {
    // other configuration (buildTypes, defaultConfig, etc.)

    packagingOptions {
        exclude 'META-INF/atomicfu.kotlin_module'
    }
}
  1. 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"
  1. 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 允许您通过实体创建表,现在就开始创建吧。

  1. 创建一个名为 Word 的新 Kotlin 类文件,其中包含 Word 数据类
    这个类用于描述字词的实体(代表 SQLite 表)。类中的每个属性代表表中的一列。Room 最终将使用这些属性来创建表并将数据库中的行对象实例化。

代码如下:

data class Word(val word: String)

为了使 Word 类对 Room 数据库有意义,您需要为其添加注解。注解可以标识该类的各个部分与数据库中的条目之间的关系。Room 会根据这些信息生成代码。

如果您自行输入注解(而非粘贴),Android Studio 将自动导入注解类。

  1. 使用以下代码所示的注解更新您的 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:

  • 获取所有字词(按字母顺序排序)
  • 插入字词
  • 删除所有字词
  1. 新建一个名为 WordDao 的 Kotlin 类文件。
  2. 复制以下代码并将其粘贴到 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>:一个用于获取所有字词并让其返回 WordsList 的方法。
  • @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 数据库实例。

我们来创建一个吧!

  1. 创建一个名为 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 字词列表来进行初始化,之所以能做到这一点,是因为我们在 getAlphabetizedWordsThe LiveData 类中定义 getAlphabetizedWords 方法以返回 LiveData。Room 将在单独的线程上执行所有查询。然后,观察到 LiveData 会在数据发生更改时在主线程上通知观察器。
  • suspend 修饰符会告知编译器需要从协程或其他挂起函数进行调用。

什么是 ViewModel?

ViewModel 的作用是向界面提供数据,不受配置变化的影响。ViewModel 充当存储库和界面之间的通信中心。您还可以使用 ViewModel 在 fragment 之间共享数据。ViewModel 是生命周期库的一部分。

如需查看有关此主题的入门指南,请参阅 ViewModel OverviewViewModel:简单示例博文。

为什么使用 ViewModel?

ViewModel 以一种可以感知生命周期的方式保存应用的界面数据,不受配置变化的影响。它会将应用的界面数据与 ActivityFragment 类区分开,让您更好地遵循单一责任原则: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 的外观应与可用操作相对应,因此,我们需要将该图标替换为 '+' 符号。

首先,我们需要添加一个新的矢量资源:

  1. 依次选择 File > New > Vector Asset
  2. 点击 Clip Art: 字段中的 Android 机器人图标。
  3. 搜索“添加”,然后选择“'+素材资源”。点击确定
  4. 然后,点击 Next
  5. 确认图标的路径为 main > drawable,然后点击 Finish 以添加资源。
  6. 仍是在 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 假定您了解 RecyclerViewRecyclerView.LayoutManagerRecyclerView.ViewHolderRecyclerView.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
}

MainActivityonCreate() 方法中添加 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 {
...
}

更新 WordViewModelinit 块中的数据库检索初始化程序也要传递范围:

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>

创建一个新的尺寸资源文件:

  1. 点击 Project 窗口中的应用模块。
  2. 依次选择 File > New > Android Resource File
  3. 在 Available Qualifiers 中,选择 Dimension
  4. 将文件名设置为“dimens”

values/dimens.xml 中添加以下尺寸资源:

<dimen name="small_padding">8dp</dimen>
<dimen name="big_padding">16dp</dimen>

使用“Empty Activity”模板创建一个空的新 Android Activity

  1. 依次选择 File > New > Activity > Empty Activity
  2. 输入 NewWordActivity 以将其用作 activity 的名称。
  3. 验证是否已将新的 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

使用 ViewModelProviderViewModelActivity 相关联。

首次启动 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,请通过调用 WordViewModelinsert() 方法将返回的字词插入到数据库中:

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:使用 RecyclerViewWordListAdapter 显示列表中的字词。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:包含单个字词的实体类。

* ViewsActivities(以及 Fragments)仅通过 ViewModel 与数据进行交互。因此,数据来自哪里并不重要。

用于界面(反应式界面)自动更新的数据流

界面能自动更新的原因是我们使用了 LiveData。MainActivity 中有一个 Observer,可用于观察数据库中的字词 LiveData,并在发生变化时接收通知。如果字词发生变化,则系统会执行观察者的 onChange() 方法来更新 WordListAdapter 中的 mWords

数据可以被观察到的原因在于它是 LiveData。被观察到的数据是由 WordViewModel allWords 属性返回的 LiveData<List<Word>>

WordViewModel 会隐藏界面层后端的一切信息。WordViewModel 提供用于访问数据层的方法,并返回 LiveData,以便 MainActivity 设置观察者关系。ViewsActivities(以及 Fragments)仅通过 ViewModel 与数据进行交互。因此,数据来自哪里并不重要。

在本例中,数据来自 RepositoryViewModel 无需知道存储库的交互对象。它只需要知道如何与 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,其中包含完整的应用。