详细讲解:Android Room+架构组件(LiveData/ViewModel) - Demo(词典)

本文篇幅会相对长些。
请耐心看完,必有收获.

目的:
通过一个完整的 原理简单结构稍微复杂 的 例子,
深入了解 Android Room 与 架构组件的使用。
以后可以基于这个样例做很多拓展。

完成这个Demo后,你会发现,整个架构体系思想和设计非常优美的!
层层封装、接口隔离的思想,职责单一的设计原则!
样例采用自底向上的构建方式:
(1) Room(SQL TABLE / DAO/RoomDatabase)
(2) 存储库Repository
(3) ViewModel/LiveData
(4) Activity

一. 基础介绍

1. Android Room + 架构组件

详细讲解:Android Room+架构组件(LiveData/ViewModel) - Demo(词典)_第1张图片

02_Android Room+架构组件 体系图.JPG

各部分作用后续会逐一介绍.
在完成Demo后理解会更深刻,值得反复研究这个架构图!

2. 词典 样例介绍

官网文档:
https://developer.android.com/codelabs/android-room-with-a-view-kotlin#0
源码GitHub 地址会在文章的最后附上.
注:本文基于Kotlin 如需Java版本,参考以下(建议学习Kotlin,趋势):
https://developer.android.com/codelabs/android-room-with-a-view#0 (JAVA)

实现功能
(1). 词典
(2). 使用RecyclerView 显示
(3). 显示所有词
(4). 提供添加入口,保存在数据库

<应用效果图>:

详细讲解:Android Room+架构组件(LiveData/ViewModel) - Demo(词典)_第2张图片

01_应用效果图.JPG

详细讲解:Android Room+架构组件(LiveData/ViewModel) - Demo(词典)_第3张图片

03_Word Sample 架构图.JPG

可以看出,Demo架构图是根据 实现的,
每个方框代表要创建的一个类(SQLite除外)

二、样例创建过程(关键步骤)

Android studio 版本: Atctic Fox | 2020.3.1
Gradle 插件版本: gradle-6.8.3
build tool插件:com.android.tools.build:gradle:4.2.1
kotlin 插件:org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21

1. 配置依赖

1.1 app/build.gradle

(1) 添加 kapt 注解处理器 并且 jvmTarget 设置为 1.8:

plugins {
    //...
    id 'kotlin-kapt'
}

android {
    //...
    kotlinOptions {
        jvmTarget = '1.8'
    }
}

(2)依赖项depenencies (其中version设置在project/build.gralde):

