Android Jetpack架构篇:带视图的Android Room
翻译至:Android Room with a View - Java
1.介绍
架构组件的目的是提供对应用程序体系结构的指导,并为诸如生命周期管理和数据持久化等常见任务提供开发库。
架构组件帮你构造一个鲁棒、易测试、可维护和少模板代码的应用。
架构组件是什么?
为了介绍相关术语,这里有简短的介绍一下各架构组件以及它们之前如何协作。注意这个代码库包含一部分架构组件,它们是:LiveData、ViewModel和Room。每个组件会在使用的时候做解释。下图是基本的架构形式。
[图片上传失败...(image-86c574-1540954630792)]
Entity: 当注解的类,用于描述数据库表。
SQLite database: SQLite数据库。
DAO: 数据访问对象。SQL查询到方法的映射。
Room database: 在SQLite数据库之上的数据库层。
Repository: 数据仓库,用于管理数据源。
ViewModel: 提供数据给UI。是Repository与UI的连接中心。
你要构建什么
这个Demo用于在Room中存储words(单词)列表,并显示在RecyclerView
中。这是个简单的示例,但也足以为它来作为开发应用的模板。
Demo的功能:
- 获取与保存"单词";
- 单词显示在MainActivity的RecyclerView中;
- 通过悬浮按钮调起另一个activity,用于输入单词。
RoomWordSample架构预览
下图展示了应用的各个部分。除了SQLite database,其他部分都用在自己创建的类中封装。
[图片上传失败...(image-30c198-1540955195680)]
你会学到什么
学会如何使用架构组件库和生命周期库设计和构建应用程序。
这里有许多步骤去使用架构组件和推荐的框架。最重要的是学会模型创建的作用、理解各部分组合与数据流向。通过这个Demo,你不单只是简单的复制和粘贴本文的代码,还要理解其内部原理。
你需要掌握什么
- Android Studio 3.0或更高版本的使用。
- 一台Android设备或模拟器。
你必须熟悉Java编程,面向对象设计,Android开发基础。尤其:
-
RecyclerView
及其适配器adapters - SQLite数据库及SQLite查询语言
- 线程与
AsyncTask
- 了解一些数据与UI分离的构架概念,如MVP、MVC
本Demo着重于Android架构组件,非主要代码可行自行复制与粘贴。
2.创建应用
打开Android Studio创建应用:
- 新建应用RoomWordSample,目标sdk为26+
- 不选include Kotlin support和include C++ support
- 下一步,选Phone & Tablet,minimum SDK选API 26
- 下一步,选择Basic Activity
- 下一步,完成。
[图片上传失败...(image-cc06f7-1540954630792)]
3.更新gradle文件
添加组件库到gradle。在Module:app的build.gradle中dependencies末尾加入:
// Room components
implementation "android.arch.persistence.room:runtime:$rootProject.roomVersion"
annotationProcessor "android.arch.persistence.room:compiler:$rootProject.roomVersion"
androidTestImplementation "android.arch.persistence.room:testing:$rootProject.roomVersion"
// Lifecycle components
implementation "android.arch.lifecycle:extensions:$rootProject.archLifecycleVersion"
annotationProcessor "android.arch.lifecycle:compiler:$rootProject.archLifecycleVersion"
中Project:RoomWordSample的build.gradle中加入版本信息:
ext {
roomVersion = '1.1.1'
archLifecycleVersion = '1.1.1'
}
4.创建实体
本demo的数据是“单词”,因此首先创建一个Word类,并为其创建构造函数与必要的get方法。这样Room才可以实例化对象。
[图片上传失败...(image-d84c98-1540954630792)]
下面是Word类:
public class Word {
private String mWord;
public Word(@NonNull String word) {this.mWord = word;}
public String getWord(){return this.mWord;}
}
为了使Word
类对Room库有意义,我们需要为它加注解。注解用了将实体与数据库相关联,Room根据相应的注解信息去生成对应的代码。
-
@Entity(tableName = "word_table")
每一个@Entity
类代表数据库中的一张表。tableName为生成表的表名。 -
@PrimaryKey
每个实体需要一个主键。 -
@NonNull
表示参数、字段或返回值不能为null。 -
@ColumnInfo(name = "word")
指定与成员变量对应的列名。 - 为一个字段需要是public的或提供get方法。
添加注解的Word
类:
@Entity(tableName = "word_table")
public class Word {
@PrimaryKey
@NonNull
@ColumnInfo(name = "word")
private String mWord;
public Word(String word) {this.mWord = word;}
public String getWord(){return this.mWord;}
}
5.创建DAO(数据访问对象)
什么是DAO
DAO即数据访问对象,你可以指定SQL查询语句,并将它与方法关联起来。编译器会对常规SQL注解进行编译检查,如@Insert
。
DAO对象必须是个接口或抽象类。
默认情况下,所有的查询必须在单独线程中执行。
Room将通过DAO对象去创建相应的接口。
DAO的写法
DAO是代码的基础,它用于提供word的增、删、改、查。
- 创建一个名为
WordDao
的接口。 - 为WordDao添加
@Dao
注解 - 声明一个插入方法
void insert(Word word);
- 为上述方法添加
@Insert
注解,并且不需要为其提供SQL语句!(同样的用法还有@Delete
and@Update
) - 声明方法
void deleteAll();
- 这里没有方便的注解可以用于删除多个实体,因此需要用
@Query
注解 - 还需要为
@Query
注解提供SQL语句@Query("DELETE FROM word_table")
- 创建方法
List
getAllWords(); - 为其添加注解与SQL
@Query("SELECT * from word_table ORDER BY word ASC")
下面是其完整的代码:
@Dao
public interface WordDao {
@Insert
void insert(Word word);
@Query("DELETE FROM word_table")
void deleteAll();
@Query("SELECT * from word_table ORDER BY word ASC")
List getAllWords();
}
6.LiveData类
当数据被改变后,通常你需要作一些操作,例如将更新的数据展示在UI上。这就意味着你必须去观察这些数据,以便于当数据改变时你能做出反应。根据数据不同的存储方式,这可能会很棘手。观察贯串多个组件中数据的变化,你必须要编写一个显式、严格依赖的调用链。这使得测试和调试变得非常困难。
LiveData
是 lifecycle library 中,用于数据观察的类,可用于解决上述难题。在你的方法中使用LiveData
为返回值。这样Room将会为你生成所有必须的代码,当数据库更新时,自动去更新LiveData
。
使用
LiveData
的目的是为了管理数据的更新。但是LiveData
类并没有提供公有的更新数据的方法。我们应当使用MutableLiveData
,它有两个公有方法(setValue(T)
、postValue(T)
)用于存储数据。通常,MutableLiveData
是在ViewModel
中使用,然后ViewModel
只向观察者暴露不可变的LiveData
对象。
在WordDao
中,改变getAllWords()
方法的返回值:
@Query("SELECT * from word_table ORDER BY word ASC")
LiveData> getAllWords();
后面我们会在MainActivity
的onCreate()
方法中创建一个Observer
对象,并覆盖其onChanged()
方法。当LiveData
改变时,观察者会被通知然后onChanged()
会被回调。这时你可以更新适配器中的缓存数据,然后在适配器中更新UI。
7.添加Room数据库
什么是Room数据库?
Room是在SQLite之上的数据库层。Room用于处理我们曾经用SQLiteOpenHelper
来处理任务。
- Room通过DAO向数据库发送查询
- 默认情况下,为了避免降低UI线程的性能,Room不允许在主线程中执行数据库操作
- Room提供了编译时的SQL语句检查
- 创建的Room类必须是抽象的,并且继承
RoomDatabase
- 通常,在整体应用中只需要一个Room数据库实例,即单例。
实现Room数据库
- 创建一个
public abstract
类WordRoomDatabase
,并继承RoomDatabase
。即public abstract class WordRoomDatabase extends RoomDatabase {}
- 标注其为一个Room数据库,
@Database(entities = {Word.class}, version = 1)
,声明其在数据库中的实体,并指定版本号。实体可以声明多个,声明的实体将在数据库中创建对应的表。 - 定义使用数据库的DAO。给每一个@Dao提供get方法。
public abstract WordDao wordDao();
完整代码如下:
@Database(entities = {Word.class}, version = 1)
public abstract class WordRoomDatabase extends RoomDatabase {
public abstract WordDao wordDao();
}
- 使
WordRoomDatabase
作为单例。
private static volatile WordRoomDatabase INSTANCE;
static WordRoomDatabase getDatabase(final Context context) {
if (INSTANCE == null) {
synchronized (WordRoomDatabase.class) {
if (INSTANCE == null) {
// Create database here
}
}
}
return INSTANCE;
}
- 实例化
RoomDatabase
对象:使用Room的databaseBuilder,从WordRoomDatabase
类的应用上下文context中创建RoomDatabase
对象,并将数据库全名为"word_database"
。
// Create database here
INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
WordRoomDatabase.class, "word_database")
.build();
下面是完整代码:
@Database(entities = {Word.class}, version = 1)
public abstract class WordRoomDatabase extends RoomDatabase {
public abstract WordDao wordDao();
private static volatile WordRoomDatabase INSTANCE;
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;
}
}
当你修改数据库的schema时,你需要去更新版本号并声明如何进行数据迁移,例如销毁并重建数据库策略。具体的数据库迁移策略可参考Understanding migrations with Room
8.创建Repository(数据仓库)
什么是Repository?
Repository是一个可访问多数据源的类。它并非构架组件库中的一部分,但它是代码分离和体系结构的最佳实践建议。Repository
用于处理数据操作,它为应用提供数据访问接口。
[图片上传失败...(image-aa37a9-1540954630792)]
为什么要使用Repository?
Repository管理查询线程,并允许您使用多个后端。在最常见的示例中,Repository实现了决定是从网络获取数据还是从本地缓存中获取结果的逻辑。
Repository的实现
- 创建一个公共类
WordRepository
- 添加两个成员变量
private WordDao mWordDao;
private LiveData> mAllWords;
- 添加一个构造函数,该构造函数获取数据库的句柄并初始化成员变量。
WordRepository(Application application) {
WordRoomDatabase db = WordRoomDatabase.getDatabase(application);
mWordDao = db.wordDao();
mAllWords = mWordDao.getAllWords();
}
- 为
getAllWords()
添加一个包装器。Room在单独的线程上执行所有查询。观察到LiveData
数据更改时,将通知观察者。
LiveData> getAllWords() {
return mAllWords;
}
- 为
insert()
方法添加一个包装器。使用AsyncTask来执行,确保其是在非UI线程中执行。
public void insert (Word word) {
new InsertAsyncTask(mWordDao).execute(word);
}
6.InsertAsyncTask的实现
private static class insertAsyncTask extends AsyncTask {
private WordDao mAsyncTaskDao;
insertAsyncTask(WordDao dao) {
mAsyncTaskDao = dao;
}
@Override
protected Void doInBackground(final Word... params) {
mAsyncTaskDao.insert(params[0]);
return null;
}
}
下面是完整代码:
public class WordRepository {
private WordDao mWordDao;
private LiveData> mAllWords;
WordRepository(Application application) {
WordRoomDatabase db = WordRoomDatabase.getDatabase(application);
mWordDao = db.wordDao();
mAllWords = mWordDao.getAllWords();
}
LiveData> getAllWords() {
return mAllWords;
}
public void insert (Word word) {
new insertAsyncTask(mWordDao).execute(word);
}
private static class insertAsyncTask extends AsyncTask {
private WordDao mAsyncTaskDao;
insertAsyncTask(WordDao dao) {
mAsyncTaskDao = dao;
}
@Override
protected Void doInBackground(final Word... params) {
mAsyncTaskDao.insert(params[0]);
return null;
}
}
}
9.创建ViewModel
什么是ViewModel?
ViewModel
的作用是向UI提供数据,并保存配置更改。ViewModel
充当Repository 和UI之间的通信中心。还可以使用ViewModel
在fragments之间共享数据。ViewModel是 lifecycle library 库的一部分。
[图片上传失败...(image-359dd8-1540954630792)]
为什么使用ViewModel?
当配置被更改时,ViewModel
以一种有生命周期感知的方式保存应用的UI数据。将应用程序的UI数据与Activity和Fragment类分离,可以让你更好地遵循单一责任原则:你的activities和fragments负责将数据绘制到屏幕上,而ViewModel
则负责保存和处理UI所需的所有数据。
在ViewModel
中,对于UI将使用或显示的可变数据,请使用LiveData
。使用LiveData
有几个好处:
- 您可以在数据上放置一个观察者(而不是轮询更改),并且只在数据实际更改时更新UI。
- Repository和UI由
ViewModel
完全分离。没有来自ViewModel
的数据库调用,使得代码更易于测试。
ViewModel的实现
- 创建
WordViewModel
类,使其继承AndroidViewModel
。
public class WordViewModel extends AndroidViewModel {}
- 添加一个私有成员变量来保存对存储库的引用。
private WordRepository mRepository;
- 添加一个私有LiveData成员变量来缓存单词列表。
private LiveData> mAllWords;
- 添加一个构造函数,该构造函数获取对存储库的引用,并从存储库获取单词列表。
public WordViewModel (Application application) {
super(application);
mRepository = new WordRepository(application);
mAllWords = mRepository.getAllWords();
}
- 为所有单词添加一个get方法。这完全隐藏了对UI的实现。
LiveData> getAllWords() { return mAllWords; }
- 创建一个调用Repository的
insert()
方法的包装器insert()
方法。这样,insert()
的实现对于UI就完全透明了。
public void insert(Word word) { mRepository.insert(word); }
下面是WordViewModel
的实现:
public class WordViewModel extends AndroidViewModel {
private WordRepository mRepository;
private LiveData> mAllWords;
public WordViewModel (Application application) {
super(application);
mRepository = new WordRepository(application);
mAllWords = mRepository.getAllWords();
}
LiveData> getAllWords() { return mAllWords; }
public void insert(Word word) { mRepository.insert(word); }
}
警告:不要将context传递到ViewModel实例中。不要在ViewModel中存储活动、片段或视图实例或它们的context。
10.添加XML布局
在value/styes.xml
中添加列表项的样式:
添加一个layout/recyclerview_item.xml
布局:
在Layout/Content_main.xml
中,将TextView
替换为ReccyclerView
:
浮动动作按钮(FAB)应与可用动作相对应。在Layout/Activitymain.xml
文件中,给FloatingActionButton
一个+符号图标:
- 在
Layout/Activitymain.xml
文件中,选择File>New>VectorAsset。 - 选择Material Icon。
- 点击Android机器人图标:点field, 然后选
+
("add") 资源。 - 按以下方式更改布局文件代码。
android:src="@drawable/ic_add_black_24dp"
11.添加RecycleView
您将在RecycleView
中显示数据,这比将数据抛到TextView
中要好一些。
注意,适配器中的mWord
变量缓存数据。在下一个任务中,添加自动更新数据的代码。
还请注意,getItemCount()
方法需要优雅地考虑数据尚未准备好且mWord
仍然为空的可能性。
添加一个类WordListAdapter
,它扩展了ReccyclerView.Adapter
。
这是代码:
public class WordListAdapter extends RecyclerView.Adapter {
class WordViewHolder extends RecyclerView.ViewHolder {
private final TextView wordItemView;
private WordViewHolder(View itemView) {
super(itemView);
wordItemView = itemView.findViewById(R.id.textView);
}
}
private final LayoutInflater mInflater;
private List mWords; // Cached copy of words
WordListAdapter(Context context) { mInflater = LayoutInflater.from(context); }
@Override
public WordViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = mInflater.inflate(R.layout.recyclerview_item, parent, false);
return new WordViewHolder(itemView);
}
@Override
public void onBindViewHolder(WordViewHolder holder, int position) {
if (mWords != null) {
Word current = mWords.get(position);
holder.wordItemView.setText(current.getWord());
} else {
// Covers the case of data not being ready yet.
holder.wordItemView.setText("No Word");
}
}
void setWords(List words){
mWords = words;
notifyDataSetChanged();
}
// getItemCount() is called many times, and when it is first called,
// mWords has not been updated (means initially, it's null, and we can't return null).
@Override
public int getItemCount() {
if (mWords != null)
return mWords.size();
else return 0;
}
}
在MainActivity
的onCreate()
方法中添加ReccyclerView
。
在onCreate()方法中:
RecyclerView recyclerView = findViewById(R.id.recyclerview);
final WordListAdapter adapter = new WordListAdapter(this);
recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
运行你的应用程序,以确保一切正常。没有项目,因为您还没有连接到数据,所以应用程序应该显示灰色背景,没有任何列表项目。
12.填充数据库
数据库中没有数据。您将以两种方式添加数据:打开数据库时添加一些数据,以及添加用于添加单词的Activity
。
要删除所有内容并在应用程序启动时重新填充数据库,您可以创建一个RoomDatabase.Callback
并覆盖onOpen()
。由于不能对UI线程执行Room数据库操作,因此onOpen()创建并执行AsyncTask
来向数据库添加内容。
下面是在WordRoomDatabase
类中创建回调的代码:
private static RoomDatabase.Callback sRoomDatabaseCallback =
new RoomDatabase.Callback(){
@Override
public void onOpen (@NonNull SupportSQLiteDatabase db){
super.onOpen(db);
new PopulateDbAsync(INSTANCE).execute();
}
};
下面是AsyncTask
的代码,它删除数据库的内容,然后用两个单词“Hello”和“World”填充数据库。欢迎加入更多的单词!
private static class PopulateDbAsync extends AsyncTask {
private final WordDao mDao;
PopulateDbAsync(WordRoomDatabase db) {
mDao = db.wordDao();
}
@Override
protected Void doInBackground(final Void... params) {
mDao.deleteAll();
Word word = new Word("Hello");
mDao.insert(word);
word = new Word("World");
mDao.insert(word);
return null;
}
}
最后,在调用.build()
之前,将回调添加到数据库构建序列。
.addCallback(sRoomDatabaseCallback)
13.添加NewWordActivity
将这些字符串资源添加到values/strings.xml
中:
Word...
Save
Word not saved because it is empty.
在value/color s.xml
中添加此颜色资源:
#d3d3d3
将这些维度资源添加到values/dimens.xml
:
6dp
16dp
在布局文件夹中创建Activity_new_word.xml
文件:
使用空活动模板创建一个新活动,NewWordActivity
。验证活动是否已添加到AndroidManifest中!
下面是该activity的代码:
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(new View.OnClickListener() {
public void onClick(View 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();
}
});
}
}
14.连接数据
最后一步是通过保存用户输入的新单词并在RecyclerView
中显示Word数据库的当前内容,将UI连接到数据库。
要显示数据库的当前内容,添加一个观察者来观察ViewModel
中的LiveData
。每当数据更改时,都会调用onchange()
回调,该回调调用适配器的setWord()
方法,以更新适配器的缓存数据并刷新显示的列表。
在MainActivity
中,为ViewModel
创建一个成员变量:
private WordViewModel mWordViewModel;
使用ViewModelProviders
将ViewModel
与UI控制器关联起来。当应用程序第一次启动时,ViewModelProviders
将创建ViewModel
。当activity 被销毁时,例如通过配置更改,ViewModel
就会持续存在。重新创建activity 时,ViewModelProviders
将返回现有的ViewModel
。参见ViewModel。
在onCreate()
中,从ViewModelProvider
获取一个ViewModel
。
mWordViewModel = ViewModelProviders.of(this).get(WordViewModel.class);
同样在onCreate()
中,为getAllWords()
返回的LiveData
添加一个观察者。当观察到的数据发生变化且activity 位于前台时,onchange()
方法就会调用。
mWordViewModel.getAllWords().observe(this, new Observer>() {
@Override
public void onChanged(@Nullable final List words) {
// Update the cached copy of the words in the adapter.
adapter.setWords(words);
}
});
在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();
}
}
定义缺少的请求代码:
public static final int NEW_WORD_ACTIVITY_REQUEST_CODE = 1;
在MainActivity
中,当用户点击Fab时启动NewWordActivity
。用以下代码替换Fab的onclick()
单击处理程序中的代码:
Intent intent = new Intent(MainActivity.this, NewWordActivity.class);
startActivityForResult(intent, NEW_WORD_ACTIVITY_REQUEST_CODE);
运行你的APP!!!
当您在NewWordActivity
中向数据库添加一个单词时,UI将自动更新。
15.总结
[图片上传失败...(image-49e45a-1540955195680)]
现在你有了一个实用的应用程序,让我们回顾一下你已经构建了什么。这是最开发的应用程序的结构。
您有一个在列表中显示单词的应用程序(MainActivity
、ReccyclerView
、WordListAdapter
)。
您可以向列表中添加单词(NewWordActivity
)。
单词是单词实体类的实例。
这些单词作为单词(mWords
) List
缓存在RecyclerViewAdapter
中。当数据库中的单词更改时,此单词列表会自动更新和重新显示。
用于自动UI更新的数据流(反应性UI)
自动更新是可能的,因为我们正在使用LiveData
。在MainActivity
中,有一个观察者从数据库中观察到LiveData
这个词,并在它们更改时得到通知。当发生更改时,将执行观察者的onChange()
方法,并更新WordListAdapter
中的mWord
。
可以观察到数据,因为它是LiveData
。观察到的是由WordViewModel
对象的getAllWords()
方法返回的LiveData
。>
WordViewModel
从UI层隐藏关于后端的所有内容。它提供访问数据层的方法,并返回LiveData
,以便MainActivity
可以设置观察者关系。Activities
(和Fragments
)仅通过ViewModel
与数据交互。因此,数据从何而来并不重要。
在这个demo,数据来自一个存储库。ViewModel
不需要知道这个仓库与什么交互。它只需要知道如何通过Repository
公开的方法与Repository
交互。
Repository 管理一个或多个数据源。在WordListSample
应用程序中,后端是一个Room数据库。Room是一个包装器,实现了SQLite数据库。房间为你做了很多工作,你以前不得不自己做。例如,Room完成了以前使用SQLiteOpenHelper
类所做的一切。
DAO映射方法调用数据库查询,以便当Repository调用getAllWords()
等方法时,Room可以通过执行SELECT * from word_table ORDER BY word ASC
。
因为从查询返回的结果被观察到LiveData
,所以每当Room中的数据发生变化时,会执行观察者接口的onChanged()
方法,并更新UI。
15.代码
单击以下链接下载此codelab的解决方案代码:
RoomWordSample源码
15.进一步探索
如果您需要迁移应用程序,请参见成功完成此代码后的7 Steps To Room。请注意,删除SQLiteOpenHelper类和大量其他代码是非常令人满意的。
当您有大量数据时,请考虑使用paging library。
- Guide to App Architecture
- Android Architecture overview (video)
- Android Persistence codelab (
LiveData
, Room, DAO) - Android lifecycle-aware components codelab (ViewModel, LiveData, LifecycleOwner, LifecycleRegistryOwner)
- Architecture Component code samples