开启Android学习之旅-2-架构组件实现数据列表及添加(kotlin)

Android Jetpack 体验-官方codelab

1. 实现功能

  1. 使用 Jetpack 架构组件 Room、ViewModel 和 LiveData 设计应用;
  2. 从sqlite获取、保存、删除数据;
  3. sqlite数据预填充功能;
  4. 使用 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

统一项目依赖版本实现

  1. 在 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
}
  1. 然后在 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}"
}
  1. build.gradle(app) 添加 kotlin 注解处理器插件
groovy 复制代码
plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'kotlin-kapt'
}
  1. 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

使用 viewModelsViewModelProvider.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()
//        }
//    }

}
相关推荐
Estar.Lee35 分钟前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
找藉口是失败者的习惯1 小时前
从传统到未来:Android XML布局 与 Jetpack Compose的全面对比
android·xml
Jinkey2 小时前
FlutterBasic - GetBuilder、Obx、GetX<Controller>、GetxController 有啥区别
android·flutter·ios
大白要努力!4 小时前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
天空中的野鸟5 小时前
Android音频采集
android·音视频
小白也想学C6 小时前
Android 功耗分析(底层篇)
android·功耗
曙曙学编程6 小时前
初级数据结构——树
android·java·数据结构
闲暇部落8 小时前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin
诸神黄昏EX10 小时前
Android 分区相关介绍
android
大白要努力!11 小时前
android 使用SQLiteOpenHelper 如何优化数据库的性能
android·数据库·oracle