dependencies {
    implementation "androidx.appcompat:appcompat:$rootProject.appCompatVersion"
    implementation "androidx.activity:activity-ktx:$rootProject.activityVersion"

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

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

    // Kotlin components
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$rootProject.coroutines"
    api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$rootProject.coroutines"

    // 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.2 project/build.gradle 设置上面所需的版本号

ext {
    activityVersion = '1.1.0'
    appCompatVersion = '1.2.0'
    constraintLayoutVersion = '2.0.2'
    coreTestingVersion = '2.1.0'
    coroutines = '1.3.9'
    lifecycleVersion = '2.2.0'
    materialVersion = '1.2.1'
    roomVersion = '2.2.5'
    // testing
    junitVersion = '4.13.1'
    espressoVersion = '3.1.0'
    androidxJunitVersion = '1.1.2'
}

2. 创建实体(Entity)

数据库中的表,每一项的数据即是 实体Entity
<数据表图>

详细讲解:Android Room+架构组件(LiveData/ViewModel) - Demo(词典)_第4张图片

04_单词表.JPG

Room 允许通过实体创建表

2.1 创建单词的 类 Word

data class Word(val word: String)

类中的 每个属性 代表 表中的一列
Room 最终会使用这些属性来创建表并将数据库行中的对象实例化。

2.2 添加注解,类与数据库(表) 建立联系。

@Entity(tableName = "word_table")
class Word(@PrimaryKey @ColumnInfo(name = "word") val word: String)

数据库则会根据这个信息自动生成代码。
注解的作用:
(1) @Entity 表明是一张SQLite 表。 可以指定表名,与类名区分,如word_table
(2) @PrimaryKey 表示主键
(3) @ColumnInfo(name = “word”) 表示列名为word

*Room 的详细注解可参考: https://developer.android.com/reference/kotlin/androidx/room/package-summary.html
*使用注解声明类: https://developer.android.com/training/data-storage/room/defining-data.html

3. 创建DAO (data access object)

3.1 DAO 重要概念

这个Object 很好的反应了Java 中一切皆对象的概念.
作用是将方法 和 SQL 查询 关联,方便在代码中调用
编译器会检查SQL 语法 并且会 根据注解生成 非常便捷的 查方法, 例如 @Insert。

DAO 必须是接口 或者 抽象类。(因为要使用@Dao注解生成它的实现类!!)

一般情况,所有查询都必须在分离的线程里执行。

Room 在Kotlin 协程里支持。因此,可以使用suspend注解并在协程里调用,或者在其它挂起函数里调用。

3.2 实现DAO 的功能:

(1) 根据字母顺序获取所有单词
(2) 插入一个单词
(3) 删除所有单词

3.3 实现DAO的步骤:

创建WordDao类并添加相应代码

@Dao
interface WordDao {
    @Query("SELECT * FROM word_table ORDER BY word ASC")
    fun getAlphabetizedWords(): List

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insert(word: Word)

    @Query("DELETE FROM word_table")
    suspend fun deleteAll()
}

注意:
(1)WordDao 是一个接口。 DAOs 必须是接口 或者 抽象类。
(2)@Dao 注解说明它是一个用于Room的DAO类
(3)suspend fun 表示是一个 挂起函数
(4)@Insert DAO特有的注解,不需要SQL 查询表达式 (@Delete 删除,@Update 更新行)
(5)onConflict = OnConflictStrategy.IGNORE 表示相同的单词会忽略
(6)@Query 需要提供SQL 查询表达式

使用DAOs 访问数据参考:
https://developer.android.com/training/data-storage/room/accessing-data.html

4. 观察数据库的变化

当数据库变化时,需要更新到UI.
这就要求监听数据库。
可以使用 Flow 异步序列 (kotlinx-coroutines库)

因此,WordDao获取所有单词的方法,可以改成这样:

   @Query("SELECT * FROM word_table ORDER BY word ASC")
   fun getAlphabetizedWords(): Flow>

在后面,我们会把Flow 转换成LiveData,保存在ViewModel中。
*协程里的Flow 介绍可以参考:
https://kotlinlang.org/docs/reference/coroutines/flow.html

5. 增加一个Room 数据库(RoomDatabase)

5.1 RoomDatabase 是什么?

(1) 在数据库层中,是位于 SQLite 数据库之上的。
(2) 负责和 SQLiteOpenHelper 一样的单调乏味的任务
(3) 使用 DAO 去执行 查询 它的数据库
(4) 在后台线程运行异步操作 (如查询返回Flow)
(5) 提供 SQLite 语句的编译时检查

5.2 实现 Room 数据库

必须是抽象类 并且 继承自 RoomDatabase。
通常仅需要一个 Room 数据库 实例。

创建 WordRoomDatabase 类, 代码:

// Annotates class to be a Room Database with a table (entity) of the Word class
@Database(entities = arrayOf(Word::class), version = 1, exportSchema = false)
public abstract class WordRoomDatabase : RoomDatabase() {

   abstract fun wordDao(): WordDao

   companion object {
        // Singleton prevents multiple instances of database opening at the
        // same time.
        @Volatile
        private var INSTANCE: WordRoomDatabase? = null

        fun getDatabase(context: Context): WordRoomDatabase {
            // if the INSTANCE is not null, then return it,
            // if it is, then create the database
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                        context.applicationContext,
                        WordRoomDatabase::class.java,
                        "word_database"
                    ).build()
                INSTANCE = instance
                // return instance
                instance
            }
        }
   }
}

