Android Room with a View - Java

1. 开始之前

架构组件 的目的是提供应用程序架构指南,并提供用于处理常见任务的库,例如生命周期管理和数据持久化。架构组件帮助您以更健壮、更可测试且更易维护的方式构建应用程序,并且代码更简洁。架构组件库是 Android Jetpack 的一部分。

这是代码实验室的 Java 编程语言版本。Kotlin 语言版本可以在 此处 找到。

如果您在完成此代码实验室时遇到任何问题(代码错误、语法错误、措辞不清楚等),请通过代码实验室左下角的“报告错误”链接报告问题。

先决条件

您需要熟悉 Java、面向对象设计概念和 Android 开发基础知识。特别是

  • RecyclerView 和适配器
  • SQLite 数据库和 SQLite 查询语言
  • 线程和 ExecutorService
  • 熟悉将数据与用户界面分离的软件架构模式很有帮助,例如 MVP 或 MVC。此代码实验室实现了 应用程序架构指南 中定义的架构。

此代码实验室专注于 Android 架构组件。提供了一些与主题无关的概念和代码,以便您简单地复制和粘贴。

此代码实验室提供构建完整应用程序所需的所有代码。

您将要做什么

在本代码实验室中,您将学习如何使用架构组件 Room、ViewModel 和 LiveData 设计和构建应用程序,并构建一个执行以下操作的应用程序:

  • 使用 Android 架构组件实现我们 推荐的架构
  • 使用数据库获取和保存数据,并使用一些单词预填充数据库。
  • MainActivity 中的 RecyclerView 中显示所有单词。
  • 当用户点击 + 按钮时,打开第二个活动。当用户输入一个单词时,将该单词添加到数据库和列表中。

该应用程序没有花哨的功能,但足够复杂,您可以将其用作构建应用程序的模板。以下是预览

您需要什么

  • 最新版本的 Android Studio 以及如何使用它的知识。确保 Android Studio 已更新,以及您的 SDK 和 Gradle。否则,您可能需要等到所有更新完成。
  • Android 设备或模拟器。

2. 使用架构组件

使用架构组件并实现推荐架构有很多步骤。最重要的是建立一个心理模型,了解各个部分是如何协同工作的以及数据是如何流动的。在完成此代码实验室时,不要仅仅复制和粘贴代码,而是尝试开始建立内在的理解。

以下是对架构组件及其工作原理的简短介绍。请注意,此代码实验室专注于组件的一个子集,即 LiveData、ViewModel 和 Room。每个组件在使用时都会进行更详细的解释。

此图显示了此架构的基本形式

8e4b761713e3a76b.png

实体使用 Room 时描述数据库表的注释类。

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

DAO数据访问对象。将 SQL 查询映射到函数。当您使用 DAO 时,调用方法,Room 会处理其余部分。

Room 数据库简化数据库工作并充当底层 SQLite 数据库的访问点(隐藏 SQLiteOpenHelper)。Room 数据库使用 DAO 向 SQLite 数据库发出查询。

存储库:用于管理多个数据源。

ViewModel充当存储库(数据)和 UI 之间的通信中心。UI 不再需要担心数据的来源。ViewModel 实例在 Activity/Fragment 重建后仍然存在。

LiveData可以被 观察 的数据持有类。始终保存/缓存数据的最新版本,并在数据发生更改时通知其观察者。 LiveData 具有生命周期感知能力。UI 组件只需观察相关数据,并且不会停止或恢复观察。LiveData 会自动管理所有这些,因为它了解观察时相关的生命周期状态更改。

RoomWordSample **架构概述**

下图显示了应用程序的所有部分。每个封闭框(SQLite 数据库除外)都代表您将要创建的类。

a70aca8d4b737712.png

3. 创建您的应用程序

  1. 打开 Android Studio,然后点击 **启动新的 Android Studio 项目**。
  2. 在“创建新项目”窗口中,选择 **空活动并点击下一步**。
  3. 在下一个屏幕上,将应用程序命名为 RoomWordSample,然后点击 **完成**。

9b6cbaec81794071.png

4. 更新 Gradle 文件

接下来,您需要将组件库添加到您的 Gradle 文件。

  1. 在 Android Studio 中,点击“项目”标签,然后展开“Gradle Scripts”文件夹。
  2. 打开 build.gradle (**模块:app**)。
  3. android 块内添加以下 compileOptions 块,将目标和源兼容性设置为 1.8,这将允许我们稍后使用 JDK 8 lambda
