有時我們想顯示一個關(guān)于列表項的額外內(nèi)容疆液,它不一定需要一個單獨的屏幕,通常稱為詳細屏幕劫窒,這就是 Expandable RecyclerView 的用武之地椎麦,我們將學(xué)習(xí)如何使用可擴展的 recyclerview 創(chuàng)建可擴展的THOUGHBOT
recyclerview回收視圖庫。我們還將使用它從本地數(shù)據(jù)庫中獲取我們的項目匪傍, Room Persistence Library
這是Android Architecture Components
我們將顯示來自本地數(shù)據(jù)庫的大陸列表及其下的一些國家/地區(qū)您市,該數(shù)據(jù)庫僅在創(chuàng)建數(shù)據(jù)庫時添加一次,最終結(jié)果應(yīng)如下圖所示役衡。
在您創(chuàng)建一個帶有空活動的新項目后墨坚,將以下依賴項添加到您的應(yīng)用級別 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'
}
確保以下插件存在于應(yīng)用程序級別構(gòu)建的頂部。
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
使用 Room 時需要三個主要的Entity
類映挂,代表數(shù)據(jù)庫中表的DAO
類,顧名思義盗尸,該類是一個包含用于訪問數(shù)據(jù)庫的方法的數(shù)據(jù)訪問對象柑船,即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 注釋泼各,將要創(chuàng)建的表名傳遞到其參數(shù)中鞍时,如果您不希望將類名用作表名,這是可選的,@ColumnInfo 告訴數(shù)據(jù)庫使用大陸作為列名所有表必須具有的continentName 變量和@PrimaryKey逆巍。還要注意@TypeConverters及塘,它是告訴空間用 ContinentConverter 類轉(zhuǎn)換 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 庫執(zhí)行轉(zhuǎn)換的方法上都有 @TypeConverter 的轉(zhuǎn)換器類
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)
}
這是訪問數(shù)據(jù)庫的 dao 接口,getAllContinent 方法有 @Query 注解锐极,它按升序獲取所有數(shù)據(jù)笙僚,它返回一個LiveData
有助于保持數(shù)據(jù)更新并自動在后臺線程上異步運行操作。insert 方法具有 @Insert 注釋灵再,用于插入數(shù)據(jù)以處理可能發(fā)生的沖突肋层,它使用掛起函數(shù)來指示該方法需要時間來執(zhí)行,因為我們不想阻塞主線程翎迁。
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()
}}}
這是一個數(shù)據(jù)庫類栋猖,它必須是一個抽象類,并且必須包含一個表示 dao 接口類的抽象方法汪榔,它具有 @Database 及其實體蒲拉、版本并將 export-schema 設(shè)置為 false,因為我們沒有將數(shù)據(jù)庫導(dǎo)出到文件夾中. getDatabase 方法是一個單例痴腌,它確保在任何時候只打開一個數(shù)據(jù)庫實例雌团,我們還添加了一個 roomCallback 以在使用其 onCreate 方法創(chuàng)建房間時只插入一次數(shù)據(jù)。請注意衷掷,插入方法是在協(xié)程范圍內(nèi)調(diào)用的辱姨,因為它是一個掛起函數(shù),以確保在后臺線程上執(zhí)行操作戚嗅。
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")
)}}}
接下來我們將創(chuàng)建適配器雨涛,它的數(shù)據(jù)類,觀察我們添加到 Room 的數(shù)據(jù)并設(shè)置 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
}
}
存儲庫模式有助于將業(yè)務(wù)邏輯與 UI 邏輯分開躏尉,這在您從不同來源獲取數(shù)據(jù)時最有用蚯根。viewmodel 類為 UI 提供數(shù)據(jù),并且在配置更改后仍然存在
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 觀察數(shù)據(jù)的活動胀糜,添加到新列表并顯示在 recyclerView 上颅拦。