Android Jetpack 体验-官方codelab
1. 实现功能
- 使用 Jetpack 架构组件 Room、ViewModel 和 LiveData 设计应用;
- 从sqlite获取、保存、删除数据;
- sqlite数据预填充功能;
- 使用 RecyclerView 展示数据列表;
2. 使用架构组件
架构组件及其协作方式:
- LiveData 是一种可观察的数据存储器,每当数据发生变化时,它都会通知观察者。 LiveData会根据负责监听变化的生命周期自动停止或恢复观察。
- ViewModel 充当存储库和UI之间的通信中心,以及应用程序中其他部分的UI相关的数据的容器。activity和fragment负责将数据绘制到屏幕上,ViewModel负责保存并处理界面所需的所有数据。
- Repository 管理数据源,可以是网络或本地的。
- RoomDatabase 简化数据库工作,它使用 DAO 向 SQLite 数据库发起请求。
- DAO 数据访问对象,一般是接口或抽象类
- SQLite 设备存储空间
- 实体:使用 Room 用于描述数据库表的带注解的类。
RoomWordSample 架构概览
3. 创建应用,配置依赖
环境:
Android Studio Flamingo | 2022.2.1 Patch 1
Android Gradle Plugin Version: 8.0.1
Gradle Version: 8.0
JDK 17
compileSdk 33
minSdk 24
targetSdk 33
统一项目依赖版本实现
- 在 build.gradle(root)下定义版本号,注意 buildscript 一定要在最上面
groovy
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext{
appCompatVersion = '1.6.1'
activityVersion = '1.6.0'
roomVersion = '2.5.0'
lifecycleVersion = '2.5.1'
coroutines = '1.6.4'
constraintLayoutVersion = '2.1.4'
materialVersion = '1.9.0'
// testing
junitVersion = '4.13.2'
androidxJunitVersion = '1.1.5'
espressoVersion = '3.5.1'
}
}
plugins {
id 'com.android.application' version '8.0.1' apply false
id 'com.android.library' version '8.0.1' apply false
id 'org.jetbrains.kotlin.android' version '1.8.20' apply false
}
- 然后在 build.gradle(app)下添加依赖
groovy
dependencies {
implementation "androidx.appcompat:appcompat:${rootProject.appCompatVersion}"
// activity-ktx 提供了 Kotlin 对 Android Activity API 的扩展。
// 这些扩展函数和属性使得在 Kotlin 中使用 Activity API 更加简洁和方便。
implementation "androidx.activity:activity-ktx:${rootProject.activityVersion}"
// Room components
implementation "androidx.room:room-ktx:${rootProject.roomVersion}"
implementation "androidx.room:room-runtime:${rootProject.roomVersion}"
annotationProcessor "androidx.room:room-compiler:${rootProject.roomVersion}"
kapt "androidx.room:room-compiler:${rootProject.roomVersion}"
testImplementation "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
// core-ktx 提供了 Kotlin 对 Android 核心库的扩展。
// 这些扩展函数和属性使得在 Kotlin 中使用 Android 核心库更加简洁和方便。
implementation "androidx.core:core-ktx:1.8.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${rootProject.coroutines}"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${rootProject.coroutines}"
// ui
implementation "com.google.android.material:material:${rootProject.materialVersion}"
implementation "androidx.constraintlayout:constraintlayout:${rootProject.constraintLayoutVersion}"
// Testing
testImplementation "junit:junit:${rootProject.junitVersion}"
androidTestImplementation "androidx.test.ext:junit:${rootProject.androidxJunitVersion}"
androidTestImplementation "androidx.test.espresso:espresso-core:${rootProject.espressoVersion}"
}
- build.gradle(app) 添加 kotlin 注解处理器插件
groovy
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt'
}
- java 版本相关设置 在build.gradle(app) 配置,解决
Execution failed for task ':app:kaptGenerateStubsDebugKotlin'
错误.
groovy
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
AndroidX 版本查询: developer.android.google.cn/jetpack/and...
4. 创建实体
创建 Word 数据类,描述在数据库中存储单词的表:
kotlin
package com.alex.roomwordssample
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
/**
* @Author : alex
* @Date : on 2024/1/4 21:05.
* @Description :Word数据类,用于定义数据库中的表
*/
@Entity(tableName = "word_table")
data class Word(@PrimaryKey @ColumnInfo(name="word") val word: String)
- 该类描述 word_table 表只有一个列:word;
@Entity(tableName =
"word_table"
)
: 表名@PrimaryKey
: 主键@ColumnInfo(name =
"word"
)
:列名
5. 创建数据访问对象 DAO
DAO 定义 SQL 查询并将其与方法调用相关联。DAO 必须是一个接口或抽象类,默认情况下,所有查询必须在单独的线程上执行。 Room 支持 kotlin 协程,可以使用 suspend
修饰符对查询进行注解,然后从协程获取其他挂起函数对其进行调用。
WordDao 接口定义:
kotlin
package com.alex.roomwordssample
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
/**
* @Author : alex
* @Date : on 2024/1/4 21:07.
* @Description :word 数据访问接口
*/
@Dao
interface WordDao {
// 按照字母顺序获取所有单词
// 为了观察数据变化情况,返回值使用了Flow
// 当数据库更新时,它会发出一个新的流,然后,您可以使用该流更新UI。
// 当Room查询返回LiveData或Flow时,查询是在单独的线程上异步执行的。
@Query("SELECT * from word_table ORDER BY word ASC")
fun getAlphabetizedWords(): Flow<List<Word>>
// 插入单词
// 将忽略与列表中的现有字词完全相同的新字词。
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(word:Word)
// 删除所有单词
@Query("DELETE FROM word_table")
suspend fun deleteAll()
}
解释:
@Dao
注解该接口表示为 Room 的 DAO类。- 删除使用的
@Query
,可以定义复杂SQL语句。 - 使用
kotlin-coroutines
中的 Flow,定义返回数据类型,是为了观察数据变化情况,当数据发生变化时,Room 会更新Flow
。
5. 添加 Room 数据库
Room 数据库类必须是抽象的,必须扩展 RoomDatabase
,整个应用通常只需要一个 Room 数据库实例。 WordRoomDatabase 定义:
kotlin
package com.alex.roomwordssample
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
/**
* @Author : alex
* @Date : on 2024/1/4 21:10.
* @Description : Room数据抽象类
*/
@Database(entities = [Word::class], version = 1, exportSchema = false)
abstract class WordRoomDatabase: RoomDatabase(){
abstract fun wordDao(): WordDao
companion object{
//单例,防止出现同时打开多个数据库实例的情况
@Volatile
private var INSTANCE: WordRoomDatabase? = null
fun getDatabase(
context: Context,
scope: CoroutineScope
): WordRoomDatabase{
return INSTANCE ?: synchronized(this){
val instance = Room.databaseBuilder(
context.applicationContext,
WordRoomDatabase::class.java,
"word_database"
)
.fallbackToDestructiveMigration()
.addCallback(WordDatabaseCallback(scope))
.build()
INSTANCE = instance
instance
}
}
// 为了在数据库创建时填充它,我们需要实现 RoomDatabase.Callback(),并覆盖 onCreate()。
private class WordDatabaseCallback(
private val scope: CoroutineScope
): RoomDatabase.Callback(){
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
INSTANCE?.let { database ->
scope.launch(Dispatchers.IO) {
populateDatabase(database.wordDao())
}
}
}
}
suspend fun populateDatabase(wordDao: WordDao){
wordDao.deleteAll()
var word=Word("Hello")
wordDao.insert(word)
word=Word("World")
wordDao.insert(word)
}
}
}
代码解释:
- Room 数据库类必须是抽象的,必须扩展
RoomDatabase
。 @Database
将该类注解为 Room 数据库,并使用注解参数声明数据库中的实体以及设置版本号。- 数据库通过每个
@Dao
的抽象getter方法公开 DAO - 定义单例 WordRoomDatabase,防止同时打开数据库的多个实例。
- getDatabase 会返回单例,首次使用时会创建数据库,并删除旧数据,填充示例数据。
6. 创建存储库
Repository 会将多个数据源的访问权限抽象化,提供一个整洁的 API,用于获取对应用其余部分的数据访问权限。
WordRepository 定义:
kotlin
package com.alex.roomwordssample
import androidx.annotation.WorkerThread
import kotlinx.coroutines.flow.Flow
/**
* @Author : alex
* @Date : on 2024/1/4 21:13.
* @Description : word 存储库,可以用于管理多个数据源
*/
class WordRepository(private val wordDao:WordDao) {
// Room在单独的线程上执行所有查询
// 观察数据变化情况,返回值使用了Flow
val allWords:Flow<List<Word>> = wordDao.getAlphabetizedWords()
// 在后台线程中执行操作
@Suppress("RedundantSuspendModifier")
@WorkerThread
suspend fun insert(word: Word){
wordDao.insert(word)
}
}
- DAO 会被传递到存储库构造函数中,而非整个数据库中。DAO 包含数据库的所有读取/写入方法,因此它只需要访问 DAO,无需向存储库公开整个数据库。
- allWords 表具有公开属性。它通过从 Room 获取
Flow
字词列表来进行初始化;您之所以能够实现该操作,是因为您在"观察数据库变化"步骤中定义getAlphabetizedWords
方法以返回Flow
的方式。Room 将在单独的线程上执行所有查询。 - Room 在主线程之外执行挂起查询。
7. 创建 ViewModel
ViewModel: 向界面提供数据,不受配置变化的影响。ViewModel 是 Lifecycle 库的一部分。
LiveData与ViewModel的关系
LiveData 是一种可观察的数据存储器,每当数据发生变化时,您都会收到通知。与 Flow 不同,LiveData 具有生命周期感知能力,即遵循其他应用组件(如 activity 或 fragment)的生命周期。LiveData 会根据负责监听变化的组件的生命周期自动停止或恢复观察。因此,LiveData 适用于界面使用或显示的可变数据。
ViewModel 会将存储库中的数据从 Flow 转换为 LiveData,并将字词列表作为 LiveData 传递给界面。这样可以确保每次数据库中的数据发生变化时,界面都会自动更新。
viewModelScope
AndroidX lifecycle-viewmodel-ktx
库将 viewModelScope
添加为 ViewModel
类的扩展函数。
WordViewModel 定义:
kotlin
package com.alex.roomwordssample
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
/**
* @Author : alex
* @Date : on 2024/1/4 21:15.
* @Description : ViewModel 充当存储库和UI之间的通信中心,以及应用程序中其他部分的UI相关的数据的容器。
* ViewModel通过使用LiveData或Flow来保留数据,这样它就可以在配置更改后继续存在。
* ViewModel可以通过调用ViewModelProvider.Factory来创建。
* activity和fragment负责将数据绘制到屏幕上,ViewModel负责保存并处理界面所需的所有数据。
*/
class WordViewModel(private val repository:WordRepository) :ViewModel(){
// LiveData 是一种可观察的数据存储器,每当数据发生变化时,它都会通知观察者。
// LiveData会根据负责监听变化的生命周期自动停止或恢复观察。
// 使用 LiveData 并缓存 allWords 返回的内容有几个好处:
// - 我们可以在数据上设置一个观察者(而不是轮询变化),并且只有当数据实际发生变化时才更新用户界面。
// - 通过 ViewModel,仓库与用户界面完全分离。
val allWords: LiveData<List<Word>> = repository.allWords.asLiveData()
// 启动一个新的协程以非阻塞方式插入数据。
fun insert(word: Word) = viewModelScope.launch {
repository.insert(word)
}
}
/**
* WordViewModelFactory 类的作用是创建 WordViewModel 实例,
* 并确保 WordViewModel 可以接收到 WordRepository 实例,以便它可以与数据源进行交互。
*/
class WordViewModelFactory(private val repository: WordRepository): ViewModelProvider.Factory{
override fun <T : ViewModel> create(modelClass: Class<T>): T {
// 检查modelClass是否是WordViewModel的子类
if (modelClass.isAssignableFrom(WordViewModel::class.java)){
@Suppress("UNCHECKED_CAST")
return WordViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
解析:
- 创建了一个名为
WordViewModel
的类,该类可获取WordRepository
作为参数并扩展ViewModel
。存储库是 ViewModel 需要的唯一依赖项。如果需要其他类,系统也会在构造函数中传递相应的类。 - 添加了一个公开的
LiveData
成员变量以缓存字词列表。 - 使用存储库中的
allWords
Flow 初始化了LiveData
。然后,您通过调用asLiveData().
将该 Flow 转换成了 LiveData。 - 创建了一个可调用存储库的
insert()
方法的封装容器insert()
方法。这样一来,便可从界面封装insert()
的实现。我们将启动新协程并调用存储库的挂起函数 insert。如上所述,ViewModel 的协程作用域基于它的名为viewModelScope
的生命周期(您将在这里使用)。 - 创建了 ViewModel,并实现了
ViewModelProvider.Factory
,后者可获取创建WordViewModel
所需的依赖项作为参数:WordRepository
。
使用 viewModels
和 ViewModelProvider.Factory
后,框架将负责 ViewModel 的生命周期。它不受配置变化的影响,即使重建 activity,您始终能得到 WordViewModel
类的正确实例。
使用asLiveData,需要添加下面的依赖:
shell
> 添加依赖:implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"
> 引入:import androidx.lifecycle.asLiveData
8. 列表页面布局实现(MainActivity)
列表页面使用了 RecyclerView 组件,需要先定义列表项布局 recyclerview_item.xml :
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>
在activity_main.xml 中引入 RecyclerView,并添加一个浮动按钮
xml
<?xml version="1.0" encoding="utf-8"?>
<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"
android:src="@drawable/id_add_black_24db"/>
</androidx.constraintlayout.widget.ConstraintLayout>
浮动按钮图标的制作,使用了 Asset Studio 工具(File->New->Vector Asset)
9. RecyclerView
MainActivity 中 使用 RecyclerView 显示数据。
添加步骤:
- 定义 WordListAdapter 类
- 定义填充列表项行为
- 在 MainActivity 中添加 RecyclerView
WordListAdapter 类定义:
kotlin
package com.alex.roomwordssample
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
/**
* @Author : alex
* @Date : on 2024/1/4 21:40.
* @Description :RecyclerView的适配器
*/
class WordListAdapter:ListAdapter<Word, WordListAdapter.WordViewHolder>(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<Word>() {
override fun areItemsTheSame(oldItem: Word, newItem: Word): Boolean {
return oldItem === newItem
}
override fun areContentsTheSame(oldItem: Word, newItem: Word): Boolean {
return oldItem.word == newItem.word
}
}
}
RecyclerView 是 Android 中用于显示大量数据集的一个组件,它优化了这些数据的显示,只创建和渲染屏幕上可见的部分,从而提高了性能。
RecyclerView 通过一个适配器来管理数据的显示,适配器负责将数据与每个列表项视图(item view)进行绑定。
RecyclerView.ViewHolder 是一个静态类,用于存储对列表项视图中的界面元素的引用。
在这个例子中,WordViewHolder 是 RecyclerView.ViewHolder 的一个子类,它存储了对 TextView 的引用,并提供了一个 bind 方法来更新 TextView 的内容。
kotlin
class WordViewHolder(itemView:View):RecyclerView.ViewHolder(itemView) {
private val wordItemView:TextView= itemView.findViewById(R.id.textView)
fun bind(text:String?){
wordItemView.text=text
}
...
}
ListAdapter 是 RecyclerView.Adapter 的一个子类,它使用 DiffUtil 来计算数据集的最小更新。当数据发生变化时,ListAdapter 会计算出新旧数据集之间的差异,并使用这些差异来更新 RecyclerView。
在这个例子中,WordListAdapter 是 ListAdapter 的一个子类,它使用 WordsComparator 来计算数据集的差异。
WordsComparator 是 DiffUtil.ItemCallback 的一个子类,它提供了两个方法:areItemsTheSame 和 areContentsTheSame。
areItemsTheSame 用于检查两个 Word 是否表示同一个对象,areContentsTheSame 用于检查两个 Word 的内容是否相同。
kotlin
class WordsComparator : DiffUtil.ItemCallback<Word>() {
override fun areItemsTheSame(oldItem: Word, newItem: Word): Boolean {
return oldItem === newItem
}
override fun areContentsTheSame(oldItem: Word, newItem: Word): Boolean {
return oldItem.word == newItem.word
}
}
WordViewHolder 中的 create 静态方法用于创建 WordViewHolder 的实例。这个方法接收一个 ViewGroup 类型的参数 parent,这通常是 RecyclerView。
在 create 方法中,首先通过 LayoutInflater 从 recyclerview_item.xml 布局文件中创建一个新的视图。然后,将这个新创建的视图作为参数传递给 WordViewHolder 的构造函数,创建一个 WordViewHolder 的实例。
这样做的好处是,WordViewHolder 的创建逻辑被封装在 WordViewHolder 类内部,使得 WordListAdapter 的代码更加简洁。同时,如果 WordViewHolder 的创建逻辑需要修改,只需要在 WordViewHolder 类内部修改,而不需要修改 WordListAdapter 的代码。
在MainActivity中添加 RecyclerView:
kotlin
setContentView(R.layout.activity_main)
val recyclerView=findViewById<RecyclerView>(R.id.recyclerview)
val adapter=WordListAdapter()
recyclerView.adapter=adapter
recyclerView.layoutManager= LinearLayoutManager(this)
10. 在应用中实例化Repository和Database
您希望应用中的数据库和存储库只有一个实例。实现该目的的一种简单的方法是,将它们作为 Application
类的成员进行创建。然后,在需要时只需从应用检索,而不是每次都进行构建。 创建 WordsApplication,继承自 Application:
kotlin
package com.alex.roomwordssample
import android.app.Application
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
/**
* @Author : alex
* @Date : on 2024/1/4 22:03.
* @Description : 实现Application类,以便在整个应用程序中使用单个实例
*/
class WordsApplication: Application(){
// 为应用程序的生命周期创建一个作用域,以便在应用程序被销毁时取消所有协程
// 不需要取消这个作用域,因为它会随着进程的结束而被销毁。
// SupervisorJob() 创建了一个新的 Job 实例,并将其作为参数传递给 CoroutineScope 的构造函数,创建了一个新的协程作用域 applicationScope。
// 这个作用域的特性是,它的子协程之间是相互独立的,一个子协程的失败不会导致其他子协程的取消。
val applicationScope = CoroutineScope(SupervisorJob())
val database by lazy {
WordRoomDatabase.getDatabase(this,applicationScope)
}
val repository by lazy { WordRepository(database.wordDao()) }
}
在 AndroidManifest 文件将 WordApplication 设为 application android:name
11. 填充数据库
在 WordRoomDatabase 定义了 WordDatabaseCallback 用于在创建数据库的时候,删除旧数据,并添加示例数据
12. 添加 新增数据页面 NewWordActivity
布局 activity_new_word.xml:
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".NewWordActivity">
<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/purple_500"
android:text="@string/button_save"
android:layout_margin="@dimen/big_padding"
android:textColor="@color/buttonLabel" />
</LinearLayout>
NewWordActivity 代码:
kotlin
package com.alex.roomwordssample
import android.app.Activity
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.text.TextUtils
import android.widget.Button
import android.widget.EditText
class NewWordActivity : AppCompatActivity() {
private lateinit var editWordView:EditText
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_new_word)
editWordView = findViewById(R.id.edit_word)
val button = findViewById<Button>(R.id.button_save)
button.setOnClickListener {
val replyIntent = Intent()
if(TextUtils.isEmpty(editWordView.text)){
setResult(Activity.RESULT_CANCELED,replyIntent)
}else{
val word = editWordView.text.toString()
replyIntent.putExtra(EXTRA_REPLY,word)
setResult(Activity.RESULT_OK,replyIntent)
}
finish()
}
}
companion object{
const val EXTRA_REPLY = "com.alex.roomwordssample.REPLY"
}
}
13. 数据与页面关联
最后一步是将界面连接到数据库,方法是保存用户输入的新字词,并在 RecyclerView
中显示当前字词数据库的内容。
如需显示数据库的当前内容,请添加可观察 ViewModel
中的 LiveData
的观察者。
每当数据发生变化时,系统都会调用 onChanged()
回调,此操作会调用适配器的 setWords()
方法来更新此适配器的缓存数据并刷新显示的列表。
1. 在 MainActivity 中创建 ViewModel
kotlin
// 通过 viewModels 委托属性实现ViewModel的实例化
private val wordViewModel: WordViewModel by viewModels {
WordViewModelFactory((application as WordsApplication).repository)
}
这里使用了 viewModels 委托,并传入了 WordViewModelFactory 实例,该实例基于从 WordApplication 中检索的存储库构建而成。
当观察到数据发生变化且 activity 在前台显示时,将触发 onChanged() 方法:
kotlin
// 通过调用 observe() 来观察 LiveData 对象,传入 LifecycleOwner 和 Observer。
wordViewModel.allWords.observe(this){ words ->
words.let { adapter.submitList(it) }
}
浮动按钮点击事件,将打开 NewWordActivity ,这里使用了registerForActivityResult 方法和 ActivityResultContracts,因为 startActivityForResult 和 onActivityResult 方法在 Android 11(API 30)中已被弃用。
完整代码:
kotlin
package com.alex.roomwordssample
import android.app.Activity
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import androidx.activity.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.floatingactionbutton.FloatingActionButton
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.result.ActivityResultLauncher
class MainActivity : AppCompatActivity() {
// 请求代码,打开 NewWordActivity 时使用
// private val newWordActivityRequestCode = 1
private lateinit var newWordActivityLauncher: ActivityResultLauncher<Intent>
// 通过 viewModels 委托属性实现ViewModel的实例化
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<RecyclerView>(R.id.recyclerview)
val adapter=WordListAdapter()
recyclerView.adapter=adapter
recyclerView.layoutManager= LinearLayoutManager(this)
// 通过调用 observe() 来观察 LiveData 对象,传入 LifecycleOwner 和 Observer。
wordViewModel.allWords.observe(this){ words ->
words.let { adapter.submitList(it) }
}
val fab = findViewById<FloatingActionButton>(R.id.fab)
newWordActivityLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val data: Intent? = result.data
data?.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()
}
}
fab.setOnClickListener {
val intent = Intent(this@MainActivity, NewWordActivity::class.java)
// startActivityForResult 和 onActivityResult 方法在 Android 11(API 30)中已被弃用。
// 取而代之的是 registerForActivityResult 方法和 ActivityResultContracts 类。
// startActivityForResult(intent, newWordActivityRequestCode)
newWordActivityLauncher.launch(intent)
}
}
// 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()
// }
// }
}