jetpack compose中实现丝滑的轮播图效果

写在前面

最近在翻Jetpack库,发现了DataStore,官方是这么说的:

Jetpack DataStore 是一种数据存储解决方案,允许您使用协议缓冲区存储键值对或类型化对象。DataStore 使用 Kotlin 协程和 Flow 以异步、一致的事务方式存储数据。 如果您目前是使用 SharedPreferences存储数据的,请考虑迁移到 DataStore。

显而易见,在需要存储较小或简单的数据集时,DataStore比起SP更加简单且安全性更高,所以学习使用DataStore是很有价值的。

基础知识
对比

DataStore的存在是为了替代SP,所以为什么可以替代呢?我们看看官方给的图来看看SP相对于DataStore有什么劣势。

  1. 界面线程上的安全调用
    SP的apply() 方法会阻断 fsync() 上的界面线程。每次有服务启动或停止以及每次 activity 在应用中的任何地方启动或停止时,系统都会触发待处理的 fsync() 调用。 界面线程在 apply() 调度的待处理 fsync() 调用上会被阻断,这通常会导致 ANR。
  2. 运行时的异常影响
    SharedPreferences 会将解析错误作为运行时异常抛出
  3. 类型安全
    例如以下代码,我们先写入数据,其中设置key所对应的值为int类型,但在后面使用相同key获取数据时却调用getString()方法,这样程序一旦运行就会报错java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String。这段代码在编译阶段完全正常,但SharedPreferences却无法对这种操作进行规避,需要完全依靠开发者本身去遵循规范。
val sp = getSharedPreferences("test", Context.MODE_PRIVATE)
val edit = sp.edit()
edit.putInt("key", 0);
edit.apply()
val value = sp.getString("key", "")
注意

在开始DataStore的学习前,我们要记住以下几个规则(引用自官方文档)

  1. 请勿在同一进程中为给定文件创建多个 DataStore 实例,否则会破坏所有 DataStore 功能。如果给定文件在同一进程中有多个有效的 DataStore 实例,DataStore 在读取或更新数据时将抛出 IllegalStateException
  2. DataStore 的通用类型必须不可变。更改 DataStore 中使用的类型会导致 DataStore 提供的所有保证都失效,并且可能会造成严重的、难以发现的 bug。强烈建议您使用可保证不可变性、具有简单的 API 且能够高效进行序列化的协议缓冲区。
  3. 切勿对同一个文件混用 SingleProcessDataStoreMultiProcessDataStore。如果您打算从多个进程访问 DataStore,请始终使用 MultiProcessDataStore
准备

我们通过一个计数器例子,在具体的情景中理解和使用DataStore
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=".MainActivity">  
    
    <TextView          
        android:id="@+id/tv"  
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content"  
        android:text="0"  
        android:textSize="28sp"  
        app:layout_constraintBottom_toBottomOf="parent"  
        app:layout_constraintEnd_toEndOf="parent"  
        app:layout_constraintStart_toStartOf="parent"  
        app:layout_constraintTop_toTopOf="parent" />  
  
    <com.google.android.material.floatingactionbutton.FloatingActionButton          
        android:id="@+id/fab"  
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content"  
        android:layout_margin="24dp"  
        android:src="@drawable/ic_baseline_exposure_plus_1_24"  
        app:backgroundTint="@color/black"  
        app:background="@color/black"  
        app:tint="@color/white"  
        app:layout_constraintBottom_toBottomOf="parent"  
        app:layout_constraintEnd_toEndOf="parent"  
        android:contentDescription="add" />  
  
</androidx.constraintlayout.widget.ConstraintLayout>

activity代码如下:

class MainActivity : AppCompatActivity() {  
	
    override fun onCreate(savedInstanceState: Bundle?) {  
        super.onCreate(savedInstanceState)  
        setContentView(R.layout.activity_main)  

        val tv = findViewById<TextView>(R.id.tv)  
        val fab = findViewById<FloatingActionButton>(R.id.fab)  
    }  
}

我们的最终目的是通过不断点击FloatingActionButton使得TextView内的数字不断+1

Preferences DataStore

Preferences DataStore 根据键访问数据。虽然不确保类型安全,但因为无需事先定义架构,Preferences DataStore相对于Proto DataStore更易上手且创建更快。

添加依赖
implementation("androidx.datastore:datastore-preferences:1.0.0")  
创建 Preferences DataStore

