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

Android Jetpack 体验-官方codelab

1. 实现功能

  1. 使用 Jetpack 架构组件 Room、ViewModel 和 LiveData 设计应用;
  2. 从sqlite获取、保存、删除数据;
  3. sqlite数据预填充功能;
  4. 使用 RecyclerView 展示数据列表;

2. 使用架构组件

架构组件及其协作方式:

image.png

  • LiveData 是一种可观察的数据存储器,每当数据发生变化时,它都会通知观察者。 LiveData会根据负责监听变化的生命周期自动停止或恢复观察。
  • ViewModel 充当存储库和UI之间的通信中心,以及应用程序中其他部分的UI相关的数据的容器。activity和fragment负责将数据绘制到屏幕上,ViewModel负责保存并处理界面所需的所有数据。
  • Repository 管理数据源,可以是网络或本地的。
  • RoomDatabase 简化数据库工作,它使用 DAO 向 SQLite 数据库发起请求。
  • DAO 数据访问对象,一般是接口或抽象类
  • SQLite 设备存储空间
  • 实体:使用 Room 用于描述数据库表的带注解的类。

RoomWordSample 架构概览

image.png

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 一定要在最上面
// 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)下添加依赖
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 注解处理器插件
plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'kotlin-kapt'
}
  1. java 版本相关设置
    在build.gradle(app) 配置,解决 Execution failed for task ':app:kaptGenerateStubsDebugKotlin' 错误.
compileOptions {
    sourceCompatibility JavaVersion.VERSION_17
    targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
    jvmTarget = '17'
}

AndroidX 版本查询:
https://developer.android.google.cn/jetpack/androidx/versions?hl=zh-cn

4. 创建实体

创建 Word 数据类,描述在数据库中存储单词的表:

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 接口定义:

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 定义:

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 定义:

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 定义:

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,需要添加下面的依赖:

> 添加依赖:implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"
> 引入:import androidx.lifecycle.asLiveData

8. 列表页面布局实现(MainActivity)

列表页面使用了 RecyclerView 组件,需要先定义列表项布局 recyclerview_item.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 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 类定义:

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 的内容。

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 的内容是否相同。

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:

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:

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 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 代码:

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

// 通过 viewModels 委托属性实现ViewModel的实例化
private val wordViewModel: WordViewModel by viewModels {
    WordViewModelFactory((application as WordsApplication).repository)
}

这里使用了 viewModels 委托,并传入了 WordViewModelFactory 实例,该实例基于从 WordApplication 中检索的存储库构建而成。

当观察到数据发生变化且 activity 在前台显示时,将触发 onChanged() 方法:

// 通过调用 observe() 来观察 LiveData 对象,传入 LifecycleOwner 和 Observer。
wordViewModel.allWords.observe(this){ words ->
    words.let { adapter.submitList(it) }
}

浮动按钮点击事件,将打开 NewWordActivity ,这里使用了registerForActivityResult 方法和 ActivityResultContracts,因为 startActivityForResult 和 onActivityResult 方法在 Android 11(API 30)中已被弃用。

完整代码:

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()
//        }
//    }

}

image.png

image.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/310211.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Untiy HTC Vive VRTK 开发记录

目录 一.概述 二.功能实现 1.模型抓取 1&#xff09;基础抓取脚本 2&#xff09;抓取物体在手柄上的角度 2.模型放置区域高亮并吸附 1&#xff09;VRTK_SnapDropZone 2&#xff09;VRTK_PolicyList 3&#xff09;VRTK_SnapDropZone_UnityEvents 3.交互滑动条 4.交互旋…

cpp_10_多重继承_钻石继承_虚继承

1 多重继承 一个类可以同时从多个基类继承实现代码。 1.1 多重继承的内存布局 子类对象内部包含多个基类子对象。 按照继承表的顺序依次被构造&#xff0c;析构的顺序与构造严格相反。 各个基类子对象按照从低地址到高地址排列。 // miorder.cpp 多重继承&#xff1a;一个子…

Rust类型之字符串

字符串 Rust 中的字符串类型是String。虽然字符串只是比字符多了一个“串”字&#xff0c;但是在Rust中这两者的存储方式完全不一样&#xff0c;字符串不是字符的数组&#xff0c;String内部存储的是Unicode字符串的UTF8编码&#xff0c;而char直接存的是Unicode Scalar Value…

大模型学习之书生·浦语大模型4——基于Xtuner大模型微调实战

基于Xtuner大模型微调实战 Fintune简介 海量数据训练的base model指令微调Instructed LLM 增量预训练微调 增量数据不需要问题&#xff0c;只需要答案&#xff0c;只需要陈述类的数据 指令跟随微调 指定角色指定问题给对应的user指定答案给assistant LIaMa2InternLM 不同的模…

什么是Modbus协议?

Modbus协议是一种在工业自动化领域广泛应用的通信协议&#xff0c;它允许不同设备之间进行可靠的数据交换和控制。该协议最初由Modicon公司于1979年创建&#xff0c;旨在提供一种简单而有效的方法&#xff0c;使PLC&#xff08;可编程逻辑控制器&#xff09;和其他自动化设备能…

前端绕过无限Debug

1.准备 burp : https://pan.baidu.com/s/1aqCywnF_S-HzIWVGLjiW-A 提取码: mpen BurpLoaderKeygen:链接: https://pan.baidu.com/s/1Vck_hFMT2YXP1cbmYfFqsA 提取码: qggp 点击Next后把Request粘贴到LoaderKeygen中&#xff0c;然后把Response粘贴到Burp Suite中 注&#xff1…