代码分析:
(1) Room 数据库类必须是抽象类 并且 继承自 RoomDatabase
(2) @Database 将该类注解为 Room 数据库;
注解参数 声明 实体 及 版本号;
每个实体 对应 一个 将在数据库中创建的;
exportSchema=false 表示不做迁移,实际上应该考虑.
(3) 通过抽象方法 公开 DAO (WordDao)

abstract fun wordDao(): WordDao

(4) 该类是单例, 通过 getDatabase 返回。
实例使用 建造者 模式 创建, 即 Room.databaseBuilder
数据库名设置为 : word_database
后面根据需要,可以灵活地添加配置.

6. 创建存储库 (Repository)

6.1 什么是存储库?

详细讲解:Android Room+架构组件(LiveData/ViewModel) - Demo(词典)_第5张图片

05_Repository存储库.JPG

它并非 架构组件库 的一部分,但它是推荐为 代码分离架构采用的最佳做法。
存储库类会将多个数据源(DAO或者网络数据)访问权限 抽象化。
存储库类会提供一个整洁的 API,用于获取对应用其余部分的数据访问权限。

6.2 为什么使用存储库?

存储库可 管理查询,且允许使用多个后端。
参考架构图,处理ViewModel 与 RoomDatabase 之间,
封装来自与DAO/网络的数据,只需要和DAO交互,不必知道具体的数据库

6.3 实现存储库

创建类: WordRepository

// Declares the DAO as a private property in the constructor. Pass in the DAO
// instead of the whole database, because you only need access to the DAO
class WordRepository(private val wordDao: WordDao) {

    // Room executes all queries on a separate thread.
    // Observed Flow will notify the observer when the data has changed.
    val allWords: Flow> = wordDao.getAlphabetizedWords()

    // By default Room runs suspend queries off the main thread, therefore, we don't need to
    // implement anything else to ensure we're not doing long running database work
    // off the main thread.
    @Suppress("RedundantSuspendModifier")
    @WorkerThread
    suspend fun insert(word: Word) {
        wordDao.insert(word)
    }
}

分析代码:
(1) DAO 作为构造函数的参数传递。
DAO 包含数据库的所有读取/写入方法,
因此存储库只需访问DAO, 无需 获取整个数据库。
(2) 单词列表(allWords) 具有公开属性。
getAlphabetizedWords 返回的是 Flow 的方式.
(3) suspend 修饰符会告知编译器需要从协程或其他挂起函数进行调用。
(4) Room 在主线程之外执行挂起查询。 (@WorkderThread)

注意:
存储库的用途是在不同的数据源之间进行协调。
在这个简单示例中,数据源只有一个,因此该存储库并未执行多少操作。
如需了解更复杂的实现,请参阅 BasicSample。

7. 创建ViewModel

7.1 什么是 ViewModel?

详细讲解:Android Room+架构组件(LiveData/ViewModel) - Demo(词典)_第6张图片

06_ViewModel 与Activity 关联图.JPG

ViewModel 的作用是向界面提供数据,不受配置变化的影响。
ViewModel 充当存储库和界面之间的通信中心。
ViewModel 是 Lifecycle 库的一部分。

7.2 为什么使用 ViewModel?

ViewModel 以一种可以感知生命周期的方式保存应用的界面数据,不受配置变化的影响.
更好地遵循单一责任原则:activity 和 fragment 负责将数据绘制到屏幕上,ViewModel 则负责保存并处理界面所需的所有数据

7.3 LiveData 和 ViewModel

LiveData 是一种可观察的数据存储器,每当数据发生变化时,都会收到通知。
与 Flow 不同,LiveData 具有生命周期感知能力,即遵循其他应用组件(如 activity 或 fragment)的生命周期。
LiveData 会根据负责监听变化的组件的生命周期自动停止或恢复观察。因此,LiveData 适用于界面使用或显示的可变数据。

ViewModel 会将存储库中的数据从 Flow 转换为 LiveData,并将字词列表作为 LiveData 传递给界面。