我们使用 preferencesDataStore 创建 Preferences DataStore 的实例,通过 preferencesDataStore 委托可确保我们有一个 DataStore 实例在应用中具有该名称。

private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "count-preferences")
定义键

由于 Preferences DataStore 不使用预定义的架构,我们必须使用相应的键类型函数为需要存储在 DataStore<Preferences> 实例中的每个值定义一个键。 官方提供以下方法用于键值的定义,而键的类型可以通过各方法的命名体现。

  • intPreferencesKey()
  • doublePreferencesKey()
  • stringPreferencesKey()
  • booleanPreferencesKey()
  • floatPreferencesKey()
  • longPreferencesKey()
  • stringSetPreferencesKey()

在计数器案例中,TextView展示的是当前计数值,所以我们需要为int值定义一个键,即使用intPreferencesKey()

val COUNTER = intPreferencesKey("counter")
写入数据

在计数器的案例中,我们通过 FloatingActionButton 的点击事件来进行DataStore的写入操作,而Preferences DataStore的写入操作通过edit函数实现。注意edit函数是一个挂起函数,所以我们需要在协程内运行。

fab.setOnClickListener {  
    MainScope().launch {  
        dataStore.edit { preferences -> 
	        // 获取当前存储在dataStore内key为COUNTER的键值
            val currentCounterValue = preferences[COUNTER] ?: 0 
            // 将改键值+1 
            preferences[COUNTER] = currentCounterValue + 1  
        }  
    }
}
读取数据

Preferences DataStore 公开 Flow<Preferences> 中存储的数据,每当偏好设置发生变化时,Flow<Preferences>就会发出该数据。我们使用DataStore.data属性,其返回值是Flow,所以每当我们点击 FloatingActionButton 修改数据,我们能及时接收改变后的数据并修改TextView状态。

MainScope().launch {  
    dataStore.data  
        .map {  
            it[COUNTER] ?: 0  
        }.collect {  
            tv.text = it.toString()  
        }  
}
从 SharedPreferences 迁移到 Preferences DataStore

为了演示怎么迁移,我们重头再来,在准备部分的代码基础上临时创建 SharedPreferences 储存数据。

val sp = getSharedPreferences("test",Context.MODE_PRIVATE)  
val edit = sp.edit()
// 为了验证顺利迁移,我们初始值设置为10
edit.putInt("number",10);  
edit.apply()

运行程序后查看数据已经成功保存在本地

现在可以开始将 SharedPreferences 迁移到 Preferences DataStore 了。因为 DataStore 的存在就是为了替代SP,所以谷歌早提供SharedPreferencesMigration属性用于SP数据迁移。其他代码与前面类似,只需向迁移列表传入 SharedPreferencesMigration属性,其中构造函数第二个参数 sharedPreferencesName 为所创建SP的文件名称,在本例中即为”test“。

private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(  
    name = "preferences-test",  
    // 新增部分
    produceMigrations = { context ->  
        listOf(SharedPreferencesMigration(context, "test"))  
    }  
)

由于键只能从 SharedPreferences 迁移一次,因此在我们迁移完毕后,需要将刚才临时创建的SP相关代码删除,此时的完整代码如下:
有一处需要注意的,在SP文件创建时,我们的key值设置为“number”,迁移后dataStore的key值也会被设置为“number”。所以与前面的例子相比,我们还需要将intPreferencesKey()函数中的key值更改为“number”。

class MainActivity : AppCompatActivity() {  
  
    // 创建:preferencesDataStore 委托可确保我们有一个 DataStore 实例在应用中具有该名称  
    private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(  
        name = "preferences-test",  
        produceMigrations = { context ->  
            listOf(SharedPreferencesMigration(context, "test"))  
        }  
    )  
    private val COUNTER = intPreferencesKey("number")  
	  
    override fun onCreate(savedInstanceState: Bundle?) {  
        super.onCreate(savedInstanceState)  
        setContentView(R.layout.activity_main)  
			  
        val tv = findViewById<TextView>(R.id.tv_test)  
        val fab = findViewById<FloatingActionButton>(R.id.fab)  
			  
        MainScope().launch {  
            dataStore.data  
                .map {  
                    it[COUNTER] ?: 0  
                }.collect {  
                    tv.text = it.toString()  
                }  
        }        
        fab.setOnClickListener {  
            MainScope().launch {  
                dataStore.edit {  
                    val currentCounterValue = it[COUNTER] ?: 0  
                    it[COUNTER] = currentCounterValue + 1  
                }  
            }        
        }    
    }    
}

