翻译自:Android Room with a View - Java
架构组件的目的是提供对应用程序体系结构的指导,并为诸如生命周期管理和数据持久化等常见任务提供开发库。
架构组件帮你构造一个鲁棒、易测试、可维护和少模板代码的应用。
为了介绍相关术语,这里有简短的介绍一下各架构组件以及它们之前如何协作。注意这个代码库包含一部分架构组件,它们是:LiveData、ViewModel和Room。每个组件会在使用的时候做解释。下图是基本的架构形式。
Entity: 用于描述数据库表。
SQLite database: SQLite数据库。
DAO: 数据访问对象。SQL查询到方法的映射。
Room database: 在SQLite数据库之上的数据库层。
Repository: 数据仓库,用于管理数据源。
ViewModel: 提供数据给UI。是Repository与UI的连接中心。
这个Demo用于在Room中存储words(单词)列表,并显示在RecyclerView
中。这是个简单的示例,但也足以为它来作为开发应用的模板。
Demo的功能:
下图展示了应用的各个部分。除了SQLite database,其他部分都用在自己创建的类中封装。
学会如何使用架构组件库和生命周期库设计和构建应用程序。
这里有许多步骤去使用架构组件和推荐的框架。最重要的是学会模型创建的作用、理解各部分组合与数据流向。通过这个Demo,你不单只是简单的复制和粘贴本文的代码,还要理解其内部原理。
你必须熟悉Java编程,面向对象设计,Android开发基础。尤其:
RecyclerView
及其适配器adaptersAsyncTask
本Demo着重于Android架构组件,非主要代码可行自行复制与粘贴。
打开Android Studio创建应用:
添加组件库到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'
}
本demo的数据是“单词”,因此首先创建一个Word类,并为其创建构造函数与必要的get方法。这样Room才可以实例化对象。
下面是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")
指定与成员变量对应的列名。添加注解的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;}
}
DAO即数据访问对象,你可以指定SQL查询语句,并将它与方法关联起来。编译器会对常规SQL注解进行编译检查,如@Insert
。
DAO对象必须是个接口或抽象类。
默认情况下,所有的查询必须在单独线程中执行。
Room将通过DAO对象去创建相应的接口。
DAO是代码的基础,它用于提供word的增、删、改、查。
WordDao
的接口。@Dao
注解void insert(Word word);
@Insert
注解,并且不需要为其提供SQL语句!(同样的用法还有@Delete
and @Update
)void deleteAll();
@Query
注解@Query
注解提供SQL语句@Query("DELETE FROM word_table")
List getAllWords();
@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<Word> getAllWords();
}
当数据被改变后,通常你需要作一些操作,例如将更新的数据展示在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<List<Word>> getAllWords();
后面我们会在MainActivity
的onCreate()
方法中创建一个Observer
对象,并覆盖其onChanged()
方法。当LiveData
改变时,观察者会被通知然后onChanged()
会被回调。这时你可以更新适配器中的缓存数据,然后在适配器中更新UI。
Room是在SQLite之上的数据库层。Room用于处理我们曾经用SQLiteOpenHelper
来处理任务。
RoomDatabase
public abstract
类WordRoomDatabase
,并继承RoomDatabase
。即public abstract class WordRoomDatabase extends RoomDatabase {}
@Database(entities = {Word.class}, version = 1)
,声明其在数据库中的实体,并指定版本号。实体可以声明多个,声明的实体将在数据库中创建对应的表。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
Repository是一个可访问多数据源的类。它并非构架组件库中的一部分,但它是代码分离和体系结构的最佳实践建议。Repository
用于处理数据操作,它为应用提供数据访问接口。
Repository管理查询线程,并允许您使用多个后端。在最常见的示例中,Repository实现了决定是从网络获取数据还是从本地缓存中获取结果的逻辑。
WordRepository
private WordDao mWordDao;
private LiveData<List<Word>> mAllWords;
WordRepository(Application application) {
WordRoomDatabase db = WordRoomDatabase.getDatabase(application);
mWordDao = db.wordDao();
mAllWords = mWordDao.getAllWords();
}
getAllWords()
添加一个包装器。Room在单独的线程上执行所有查询。观察到LiveData
数据更改时,将通知观察者。LiveData<List<Word>> getAllWords() {
return mAllWords;
}
insert()
方法添加一个包装器。使用AsyncTask来执行,确保其是在非UI线程中执行。public void insert (Word word) {
new InsertAsyncTask(mWordDao).execute(word);
}
6.InsertAsyncTask的实现
private static class insertAsyncTask extends AsyncTask<Word, Void, Void> {
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<List<Word>> mAllWords;
WordRepository(Application application) {
WordRoomDatabase db = WordRoomDatabase.getDatabase(application);
mWordDao = db.wordDao();
mAllWords = mWordDao.getAllWords();
}
LiveData<List<Word>> getAllWords() {
return mAllWords;
}
public void insert (Word word) {
new insertAsyncTask(mWordDao).execute(word);
}
private static class insertAsyncTask extends AsyncTask<Word, Void, Void> {
private WordDao mAsyncTaskDao;
insertAsyncTask(WordDao dao) {
mAsyncTaskDao = dao;
}
@Override
protected Void doInBackground(final Word... params) {
mAsyncTaskDao.insert(params[0]);
return null;
}
}
}
ViewModel
的作用是向UI提供数据,并保存配置更改。ViewModel
充当Repository 和UI之间的通信中心。还可以使用ViewModel
在fragments之间共享数据。ViewModel是 lifecycle library 库的一部分。
当配置被更改时,ViewModel
以一种有生命周期感知的方式保存应用的UI数据。将应用程序的UI数据与Activity和Fragment类分离,可以让你更好地遵循单一责任原则:你的activities和fragments负责将数据绘制到屏幕上,而ViewModel
则负责保存和处理UI所需的所有数据。
在ViewModel
中,对于UI将使用或显示的可变数据,请使用LiveData
。使用LiveData
有几个好处:
ViewModel
完全分离。没有来自ViewModel
的数据库调用,使得代码更易于测试。WordViewModel
类,使其继承AndroidViewModel
。public class WordViewModel extends AndroidViewModel {}
private WordRepository mRepository;
private LiveData<List<Word>> mAllWords;
public WordViewModel (Application application) {
super(application);
mRepository = new WordRepository(application);
mAllWords = mRepository.getAllWords();
}
LiveData<List<Word>> getAllWords() { return mAllWords; }
insert()
方法的包装器insert()
方法。这样,insert()
的实现对于UI就完全透明了。public void insert(Word word) { mRepository.insert(word); }
下面是WordViewModel
的实现:
public class WordViewModel extends AndroidViewModel {
private WordRepository mRepository;
private 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); }
}
警告:不要将context传递到ViewModel实例中。不要在ViewModel中存储活动、片段或视图实例或它们的context。
在value/styes.xml
中添加列表项的样式:
<style name="word_title">
- "android:layout_width"
>match_parent
- "android:layout_height">26dp
- "android:textSize">24sp
- "android:textStyle">bold
- "android:layout_marginBottom">6dp
- "android:paddingLeft">8dp
style>
添加一个layout/recyclerview_item.xml
布局:
<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
替换为ReccyclerView
:
<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/Activitymain.xml
文件中,给FloatingActionButton
一个+
符号图标:
Layout/Activitymain.xml
文件中,选择File>New>VectorAsset。+
(“add”) 资源。android:src="@drawable/ic_add_black_24dp"
您将在RecycleView
中显示数据,这比将数据抛到TextView
中要好一些。
注意,适配器中的mWord
变量缓存数据。在下一个任务中,添加自动更新数据的代码。
还请注意,getItemCount()
方法需要优雅地考虑数据尚未准备好且mWord
仍然为空的可能性。
添加一个类WordListAdapter
,它扩展了ReccyclerView.Adapter
。
这是代码:
public class WordListAdapter extends RecyclerView.Adapter<WordListAdapter.WordViewHolder> {
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<Word> 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<Word> 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));
运行你的应用程序,以确保一切正常。没有项目,因为您还没有连接到数据,所以应用程序应该显示灰色背景,没有任何列表项目。
数据库中没有数据。您将以两种方式添加数据:打开数据库时添加一些数据,以及添加用于添加单词的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<Void, Void, Void> {
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)
将这些字符串资源添加到values/strings.xml
中:
<string name="hint_word">Word...string>
<string name="button_save">Savestring>
<string name="empty_not_saved">Word not saved because it is empty.string>
在value/color s.xml
中添加此颜色资源:
<color name="buttonLabel">#d3d3d3color>
将这些维度资源添加到values/dimens.xml
:
<dimen name="small_padding">6dpdimen>
<dimen name="big_padding">16dpdimen>
在布局文件夹中创建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: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>
使用空活动模板创建一个新活动,NewWordActivity
。验证活动是否已添加到AndroidManifest中!
<activity android:name=".NewWordActivity">activity>
下面是该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();
}
});
}
}
最后一步是通过保存用户输入的新单词并在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<List<Word>>() {
@Override
public void onChanged(@Nullable final List<Word> 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将自动更新。
现在你有了一个实用的应用程序,让我们回顾一下你已经构建了什么。这是最开发的应用程序的结构。
您有一个在列表中显示单词的应用程序(MainActivity
、ReccyclerView
、WordListAdapter
)。
您可以向列表中添加单词(NewWordActivity
)。
单词是单词实体类的实例。
这些单词作为单词(mWords
) List
缓存在RecyclerViewAdapter
中。当数据库中的单词更改时,此单词列表会自动更新和重新显示。
自动更新是可能的,因为我们正在使用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为你做了很多工作,你以前不得不自己做。例如,Room完成了以前使用SQLiteOpenHelper
类所做的一切。
DAO映射方法调用数据库查询,以便当Repository调用getAllWords()
等方法时,Room可以通过执行SELECT * from word_table ORDER BY word ASC
。
因为从查询返回的结果被观察到LiveData
,所以每当Room中的数据发生变化时,会执行观察者接口的onChanged()
方法,并更新UI。
单击以下链接下载此codelab的解决方案代码:
RoomWordSample源码
如果您需要迁移应用程序,请参见成功完成此代码后的7 Steps To Room。请注意,删除SQLiteOpenHelper类和大量其他代码是非常令人满意的。
当您有大量数据时,请考虑使用paging library。
LiveData
, Room, DAO)