目录
1、从本地Room数据库加载数据
?viewmodel
fragment中使用
页面
?数据库相关
2、直接网络获取数据加载
3、网络访问数据到Room数据库再加载数据
?自定义RemoteMediator访问网络数据
4、封装使用
BaseViewHolder
列表单个item封装的基类
封装统一的adapter
使用方法
自定义PagingItemView
viewmodel获取数据后,把获取到的pagingData装到自定义的PagingItemView中
fragment中使用
1、从本地Room数据库加载数据
参照官方示例代码,在原代码基础上做了一个小改动增加了一个BaseViewHolder,这样不用每次都要新建一个viewholder,直接在adapter中编写绑定数据的代码
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
class BaseViewHolder(viewGroup: ViewGroup, layoutRes: Int): RecyclerView.ViewHolder(
LayoutInflater.from(viewGroup.context).inflate(layoutRes, viewGroup, false)
){
fun bind( bindView: (View)-> Unit){
bindView(itemView)
}
}
?绑定数据的代码移到adapter中,其他代码没有多少改变
class CheeseAdapter : PagingDataAdapter<CheeseListItem, BaseViewHolder>(diffCallback) {
override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
holder.bind {
val nameView = it.findViewById<TextView>(R.id.name)
val item = getItem(position)
if (item is CheeseListItem.Separator) {
nameView.text = "${item.name} Cheeses"
nameView.setTypeface(null, Typeface.BOLD)
} else {
nameView.text = item?.name
nameView.setTypeface(null, Typeface.NORMAL)
}
nameView.text = item?.name
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
return BaseViewHolder(parent,viewType)
}
override fun getItemViewType(position: Int): Int {
return R.layout.home_item
}
companion object {
/**
* This diff callback informs the PagedListAdapter how to compute list differences when new
* PagedLists arrive.
*
* When you add a Cheese with the 'Add' button, the PagedListAdapter uses diffCallback to
* detect there's only a single item difference from before, so it only needs to animate and
* rebind a single view.
*
* @see DiffUtil
*/
val diffCallback = object : DiffUtil.ItemCallback<CheeseListItem>() {
override fun areItemsTheSame(oldItem: CheeseListItem, newItem: CheeseListItem): Boolean {
return if (oldItem is CheeseListItem.Item && newItem is CheeseListItem.Item) {
oldItem.cheese.id == newItem.cheese.id
} else if (oldItem is CheeseListItem.Separator && newItem is CheeseListItem.Separator) {
oldItem.name == newItem.name
} else {
oldItem == newItem
}
}
/**
* Note that in kotlin, == checking on data classes compares all contents, but in Java,
* typically you'll implement Object#equals, and use it to compare object contents.
*/
override fun areContentsTheSame(oldItem: CheeseListItem, newItem: CheeseListItem): Boolean {
return oldItem == newItem
}
}
}
}
?
sealed class CheeseListItem(val name: String) {
data class Item(val cheese: Cheese) : CheeseListItem(cheese.name)
data class Separator(private val letter: Char) : CheeseListItem(letter.toUpperCase().toString())
}
?viewmodel
class AccountBookViewModel(private val dao: CheeseDao) : BaseViewModel() {
/**
* We use the Kotlin [Flow] property available on [Pager]. Java developers should use the
* RxJava or LiveData extension properties available in `PagingRx` and `PagingLiveData`.
*/
val allCheeses: Flow<PagingData<CheeseListItem>> = Pager(
config = PagingConfig(
pageSize = 10,
enablePlaceholders = true,
/**
* Maximum number of items a PagedList should hold in memory at once.
*
* This number triggers the PagedList to start dropping distant pages as more are loaded.
*/
maxSize = 200
)
) {
dao.allCheesesByName()
}.flow
.map { pagingData ->
pagingData
// Map cheeses to common UI model.
.map { cheese -> CheeseListItem.Item(cheese) }
.insertSeparators { before: CheeseListItem?, after: CheeseListItem? ->
if (before == null && after == null) {
// List is empty after fully loaded; return null to skip adding separator.
null
} else if (after == null) {
// Footer; return null here to skip adding a footer.
null
} else if (before == null) {
// Header
CheeseListItem.Separator(after.name.first())
} else if (!before.name.first().equals(after.name.first(), ignoreCase = true)){
// Between two items that start with different letters.
CheeseListItem.Separator(after.name.first())
} else {
// Between two items that start with the same letter.
null
}
}
}
.cachedIn(viewModelScope)
}
fragment中使用
class AccountBookFragment : BaseViewBindingFragment<FragmentAccountBookBinding>() {
override fun getViewBinding() = FragmentAccountBookBinding.inflate(layoutInflater)
private val mViewModel: AccountBookViewModel by viewModels {
object : ViewModelProvider.Factory{
override fun <T : ViewModel> create(modelClass: Class<T>): T {
val cheeseDao = LocalDb.get(BaseApplication.instance).cheeseDao()
return AccountBookViewModel(cheeseDao)as T
}
}
}
override fun initView() {
val adapter = CheeseAdapter()
mViewBinding?.cheeseList?.adapter = adapter
lifecycleScope.launch {
mViewModel.allCheeses.collectLatest { adapter.submitData(it) }
}
}
}
页面
<?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=".ui.accountbook.AccountBookFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/cheeseList"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
app:layoutManager="LinearLayoutManager"/>
</androidx.constraintlayout.widget.ConstraintLayout>
?数据库相关
/**
* Data class that represents our items.
*/
@Entity
data class Cheese(@PrimaryKey(autoGenerate = true) val id: Int, val name: String)
/**
* Database Access Object for the Cheese database.
*/
@Dao
interface CheeseDao {
/**
* Room knows how to return a LivePagedListProvider, from which we can get a LiveData and serve
* it back to UI via ViewModel.
*/
@Query("SELECT * FROM Cheese ORDER BY name COLLATE NOCASE ASC")
fun allCheesesByName(): PagingSource<Int, Cheese>
@Insert
fun insert(cheeses: List<Cheese>)
@Insert
fun insert(cheese: Cheese)
@Delete
fun delete(cheese: Cheese)
}
/**
* Singleton database object. Note that for a real app, you should probably use a Dependency
* Injection framework or Service Locator to create the singleton database.
*/
@Database(entities = [Cheese::class,UserBean::class,WaitDealBean::class], version = 1)
abstract class LocalDb : RoomDatabase() {
abstract fun cheeseDao(): CheeseDao
abstract fun userDao(): UserDao
abstract fun waitdealDao(): WaitDealDao
companion object {
private var instance: LocalDb? = null
@Synchronized
fun get(context: Context): LocalDb {
if (instance == null) {
instance = Room.databaseBuilder(context.applicationContext,
LocalDb::class.java, "TestDatabase")
.addCallback(object : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
fillInDb(context.applicationContext)
}
}).build()
}
return instance!!
}
/**
* fill database with list of cheeses
*/
private fun fillInDb(context: Context) {
// inserts in Room are executed on the current thread, so we insert in the background
ioThread {
get(context).cheeseDao().insert(CHEESE_DATA.map { Cheese(id = 0, name = it) })
}
}
}
}
private val CHEESE_DATA = arrayListOf(
"Abbaye de Belloc", "Abbaye du Mont des Cats", "Abertam", "Abondance", "Ackawi",
"Acorn", "Adelost", "Affidelice au Chablis", "Afuega'l Pitu", "Airag", "Airedale",
"Aisy Cendre", "Allgauer Emmentaler", "Alverca", "Ambert", "American Cheese",
"Ami du Chambertin", "Anejo Enchilado", "Anneau du Vic-Bilh", "Anthoriro", "Appenzell",
"Aragon", "Ardi Gasna", "Ardrahan", "Armenian String", "Aromes au Gene de Marc",
"Asadero", "Asiago", "Aubisque Pyrenees", "Autun", "Avaxtskyr", "Baby Swiss",
"Babybel", "Baguette Laonnaise", "Bakers", "Baladi", "Balaton", "Bandal", "Banon",
"Barry's Bay Cheddar", "Basing", "Basket Cheese", "Bath Cheese", "Bavarian Bergkase",
"Baylough", "Beaufort", "Beauvoorde", "Beenleigh Blue", "Beer Cheese", "Bel Paese",
"Bergader", "Bergere Bleue", "Berkswell", "Beyaz Peynir", "Bierkase", "Bishop
"Tupi", "Turunmaa", "Tymsboro", "Tyn Grug", "Tyning", "Ubriaco", "Ulloa",
"Vacherin-Fribourgeois", "Valencay", "Vasterbottenost", "Venaco", "Vendomois",
"Vieux Corse", "Vignotte", "Vulscombe", "Waimata Farmhouse Blue",
"Washed Rind Cheese (Australian)", "Waterloo", "Weichkaese", "Wellington",
"Wensleydale", "White Stilton", "Whitestone Farmhouse", "Wigmore", "Woodside Cabecou",
"Xanadu", "Xynotyro", "Yarg Cornish", "Yarra Valley Pyramid", "Yorkshire Blue",
"Zamorano", "Zanetti Grana Padano", "Zanetti Parmigiano Reggiano")
文件名和类名不要太在意,因为参考的官网示例,做具体业务的时候再自己调整
2、直接网络获取数据加载
参考1、从本地Room数据库加载数据
自定义pagingsource访问网络数据
class WaitDealDataSource : PagingSource<Int, WaitDealBean>() {
override fun getRefreshKey(state: PagingState<Int, WaitDealBean>): Int? = null
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, WaitDealBean> {
val nextPageNumber = params.key ?: 1
val size = params.loadSize
val result = NetworkService.api.getPlanList(nextPageNumber, size,"planTime")
return LoadResult.Page(
data = if(result.success) result.data!! else ArrayList(),
prevKey = null, // Only paging forward. 只向后加载就给 null
//nextKey 下一页页码; 尾页给 null; 否则当前页码加1
nextKey = if(result.currentPage!! >= result.totalPageSize!!) null else (nextPageNumber + 1)
)
}
}
ViewModel中使用
val allWaitDeal: Flow<PagingData<WaitDealItem>> = Pager(
config = PagingConfig(
pageSize = 10,
enablePlaceholders = true
)
) {
WaitDealDataSource()
}.flow
.map { pagingData ->
pagingData
// Map cheeses to common UI model.
.map { cheese -> WaitDealItem.Item(cheese) }
.insertSeparators { before: WaitDealItem?, after: WaitDealItem? ->
if (before == null && after == null) {
// List is empty after fully loaded; return null to skip adding separator.
null
} else if (after == null) {
// Footer; return null here to skip adding a footer.
null
} else if (before == null) {
// Header
WaitDealItem.Separator(after.sTime.substring(0,10))
} else if (!before.sTime.substring(0,10).equals(after.sTime.substring(0,10), ignoreCase = true)){
// Between two items that start with different letters.
WaitDealItem.Separator(after.sTime.substring(0,10))
} else {
// Between two items that start with the same letter.
null
}
}
}
.cachedIn(viewModelScope)
3、网络访问数据到Room数据库再加载数据
参考1、从本地Room数据库加载数据
@OptIn(ExperimentalPagingApi::class)
class WaitDealRemoteMediator (
private val pageSize: Int,
private val database: LocalDb
) : RemoteMediator<Int, WaitDealBean>() {
val waitDealDao = database.waitdealDao()
override suspend fun initialize(): InitializeAction {
return InitializeAction.SKIP_INITIAL_REFRESH
}
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, WaitDealBean>
): MediatorResult {
return try {
val pageNum = when (loadType) {
//首次访问 或者调用 PagingDataAdapter.refresh()时
LoadType.REFRESH -> 1
//在当前加载的数据集的开头加载数据时
LoadType.PREPEND ->
return MediatorResult.Success(true)
//下拉加载更多时
LoadType.APPEND -> {
val lastItem = state.lastItemOrNull()
if (lastItem == null) {//最后一项为空直接返回
return MediatorResult.Success( true)
}
//获取最后一个page
val page = state.pages[state.pages.size - 1]
//下一页pageNum = ((列表总数量)/每页数量 )+1
(page.itemsBefore+page.data.size)/ pageSize+1
}
}
//无网络则加载本地数据
if (!NetworkUtils.isConnected()) {
return MediatorResult.Success(endOfPaginationReached = true)
}
//region 更新数据库
val response = NetworkService.api.getPlanList(pageNum,pageSize)
database.withTransaction {
if (loadType == LoadType.REFRESH) {
waitDealDao.clearAll()
}
waitDealDao.insertAll(response.data)
}
//endregion
//返回true则表示加载完成,返回false会继续加载下一页数据
val isFinished = response.data == null || response.data!!.size.toLong() >= response.totalSize!!
MediatorResult.Success(isFinished)
} catch (e: Exception) {
ExceptionUtil.catchException(e)
MediatorResult.Error(e)
}
}
}
使用其他地方不变,在pager中添加remoteMediator参数即可
?
4、封装使用
BaseViewHolder
class BaseViewHolder(val viewGroup: ViewGroup, viewType: Int): RecyclerView.ViewHolder(
LayoutInflater.from(viewGroup.context).inflate(viewType, viewGroup, false)
)
列表单个item封装的基类
import androidx.annotation.LayoutRes
/**
* 单个item
*/
abstract class PagingItemView<T : Any>(@LayoutRes val layoutRes: Int,val entity: T) {
abstract fun onBindView(holder: BaseViewHolder, position: Int)
abstract fun areItemsTheSame(data: T): Boolean
abstract fun areContentsTheSame(data: T): Boolean
}
封装统一的adapter
import android.content.Context
import android.view.ViewGroup
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
/**
* 统一的adapter
*/
class PagingAdapter<T:Any>(val context: Context) : PagingDataAdapter<PagingItemView<T>, RecyclerView.ViewHolder>(
object : DiffUtil.ItemCallback<PagingItemView<T>>() {
override fun areItemsTheSame(oldItem: PagingItemView<T>, newItem: PagingItemView<T>): Boolean {
return oldItem.areItemsTheSame(newItem.entity)
}
override fun areContentsTheSame(oldItem: PagingItemView<T>, newItem: PagingItemView<T>): Boolean {
return oldItem.areContentsTheSame(newItem.entity)
}
}
){
// 获取到对应的itemview去调用onBindView方法设置UI
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (position != RecyclerView.NO_POSITION) {
getItem(position)?.onBindView(holder = holder as BaseViewHolder, position = position)
}
}
// 这里用itemView的layoutRes去作为viewtype,这样不同布局的itemview就可以区分开来
override fun getItemViewType(position: Int): Int {
return getItem(position)!!.layoutRes
}
// 因为上面是用layoutRes来作为itemType,所以创建viewholder的时候直接用viewType来创建
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return BaseViewHolder(parent,viewType)
}
}
使用方法
自定义PagingItemView
class ItemWaitDeal(entity: WaitDealBean) : PagingItemView<WaitDealBean>(R.layout.wait_deal_item,entity) {
override fun areItemsTheSame(data: WaitDealBean): Boolean {
return entity.id!! == data.id
}
override fun areContentsTheSame(data: WaitDealBean): Boolean {
return entity.id!! == data.id
}
override fun onBindView(holder: BaseViewHolder, position: Int) {
val tvDay = holder.itemView.findViewById<TextView>(R.id.tv_day)
val tvMinute = holder.itemView.findViewById<TextView>(R.id.tv_minute)
val titleView = holder.itemView.findViewById<TextView>(R.id.title)
val contentView = holder.itemView.findViewById<TextView>(R.id.content)
titleView.text = entity.title
contentView.text = entity.content
contentView.visibility = if(StringUtils.isTrimEmpty(entity.content)) View.GONE else View.VISIBLE
val planTime = entity.planTime?.substring(0,10)
tvDay.text = planTime
}
}
viewmodel获取数据后,把获取到的pagingData装到自定义的PagingItemView中
class WaitDealViewModel : BaseViewModel() {
fun <T : Any> queryData(): Flow<PagingData<PagingItemView<T>>> {
val database = LocalDb.get(BaseApplication.instance)
val NETWORK_PAGE_SIZE = 10
@OptIn(ExperimentalPagingApi::class)
return Pager(
config = PagingConfig(
pageSize = NETWORK_PAGE_SIZE,
enablePlaceholders = false
),
//访问网络数据,检查数据更新
remoteMediator = WaitDealRemoteMediator(NETWORK_PAGE_SIZE, database)
) {
//查询数据库数据
database.waitdealDao().queryAll()
}.flow
.map { pagingData->
pagingData.map { item ->
//转换数据
ItemWaitDeal(item) as (PagingItemView<T>)
}
}.cachedIn(viewModelScope)
}
}
fragment中使用
class WaitDealFragment : BaseViewBindingFragment<FragmentWaitDealBinding>() {
override fun getViewBinding() = FragmentWaitDealBinding.inflate(layoutInflater)
private val mViewModel: WaitDealViewModel by viewModels{
object : ViewModelProvider.Factory{
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return WaitDealViewModel() as T
}
}
}
override fun initView() {
val pagingAdapter = PagingAdapter<WaitDealBean>(requireContext())
mViewBinding?.recyclerView?.layoutManager = LinearLayoutManager(context)
mViewBinding?.recyclerView?.adapter = pagingAdapter
mViewBinding?.swipeRefreshLayout?.bindAdapter(pagingAdapter)
lifecycleScope.launch {
mViewModel.queryData<WaitDealBean>().collectLatest { pagingAdapter.submitData(it) }
}
}
}
|