运行程序后可以看到界面初始数值为10,说明迁移完毕

Proto DataStore

SharedPreferences 和 Preferences DataStore 的一个缺点是无法定义架构,保证不了存取键时使用了正确的数据类型。而 Proto DataStore 利用协议缓冲区来定义架构来解决此问题,确保了类型安全。 协议缓冲区可持久保留强类型数据。与 XML 和其他类似的数据格式相比,协议缓冲区速度更快、规格更小、使用更简单,并且更清楚明了。虽然使用 Proto DataStore 需要学习新的序列化机制,但因为 Proto DataStore 的强大类型优势,所以非常值得我们学习。

添加依赖
  1. 添加协议缓冲区插件
plugins {  
    ……
    id "com.google.protobuf" version "0.8.17"  
}
  1. 添加协议缓冲区和 Proto DataStore 依赖项
dependencies {  
    ……
    implementation "androidx.datastore:datastore:1.0.0"
    implementation "com.google.protobuf:protobuf-javalite:3.18.0"  
}
  1. 配置协议缓冲区
    如果在这步Sync Now报错,先Sync Now前两步再Sync Now对协议缓冲区的配置
protobuf {  
    protoc{  
        // 设置 protoc 的版本号
        artifact = "com.google.protobuf:protoc:3.14.0"  
    }  
    generateProtoTasks {  
        all().each { task ->  
            task.builtins {  
                java {  
                    option 'lite'  
                }  
            }  
        }    
    }
}
定义架构

app/src/main/ 目录下创建proto文件夹,我们将在里面创建proto文件,如图所示

创建count.proto文件,文件和相关注释内容如下:
其中内部类字段类型与Java的对应关系:
string->String,int32->int,int64->long,bool->Boolean,float->float,double->double

// 声明proto的版本
syntax = "proto3";

option java_package = "com.wg.jetpackDemos.dataStore.proto";  // 指定了生成的Java类的包名
option java_multiple_files = true;  // 设置生成的Java类是一个文件还是多个文件

// message 声明的是内部类
message Count {  
  int32 counter = 1;  
}

每当我们创建或者变更proto文件时都需要Rebuild Project,即可生成对应的Java文件

创建 Proto DataStore
  1. 创建序列化器:
    定义一个实现 Serializer<T>的类,其中 T 是 proto 文件中定义的类型。通过实现序列化器告知DataStore如何读取和写入我们在 proto 文件中定义的数据类型,如果磁盘上没有数据,序列化器还会定义默认返回值。
object CountData : Serializer<Count> {  
    override val defaultValue: Count  
        get() = Count.getDefaultInstance()  
		  
    override suspend fun readFrom(input: InputStream): Count {  
        try {  
            return Count.parseFrom(input)  
        }catch (exception:InvalidProtocolBufferException){  
            throw CorruptionException("Cannot read proto.", exception)  
        }  
    }  
		  
    override suspend fun writeTo(t: Count, output: OutputStream)  
        = t.writeTo(output)  
}
  1. 创建 Proto DataStore 实例
    使用 dataStore 所创建的属性委托来创建 DataStore<T> 实例,其中 T 是在 proto 文件中定义的类型。
    fileName参数:告知 DataStore 使用哪个文件存储数据
    serializer 参数:告知 DataStore 在第一步中定义的序列化器类的名称
val Context.counterDataStore : DataStore<Count> by dataStore(  
    fileName = "count.pb",  
    serializer = CountData  
)
写入数据

与Preferences DataStore不同,Proto DataStore使用updatData()函数用于更新存储的对象。

fab.setOnClickListener {  
    MainScope().launch {  
        counterDataStore.updateData { count ->
            count.toBuilder()  
                .setCounter(count.counter + 1)  
                .build()  
        }  
    }
}
读取数据

读取数据则与 Preferences DataStore 类似

MainScope().launch {  
    counterDataStore.data.collect {  count ->
        tv.text = count.counter.toString()  
    }  
}
从 SharedPreferences 迁移到 Proto DataStore