7.4 viewModelScope

在 Kotlin 中,所有协程都在 CoroutineScope 中运行。
AndroidX lifecycle-viewmodel-ktx 库将 viewModelScope 添加为ViewModel 类的扩展函数

7.5 实现 ViewModel

创建为 WordViewModel, 代码:

class WordViewModel(private val repository: WordRepository) : ViewModel() {

    // Using LiveData and caching what allWords returns has several benefits:
    // - We can put an observer on the data (instead of polling for changes) and only update the
    //   the UI when the data actually changes.
    // - Repository is completely separated from the UI through the ViewModel.
    val allWords: LiveData> = repository.allWords.asLiveData()

    /**
     * Launching a new coroutine to insert the data in a non-blocking way
     */
    fun insert(word: Word) = viewModelScope.launch {
        repository.insert(word)
    }
}

class WordViewModelFactory(private val repository: WordRepository) : ViewModelProvider.Factory {
    override fun  create(modelClass: Class): T {
        if (modelClass.isAssignableFrom(WordViewModel::class.java)) {
            @Suppress("UNCHECKED_CAST")
            return WordViewModel(repository) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

分析代码:
(1) 构造函数使用 WordRepository作为参数.
(2) LiveData 成员变量 以 缓存字词列表

val allWords: LiveData> = repository.allWords.asLiveData().

并且将 Flow数据使用asLiveData转成 LiveData数据
(3) 封装存储库的insert方法
启动新协程并调用存储库的挂起函数 insert
viewModelScope.launch{…}
(4) 实现 ViewModelProvider.Factory 并创建 WordViewModel

警告:请勿保留对生命周期短于 ViewModel 的 Context 的引用!例如:activity fragment view
保留引用可能会导致内存泄漏,例如 ViewModel 对已销毁的 activity 的引用
重要提示:操作系统需要更多资源时,ViewModel 不会保留已在后台终止的应用进程中。 可以参考SavedStateHandle
实测, 这个SavedStateHandle在设备中也不起效的(Github上也有人提出来)

8. 添加 XML 布局

这部分并非本文的重点.

8.1 样式资源

values/styles.xml


    
    


8.2 尺寸资源

values/dimens.xml

16dp

8.3 RecyclerView 每个条目的布局

layout/recyclerview_item.xml




    


8.3 修改主Activity 布局

layout/activity_main.xml



    

    



其中 FloatingActionButton 是一悬浮操作按钮(FAB).
我们可以为它修改成 “+” 的符号,表示 添加 新的 单词.
因此,新的矢量资源:

依次选择 File > New > Vector Asset。
点击 Clip Art: 字段中的 Android 机器人图标
搜索“add”,然后选择“+”资源。点击 OK。
在 Asset Studio 窗口中,点击 Next。
确认图标的路径为 main > drawable,然后点击 Finish 以添加资源。

然后在 fab 按钮上添加属性:

android:src="@drawable/ic_add_black_24dp"

9. 添加 RecyclerView

这部分并非本文的重点.
需要熟悉 RecyclerView / ViewHolder / Adapter 等原理.

9.1 创建WordListAdapter

class WordListAdapter : ListAdapter(WordsComparator()) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WordViewHolder {
        return WordViewHolder.create(parent)
    }

    override fun onBindViewHolder(holder: WordViewHolder, position: Int) {
        val current = getItem(position)
        holder.bind(current.word)
    }

    class WordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        private val wordItemView: TextView = itemView.findViewById(R.id.textView)

        fun bind(text: String?) {
            wordItemView.text = text
        }

        companion object {
            fun create(parent: ViewGroup): WordViewHolder {
                val view: View = LayoutInflater.from(parent.context)
                    .inflate(R.layout.recyclerview_item, parent, false)
                return WordViewHolder(view)
            }
        }
    }

    class WordsComparator : DiffUtil.ItemCallback() {
        override fun areItemsTheSame(oldItem: Word, newItem: Word): Boolean {
            return oldItem === newItem
        }

        override fun areContentsTheSame(oldItem: Word, newItem: Word): Boolean {
            return oldItem.word == newItem.word
        }
    }
}

代码相对简单,无非就是做适配器的功能,绑定到VIEW上

9.2 添加RecyclerView

在 MainActivity 的 onCreate()中添加:

