Android笔记(二十三):Paging3分页加载库结合Compose的实现分层数据源访问

Android笔记(二十二):Paging3分页加载库结合Compose的实现网络单一数据源访问一文中,实现了单一数据源的访问。在实际运行中,往往希望不是单纯地访问网络数据,更希望将访问的网络数据保存到移动终端的SQLite数据库中,使得移动应用在离线的状态下也可以从数据库中获取数据进行访问。在本笔记中,将讨论多层次数据的访问,即结合网络资源+本地SQLite数据库中的数据的处理。在本笔记中,仍然采用Android笔记(二十二)中的网络资源:

上列展示的json数组包含了多个json对象,每个json对象的格式类似下列形式:

kotlin 复制代码
{"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 可确保应用以适当的顺序加载用户要查看的数据。

二、定义实体类

1.定义Film类

kotlin 复制代码
@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。对应的数据表结构如下所示:

2.定义FilmRemoteKey类

因为从网络访问每一个条电影记录需要知道记录的上一页和下一页的内容,因此定义FilmRemoteKey类,代码如下:

kotlin 复制代码
@Entity(tableName = "filmRemoteKeys")
data class FilmRemoteKey(
    @PrimaryKey(autoGenerate = false)
    val name:String,
    val prePage:Int?,
    val nextPage:Int?
)

FilmRemoteKey对应的数据表结构如下:

name表示电影名,也是关键字

prePage表示记录的上一页的页码,因为第一页的所有记录没有上一页,因此,前5条记录的prePage均为空

nextPage表示记录的下一页的页面。

三、定义网络访问

1.网络访问服务接口

kotlin 复制代码
interface FilmApi {
    @GET("film.json")
    suspend fun getData(
        @Query("page") page:Int,
        @Query("size") size:Int
    ):List<Film>
}

2.Retrofit构建网络服务

kotlin 复制代码
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)
}

四、定义数据库的访问

1.电影数据访问对象的接口

kotlin 复制代码
@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()
}

2.电影页码数据访问对象的接口

kotlin 复制代码
@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()
}

3.创建数据库

kotlin 复制代码
@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()
        }
    }
}

五、定义代码层

1.定义RemoteMediator类

kotlin 复制代码
@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)
        }
}

2.定义PagingSource数据源

kotlin 复制代码
@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
    }
}

六、定义视图模型层

kotlin 复制代码
@OptIn(ExperimentalPagingApi::class)
class MainViewModel(): ViewModel() {
    val filmRepository:FilmRepository = FilmRepository(RetrofitBuilder.apiService,FilmDatabase.getInstance())
    fun getFilms()=filmRepository.getAllFilms()
}

七、定义界面层

1.单独电影界面的定义

kotlin 复制代码
@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 = "更多...")
                }
            }

        }
    }
}

2.定义电影列表

kotlin 复制代码
@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])
            }
        }
    }
}

八、定义主活动MainActivity

kotlin 复制代码
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

相关推荐
月阳羊2 小时前
【硬件-笔试面试题】硬件/电子工程师,笔试面试题-26,(知识点:硬件电路的调试方法:信号追踪,替换,分段调试)
笔记·嵌入式硬件·面试·职场和发展
Star在努力3 小时前
14-C语言:第14天笔记
c语言·笔记·算法
Dnelic-4 小时前
Android 5G NR 状态类型介绍
android·5g·telephony·connectivity·自学笔记·移动网络数据
霜绛4 小时前
机器学习笔记(三)——决策树、随机森林
人工智能·笔记·学习·决策树·随机森林·机器学习
吗喽对你问好5 小时前
Android UI 控件详解实践
android·ui
xzkyd outpaper7 小时前
JVM、Dalvik、ART垃圾回收机制
jvm·dalvik
charlie1145141918 小时前
快速入门Socket编程——封装一套便捷的Socket编程——导论
linux·网络·笔记·面试·网络编程·socket
淮北枳丶8 小时前
Java常用命令、JVM常用命令
java·开发语言·jvm
thginWalker8 小时前
Java JVM
java·jvm
东风西巷8 小时前
X-plore File Manager v4.34.02 修改版:安卓设备上的全能文件管理器
android·网络·软件需求