前期准备与上面“从 SharedPreferences 迁移到 Preferences DataStore”部分相同,如果有跳过 Preferences DataStore 部分直接看 Proto DataStore 的朋友需要往回翻看一下。
同迁移到 Preferences DataStore 的思路一样,我们只需在DataStore构造器中向迁移列表传入 SharedPreferencesMigration属性。这里需要注意的是,SharedPreferencesMigration的包为androidx.datastore.migrations.SharedPreferencesMigration,我那时候因为导错包找了好久的bug,请以我为戒。

val Context.counterDataStore : DataStore<Count> by dataStore(  
    fileName = "count.pb",  
    serializer = CountData,  
    produceMigrations = { context ->  
        listOf(  
            SharedPreferencesMigration(context,"test"){  
                sharedPreferencesView, counter ->  
                // 获取 SharedPreferences 的数据  
                val count = sharedPreferencesView.getInt("number",0)  
                counter.toBuilder().setCounter(count).build()  
            }  
        )  
    }  
)

在运行前记得删掉SP相关代码,迁移完毕结果如下:

Android 学习笔录

Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
Android 性能优化篇:https://qr18.cn/FVlo89
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android 音视频篇:https://qr18.cn/Ei3VPD
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

听说90%的人都没搞定手撕协程池这道面试题!

特别的缘分 听说90%的人都没搞定手撕协程池这道面试题&#xff01; 能看到这篇文章一定是特殊的缘分&#xff0c;请务必珍惜&#xff0c;请详细看看吧&#xff0c;哈哈。 不止上图&#xff0c;最近 Go就业训练营 中不少小伙伴说&#xff0c;面试中碰到了好几次手撕协程池的问题…

Arcgis中通过函数实现字符串截取

效果 从字符串中提取最右侧的符号&#xff0c;如“/”后面的字符串 步骤 1、VB dim bbindexinstrrev( [WGCJ] ,"/")bbright( [WGCJ] ,len( [WGCJ] )- index )2、python def bb(aa):index(aa.rfind("/"))bbaa[index1:]return bb

《QT从基础到进阶·三十七》QWidget实现左侧导航栏效果

NavigationBarPlugin插件类实现了对左侧导航栏的管理&#xff0c;我们可以在导航栏插件中添加界面&#xff0c;并用鼠标点击导航栏能够切换对应的界面。 源码在文章末尾 实现效果如下&#xff1a; NavigationBarPlugin实现的接口如下&#xff1a; class NAVIGATIONBAR_EXP…

企业数字化建设诊断报告

市场竞争越来越激烈 不管是初创企业&#xff0c;还是面临转型的发展型企业 亦或是稳定发展突破瓶颈的传统企业 或多或少都面临着企业数字化建设的问题...... 联系亿达四方限时免费领取&#xff0c; “企业数字化建设诊断报告”。

C语言获取win11新版终端WindowsTerminal窗口句柄

随着Win11的普及&#xff0c;越来越多的人都能发现获取控制台窗口不能再使用以下两种传统方法了&#xff1a; HWND hwnd GetConsoleWindow();HWND hwnd FindWindowA("ConsoleWindowClass",NULL);那是因为win11换了新的终端窗口&#xff0c;叫做WindowsTerminal&am…

如何用CHAT写励志文章?

问CHAT&#xff1a;写一篇以《过了60岁要积极面对身体疾病的坎儿》为题目&#xff0c;写一篇300字励志文章 CHAT回复&#xff1a; 标题&#xff1a;《过了60岁要积极面对身体疾病的坎儿》 人生&#xff0c;有时会像一趟不期而遇的旅程&#xff0c;各自带着乐观或悲观、阳光或…

区间第k小数 (可持久化线段树、主席树)

题意&#xff1a;多次询问&#xff0c;每次询问某区间的第k小数。 可持久化线段树&#xff1a; 掺杂了一点前缀和的思想&#xff0c;对于每一个1 ~ i 的区间都建一个树&#xff0c;每个节点存的都是一个线段树&#xff0c;值存的是当前区间中初始数组按大小排序后[l, r]之间的…

生产订单自动下达

文章目录 1 Introduction2 Detail2.1 input MM02 3 Summary 1 Introduction Production order is released order by automation . We can set the material for it . The method is the detial . 2 Detail 2.1 input MM02 please choose work Scheduling please choose S…

算法分析-三壶谜题