2024年【熔化焊接与热切割】考试内容及熔化焊接与热切割免费试题

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 熔化焊接与热切割考试内容是安全生产模拟考试一点通总题库中生成的一套熔化焊接与热切割免费试题&#xff0c;安全生产模拟考试一点通上熔化焊接与热切割作业手机同步练习。2024年【熔化焊接与热切割】考试内容及熔化…

in <module> input = brower.find_element(‘kw‘)

1. 概念名称&#xff1a; in <module> input brower.find_element(kw) 2. 概念定义&#xff1a; 这行代码使用了Selenium WebDriver的find_element方法来定位页面上的一个元素 3. 我对概念的理解&#xff1a; find_element方法用于查找页面上的元素&#xff0c;但这里的…

Mysql是怎样运行的--下

文章目录 Mysql是怎样运行的--下查询优化explainoptimizer_trace InnoDB的Buffer Pool&#xff08;缓冲池&#xff09;Buffer Pool的存储结构空闲页存储--free链表脏页&#xff08;修改后的数据&#xff09;存储--flush链表 使用Buffer PoolLRU链表的管理 事务ACID事务的状态事…

Triumphcore FPGA调测试记录

FPGA采用Xilinx pynq Z2开发板。基于V2.5版本开发 OverView uart端口映射 BUG调试记录 2024.1.7 复位状态导致取指时序错误 错误波形&#xff1a; 正确波形 问题代码&#xff1a; 2024.1.9 clock_wizard设置输入时钟是输出时钟的2^n倍&#xff0c;输出时钟的占空比才…

电能质量Python实现全家桶——全网最低价

往期精彩内容&#xff1a; 电能质量扰动信号数据介绍与分类-Python实现-CSDN博客 Python电能质量扰动信号分类(一)基于LSTM模型的一维信号分类-CSDN博客 Python电能质量扰动信号分类(二)基于CNN模型的一维信号分类-CSDN博客 Python电能质量扰动信号分类(三)基于Transformer…

数据结构之单调栈、单调队列

今天学习了单调栈还有单调队列的概念和使用&#xff0c;接下来我将对其定义并配合几道习题进行讲解&#xff1a; 首先先来复习一下栈与队列&#xff1a; 然后我们来看一下单调栈的定义&#xff1a; 单调栈中的元素从栈底到栈顶的元素的大小是按照单调递增或者单调递减的关系进…

Spring之整合Mybatis底层源码

文章目录 一、整体核心思路1 . 简介2. 整合思路 二、源码分析1. 环境准备2. 源码分析 一、整体核心思路 1 . 简介 有很多框架需要与Spring进行整合&#xff0c;而整合的核心思路就是把其他框架所产生的对象放到Spring容器中&#xff0c;让其成为一个bean。比如Mybatis&#x…

使用requests库测试post请求 操作流程

第一步 谷歌f12或其他抓包工具抓包&#xff0c;这里随机抓一个post请求 url&#xff1a;https://eva2.csdn.net/v3/06981375190026432f77c01bfca33e32/lts/groups/dadde766-b087-42da-8e67-d2499a520ee7/streams/a0119567-bf91-4314-ab75-f683ba6c0c0a/logs 第二步 导包 impo…

uniapp在web端怎么使用svg图标呢

在图标库中添加好项目用到的图标&#xff0c;点击symbol点击生成在线链接 点击生成的在线链接&#xff0c;此时会跳转到一个新窗口&#xff0c;是一个js文件 复制这个js文件的内容 然后在uniapp中新建svg.js文件&#xff0c;把从上面复制的代码粘贴到这个svg.js中 在main.js中引…

在本地测试nginx中localhost不行,需要写成127.0.0.1

在Windows 10系统的命令提示符cmd中&#xff0c;执行命令ping localhost&#xff0c;并没有出现我与其的ip地址“127.0.0.1”&#xff0c;而是“[::1]”。 问题原因 在cmd中ping localhost解析出来的是ipv6的::1的原因是windows有个优先解析列表&#xff0c;当ipv6的优先级高于…

C++学习笔记——对象的指针

目录 一、对象的指针 二、减少对象的复制开销 三、应用案例 游戏引擎 图像处理库 数据库管理系统 航空航天软件 金融交易系统 四、代码的案例应用 一、对象的指针 是一种常用的技术&#xff0c;用于处理对象的动态分配和管理。使用对象的指针可以实现以下几个方面的功…

pythroch abaconda 安装 cuda、版本确定、pytorch 安装

一、简述 公司有一个深度学习的项目&#xff0c;身上也没有其他项目&#xff0c;恰好乘着个机会学一下pytorch 和YOLOv8. 1、下载abaconda https://repo.anaconda.com/archive/ 2、安装 环境变量要✔ 其他一直下一步 3、测试 (base) C:\Users\alber>conda -V cond…

Tensorflow Lite从入门到精通

TensorFlow Lite 是 TensorFlow 在移动和 IoT 等边缘设备端的解决方案&#xff0c;提供了 Java、Python 和 C API 库&#xff0c;可以运行在 Android、iOS 和 Raspberry Pi 等设备上。目前 TFLite 只提供了推理功能&#xff0c;在服务器端进行训练后&#xff0c;经过如下简单处…

C++11_lambda表达式

文章目录 一、lambda表达式1.lambda的组成2.[capture-list] 的其他使用方法2.1混合捕捉 二、lambda表达式的使用场景1.替代仿函数 总结 一、lambda表达式 lambda表达式是C11新引入的功能&#xff0c;它的用法与我们之前学过的C语法有些不同。 1.lambda的组成 [capture-list] …