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

news2024/11/22 20:30:17

在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 可确保应用以适当的顺序加载用户要查看的数据。

二、定义实体类

1.定义Film类

@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类,代码如下:

@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.网络访问服务接口

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

2.Retrofit构建网络服务

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.电影数据访问对象的接口

@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.电影页码数据访问对象的接口

@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.创建数据库

@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类

@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数据源

@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()
}

七、定义界面层

1.单独电影界面的定义

@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.定义电影列表

@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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1343896.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

windows高效的文件夹管理工具QTTabBar

1、介绍&#xff1a; 功能介绍视频https://www.bilibili.com/video/BV13y4y1V782/?spm_id_from333.880.my_history.page.click&vd_sourceef5003729aa0776908befb4d20108803 这个视频很坑&#xff0c;介绍了功能&#xff0c;但没介绍安装方法 2、下载&#xff1a; 下载链…

自检服务器,无需服务器、不用编程。

自检服务器&#xff0c;无需服务器、不用编程。 大家好&#xff0c;我是JavaPub. 这几年自媒体原来热&#xff0c;很多人都知道了个人 IP 的重要性。连一个搞中医的朋友都要要做一个自己的网站&#xff0c;而且不想学编程、还不想花 RMB 租云服务。 老读者都知道&#xff0c…

Spring Boot 基于Redisson实现注解式分布式锁

依赖版本 JDK 17 Spring Boot 3.2.0 Redisson 3.25.0 源码地址&#xff1a;Gitee 导入依赖 <properties><redisson.version>3.25.0</redisson.version> </properties><dependencies><dependency><groupId>org.projectlombok</…

OpenHarmony南向之Camera简述

Camera驱动框架 该驱动框架模型内部分为三层&#xff0c;依次为HDI实现层、框架层和设备适配层&#xff1a; HDI实现层&#xff1a;实现OHOS&#xff08;OpenHarmony Operation System&#xff09;相机标准南向接口。框架层&#xff1a;对接HDI实现层的控制、流的转发&#x…

ECMAScript 6 - 通过Promise输出题理解Promise

1 题目(1) 题目背景&#xff1a;分享洛千陨 珍藏题 const p1 () > (new Promise((resolve, reject) > {console.log(1);let p2 new Promise((resolve, reject) > {console.log(2);const timeOut1 setTimeout(() > {console.log(3);resolve(4);}, 0)resolve(5)…

js for和forEach 跳出循环 替代方案

