• 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", "")
    
    • 1
    • 2
    • 3
    • 4
    • 5
    注意

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

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

    我们通过一个计数器例子,在具体的情景中理解和使用DataStore
    xml布局如下:

      
      
        
          
      
          
      
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    activity代码如下:

    class MainActivity : AppCompatActivity() {  
    	
        override fun onCreate(savedInstanceState: Bundle?) {  
            super.onCreate(savedInstanceState)  
            setContentView(R.layout.activity_main)  
    
            val tv = findViewById(R.id.tv)  
            val fab = findViewById(R.id.fab)  
        }  
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

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

    Preferences DataStore

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

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

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

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

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

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

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

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

    在计数器的案例中,我们通过 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  
            }  
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    读取数据

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

    MainScope().launch {  
        dataStore.data  
            .map {  
                it[COUNTER] ?: 0  
            }.collect {  
                tv.text = it.toString()  
            }  
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    从 SharedPreferences 迁移到 Preferences DataStore

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

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

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

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

    private val Context.dataStore: DataStore by preferencesDataStore(  
        name = "preferences-test",  
        // 新增部分
        produceMigrations = { context ->  
            listOf(SharedPreferencesMigration(context, "test"))  
        }  
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

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

    class MainActivity : AppCompatActivity() {  
      
        // 创建:preferencesDataStore 委托可确保我们有一个 DataStore 实例在应用中具有该名称  
        private val Context.dataStore: DataStore 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(R.id.tv_test)  
            val fab = findViewById(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  
                    }  
                }        
            }    
        }    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

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

    Proto DataStore

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

    添加依赖
    1. 添加协议缓冲区插件
    plugins {  
        ……
        id "com.google.protobuf" version "0.8.17"  
    }
    
    • 1
    • 2
    • 3
    • 4
    1. 添加协议缓冲区和 Proto DataStore 依赖项
    dependencies {  
        ……
        implementation "androidx.datastore:datastore:1.0.0"
        implementation "com.google.protobuf:protobuf-javalite:3.18.0"  
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    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'  
                    }  
                }  
            }    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    定义架构

    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;  
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

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

    创建 Proto DataStore
    1. 创建序列化器:
      定义一个实现 Serializer的类,其中 T 是 proto 文件中定义的类型。通过实现序列化器告知DataStore如何读取和写入我们在 proto 文件中定义的数据类型,如果磁盘上没有数据,序列化器还会定义默认返回值。
    object CountData : Serializer {  
        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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    1. 创建 Proto DataStore 实例
      使用 dataStore 所创建的属性委托来创建 DataStore 实例,其中 T 是在 proto 文件中定义的类型。
      fileName参数:告知 DataStore 使用哪个文件存储数据
      serializer 参数:告知 DataStore 在第一步中定义的序列化器类的名称
    val Context.counterDataStore : DataStore by dataStore(  
        fileName = "count.pb",  
        serializer = CountData  
    )
    
    • 1
    • 2
    • 3
    • 4
    写入数据

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

    fab.setOnClickListener {  
        MainScope().launch {  
            counterDataStore.updateData { count ->
                count.toBuilder()  
                    .setCounter(count.counter + 1)  
                    .build()  
            }  
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    读取数据

    读取数据则与 Preferences DataStore 类似

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

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

    val Context.counterDataStore : DataStore 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()  
                }  
            )  
        }  
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在运行前记得删掉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

  • 相关阅读:
    口碑最好的运动蓝牙耳机推荐,2022年最值得入手的六款运动耳机
    (附源码)springboot高校机房自动排课系统 毕业设计 211004
    Flutter高仿微信-第39篇-单聊-删除单条信息
    【前后端分离系列】 Spring Boot + Vue 实现 EasyPOI 导入导出
    《红蓝攻防对抗实战》三.内网探测协议出网之HTTP/HTTPS协议探测出网
    qt线程介绍
    Spring RCE 漏洞 CVE-2022-22965复现分析
    Flask使用Nacos作为服务的配置中心
    【ARMv8 SIMD和浮点指令编程】NEON 加载指令——如何将数据从内存搬到寄存器(其它指令)?
    互联网大厂大佬教你用300 行代码带你秒懂 Java 多线程!
  • 原文地址:https://blog.csdn.net/maniuT/article/details/134553588