compileOptions {
    sourceCompatibility = 1.8
    targetCompatibility = 1.8
}
  1. dependencies 块替换为
dependencies {
    implementation "androidx.appcompat:appcompat:$rootProject.appCompatVersion"

    // 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-runtime:$rootProject.roomVersion"
    annotationProcessor "androidx.room:room-compiler:$rootProject.roomVersion"
    androidTestImplementation "androidx.room:room-testing:$rootProject.roomVersion"

    // Lifecycle components
    implementation "androidx.lifecycle:lifecycle-viewmodel:$rootProject.lifecycleVersion"
    implementation "androidx.lifecycle:lifecycle-livedata:$rootProject.lifecycleVersion"
    implementation "androidx.lifecycle:lifecycle-common-java8:$rootProject.lifecycleVersion"

    // 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. 在您的 build.gradle (**项目:RoomWordsSample** ) 文件中,根据下方的代码,在文件的末尾添加版本号
ext {
    appCompatVersion = '1.5.1'
    constraintLayoutVersion = '2.1.4'
    coreTestingVersion = '2.1.0'
    lifecycleVersion = '2.3.1'
    materialVersion = '1.3.0'
    roomVersion = '2.3.0'
    // testing
    junitVersion = '4.13.2'
    espressoVersion = '3.4.0'
    androidxJunitVersion = '1.1.2'
}
  1. 同步您的项目。

5. 创建实体

此应用程序的数据是单词,您需要一个简单的表来保存这些值

3821ac1a6cb01278.png

架构组件允许您通过 实体 创建一个。现在让我们来做这个。

  1. 创建一个名为 Word 的新类文件。此类将描述用于您的单词的实体(表示 SQLite 表)。类中的每个属性都表示表中的一个列。Room 最终将使用这些属性来创建表并从数据库行实例化对象。以下是如何编写代码
public class Word {

   private String mWord;

   public Word(@NonNull String word) {this.mWord = word;}

   public String getWord(){return this.mWord;}
}

为了使 Word 类对 Room 数据库有意义,您需要对其进行注释。注释标识此类各部分与数据库条目的关系。Room 使用此信息生成代码。

  1. 使用注释更新您的 Word 类,如下面的代码所示
@Entity(tableName = "word_table")
public class Word {

   @PrimaryKey
   @NonNull
   @ColumnInfo(name = "word")
   private String mWord;

   public Word(@NonNull String word) {this.mWord = word;}

   public String getWord(){return this.mWord;}
}

让我们看看这些注释的作用

  • @Entity(tableName = "word_table") 每个 @Entity 类都表示一个 SQLite 表。注释您的类声明以指示它是一个实体。如果希望表名称与类名称不同,则可以指定表名称。这将表命名为“word_table”。
  • @PrimaryKey 每个实体都需要一个主键。为了简单起见,每个单词都充当其自己的主键。
  • @NonNull 表示参数、字段或方法返回值永远不能为 null。
  • @ColumnInfo(name = "word") 如果希望列名与成员变量名称不同,则指定表中列的名称。
  • 存储在数据库中的每个字段都必须是公共的或具有“getter”方法。此示例提供了一个 getWord() 方法。

您可以在 Room 包摘要参考 中找到完整的注释列表。

提示:您可以通过以下方式 自动生成 唯一键

@Entity(tableName = "word_table")
public class Word {

    @PrimaryKey(autoGenerate = true)
    private int id;

    @NonNull
    private String word;
    //..other fields, getters, setters
}

6. 创建 DAO

什么是 DAO?

一个 DAO(数据访问对象)在编译时验证您的 SQL 并将其与方法相关联。在您的 Room DAO 中,您可以使用方便的注释,例如 @Insert,来表示最常见的数据库操作!Room 使用 DAO 为您的代码创建干净的 API。

DAO 必须是接口或抽象类。默认情况下,所有查询都必须在单独的线程上执行。

实现 DAO

让我们编写一个 DAO,它提供以下查询:

  • 按字母顺序获取所有单词
  • 插入单词
  • 删除所有单词
  1. 创建一个名为 WordDao 的新类文件。
  2. 将以下代码复制粘贴到 WordDao 中,并根据需要修复导入以使其编译
@Dao
public interface WordDao {

   // allowing the insert of the same word multiple times by passing a 
   // conflict resolution strategy
   @Insert(onConflict = OnConflictStrategy.IGNORE)
   void insert(Word word);

   @Query("DELETE FROM word_table")
   void deleteAll();

   @Query("SELECT * FROM word_table ORDER BY word ASC")
   List<Word> getAlphabetizedWords();
}

让我们一步一步地来

  • WordDao 是一个接口;DAOs 必须是接口或抽象类。
  • @Dao 注解将其标识为 Room 的 DAO 类。
  • void insert(Word word); 声明了一个插入一个单词的方法
  • @Insert 注解 是一个特殊的 DAO 方法注解,您无需提供任何 SQL! (还有 @Delete@Update 注解用于删除和更新行,但您在这个应用程序中不使用它们。)
  • onConflict = OnConflictStrategy.IGNORE: 选择的冲突策略会忽略与列表中已有的完全相同的新的单词。要了解有关可用冲突策略的更多信息,请查看 文档
  • deleteAll(): 声明了一个删除所有单词的方法。
  • 没有用于删除多个实体的便捷注解,因此它用通用 @Query 进行注解。
  • @Query("DELETE FROM word_table"): @Query 需要您将 SQL 查询作为字符串参数提供给注解。
  • List<Word> getAlphabetizedWords(): 一个获取所有单词并将它返回 List Words 的方法。
  • @Query("SELECT * FROM word_table ORDER BY word ASC"): 返回按升序排序的单词列表。

7. LiveData 类

当数据发生变化时,您通常希望采取一些操作,例如在 UI 中显示更新后的数据。这意味着您必须观察数据,以便在数据发生变化时做出反应。

根据数据的存储方式,这可能很棘手。观察应用程序多个组件中的数据变化可能会在组件之间创建显式、严格的依赖路径。这使得测试和调试变得困难,等等。

LiveData,一个用于数据观察的 生命周期库 类,解决了这个问题。在您的方法描述中使用 LiveData 类型的返回值,Room 会生成所有必要的代码来在数据库更新时更新 LiveData

WordDao 中,更改 getAlphabetizedWords() 方法签名,以便用 LiveData 包裹返回的 List<Word>

   @Query("SELECT * FROM word_table ORDER BY word ASC")
   LiveData<List<Word>> getAlphabetizedWords();

稍后在本代码实验室中,您将在 MainActivity 中通过 Observer 跟踪数据更改。

8. 添加 Room 数据库

什么是 Room 数据库?

  • Room 是 SQLite 数据库之上的数据库层。
  • Room 负责处理您过去用 SQLiteOpenHelper 处理的平凡任务。
  • Room 使用 DAO 向其数据库发出查询。
  • 默认情况下,为了避免糟糕的 UI 性能,Room 不允许您在主线程上发出查询。当 Room 查询返回 LiveData 时,查询会自动在后台线程上异步运行。
  • Room 提供了 SQLite 语句的编译时检查。

实现 Room 数据库

您的 Room 数据库类必须是抽象的,并扩展 RoomDatabase。通常,您只需要应用程序中一个 Room 数据库的实例。

让我们现在创建一个。创建一个名为 WordRoomDatabase 的类文件,并将以下代码添加到其中

@Database(entities = {Word.class}, version = 1, exportSchema = false)
public abstract class WordRoomDatabase extends RoomDatabase {

   public abstract WordDao wordDao();

   private static volatile WordRoomDatabase INSTANCE;
   private static final int NUMBER_OF_THREADS = 4;
   static final ExecutorService databaseWriteExecutor =
        Executors.newFixedThreadPool(NUMBER_OF_THREADS);

   static WordRoomDatabase getDatabase(final Context context) {
        if (INSTANCE == null) {
            synchronized (WordRoomDatabase.class) {
                if (INSTANCE == null) {
                    INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
                            WordRoomDatabase.class, "word_database")
                            .build();
                }
            }
        }
        return INSTANCE;
    }
}

