有时我们想显示一个关于列表项的额外内容,它不一定需要一个单独的屏幕,通常称为详细屏幕,这就是 Expandable RecyclerView 的用武之地,我们将学习如何使用可扩展的 recyclerview 创建可扩展的THOUGHBOT
recyclerview回收视图库。我们还将使用它从本地数据库中获取我们的项目, Room Persistence Library
这是Android Architecture Components
我们将显示来自本地数据库的大陆列表及其下的一些国家/地区,该数据库仅在创建数据库时添加一次,最终结果应如下图所示。
在您创建一个带有空活动的新项目后,将以下依赖项添加到您的应用级别 build.gradle
// Room components
implementation "androidx.room:room-runtime:$rootProject.roomVersion"
kapt "androidx.room:room-compiler:$rootProject.roomVersion"
implementation "androidx.room:room-ktx:$rootProject.roomVersion"
androidTestImplementation "androidx.room:room-testing:$rootProject.roomVersion"
//Gson
implementation 'com.google.code.gson:gson:2.8.6'
// Lifecycle components
implementation "androidx.lifecycle:lifecycle-extensions:$rootProject.archLifecycleVersion"
kapt "androidx.lifecycle:lifecycle-compiler:$rootProject.archLifecycleVersion"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.archLifecycleVersion"
// 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"
// Material design
implementation "com.google.android.material:material:$rootProject.materialVersion"
//Expandable
implementation 'com.thoughtbot:expandablerecyclerview:1.3'
implementation 'com.thoughtbot:expandablecheckrecyclerview:1.4'
还将版本添加到项目级别 build.gradle
ext {
roomVersion = '2.2.5'
archLifecycleVersion = '2.2.0'
coreTestingVersion = '2.1.0'
materialVersion = '1.1.0'
coroutines = '1.3.4'
}
确保以下插件存在于应用程序级别构建的顶部。
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
使用 Room 时需要三个主要的Entity
类,代表数据库中表的DAO
类,顾名思义,该类是一个包含用于访问数据库的方法的数据访问对象,即database
类。
ContinentEntity.kt
package com.developer.kulloveth.expandablelistsamplewithroom.data.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.TypeConverters
@Entity(tableName = "continent-table")
@TypeConverters(ContinentConverter::class)
data class ContinentEntity
(@PrimaryKey @ColumnInfo(name = "continent")
val continentName: String, val countrys: List<Country>
)
此类具有 @Entity 注释,将要创建的表名传递到其参数中,如果您不希望将类名用作表名,这是可选的,@ColumnInfo 告诉数据库使用大陆作为列名所有表必须具有的continentName 变量和@PrimaryKey。还要注意@TypeConverters,它是告诉空间用 ContinentConverter 类转换 List 的注释
package com.developer.kulloveth.expandablelistsamplewithroom.data.model
import androidx.room.TypeConverter
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import java.lang.reflect.Type
import java.util.*
class ContinentConverter {
companion object {
var gson: Gson = Gson()
@TypeConverter
@JvmStatic
fun stringToSomeObjectList(data: String?): List<Country> {
val listType: Type =
object : TypeToken<List<Country?>?>() {}.getType()
return gson.fromJson(data, listType)
}
@TypeConverter
@JvmStatic
fun someObjectListToString(someObjects: List<Country>?): String {
return gson.toJson(someObjects)
}
}
}
这是在每个使用 Gson 库执行转换的方法上都有 @TypeConverter 的转换器类
ContinentDao.kt
package com.developer.kulloveth.expandablelistsamplewithroom.data.db
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.developer.kulloveth.expandablelistsamplewithroom.data.model.ContinentEntity
import
com.developer.kulloveth.expandablelistsamplewithroom.data.model.Continents
@Dao
interface ContinentDao {
@Query("SELECT * from `continent-table` ORDER BY continent ASC")
fun getAllContinent(): LiveData<List<ContinentEntity>>
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(continent: ContinentEntity)
}
这是访问数据库的 dao 接口,getAllContinent 方法有 @Query 注解,它按升序获取所有数据,它返回一个LiveData
有助于保持数据更新并自动在后台线程上异步运行操作。insert 方法具有 @Insert 注释,用于插入数据以处理可能发生的冲突,它使用挂起函数来指示该方法需要时间来执行,因为我们不想阻塞主线程。
ContinentDatabase.kt
package com.developer.kulloveth.expandablelistsamplewithroom.data.db
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import com.developer.kulloveth.expandablelistsamplewithroom.data.DataGenerator
import com.developer.kulloveth.expandablelistsamplewithroom.data.model.ContinentEntity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@Database(entities = [ContinentEntity::class], version = 1, exportSchema = false)
abstract class ContinentDatabase : RoomDatabase() {
abstract fun continentDao(): ContinentDao
companion object {
@Volatile
private var INSTANCE: ContinentDatabase? = null
fun getDatabase(context: Context, scope: CoroutineScope): ContinentDatabase {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: buildDatabase(context, scope).also {
INSTANCE = it
}
}
}
private fun buildDatabase(context: Context, scope: CoroutineScope): ContinentDatabase {
return Room.databaseBuilder(context, ContinentDatabase::class.java, "place_db")
.addCallback(object : RoomDatabase.Callback()
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
scope.launch {
INSTANCE?.let {
for (continent: ContinentEntity in DataGenerator.getContinents()) {
it.continentDao().insert(
ContinentEntity(
continent.continentName,
continent.countrys
) )
}}}}}).build()
}}}
这是一个数据库类,它必须是一个抽象类,并且必须包含一个表示 dao 接口类的抽象方法,它具有 @Database 及其实体、版本并将 export-schema 设置为 false,因为我们没有将数据库导出到文件夹中. getDatabase 方法是一个单例,它确保在任何时候只打开一个数据库实例,我们还添加了一个 roomCallback 以在使用其 onCreate 方法创建房间时只插入一次数据。请注意,插入方法是在协程范围内调用的,因为它是一个挂起函数,以确保在后台线程上执行操作。
DataGenerator.kt
package com.developer.kulloveth.expandablelistsamplewithroom.data
import com.developer.kulloveth.expandablelistsamplewithroom.data.model.ContinentEntity
import com.developer.kulloveth.expandablelistsamplewithroom.data.model.Country
class DataGenerator {
companion object {
fun getContinents(): List<ContinentEntity> {
return listOf(
ContinentEntity("Europe", europeCountrys()),
ContinentEntity("Africa", africaCountrys()),
ContinentEntity("Asia", asiaCountrys()),
ContinentEntity("North America", northAmericaCountrys()),
ContinentEntity("South America", southAmericaCountrys()),
ContinentEntity("Antarctica", antarcticaCountrys()),
ContinentEntity("Oceania", oceaniaCountrys())
)
}
fun europeCountrys(): List<Country> {
return listOf(
Country("Germany"),
Country("Italy"),
Country("France"),
Country("United Kingdom"),
Country("NertherLand")
)
}
fun africaCountrys(): List<Country> {
return listOf(
Country("South Africa"),
Country("Nigeria"),
Country("Kenya"),
Country("Ghana"),
Country("Ethiopia")
)
}
fun asiaCountrys(): List<Country> {
return listOf(
Country("Japan"),
Country("India"),
Country("Indonesi"),
Country("China"),
Country("Thailand")
)
}
fun northAmericaCountrys(): List<Country> {
return listOf(
Country("United States"),
Country("Mexico"),
Country("Cuba"),
Country("Green Land")
)
}
fun southAmericaCountrys(): List<Country> {
return listOf(
Country("Brazil"),
Country("Argentina"),
Country("Columbia"),
Country("Peru"),
Country("Chile")
)}
fun antarcticaCountrys(): List<Country> {
return listOf(
Country("Esperenza Base"),
Country("Villa az Estrellaz"),
Country("General Bernando O'Higging"),
Country("Bellgrano II base"),
Country("Carlini Base") )}
fun oceaniaCountrys(): List<Country> {
return listOf(
Country("Australia"),
Country("Newzeland"),
Country("Fiji"),
Country("Samao"),
Country("Federated States")
)}}}
接下来我们将创建适配器,它的数据类,观察我们添加到 Room 的数据并设置 recyclerView。
Continent.kt
package com.developer.kulloveth.expandablelistsamplewithroom.data.model
import com.thoughtbot.expandablerecyclerview.models.ExpandableGroup
data class Continent(
val continentName: String, val countries: List<Country>
): ExpandableGroup<Country>(continentName, countries)
Country.kt
package com.developer.kulloveth.expandablelistsamplewithroom.data.model
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.android.parcel.Parcelize
@Parcelize
data class Country(val countryName: String) : Parcelable
Continent 类是与适配器一起使用的父类,它将通过子类 Country 扩展 ExpandableGroup
continent_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="0dp"
app:cardUseCompatPadding="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/arrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_marginEnd="16dp"
android:src="@drawable/ic_arrow_drop_down_black_24dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/continent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="?listPreferredItemPaddingLeft"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
countrys_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="50dp"
android:padding="0dp">
<TextView
android:id="@+id/countryName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
tools:text="Niger" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@android:color/black" />
</androidx.cardview.widget.CardView>
以上布局是要在各自的视图中引用的项目的父子布局
MainViewHolder.kt
package com.developer.kulloveth.expandablelistsamplewithroom.data
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import com.developer.kulloveth.expandablelistsamplewithroom.R
import com.developer.kulloveth.expandablelistsamplewithroom.data.model.Continent
import com.developer.kulloveth.expandablelistsamplewithroom.data.model.Country
import com.thoughtbot.expandablerecyclerview.viewholders.ChildViewHolder
import com.thoughtbot.expandablerecyclerview.viewholders.GroupViewHolder
class CountryViewHolder(itemView: View) : ChildViewHolder(itemView) {
val countryName = itemView.findViewById<TextView>(R.id.countryName)
fun bind(country: Country) {
countryName.text = country.countryName
}
}
class ContinentViewHolder(itemView: View) : GroupViewHolder(itemView) {
val continentName = itemView.findViewById<TextView>(R.id.continent)
val arrow = itemView.findViewById<ImageView>(R.id.arrow)
fun bind(continent: Continent) {
continentName.text = continent.continentName
}
}
MainViewHolder 是一个 kotlin 文件,包含父视图和子视图
ContinentAdapter.kt
package com.developer.kulloveth.expandablelistsamplewithroom.data.ui
import android.view.LayoutInflater
import android.view.ViewGroup
import com.developer.kulloveth.expandablelistsamplewithroom.R
import com.developer.kulloveth.expandablelistsamplewithroom.data.ContinentViewHolder
import com.developer.kulloveth.expandablelistsamplewithroom.data.CountryViewHolder
import com.developer.kulloveth.expandablelistsamplewithroom.data.model.Continent
import com.developer.kulloveth.expandablelistsamplewithroom.data.model.Country
import com.thoughtbot.expandablerecyclerview.ExpandableRecyclerViewAdapter
import com.thoughtbot.expandablerecyclerview.models.ExpandableGroup
class ContinentAdapter(groups: List<ExpandableGroup<*>>?) :
ExpandableRecyclerViewAdapter<ContinentViewHolder, CountryViewHolder>(
groups
) {
override fun onCreateGroupViewHolder(parent: ViewGroup?, viewType: Int): ContinentViewHolder {
val itemView =
LayoutInflater.from(parent?.context).inflate(R.layout.continent_layout, parent, false)
return ContinentViewHolder(itemView)
}
override fun onCreateChildViewHolder(parent: ViewGroup?, viewType: Int): CountryViewHolder {
val itemView =
LayoutInflater.from(parent?.context).inflate(R.layout.countrys_layout, parent, false)
return CountryViewHolder(itemView)
}
override fun onBindChildViewHolder(
holder: CountryViewHolder?,
flatPosition: Int,
group: ExpandableGroup<*>?,
childIndex: Int
) {
val country: Country = group?.items?.get(childIndex) as Country
holder?.bind(country)
}
override fun onBindGroupViewHolder(
holder: ContinentViewHolder?,
flatPosition: Int,
group: ExpandableGroup<*>?
) {
val continent: Continent = group as Continent
holder?.bind(continent)
}
}
适配器类接受一个扩展ExpandableAdapter的ExpandableGroup类型的List
Repository.kt
package com.developer.kulloveth.expandablelistsamplewithroom.data.model
import androidx.lifecycle.LiveData
import com.developer.kulloveth.expandablelistsamplewithroom.data.db.ContinentDao
class Repository(continentDao: ContinentDao) {
val allContinents: LiveData<List<ContinentEntity>> = continentDao.getAllContinent()
}
MainActivityViewModel.kt
package com.developer.kulloveth.expandablelistsamplewithroom.ui
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import com.developer.kulloveth.expandablelistsamplewithroom.data.db.ContinentDatabase
import com.developer.kulloveth.expandablelistsamplewithroom.data.model.ContinentEntity
import com.developer.kulloveth.expandablelistsamplewithroom.data.model.Repository
class MainActivityViewModel(application: Application) : AndroidViewModel(application) {
private val repository: Repository
val continents: LiveData<List<ContinentEntity>>
init {
val continentDao = ContinentDatabase.getDatabase(application, viewModelScope).continentDao()
repository = Repository(continentDao)
continents = repository.allContinents
}
}
存储库模式有助于将业务逻辑与 UI 逻辑分开,这在您从不同来源获取数据时最有用。viewmodel 类为 UI 提供数据,并且在配置更改后仍然存在
activity_main.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=".data.ui.MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="0dp"
android:id="@+id/rvConinent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity.kt
package com.developer.kulloveth.expandablelistsamplewithroom.ui
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.developer.kulloveth.expandablelistsamplewithroom.R
import com.developer.kulloveth.expandablelistsamplewithroom.data.model.ContinentEntity
import com.developer.kulloveth.expandablelistsamplewithroom.data.model.Continent
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
lateinit var viewModel: MainActivityViewModel
val continents = ArrayList<Continent>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProvider(this)[MainActivityViewModel::class.java]
viewModel.continents.observe(this, Observer {
for (continentEntity: ContinentEntity in it) {
val continent = Continent(continentEntity.continentName, continentEntity.countrys)
continents.add(continent)}
val adapter = ContinentAdapter(continents)
rvConinent.apply {
layoutManager = LinearLayoutManager(this@MainActivity)
rvConinent.adapter = adapter
} })}}
最后是主要布局及其从 MainActivityViewModel 观察数据的活动,添加到新列表并显示在 recyclerView 上。