   val recyclerView = findViewById(R.id.recyclerview)
   val adapter = WordListAdapter()
   recyclerView.adapter = adapter
   recyclerView.layoutManager = LinearLayoutManager(this)

此时,编译运行后没有数据,因此是显示空的.

10. 存储库(Repository)和 数据库(RoomDatabase) 实例化

这里设计 数据库和存储库只有一个实例。
因此,作为 Application 类的成员进行创建。
然后,在需要时只需从应用检索,而不是每次都进行构建。

10.1 创建 WordsApplication

class WordsApplication : Application() {
    // Using by lazy so the database and the repository are only created when they're needed
    // rather than when the application starts
    val database by lazy { WordRoomDatabase.getDatabase(this) }
    val repository by lazy { WordRepository(database.wordDao()) }
}

分析代码:
(1) 创建了 数据库实例 database
(2) 创建了 *存储库实例 repository , 基于数据库的DAO
同时,需要更新 AndroidManifest 配置文件

(3) 使用懒加载 lazy 的方式.

11. 填充数据库

该样例 添加数据的方式 有两种:
(1) 在创建数据库时添加一些数据
(2) 用于提供手动添加子词的 Activity

先实现(1), 这就需要在 创建数据库 后有回调,然后再添加数据.
RoomDatabase.Callback 正是提供的回调接口, 并覆写它的onCreate()函数.
注意: 数据库的操作不能在主线程上操作,需启动协程.
要启动协程,则可以使用 CoroutineScope.
这就需要使用 应用的 applicationScope。

11.1 修改创建数据库时传递applicationScope

数据库是在WordsApplication 上创建的,因此需要修改:

class WordsApplication : Application() {
    // No need to cancel this scope as it'll be torn down with the process
    val applicationScope = CoroutineScope(SupervisorJob())

    // Using by lazy so the database and the repository are only created when they're needed
    // rather than when the application starts
    val database by lazy { WordRoomDatabase.getDatabase(this, applicationScope) }
    val repository by lazy { WordRepository(database.wordDao()) }
}

applicationScope 是用于数据库创建成功后, callBack回调onCreate时,执行插入数据的操作.

11.2 实现 RoomDatabase.Callback()

在 WordRoomDatabase 类中创建回调所用的代码:

private class WordDatabaseCallback(
    private val scope: CoroutineScope
) : RoomDatabase.Callback() {

    override fun onCreate(db: SupportSQLiteDatabase) {
        super.onCreate(db)
        INSTANCE?.let { database ->
            scope.launch {
                populateDatabase(database.wordDao())
            }
        }
    }

    suspend fun populateDatabase(wordDao: WordDao) {
        // Delete all content here.
        wordDao.deleteAll()

        // Add sample words.
        var word = Word("Hello")
        wordDao.insert(word)
        word = Word("World!")
        wordDao.insert(word)

        // TODO: Add your own words!
    }
}

11.3 将回调添加到数据库构建序列

.addCallback(WordDatabaseCallback(scope))

然后在 Room.databaseBuilder() 上调用 .build()

11.4 WordRoomDatabase 完整代码:

@Database(entities = arrayOf(Word::class), version = 1, exportSchema = false)
abstract class WordRoomDatabase : RoomDatabase() {

   abstract fun wordDao(): WordDao

   private class WordDatabaseCallback(
       private val scope: CoroutineScope
   ) : RoomDatabase.Callback() {

       override fun onCreate(db: SupportSQLiteDatabase) {
           super.onCreate(db)
           INSTANCE?.let { database ->
               scope.launch {
                   var wordDao = database.wordDao()

                   // Delete all content here.
                   wordDao.deleteAll()

                   // Add sample words.
                   var word = Word("Hello")
                   wordDao.insert(word)
                   word = Word("World!")
                   wordDao.insert(word)

                   // TODO: Add your own words!
                   word = Word("TODO!")
                   wordDao.insert(word)
               }
           }
       }
   }