让我们逐步浏览代码

  • Room 的数据库类必须是 abstract 并扩展 RoomDatabase
  • 您使用 @Database 注解数据库类,并使用注解参数来声明数据库中的实体并设置版本号。每个实体对应于数据库中将创建的一个表。数据库迁移超出了本代码实验室的范围,因此我们在这里将 exportSchema 设置为 false 以避免构建警告。在实际应用中,您应该考虑设置 Room 用于导出模式的目录,以便您可以将当前模式检查到您的版本控制系统中。
  • 数据库通过每个 @Dao 的抽象“getter”方法公开 DAOs。
  • 我们定义了一个 单例WordRoomDatabase, 以防止同时打开数据库的多个实例。
  • getDatabase 返回单例。它会在第一次访问时创建数据库,使用 Room 的数据库生成器根据 WordRoomDatabase 类在应用程序上下文中创建 RoomDatabase 对象,并将其命名为 "word_database"
  • 我们创建了一个具有固定线程池的 ExecutorService,您将使用它在后台线程上异步运行数据库操作。

9. 创建 Repository

什么是 Repository?

Repository 类抽象访问多个数据源。Repository 不是架构组件库的一部分,而是代码分离和架构的建议最佳实践。 Repository 类为应用程序的其他部分提供干净的数据访问 API。

