Android Jetpack架构篇:带视图的Android Room

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,用于输入单词。
Android Jetpack架构篇:带视图的Android Room_第1张图片
Demo的功能

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的增、删、改、查。

  1. 创建一个名为WordDao的接口。
  2. 为WordDao添加@Dao注解
  3. 声明一个插入方法void insert(Word word);
  4. 为上述方法添加@Insert注解,并且不需要为其提供SQL语句!(同样的用法还有@Delete and @Update
  5. 声明方法void deleteAll();
  6. 这里没有方便的注解可以用于删除多个实体,因此需要用@Query注解
  7. 还需要为@Query注解提供SQL语句@Query("DELETE FROM word_table")
  8. 创建方法List getAllWords();
  9. 为其添加注解与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上。这就意味着你必须去观察这些数据,以便于当数据改变时你能做出反应。根据数据不同的存储方式,这可能会很棘手。观察贯串多个组件中数据的变化,你必须要编写一个显式、严格依赖的调用链。这使得测试和调试变得非常困难。
LiveDatalifecycle 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();

后面我们会在MainActivityonCreate()方法中创建一个Observer对象,并覆盖其onChanged()方法。当LiveData改变时,观察者会被通知然后onChanged()会被回调。这时你可以更新适配器中的缓存数据,然后在适配器中更新UI。

7.添加Room数据库

什么是Room数据库?

Room是在SQLite之上的数据库层。Room用于处理我们曾经用SQLiteOpenHelper来处理任务。

  • Room通过DAO向数据库发送查询
  • 默认情况下,为了避免降低UI线程的性能,Room不允许在主线程中执行数据库操作
  • Room提供了编译时的SQL语句检查
  • 创建的Room类必须是抽象的,并且继承RoomDatabase
  • 通常,在整体应用中只需要一个Room数据库实例,即单例。

实现Room数据库

  1. 创建一个public abstractWordRoomDatabase,并继承RoomDatabase。即public abstract class WordRoomDatabase extends RoomDatabase {}
  2. 标注其为一个Room数据库,@Database(entities = {Word.class}, version = 1),声明其在数据库中的实体,并指定版本号。实体可以声明多个,声明的实体将在数据库中创建对应的表。
  3. 定义使用数据库的DAO。给每一个@Dao提供get方法。public abstract WordDao wordDao();

完整代码如下:

@Database(entities = {Word.class}, version = 1)
public abstract class WordRoomDatabase extends RoomDatabase {
   public abstract WordDao wordDao();

}
  1. 使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;
}
  1. 实例化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的实现

  1. 创建一个公共类WordRepository
  2. 添加两个成员变量
private WordDao mWordDao;
private LiveData> mAllWords;
  1. 添加一个构造函数,该构造函数获取数据库的句柄并初始化成员变量。
WordRepository(Application application) {
    WordRoomDatabase db = WordRoomDatabase.getDatabase(application);
    mWordDao = db.wordDao();
    mAllWords = mWordDao.getAllWords();
}
  1. getAllWords()添加一个包装器。Room在单独的线程上执行所有查询。观察到LiveData数据更改时,将通知观察者。
LiveData> getAllWords() {
   return mAllWords;
}
  1. 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的实现

  1. 创建WordViewModel类,使其继承AndroidViewModel
public class WordViewModel extends AndroidViewModel {}
  1. 添加一个私有成员变量来保存对存储库的引用。
   private WordRepository mRepository;
  1. 添加一个私有LiveData成员变量来缓存单词列表。
  private LiveData> mAllWords;
  1. 添加一个构造函数,该构造函数获取对存储库的引用,并从存储库获取单词列表。
   public WordViewModel (Application application) {
       super(application);
       mRepository = new WordRepository(application);
       mAllWords = mRepository.getAllWords();
   }
  1. 为所有单词添加一个get方法。这完全隐藏了对UI的实现。
   LiveData> getAllWords() { return mAllWords; }
  1. 创建一个调用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一个+符号图标:

  1. Layout/Activitymain.xml文件中,选择File>New>VectorAsset。
  2. 选择Material Icon。
  3. 点击Android机器人图标:点field, 然后选 + ("add") 资源。
  4. 按以下方式更改布局文件代码。
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;
   }
}

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

使用ViewModelProvidersViewModel与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,则通过调用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();
   }
}

定义缺少的请求代码:

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)]

现在你有了一个实用的应用程序,让我们回顾一下你已经构建了什么。这是最开发的应用程序的结构。
您有一个在列表中显示单词的应用程序(MainActivityReccyclerViewWordListAdapter)。
您可以向列表中添加单词(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

你可能感兴趣的:(Android Jetpack架构篇:带视图的Android Room)