1 for循环跳出 for(let i0;i<10;i){if(i5){break;}console.log(i) }在函数中也可以return跳出循环 function fn(){for(let i0;i<10;i){if(i5){return;}console.log(i)} } fn()for ... of效果同上 2 forEach循环跳出 break会报错 [1,2,3,4,5,6,7,8,9,10].forEach(i>…

Evidential Deep Learning to Quantify Classification Uncertainty

本片文章发表于NeurIPS 2018。 文章链接&#xff1a;https://arxiv.org/abs/1806.01768 一、概述 近年来&#xff0c;神经网络在不同领域取得了革命性的进步&#xff0c;尤其是在dropout、normalization以及skip connection等方法被提出之后&#xff0c;撼动了整个机器学习领…

新火种AI|AI正在让汽车成为“消费电子产品”

作者&#xff1a;一号 编辑&#xff1a;小迪 AI正在让汽车产品消费电子化 12月28日&#xff0c;铺垫许久的小米汽车首款产品——小米SU7正式在北京亮相。命里注定要造“电车”的雷军&#xff0c;在台上重磅发布了小米的五大自研核心技术。在车型设计、新能源技术以及智能科技…

算法专题四:前缀和

前缀和 一.一维前缀和(模板)&#xff1a;1.思路一&#xff1a;暴力解法2.思路二&#xff1a;前缀和思路 二. 二维前缀和(模板)&#xff1a;1.思路一&#xff1a;构造前缀和数组 三.寻找数组的中心下标&#xff1a;1.思路一&#xff1a;前缀和 四.除自身以外数组的乘积&#xff…

002文章解读与程序——中国电机工程学报EI\CSCD\北大核心《计及源荷不确定性的综合能源生产单元运行调度与容量配置两阶段随机优化》已提供下载资源

&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;&#x1f446;下载资源链接&#x1f4…

nacos入门篇001-安装与启动

1、下载zip包 我这里下载的是版本2.2.0 Nacos 快速开始 2、修改配置文件 2.1集群模式修改成单例模式 vi startup.sh 2.2 修改数据库配置信息 3、初始化数据库 3.1 创建db名称&#xff1a;db_nacos 3.2 执行mysql-schema.sql 3.3 执行完截图&#xff1a; 4、运行脚本启动 …

leetcode贪心算法题总结(二)

本节目录 1.最长回文串2.增减字符串匹配3.分发饼干4.最优除法5.跳跃游戏II6.跳跃游戏7.加油站8.单调递增的数字9.坏了的计算器 1.最长回文串 最长回文串 class Solution { public:int longestPalindrome(string s) {//计数一&#xff1a;用数组模拟哈希表int hash[127] {0}…

SimpleCG小游戏开发系列(2)--贪吃蛇

一、前言 在之前的C语言小游戏开发系列我们已经介绍了扫雷游戏的开发&#xff0c;本篇我们继续此系列第二篇&#xff0c;同样是比较简单但好玩的一个游戏--贪吃蛇。因为有了之前的游戏框架&#xff0c;我们只需要直接搬来原来的框架即可&#xff0c;可以省去不少活。 先看看游…

命令行创建Vue项目

Vue项目创建 1. 打开UI界面 在命令行中&#xff0c;执行如下指令&#xff1a; vue ui 2. 打开项目管理器 3. 创建项目 创建项目的过程&#xff0c;需要联网进行&#xff0c;这可能会耗时比较长的时间&#xff0c;请耐心等待。 windows的命令行&#xff0c;容易卡顿&#xff0c…

使用Node Exporter采集主机数据

安装 Node Exporter 在 Prometheus 的架构设计中&#xff0c;Prometheus Server 并不直接服务监控特定的目标&#xff0c;其主要任务负责数据的收集&#xff0c;存储并且对外提供数据查询支持。因此为了能够能够监控到某些东西&#xff0c;如主机的 CPU 使用率&#xff0c;我们…

3D 渲染如何帮助电商促进销售?

在线工具推荐&#xff1a; 3D数字孪生场景编辑器 - GLTF/GLB材质纹理编辑器 - 3D模型在线转换 - Three.js AI自动纹理开发包 - YOLO 虚幻合成数据生成器 - 三维模型预览图生成器 - 3D模型语义搜索引擎 3D 渲染图像因其高转化率而成为亚马逊卖家的最新趋势。它是电子商务平…

Linux 线程安全 (1)

文章目录 线程互斥概念互斥实际使用互斥锁的原理死锁问题说明 线程互斥概念 执行流 执行流是指操作系统对进程或线程的调度和执行顺序。它决定了程序中的指令按照何种顺序被执行。 现阶段可以粗浅的理解为&#xff0c;执行流决定执行哪个线程或进程的代码(或者说执行流决定了…

MyBatis标签及其应用示例

MyBatis标签及其应用示例 1. select 1.1 标签属性 id唯一的标识符parameterType传给此语句的参数的全路径名或别名如&#xff1a;com.xxx.xxx.demo.entity.User或userresultType语句返回值类型或别名。如果是集合List&#xff0c;此处填写集合的泛型T&#xff0c;而不是集合…

人机交互中信息数量与质量

在人机交互中&#xff0c;信息的数量和质量都是非常重要的因素。 信息的数量指的是交互过程中传递的信息的多少。信息的数量直接影响到交互的效率和效果&#xff0c;如果交互中传递的信息量太少&#xff0c;可能导致交互过程中的信息不足&#xff0c;用户无法得到想要的结果或者…

js实时监听input输入框值的变化

实习日记之通过调用common chemistry的api接口实现输入keyword查找cas号和mw。做了一个简单的html网页&#xff0c;用到了ajax技术。比较简单&#xff0c;适合刚入门的宝学习参考。代码如下&#xff1a; <!DOCTYPE html> <html lang"en"> <head>&l…