在Android笔记(二十二):Paging3分页加载库结合Compose的实现网络单一数据源访问一文中,实现了单一数据源的访问。在实际运行中,往往希望不是单纯地访问网络数据,更希望将访问的网络数据保存到移动终端的SQLite数据库中,使得移动应用在离线的状态下也可以从数据库中获取数据进行访问。在本笔记中,将讨论多层次数据的访问,即结合网络资源+本地SQLite数据库中的数据的处理。在本笔记中,仍然采用Android笔记(二十二)中的网络资源:
上列展示的json数组包含了多个json对象,每个json对象的格式类似下列形式:
{"actors":"演员",
"directors":"导演",
"intro":"电影简介",
"poster":"http://localhost:5000/photo/s_ratio_poster/public/p2626067725.jpg",
"region":"地区",
"release":"发布年份",
"trailer_url":"https://localhost:5000/trailer/268661/#content",
"video_url":"https://localhost:5000/d04d3c0d2132a29410dceaeefa97e725/view/movie/M/402680661.mp4"}
与单一数据源结构不同在于增加了RemoteMediator。当应用的已缓存数据用尽时,RemoteMediator 会充当来自 Paging 库的信号。可以使用此信号从网络加载更多数据并将其存储在本地数据库中,PagingSource 可以从本地数据库加载这些数据并将其提供给界面进行显示。
当需要更多数据时,Paging 库从 RemoteMediator 实现调用 load() 方法。这是一项挂起功能,因此可以放心地执行长时间运行的工作。此功能通常从网络源提取新数据并将其保存到本地存储空间。
此过程会处理新数据,但长期存储在数据库中的数据需要进行失效处理(例如,当用户手动触发刷新时)。这由传递到 load() 方法的 LoadType 属性表示。LoadType 会通知 RemoteMediator 是需要刷新现有数据,还是提取需要附加或前置到现有列表的更多数据。
通过这种方式,RemoteMediator 可确保应用以适当的顺序加载用户要查看的数据。
@Entity(tableName="films")
data class Film(
@PrimaryKey(autoGenerate = false)
@SerializedName("name")
val name:String,
@SerializedName("release")
val release:String,
@SerializedName("region")
val region:String,
@SerializedName("directors")
val directors:String,
@SerializedName("actors")
val actors:String,
@SerializedName("intro")
val intro:String,
@SerializedName("poster")
val poster:String,
@SerializedName("trailer_url")
val trailer:String,
@SerializedName("video_url")
val video:String
)
在上述代码中,将Film类映射为数据库中的数据表films。对应的数据表结构如下所示:
因为从网络访问每一个条电影记录需要知道记录的上一页和下一页的内容,因此定义FilmRemoteKey类,代码如下:
@Entity(tableName = "filmRemoteKeys")
data class FilmRemoteKey(
@PrimaryKey(autoGenerate = false)
val name:String,
val prePage:Int?,
val nextPage:Int?
)
FilmRemoteKey对应的数据表结构如下:
name表示电影名,也是关键字
prePage表示记录的上一页的页码,因为第一页的所有记录没有上一页,因此,前5条记录的prePage均为空
nextPage表示记录的下一页的页面。
interface FilmApi {
@GET("film.json")
suspend fun getData(
@Query("page") page:Int,
@Query("size") size:Int
):List<Film>
}
object RetrofitBuilder {
private const val BASE_URL = "http://10.0.2.2:5000/"
private fun getRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
val apiService:FilmApi = getRetrofit().create(FilmApi::class.java)
}
@Dao
interface FilmDao {
/**
* 插入数据列表
* @param films List<Film>
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(films: List<Film>)
/**
* 检索所有的Film记录
* @return PagingSource<Int, Film>
*/
@Query("select * from films")
fun queryAll(): PagingSource<Int, Film>
/**
* Delete all
* 删除表films中所有记录
*/
@Query("DELETE FROM films")
suspend fun deleteAll()
}
@Dao
interface FilmRemoteKeyDao {
@Query("SELECT * FROM filmRemoteKeys WHERE name = :name")
suspend fun findByName(name:String):FilmRemoteKey
@Insert(onConflict =OnConflictStrategy.REPLACE)
suspend fun insertAllKeys(remoteKeys:List<FilmRemoteKey>)
@Query("DELETE FROM filmRemoteKeys")
suspend fun deleteAllKeys()
}
@Database(entities = [Film::class,FilmRemoteKey::class], version = 1)
abstract class FilmDatabase : RoomDatabase() {
abstract fun filmDao(): FilmDao
abstract fun filmRemoteKeyDao():FilmRemoteKeyDao
companion object{
private var instance: FilmDatabase? = null
/**
* 单例模式创建为一个FilmDB对象实例
*/
@Synchronized
fun getInstance(context:Context = FilmApp.context): FilmDatabase {
instance?.let{
return it
}
return Room.databaseBuilder(
context,
FilmDatabase::class.java,
"filmDB.db"
).build()
}
}
}
@OptIn(ExperimentalPagingApi::class)
class FilmRemoteMediator(
private val database:FilmDatabase,
private val networkService:FilmApi
) : RemoteMediator<Int, Film>() {
private val filmDao = database.filmDao()
private val filmRemoteKeyDao = database.filmRemoteKeyDao()
override suspend fun load(loadType: LoadType,state: PagingState<Int, Film>): MediatorResult {
return try{
/**
* 从数据库获取缓存的当前页面
*/
val currentPage:Int = when(loadType){
//UI初始化刷新
LoadType.REFRESH-> {
val remoteKey:FilmRemoteKey? = getRemoteKeyToCurrentPosition(state)
remoteKey?.nextPage?.minus(1)?:1
}
//在当前列表头添加数据使用
LoadType.PREPEND-> {
val remoteKey = getRemoteKeyForTop(state)
val prevPage = remoteKey?.prePage?:return MediatorResult.Success(remoteKey!=null)
prevPage
}
//尾部加载更多的记录
LoadType.APPEND->{
val remoteKey = getRemoteKeyForTail(state)
val nextPage = remoteKey?.nextPage?:return MediatorResult.Success(remoteKey!=null)
nextPage
}
}
/**
* 联网状态下的处理
* 获取网络资源
* response
*/
val response = networkService.getData(currentPage,5)
val endOfPaginationReached = response.isEmpty()
val prePage = if(currentPage == 1) null else currentPage-1
val nextPage = if(endOfPaginationReached) null else currentPage+1
database.withTransaction{
//刷新记录,需要删除原有的记录
if(loadType == LoadType.REFRESH){
filmDao.deleteAll()
filmRemoteKeyDao.deleteAllKeys()
}
//获取的记录映射成对应的索引记录
val keys:List<FilmRemoteKey> = response.map{film:Film->
FilmRemoteKey(film.name,prePage,nextPage)
}
filmRemoteKeyDao.insertAllKeys(keys)
filmDao.insertAll(response)
}
MediatorResult.Success(endOfPaginationReached)
}catch(e:IOException){
MediatorResult.Error(e)
}catch(e:HttpException){
MediatorResult.Error(e)
}
}
/**
* 获取当前位置对应的FilmRemoteKey
* @param state PagingState<Int, Film>
* @return FilmRemoteKey?
*/
private suspend fun getRemoteKeyToCurrentPosition(state:PagingState<Int,Film>):FilmRemoteKey?=
state.anchorPosition?.let{position:Int->
state.closestItemToPosition(position)?.name?.let{name:String->
filmRemoteKeyDao.findByName(name)
}
}
/**
* 获取当前页面从头部第一个位置对应的FilmRemoteKey
* @param state PagingState<Int, Film>
* @return FilmRemoteKey?
*/
private suspend fun getRemoteKeyForTop(state:PagingState<Int,Film>):FilmRemoteKey?=
state.pages.firstOrNull{ it:PagingSource.LoadResult.Page<Int,Film>->
it.data.isNotEmpty()
}?.data?.firstOrNull()?.let{film:Film->
filmRemoteKeyDao.findByName(film.name)
}
/**
* 获取当前尾部最后一个位置对应的FilmRemoteKey
* @param state PagingState<Int, Film>
* @return FilmRemoteKey?
*/
private suspend fun getRemoteKeyForTail(state:PagingState<Int,Film>):FilmRemoteKey?=
state.pages.lastOrNull{it:PagingSource.LoadResult.Page<Int,Film>->
it.data.isNotEmpty()
}?.data?.lastOrNull()?.let{film:Film->
filmRemoteKeyDao.findByName(film.name)
}
}
@ExperimentalPagingApi
class FilmRepository(
private val filmApi:FilmApi,
private val filmDatabase:FilmDatabase
) {
fun getAllFilms(): Flow<PagingData<Film>> {
val pagingSourceFactory:()->PagingSource<Int, Film> = {
filmDatabase.filmDao().queryAll()
}
return Pager(
config = PagingConfig(pageSize = 5),
initialKey = null,
remoteMediator = FilmRemoteMediator(filmDatabase,filmApi),
pagingSourceFactory = pagingSourceFactory
).flow
}
}
@OptIn(ExperimentalPagingApi::class)
class MainViewModel(): ViewModel() {
val filmRepository:FilmRepository = FilmRepository(RetrofitBuilder.apiService,FilmDatabase.getInstance())
fun getFilms()=filmRepository.getAllFilms()
}
@Composable
fun FilmCard(film: Film?) {
Card(modifier = Modifier
.fillMaxSize()
.padding(2.dp),
elevation = CardDefaults.cardElevation(5.dp),
colors = CardDefaults.cardColors(containerColor = Color.DarkGray)){
Column{
Row(modifier = Modifier.fillMaxSize()){
AsyncImage(
modifier=Modifier.width(180.dp).height(240.dp),
model = "${film?.poster}",
contentDescription = "${film?.name}")
Column{
Text("${film?.name}",fontSize = 18.sp,color = Color.Green)
Text("导演:${film?.directors}",fontSize = 14.sp,color = Color.White)
Text("演员:${film?.actors}", fontSize = 14.sp,color = Color.Green)
}
}
Text("${film?.intro?.subSequence(0,60)} ...",fontSize = 14.sp,color= Color.White)
Row(horizontalArrangement = Arrangement.End,
modifier = Modifier.fillMaxSize()){
Text("More",fontSize=12.sp)
IconButton(onClick ={}){
Icon(imageVector = Icons.Default.MoreVert,tint= Color.Green,contentDescription = "更多...")
}
}
}
}
}
@Composable
fun FilmScreen(mainViewmodel:MainViewModel){
val films = mainViewmodel.getFilms().collectAsLazyPagingItems()
Column(horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.background(Color.White)){
LazyColumn{
items(films.itemCount){
FilmCard(films[it])
}
}
}
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val mainViewModel:MainViewModel = viewModel()
Ch11_DemoTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
FilmScreen(mainViewmodel = mainViewModel)
}
}
}
}
}
Paging库概览
https://developer.android.google.cn/topic/libraries/architecture/paging/v3-overview?hl=zh-cn