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。在您使用每个组件时,都会对其进行更多解释。
此图显示了此架构的基本形式
SQLite 数据库:设备上的存储。Room 持久性库为您创建并维护此数据库。
DAO:数据访问对象。SQL 查询到函数的映射。当您使用 DAO 时,您调用方法,Room 负责其余操作。
Room 数据库:简化数据库工作并作为底层 SQLite 数据库的访问点(隐藏SQLiteOpenHelper)
。Room 数据库使用 DAO 向 SQLite 数据库发出查询。
存储库:用于管理多个数据源。
ViewModel:充当存储库(数据)和 UI 之间的通信中心。UI 不再需要担心数据的来源。ViewModel 实例在 Activity/Fragment 重新创建后仍然存在。
LiveData:一个可以观察的数据持有者类。始终持有/缓存最新版本的数据,并在数据更改时通知其观察者。LiveData
是生命周期感知的。UI 组件只需观察相关数据,而无需停止或恢复观察。LiveData 自动管理所有这些操作,因为它在观察时了解相关生命周期状态更改。
RoomWordSample架构概述
下图显示了应用程序的所有部分。每个封闭框(SQLite 数据库除外)都表示您将创建的一个类。
3. 创建您的应用程序
- 打开 Android Studio 并点击开始一个新的 Android Studio 项目。
- 在“创建新项目”窗口中,选择空活动并点击下一步。
- 在下一个屏幕上,将应用程序命名为 RoomWordSample,然后点击完成。
4. 更新 Gradle 文件
接下来,您必须将组件库添加到您的 Gradle 文件中。
- 在 Android Studio 中,点击“项目”选项卡并展开“Gradle 脚本”文件夹。
- 打开
build.gradle
(模块:应用程序)。 - 添加以下
compileOptions
块到android
块中,以将目标和源兼容性设置为 1.8,这将允许我们稍后使用 JDK 8 lambda 表达式
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
- 替换
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"
}
- 在您的
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'
}
- 同步您的项目。
5. 创建实体
此应用程序的数据是单词,您将需要一个简单的表来保存这些值
架构组件允许您通过实体来创建一个。让我们现在就来做这件事。
- 创建一个名为
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 使用此信息生成代码。
- 使用注释更新您的
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,它提供以下查询:
- 按字母顺序获取所有单词
- 插入一个单词
- 删除所有单词
- 创建一个名为
WordDao
的新类文件。 - 将以下代码复制粘贴到
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
是一个接口;DAO 必须是接口或抽象类。@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():
一个获取所有单词并将其返回为Word
的List
的方法。@Query(
"SELECT * FROM word_table ORDER BY word ASC"
)
:返回按升序排序的单词列表。
7. LiveData 类
当数据发生变化时,您通常希望执行某些操作,例如在 UI 中显示更新后的数据。这意味着您必须观察数据,以便在数据发生变化时,您可以做出反应。
根据数据的存储方式,这可能很棘手。在应用的多个组件之间观察数据变化可能会在组件之间创建显式、僵化的依赖路径。这使得测试和调试变得困难,等等。
LiveData
,一个用于数据观察的生命周期库 类,解决了这个问题。在方法描述中使用 LiveData
类型的返回值,Room 会生成所有必要的代码来更新数据库更新时的 LiveData
。
在 WordDao
中,更改 getAlphabetizedWords()
方法签名,以便将返回的 List<Word>
包裹在 LiveData
中。
@Query("SELECT * FROM word_table ORDER BY word ASC")
LiveData<List<Word>> getAlphabetizedWords();
在本 Codelab 的后面部分,您将通过 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
注解类为 Room 数据库,并使用注解参数声明属于数据库的实体并设置版本号。每个实体对应于将在数据库中创建的表。数据库迁移超出了本 Codelab 的范围,因此我们在此处将exportSchema
设置为 false 以避免构建警告。在实际应用中,您应该考虑为 Room 设置一个目录以用于导出架构,以便您可以将当前架构检入您的版本控制系统。 - 数据库通过每个 @Dao 的抽象“getter”方法公开 DAO。
- 我们定义了一个 单例,
WordRoomDatabase,
以防止同时打开数据库的多个实例。 getDatabase
返回单例。它将在第一次访问时创建数据库,使用 Room 的数据库构建器从WordRoomDatabase
类在应用上下文中创建一个RoomDatabase
对象,并将其命名为"word_database"
。- 我们创建了一个具有固定线程池的
ExecutorService
,您将使用它在后台线程上异步运行数据库操作。
9. 创建存储库
什么是存储库?
Repository
类抽象了对多个数据源的访问。存储库不是架构组件库的一部分,但它是代码分离和架构的建议最佳实践。Repository
类为应用的其余部分提供了干净的数据访问 API。
为什么要使用存储库?
存储库管理查询并允许您使用多个后端。在最常见的示例中,存储库实现用于确定是从网络获取数据还是使用本地数据库中缓存的结果的逻辑。
实现存储库
创建一个名为 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 传递到存储库构造函数中,而不是整个数据库。这是因为您只需要访问 DAO,因为它包含数据库的所有读/写方法。无需将整个数据库公开给存储库。
getAllWords
方法返回来自 Room 的Word
的LiveData
列表;我们可以这样做是因为我们在“LiveData 类”步骤中将getAlphabetizedWords
方法定义为返回LiveData
。Room 在单独的线程上执行所有查询。然后,观察到的LiveData
将在数据更改时通知主线程上的观察者。- 我们需要不要在主线程上运行插入操作,因此我们使用在
WordRoomDatabase
中创建的ExecutorService
在后台线程上执行插入操作。
10. 创建 ViewModel
什么是 ViewModel?
ViewModel
的作用是向 UI 提供数据并在配置更改后保持数据。 ViewModel
充当存储库和 UI 之间的通信中心。您还可以使用 ViewModel
在片段之间共享数据。ViewModel 是 生命周期库 的一部分。
有关此主题的入门指南,请参阅 ViewModel 概述 或 ViewModels:一个简单的示例 博客文章。
为什么要使用 ViewModel?
ViewModel
以生命周期感知的方式保存应用程序的 UI 数据,使其能够在配置更改后继续存在。将应用程序的 UI 数据与 Activity
和 Fragment
类分离,使您能够更好地遵循单一职责原则:您的活动和片段负责将数据绘制到屏幕上,而您的 ViewModel
可以负责保存和处理 UI 所需的所有数据。
在 ViewModel
中,请使用 LiveData
来表示 UI 将使用或显示的可变数据。使用 LiveData
有几个好处。
- 您可以为数据设置观察者(而不是轮询更改),并且仅在数据实际更改时才更新 UI。
- 存储库和 UI 通过
ViewModel
完全分离。 - 没有来自
ViewModel
的数据库调用(这全部在存储库中处理),使代码更易于测试。
实现 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 布局。
本 Codelab 假设您熟悉在 XML 中创建布局,因此我们仅为您提供代码。
通过将 AppTheme
父级设置为 Theme.MaterialComponents.Light.DarkActionBar
使您的应用程序主题成为 Material 主题。在 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
中,用 RecyclerView
替换 TextView
并添加一个浮动操作按钮 (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) 的外观应与可用的操作相对应,因此我们希望用“+”符号替换图标。
首先,我们需要添加一个新的矢量资源。
- 选择文件 > 新建 > 矢量资源。
- 点击图标中的 Android 机器人图标。
- 搜索“add”并选择“+”资源。点击确定。
- 之后,点击下一步。
- 确认图标路径为
main > drawable
并点击完成以添加资源。
- 仍在
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"/>
12. 添加 RecyclerView
您将在一个 RecyclerView
中显示数据,这比直接将数据放入 TextView
中要好一些。本 Codelab 假设您知道 RecyclerView
、RecyclerView.ViewHolder
和 ListAdapter
的工作原理。
首先创建一个新文件,用于保存显示 Word
的 ViewHolder
。创建一个绑定方法来设置文本。
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
。在 WordListAdapter
中创建一个静态类作为 DiffUtil.ItemCallback
的实现。代码如下所示。
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());
}
}
}
在 MainActivity
的 onCreate()
方法中添加 RecyclerView
。
在 onCreate()
方法中,在 setContentView
之后。
RecyclerView recyclerView = findViewById(R.id.recyclerview);
final WordListAdapter adapter = new WordListAdapter(new WordListAdapter.WordDiff());
recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
运行您的应用程序以确保一切正常。没有项目,因为您尚未连接数据。
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>
创建一个新的维度资源文件。
- 选择文件 > 新建 > Android 资源文件。
- 从可用限定符中选择维度。
- 设置文件名:dimens
在 values/dimens.xml
中添加以下维度资源。
<dimen name="small_padding">8dp</dimen>
<dimen name="big_padding">16dp</dimen>
使用“空活动”模板创建一个新的空 Android Activity
。
- 选择文件 > 新建 > 活动 > 空活动。
- 为活动名称输入
NewWordActivity
。 - 验证新活动是否已添加到 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>
更新活动的代码。
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
添加一个观察者。当观察到的数据发生变化且 Activity 处于前台时,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
中,为 NewWordActivity
添加 onActivityResult()
代码。
如果 Activity 返回 RESULT_OK
,通过调用 WordViewModel
的 insert()
方法将返回的单词插入数据库。
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. 总结
既然你已经拥有了一个可运行的应用,让我们回顾一下你构建的内容。以下是应用的结构图。
应用的组件包括:
MainActivity
:使用RecyclerView
和WordListAdapter
在列表中显示单词。在MainActivity
中,有一个观察者 (Observer) 观察数据库中的单词 LiveData,并在它们发生变化时收到通知。NewWordActivity
:向列表中添加新单词。WordViewModel
:提供访问数据层的方法,并返回 LiveData,以便 MainActivity 可以设置观察者关系。*LiveData<List<Word>>
:使 UI 组件的自动更新成为可能。在MainActivity
中,有一个观察者 (Observer) 观察数据库中的单词 LiveData,并在它们发生变化时收到通知。Repository
:管理一个或多个数据源。Repository 为 ViewModel 提供方法,以便与底层数据提供程序交互。在这个应用中,后端是 Room 数据库。Room
:是 SQLite 数据库的包装器并实现了它。Room 为你完成了许多以前需要手动完成的工作。- DAO:将方法调用映射到数据库查询,因此当 Repository 调用
getAlphabetizedWords()
等方法时,Room 可以执行 **SELECT * FROM word_table ORDER BY word ASC
**。 Word
:是包含单个单词的实体类。Views
和Activities
(以及Fragments
)仅通过ViewModel
与数据交互。因此,数据来自哪里并不重要。
17. 祝贺你!
[可选] 下载解决方案代码
如果你还没有,你可以查看此 Codelab 的解决方案代码。你可以查看 github 仓库 或在此处下载代码。
解压下载的 zip 文件。这将解压一个根文件夹 android-room-with-a-view-master
,其中包含完整的应用。