   companion object {
       @Volatile
       private var INSTANCE: WordRoomDatabase? = null

       fun getDatabase(
           context: Context,
           scope: CoroutineScope
       ): WordRoomDatabase {
            // if the INSTANCE is not null, then return it,
            // if it is, then create the database
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                        context.applicationContext,
                        WordRoomDatabase::class.java,
                        "word_database"
                )
                 .addCallback(WordDatabaseCallback(scope))
                 .build()
                INSTANCE = instance
                // return instance
                instance
        }
     }
   }
}

12. 添加 NewWordActivity - 提供手动添加子词

这里逻辑也很简单。
先添加资源:
(1) values/strings.xml 字符串资源

Word...
Save
Word not saved because it is empty.
Add word

(2) values/dimens.xml 尺寸资源

48dp

(3) 新建 NewWordActivity
注意要添加到 AndroidManifest中.
修改布局资源为:



    

    

更新 activity 的代码:

class NewWordActivity : AppCompatActivity() {

    private lateinit var editWordView: EditText

    public override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_new_word)
        editWordView = findViewById(R.id.edit_word)

        val button = findViewById

代码说明:
在 “save” button 被点击后,如果输入有单词,
则把单词保存在 EXTRA_REPLY (setResult则返回结果)

13. 与数据建立关联

最后一步是将界面连接到数据库,方法是保存用户输入的新字词,并在 RecyclerView显示当前字词数据库的内容

13.1 MainActivity 创建 ViewModel

private val wordViewModel: WordViewModel by viewModels {
    WordViewModelFactory((application as WordsApplication).repository)
}

使用了 viewModels 委托,并传入了 WordViewModelFactory 的实例.
基于从 WordsApplication 中检索的存储库构建而成.

13.2 为所有字词添加观察者

wordViewModel.allWords.observe(this, Observer { words ->
            // Update the cached copy of the words in the adapter.
            words?.let { adapter.submitList(it) }
})

13.3 响应点击 添加(FAB) 按钮,进入NewWordActivity

val fab = findViewById(R.id.fab)
fab.setOnClickListener {
  val intent = Intent(this@MainActivity, NewWordActivity::class.java)
  startActivityForResult(intent, newWordActivityRequestCode)
}

在 NewWordActivity 完成后,处理返回的结果:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)

    if (requestCode == newWordActivityRequestCode && resultCode == Activity.RESULT_OK) {
        data?.getStringExtra(NewWordActivity.EXTRA_REPLY)?.let {
            val word = Word(it)
            wordViewModel.insert(word)
        }
    } else {
        Toast.makeText(
            applicationContext,
            R.string.empty_not_saved,
            Toast.LENGTH_LONG).show()
    }
}

如果 activity 返回 RESULT_OK,请通过调用 WordViewModel 的 insert() 方法将返回的字词插入到数据库中

13.4 MainActivity 完整代码

class MainActivity : AppCompatActivity() {