cdfae5b9b10da57f.png

为什么要使用 Repository?

Repository 管理查询,并允许您使用多个后端。在最常见的示例中,Repository 实现逻辑以确定是 从网络获取数据还是使用本地数据库中缓存的结果。

实现 Repository

创建一个名为 WordRepository 的类文件,并将以下代码粘贴到其中

class WordRepository {

    private WordDao mWordDao;
    private LiveData<List<Word>> mAllWords;

    // Note that in order to unit test the WordRepository, you have to remove the Application
    // dependency. This adds complexity and much more code, and this sample is not about testing.
    // See the BasicSample in the android-architecture-components repository at
    // https://github.com/googlesamples
    WordRepository(Application application) {
        WordRoomDatabase db = WordRoomDatabase.getDatabase(application);
        mWordDao = db.wordDao();
        mAllWords = mWordDao.getAlphabetizedWords();
    }

    // Room executes all queries on a separate thread.
    // Observed LiveData will notify the observer when the data has changed.
    LiveData<List<Word>> getAllWords() {
        return mAllWords;
    }

    // You must call this on a non-UI thread or your app will throw an exception. Room ensures
    // that you're not doing any long running operations on the main thread, blocking the UI.
    void insert(Word word) {
        WordRoomDatabase.databaseWriteExecutor.execute(() -> {
            mWordDao.insert(word);
        });
    }
}

主要要点

  • DAO 作为参数传递到 Repository 构造函数,而不是整个数据库。这是因为您只需要访问 DAO,因为它包含数据库的所有读/写方法。您不需要将整个数据库暴露给 Repository。
  • getAllWords 方法返回来自 Room 的 LiveData 单词列表;我们可以这样做,因为我们在“LiveData 类”步骤中如何定义 getAlphabetizedWords 方法以返回 LiveData。Room 在单独的线程上执行所有查询。然后,观察的 LiveData 将在主线程上通知观察者数据已更改。
  • 我们需要不在主线程上运行插入,因此我们使用在 WordRoomDatabase 中创建的 ExecutorService 在后台线程上执行插入。

10. 创建 ViewModel

什么是 ViewModel?

ViewModel 的作用是为 UI 提供数据并生存配置更改。 ViewModel 充当 Repository 和 UI 之间的通信中心。您还可以使用 ViewModel 在片段之间共享数据。 ViewModel生命周期库 的一部分。

72848dfccfe5777b.png

有关此主题的入门指南,请参阅 ViewModel 概览ViewModels: A Simple Example 博客文章。

为什么要使用 ViewModel?

ViewModel 以生命周期感知的方式保存应用程序的 UI 数据,并生存配置更改。将应用程序的 UI 数据与 ActivityFragment 类分离,可以让您更好地遵循单一职责原则:您的活动和片段负责将数据绘制到屏幕上,而您的 ViewModel 可以负责保存和处理 UI 所需的所有数据。

ViewModel 中,使用 LiveData 表示可变数据,UI 将使用或显示这些数据。使用 LiveData 有几个好处

  • 您可以对数据设置观察者(而不是轮询更改),并且只有在数据实际发生更改时才更新 UI。
  • Repository 和 UI 通过 ViewModel 完全分离。
  • ViewModel 没有数据库调用(所有这些都由 Repository 处理),这使得代码更易于测试。