一.题目需求 有一个充满水的8品脱的水壶和两个空水壶&#xff08;容积分别是5品脱和3品脱&#xff09;。 通过将水壶完全倒满水和将水壶的水完全倒空这两种方式&#xff0c;在其中的一个水壶中得到4品脱的水。 二、算法思想 1.算法分析 1.1. 采用的算法思想是将某个时刻水壶…

9.3 Windows驱动开发:内核解析PE结构节表

在笔者上一篇文章《内核解析PE结构导出表》介绍了如何解析内存导出表结构&#xff0c;本章将继续延申实现解析PE结构的PE头&#xff0c;PE节表等数据&#xff0c;总体而言内核中解析PE结构与应用层没什么不同&#xff0c;在上一篇文章中LyShark封装实现了KernelMapFile()内存映…

priority_queue简单实现(优先级队列)(c++)

priority_queue priority_queue介绍逻辑实现框架调整算法adjust_up()adjust_down() 仿函数/比较函数仿函数特性 构造函数迭代器区间构造 完整优先级队列代码 priority_queue介绍 pri_que是一个容器适配器&#xff0c;它的底层是其他容器&#xff0c;并由这些容器再封装而来。类…

Win10之bandicam录音无声音问题(七十六)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…

分布式进阶-链路追踪SpringCloudSleuth、Zipkin【实战篇】

一、前言 我们在使用微服务的时候&#xff0c;往往设计到各个微服务之间的调用&#xff0c;肯定会存在深度的调用链路&#xff0c;如果出现BUG或者异常&#xff0c;就会让问题定位和处理效率非常低。 有了Sleuth &#xff0c;就可以帮助我们记录、跟踪应用程序中的请求和操作。…

C++:哈希表的模拟实现

文章目录 哈希哈希冲突哈希函数 解决哈希冲突闭散列&#xff1a;开散列 哈希 在顺序结构和平衡树中&#xff0c;元素的Key和存储位置之间没有必然的联系&#xff0c;在进行查找的时候&#xff0c;要不断的进行比较&#xff0c;时间复杂度是O(N)或O(logN) 而有没有这样一种方案…

数据库基本操作-----数据库用户管理和授权

一、数据库用户管理 1&#xff0e;新建用户 CREATE USER 用户名来源地址 [IDENTIFIED BY [PASSWORD] 密码];‘用户名’&#xff1a;指定将创建的用户名 ‘来源地址’&#xff1a;指定新创建的用户可在哪些主机上登录&#xff0c;可使用IP地址、网段、主机名的形式&#xff0c…

linux下流媒体压力测试工具的使用

前言 因为领导要求做linux的推拉流时服务器压力测试&#xff0c;于是在网上找了找。一顿操作下来&#xff0c;发现很多软件盗用一款名为srs-bench的开源软件。 该代码仓库有详细的使用说明&#xff0c;而且可以在issues中找到可能会遇到的问题的解决办法 需要下载该仓库的源…

C# Onnx 百度PaddleSeg发布的实时人像抠图PP-MattingV2

目录 效果 模型信息 项目 代码 下载 效果 图片源自网络侵删 模型信息 Inputs ------------------------- name&#xff1a;img tensor&#xff1a;Float[1, 3, 480, 640] --------------------------------------------------------------- Outputs -----------------…

ZLMediaKit安装配置和推拉流

一、ZLMediaKit 库简介 ZLMediaKit 是一个基于 C11 的高性能运营级流媒体服务框架 官方写的项目特点&#xff1a; 基于 C11 开发&#xff0c;避免使用裸指针&#xff0c;代码稳定可靠&#xff0c;性能优越。 支持多种协议(RTSP/RTMP/HLS/HTTP-FLV/Websocket-FLV/GB28181/MP…

栈的生长方向不总是向下

据我了解&#xff0c;栈的生长方向向下&#xff0c;内存地址由高到低 测试 windows下&#xff1a; 符合上述情况 测试Linux下&#xff1a; 由此可见&#xff0c;栈在不同操作系统环境下&#xff0c;生长方向不总是向下

【Python】Vscode解决Python中制表符和空格混用导致的缩进问题

【Python】Vscode解决Python中制表符和空格混用导致的缩进问题 文章目录 【Python】Vscode解决Python中制表符和空格混用导致的缩进问题1. 问题来源2. 解决Reference 1. 问题来源 在python中使用缩进来进行代码块的分区&#xff0c;通常来说python的一个缩进包含4个空格&#…