    private val newWordActivityRequestCode = 1
    private val wordViewModel: WordViewModel by viewModels {
        WordViewModelFactory((application as WordsApplication).repository)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val recyclerView = findViewById(R.id.recyclerview)
        val adapter = WordListAdapter()
        recyclerView.adapter = adapter
        recyclerView.layoutManager = LinearLayoutManager(this)

        // Add an observer on the LiveData returned by getAlphabetizedWords.
        // The onChanged() method fires when the observed data changes and the activity is
        // in the foreground.
        wordViewModel.allWords.observe(owner = this) { words ->
            // Update the cached copy of the words in the adapter.
            words.let { adapter.submitList(it) }
        }

        val fab = findViewById(R.id.fab)
        fab.setOnClickListener {
            val intent = Intent(this@MainActivity, NewWordActivity::class.java)
            startActivityForResult(intent, newWordActivityRequestCode)
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, intentData: Intent?) {
        super.onActivityResult(requestCode, resultCode, intentData)

        if (requestCode == newWordActivityRequestCode && resultCode == Activity.RESULT_OK) {
            intentData?.getStringExtra(NewWordActivity.EXTRA_REPLY)?.let { reply ->
                val word = Word(reply)
                wordViewModel.insert(word)
            }
        } else {
            Toast.makeText(
                applicationContext,
                R.string.empty_not_saved,
                Toast.LENGTH_LONG
            ).show()
        }
    }
}

至此, 所有代码已结束。
运行后即可以体验!!!

14. 总结

让我们再看一遍Demo的架构图:

详细讲解:Android Room+架构组件(LiveData/ViewModel) - Demo(词典)_第7张图片

03_Word Sample 架构图.JPG

应用的组件为:
(1) MainActivity:使用 RecyclerView 和 WordListAdapter 显示列表中的字词。MainActivity 中有一个 Observer,可观察数据库中的字词,且可在字词发生变化时接收通知。
(2) NewWordActivity: 可将新字词添加到列表中。
(3) WordViewModel:提供访问数据层所用的方法,并返回 LiveData,以便 MainActivity 可以设置观察者关系。*
(4) LiveData:让界面组件的自动更新得以实现。您可以通过调用 flow.toLiveData() 从 Flow 转换为 LiveData。
(5) Repository: 可管理一个或多个数据源。Repository 用于提供 ViewModel 与底层数据提供程序交互的方法。在此应用中,后端是一个 Room 数据库。
(6) Room:是一个封装容器,用于实现 SQLite 数据库。Room 可完成许多以前由您自己完成的工作。
(7) DAO:将方法调用映射到数据库查询,以便在存储库调用 getAlphabetizedWords() 等方法时,Room 可以执行 SELECT * FROM word_table ORDER BY word ASC**。**
如果您希望在数据库发生变化时接收通知,DAO 可以提供适用于单发请求的 suspend 查询以及 Flow 查询。
(8) Word:包含单个字词的实体类。
(9) Views 和 Activities(以及 Fragments)仅通过 ViewModel 与数据进行交互。因此,数据的来源并不重要。

用于界面(反应式界面)自动更新的数据流
(1) 由于您使用了 LiveData,因此可以实现自动更新。MainActivity 中有一个 Observer,可用于观察数据库中的字词 LiveData,并在发生变化时接收通知。如果字词发生变化,则系统会执行观察者的 onChange() 方法来更新 WordListAdapter 中的 mWords。

(2) 数据可以被观察到的原因在于它是 LiveData。被观察到的数据是由 WordViewModel allWords 属性返回的 LiveData

(3) WordViewModel 会隐藏界面层后端的一切信息。WordViewModel 提供用于访问数据层的方法,并返回 LiveData,以便 MainActivity 设置观察者关系。Views 和 Activities(以及 Fragments)仅通过 ViewModel 与数据进行交互。因此,数据的来源并不重要。

(4) 在本例中,数据来自 Repository。ViewModel 无需知道存储库的交互对象。只需知道如何与 Repository 交互(通过 Repository 提供的方法)。

存储库可管理一个或多个数据源。在 WordListSample 应用中,后端是一个 Room 数据库。Room 是一个封装容器,用于实现 SQLite 数据库。Room 可完成许多以前由您自己完成的工作。例如,Room 会执行您以前使用 SQLiteOpenHelper 类执行的所有操作。

(5) DAO:将方法调用映射到数据库查询,以便在存储库调用 getAllWords() 等方法时,Room 可以执行 SELECT * FROM word_table ORDER BY word ASC。

由于在从查询返回的结果中观察到了 LiveData,因此每当 Room 中的数据发生变化时,系统都会执行 Observer 接口的 onChanged() 方法并更新界面。

本文转自 https://www.jianshu.com/p/a11c89cd5af4,如有侵权,请联系删除。

你可能感兴趣的:(Android开发,android,架构,kotlin)