实现 ViewModel

WordViewModel 创建一个类文件,并将以下代码添加到其中

public class WordViewModel extends AndroidViewModel {

   private WordRepository mRepository;

   private final LiveData<List<Word>> mAllWords;

   public WordViewModel (Application application) {
       super(application);
       mRepository = new WordRepository(application);
       mAllWords = mRepository.getAllWords();
   }

   LiveData<List<Word>> getAllWords() { return mAllWords; }

   public void insert(Word word) { mRepository.insert(word); }
}

这里我们

  • 创建一个名为 WordViewModel 的类,该类获取 Application 作为参数,并扩展 AndroidViewModel
  • 添加一个私有成员变量来保存对存储库的引用。
  • 添加一个 getAllWords() 方法,以返回缓存的单词列表。
  • 实现一个构造函数,用于创建 WordRepository
  • 在构造函数中,使用存储库初始化 allWords LiveData。
  • 创建了一个包装器 insert() 方法,该方法调用存储库的 insert() 方法。通过这种方式,可以将 insert() 的实现封装在 UI 之外。

11. 添加 XML 布局

接下来,您需要为列表和项目添加 XML 布局。

本代码示例假定您熟悉在 XML 中创建布局,因此我们只为您提供代码。

通过将 AppTheme 父级设置为 Theme.MaterialComponents.Light.DarkActionBar,使您的应用程序主题为材料主题。在 values/styles.xml 中添加列表项的样式

<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.MaterialComponents.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

    <!-- The default font for RecyclerView items is too small.
    The margin is a simple delimiter between the words. -->
    <style name="word_title">
        <item name="android:layout_width">match_parent</item>
        <item name="android:layout_marginBottom">8dp</item>
        <item name="android:paddingLeft">8dp</item>
        <item name="android:background">@android:color/holo_orange_light</item>
        <item name="android:textAppearance">@android:style/TextAppearance.Large</item>
    </style>
</resources>

添加一个 layout/recyclerview_item.xml 布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" 
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/textView"
        style="@style/word_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/holo_orange_light" />
</LinearLayout>

layout/activity_main.xml 中,将 TextView 替换为 RecyclerView,并添加一个悬浮操作按钮 (FAB)。现在您的布局应该如下所示

<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerview"
        android:layout_width="0dp"
        android:layout_height="0dp"
        tools:listitem="@layout/recyclerview_item"
        android:padding="@dimen/big_padding"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:contentDescription="@string/add_word"/>

</androidx.constraintlayout.widget.ConstraintLayout>

您的悬浮操作按钮 (FAB) 的外观应对应于可用的操作,因此我们将希望将图标替换为 + 符号。

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

  1. 选择 **文件 > 新建 > 矢量资产**。
  2. 点击 **图标** 中的 Android 机器人图标

de077eade4adf77.png

  1. 搜索“添加”,并选择 ‘+’ 资产。点击 **确定**。

6e16b8d3a4342be9.png

  1. 之后,点击 **下一步**。 de62e3548d443c7f.png
  2. 确认图标路径为 main > drawable,然后点击 **完成** 添加资产。

2922fa2214257ec1.png

  1. 仍然在 layout/activity_main.xml 中,更新 FAB 以包含新的 drawable
<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"/>

12. 添加 RecyclerView

您将使用 RecyclerView 显示数据,这比仅仅将数据扔进 TextView 好一些。本代码示例假定您了解 RecyclerViewRecyclerView.ViewHolderListAdapter 的工作原理。

首先创建一个新文件来保存 ViewHolder,该文件显示一个 Word。创建一个绑定方法来设置文本。

class WordViewHolder extends RecyclerView.ViewHolder {
    private final TextView wordItemView;

    private WordViewHolder(View itemView) {
        super(itemView);
        wordItemView = itemView.findViewById(R.id.textView);
    }

    public void bind(String text) {
        wordItemView.setText(text);
    }

    static WordViewHolder create(ViewGroup parent) {
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.recyclerview_item, parent, false);
        return new WordViewHolder(view);
    }
}

创建一个类 WordListAdapter,该类扩展 ListAdapter。创建 DiffUtil.ItemCallback 实现为 WordListAdapter 中的静态类。以下是代码

public class WordListAdapter extends ListAdapter<Word, WordViewHolder> {

    public WordListAdapter(@NonNull DiffUtil.ItemCallback<Word> diffCallback) {
        super(diffCallback);
    }

    @Override
    public WordViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return WordViewHolder.create(parent);
    }

    @Override
    public void onBindViewHolder(WordViewHolder holder, int position) {
        Word current = getItem(position);
        holder.bind(current.getWord());
    }

    static class WordDiff extends DiffUtil.ItemCallback<Word> {

        @Override
        public boolean areItemsTheSame(@NonNull Word oldItem, @NonNull Word newItem) {
            return oldItem == newItem;
        }

        @Override
        public boolean areContentsTheSame(@NonNull Word oldItem, @NonNull Word newItem) {
            return oldItem.getWord().equals(newItem.getWord());
        }
    }
}

onCreate() 方法中添加 RecyclerView MainActivity

onCreate() 方法中,在 setContentView 之后

RecyclerView recyclerView = findViewById(R.id.recyclerview);
final WordListAdapter adapter = new WordListAdapter(new WordListAdapter.WordDiff());
recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(new LinearLayoutManager(this));

运行您的应用程序以确保一切正常。没有项目,因为您还没有连接数据。

79cb875d4296afce.png

13. 填充数据库

数据库中没有数据。您将通过两种方式添加数据:在数据库打开时添加一些数据,以及添加一个添加单词的 Activity

为了在安装应用程序时删除所有内容并填充数据库,您可以创建一个 RoomDatabase.Callback 并覆盖 onCreate()

以下是在 WordRoomDatabase 类中创建回调的代码。由于您无法在 UI 线程上执行 Room 数据库操作,因此 onCreate() 使用先前定义的 databaseWriteExecutor 在后台线程上执行 lambda。lambda 删除数据库的内容,然后用两个单词“Hello”和“World”填充它。随意添加更多单词!

private static RoomDatabase.Callback sRoomDatabaseCallback = new RoomDatabase.Callback() {
    @Override
    public void onCreate(@NonNull SupportSQLiteDatabase db) {
        super.onCreate(db);

        // If you want to keep data through app restarts,
        // comment out the following block
        databaseWriteExecutor.execute(() -> {
            // Populate the database in the background.
            // If you want to start with more words, just add them.
            WordDao dao = INSTANCE.wordDao();
            dao.deleteAll();

            Word word = new Word("Hello");
            dao.insert(word);
            word = new Word("World");
            dao.insert(word);
        });
    }
};

然后,在数据库构建序列中添加回调,在 Room.databaseBuilder() 上调用 .build() 之前。

.addCallback(sRoomDatabaseCallback)

14. 添加 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">#FFFFFF</color>

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

  1. 选择 **文件 > 新建 > Android 资源文件**。
  2. 从 **可用限定符** 中选择 **尺寸**。
  3. 设置文件名:dimens

aa5895240838057.png

values/dimens.xml 中添加这些尺寸资源

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

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

  1. 选择 **文件 > 新建 > Activity > 空 Activity**
  2. 输入 NewWordActivity 作为 Activity 名称。
  3. 验证新活动是否已添加到 Android Manifest。
<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>

更新活动代码

public class NewWordActivity extends AppCompatActivity {

   public static final String EXTRA_REPLY = "com.example.android.wordlistsql.REPLY";

   private  EditText mEditWordView;

   @Override
   public void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_new_word);
       mEditWordView = findViewById(R.id.edit_word);

       final Button button = findViewById(R.id.button_save);
       button.setOnClickListener(view -> {
           Intent replyIntent = new Intent();
           if (TextUtils.isEmpty(mEditWordView.getText())) {
               setResult(RESULT_CANCELED, replyIntent);
           } else {
               String word = mEditWordView.getText().toString();
               replyIntent.putExtra(EXTRA_REPLY, word);
               setResult(RESULT_OK, replyIntent);
           }
           finish();
       });
   }
}

15. 连接数据

最后一步是通过保存用户输入的新单词并将单词数据库的当前内容显示在 RecyclerView 中来连接 UI 到数据库。

要显示数据库的当前内容,请添加一个观察者来观察 ViewModel 中的 LiveData

每当数据发生变化时,都会调用 onChanged() 回调,该回调调用适配器的 setWords() 方法来更新适配器缓存的数据并刷新显示列表。

MainActivity 中,为 ViewModel 创建一个成员变量

private WordViewModel mWordViewModel;

使用 ViewModelProvider 将您的 ViewModel 与您的 Activity 关联。

当您的 Activity 首次启动时,ViewModelProviders 将创建 ViewModel。当活动被销毁时(例如通过配置更改),ViewModel 会保留。当活动重新创建时,ViewModelProviders 返回现有的 ViewModel。有关更多信息,请参阅 ViewModel

onCreate()RecyclerView 代码块下方,从 ViewModelProvider 获取 ViewModel

mWordViewModel = new ViewModelProvider(this).get(WordViewModel.class);

同样在 onCreate() 中,为 getAlphabetizedWords() 返回的 LiveData 添加一个观察者。当观察到的数据发生变化且活动处于前台时,onChanged() 方法触发

mWordViewModel.getAllWords().observe(this, words -> {
    // Update the cached copy of the words in the adapter.
    adapter.submitList(words);
});

将请求码定义为 MainActivity 的成员

public static final int NEW_WORD_ACTIVITY_REQUEST_CODE = 1;

MainActivity 中,添加 onActivityResult() 代码以处理 NewWordActivity

如果活动返回 RESULT_OK,则通过调用 WordViewModelinsert() 方法将返回的单词插入数据库。

public void onActivityResult(int requestCode, int resultCode, Intent data) {
   super.onActivityResult(requestCode, resultCode, data);

   if (requestCode == NEW_WORD_ACTIVITY_REQUEST_CODE && resultCode == RESULT_OK) {
       Word word = new Word(data.getStringExtra(NewWordActivity.EXTRA_REPLY));
       mWordViewModel.insert(word);
   } else {
       Toast.makeText(
               getApplicationContext(),
               R.string.empty_not_saved,
               Toast.LENGTH_LONG).show();
   }
}

MainActivity, 当用户点击 FAB 时启动 NewWordActivity。在 MainActivity onCreate 中,找到 FAB 并添加一个 onClickListener,其代码如下

FloatingActionButton fab = findViewById(R.id.fab);
fab.setOnClickListener( view -> {
   Intent intent = new Intent(MainActivity.this, NewWordActivity.class);
   startActivityForResult(intent, NEW_WORD_ACTIVITY_REQUEST_CODE);
});

现在,运行您的应用程序!当您在 NewWordActivity 中将单词添加到数据库时,UI 将自动更新。

16. 总结

现在您已经拥有一个正在运行的应用程序,让我们回顾一下您构建的内容。以下是应用程序结构:

a70aca8d4b737712.png

应用程序的组件是

  • MainActivity:使用 RecyclerViewWordListAdapter 显示列表中的单词。在 MainActivity 中,有一个观察者观察来自数据库的 words LiveData,并在它们发生变化时收到通知。
  • NewWordActivity:将新单词添加到列表。
  • WordViewModel:提供访问数据层的函数,并返回 LiveData 以便 MainActivity 可以建立观察者关系。*
  • LiveData<List<Word>>:使 UI 组件中的自动更新成为可能。在 MainActivity 中,有一个观察者观察来自数据库的 words LiveData,并在它们发生变化时收到通知。
  • Repository:管理一个或多个数据源。Repository 公开方法供 ViewModel 与底层数据提供程序交互。在本应用程序中,后端是 Room 数据库。
  • Room: 是对 SQLite 数据库的封装和实现。Room 为你做了很多以前需要你自己做的工作。
  • DAO: 将方法调用映射到数据库查询,因此当 Repository 调用诸如 getAlphabetizedWords() 之类的方法时,Room 可以执行 **SELECT * FROM word_table ORDER BY word ASC**。
  • Word: 是包含单个单词的实体类。
  • 视图和活动(以及片段)仅通过 ViewModel 与数据交互。因此,数据来自哪里并不重要。

17. 恭喜你!

[可选] 下载解决方案代码

如果您还没有,您可以查看代码实验室的解决方案代码。您可以查看 GitHub 存储库 或在此处下载代码

解压缩下载的 zip 文件。这将解压缩一个根文件夹,android-room-with-a-view-master,其中包含完整的应用程序。