Android Jetpack组件 DataStore的使用和简单封装

简介: Android Jetpack组件 DataStore的使用和简单封装

前言


 也许你是第一次听说这个DataStore,也许你有所耳闻,但从未使用过,不过都没有关系,随着这篇文章去熟悉DataStore。


正文


 DataStore是Jetpack中的一个组件,用于做数据持久化,DataStore以异步、一致的事务方式存储数据,克服了SharedPreferences的一些缺点,DataStore基于Kotlin协程和Flow实现,就是用来取代SharedPreferences的。我们废话不多说,开始吧。按照惯例,我们新建一个项目去做演示,不过稍微有一些不同,这次我们新建的项目时Kotlin语言的,请注意。

33199f1a87d644ce902d6dea65f1b8e9.png


创建好项目,待项目配置完成之后,我们添加依赖。


一、添加依赖


在app模块下的build.gradle中的dependencies{}闭包中添加如下依赖:

  //DataStore
    implementation 'androidx.datastore:datastore-preferences:1.0.0'
    implementation 'androidx.datastore:datastore-preferences-core:1.0.0'


同时添加开启ViewBinding和DataBinding,如下图所示:

9f0448ece28f4a7ead271b08d469a29a.png


然后Sync Now。


二、数据存取


 首先我们改一下activity_main.xml布局,里面的代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:gravity="center"
    android:orientation="vertical"
    tools:context=".MainActivity">
    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!" />
    <Button
        android:id="@+id/btn_put"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="存数据" />
    <Button
        android:id="@+id/btn_get"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="取数据" />
</LinearLayout>


里面就是两个按钮一个文本,回到MainActivity中,首先完成点击事件的监听。

  private lateinit var binding: ActivityMainBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        //存数据
        binding.btnPut.setOnClickListener {
        }
        //取数据
        binding.btnGet.setOnClickListener {
        }
    }

这应该没啥是好说的,就是使用了viewBinding,获取视图xml的控件id。


下面就是正式来使用DataStore了,首先我们需要定义一个变量。

  //定义dataStore
    private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "Study")

这里的变量就是dataStore,我们在定义的时候给了一个Study的名称,就像你使用SP时需要先给一个名字一样,然后才是键值的操作。


在DataStore中操作数据会麻烦一些,Key需要我们去定义,例如我定义一个String类型的key。

  //定义要操作的key
    private val key = stringPreferencesKey("name")

这就是定义String类型的Key,通过这个Key去进行数据存取,还有一些其他的方法可供你使用。

2465c248242e44acbaf09ac1dc597d71.png


基本上满足你的要求,SP的功能它肯定都会有的,这里这些方法可以快速构建一个符合类型的Key。


下面我们写一个方法进行存数据,代码如下:

  private suspend fun put() = dataStore.edit { it[key] = "疫情" }

这里用到了Kotlin的协程,如果你对这个不太了解,那么也没有关系,你先知道这么用,然后再去了解协程。这个方法这样不太清晰,换种方式:

4f89c7845fec422bb5c5dacc31a0bc78.png


通过dataStore.edit函数,里面的it就是MutablePreferences,然后我们通过key去设置它的值,这里是设置疫情两个字。而这个suspend是协程中的关键字,你现在可以将这个put()当成是在子线程中执行的,那么执行结束之后需要怎么做呢?需要切换到主线程。这是在调用的地方进行切换,比如我们在点击存数据按钮的时候调用,如下图所示:

fda7ba31da7246348f6adc2359580a14.png


就是这样的。


下面我们再写一个取数据的方法。

  private fun get() = runBlocking {
        return@runBlocking dataStore.data.map { it[key] ?: "新冠" }.first()
    }


你会发现和存数据又有不同,这里的first()就是取值,这个方法换个方式来看就清晰一些。

a9d14ecd43904db79d4a36436faa9fe1.png


然后我们在取数据按钮的点击事件中调用。

7c4564e361fb42c4844f9422bf122891.png


下面我们运行一下:

dc837eed09664bc7a01a8b60631789fe.gif


 第一次我先取数据,显示的是默认值,然后我存数据再取数据。效果就是这样,但你会觉得使用起来很麻烦,不如SP好用,这个我们后面再去封装,先了解一些它的功能特性。


三、数据查看和清除


 在进行定义dataStore时,会在手机中生成一个pb文件,这里我们用虚拟机来看,

6ae182add8dc46a08b9c461737c8ae6a.png


然后通过你的程序包名去找

f782ac8830ca46919aacb8f065f9bba8.png


这里的文件就是存放你的缓存信息的文件。这里我用txt打开看一下

4a5127f5ae9a4fd9a21d8f12567fae26.png


可以看到键和值,也许是浏览文件不对,下面我们清理一下这个数据。在布局中增加一个按钮

cf5a3e63998d4f519fd6cb6b1dc6b3d0.png


在代码中

88354453cd44423b828bc30a4cfd1be9.png


通过clear方法调用进行数据的清除,清除后我们再看看这个pb文件

94aed5d6e7724dcbbb3560adc59873f2.png


这个文件就什么都没有了,清除的干干净净。


四、封装


 这个DataStore是肯定需要封装之后再使用的,直接使用太麻烦了,我们需要封装的像SP那样好用,数据类型就参考这个方法中的数据类型。

2465c248242e44acbaf09ac1dc597d71.png


在写封装代码之前呢,我们先创建一个App类,里面的代码如下:

class App : Application() {
    companion object {
        lateinit var instance : App
    }
    override fun onCreate() {
        super.onCreate()
        instance = this
    }
}


然后我们在AndroidManifest中设置

b72da2f05fd54a88ac9250339220e626.png


下面我们新建一个EasyDataStore类,将它设置为object,先创建DataStore,代码如下:

  // 创建DataStore
    val App.dataStore: DataStore<Preferences> by preferencesDataStore(
        name = "Study"
    )
    // DataStore变量
    val dataStore = App.instance.dataStore


下面我们先写好各个数据类型的存取方法,先写存数据的方法:

  /**
     * 存放Int数据
     */
    private suspend fun putIntData(key: String, value: Int) = dataStore.edit {
        it[intPreferencesKey(key)] = value
    }
    /**
     * 存放Long数据
     */
    private suspend fun putLongData(key: String, value: Long) = dataStore.edit {
        it[longPreferencesKey(key)] = value
    }
    /**
     * 存放String数据
     */
    private suspend fun putStringData(key: String, value: String) = dataStore.edit {
        it[stringPreferencesKey(key)] = value
    }
    /**
     * 存放Boolean数据
     */
    private suspend fun putBooleanData(key: String, value: Boolean) = dataStore.edit {
        it[booleanPreferencesKey(key)] = value
    }
    /**
     * 存放Float数据
     */
    private suspend fun putFloatData(key: String, value: Float) = dataStore.edit {
        it[floatPreferencesKey(key)] = value
    }
    /**
     * 存放Double数据
     */
    private suspend fun putDoubleData(key: String, value: Double) = dataStore.edit {
        it[doublePreferencesKey(key)] = value
    }

然后是取数据的方法:

  /**
     * 取出Int数据
     */
    private fun getIntData(key: String, default: Int = 0): Int = runBlocking {
        return@runBlocking dataStore.data.map {
            it[intPreferencesKey(key)] ?: default
        }.first()
    }
    /**
     * 取出Long数据
     */
    private fun getLongData(key: String, default: Long = 0): Long = runBlocking {
        return@runBlocking dataStore.data.map {
            it[longPreferencesKey(key)] ?: default
        }.first()
    }
    /**
     * 取出String数据
     */
    private fun getStringData(key: String, default: String? = null): String = runBlocking {
        return@runBlocking dataStore.data.map {
            it[stringPreferencesKey(key)] ?: default
        }.first()!!
    }
    /**
     * 取出Boolean数据
     */
    private fun getBooleanData(key: String, default: Boolean = false): Boolean = runBlocking {
        return@runBlocking dataStore.data.map {
            it[booleanPreferencesKey(key)] ?: default
        }.first()
    }
    /**
     * 取出Float数据
     */
    private fun getFloatData(key: String, default: Float = 0.0f): Float = runBlocking {
        return@runBlocking dataStore.data.map {
            it[floatPreferencesKey(key)] ?: default
        }.first()
    }
    /**
     * 取出Double数据
     */
    private fun getDoubleData(key: String, default: Double = 0.00): Double = runBlocking {
        return@runBlocking dataStore.data.map {
            it[doublePreferencesKey(key)] ?: default
        }.first()
    }

最后我们根据存取的数据类型去做一个封装,存数据,代码如下:

  /**
     * 存数据
     */
    fun <T> putData(key: String, value: T) {
        runBlocking {
            when (value) {
                is Int -> putIntData(key, value)
                is Long -> putLongData(key, value)
                is String -> putStringData(key, value)
                is Boolean -> putBooleanData(key, value)
                is Float -> putFloatData(key, value)
                is Double -> putDoubleData(key, value)
                else -> throw IllegalArgumentException("This type cannot be saved to the Data Store")
            }
        }
    }

取数据:

  /**
     * 取数据
     */
    fun <T> getData(key: String, defaultValue: T): T {
        val data = when (defaultValue) {
            is Int -> getIntData(key, defaultValue)
            is Long -> getLongData(key, defaultValue)
            is String -> getStringData(key, defaultValue)
            is Boolean -> getBooleanData(key, defaultValue)
            is Float -> getFloatData(key, defaultValue)
            is Double -> getDoubleData(key, defaultValue)
            else -> throw IllegalArgumentException("This type cannot be saved to the Data Store")
        }
        return data as T
    }

对了,还有一个清除数据的方法:

  /**
     * 清空数据
     */
    fun clearData() = runBlocking { dataStore.edit { it.clear() } }

这样我们的DataStore就封装好了,下面我们在MainActivity中使用一下:

09d8a7e53a764332a324edcfe1719b26.png


这里我们存数据、取数据、清空数据都用到了,下面运行一下:

c4d694daf5d6480ebc94f5ab0841d9f5.gif


对于DataStore最基本的操作就完成了,那么下面来进阶一下。


五、对象存取


 其实我们刚才使用的是Preferences DataStore,是对数据进行操作,下面要操作的是Proto DataStore,官网上的说法是Proto DataStore 将数据作为自定义数据类型的实例进行存储。此实现要求您使用协议缓冲区来定义架构,但可以确保类型安全。


 Proto DataStore中采用的是ProtorBuffer,优势是性能好、效率高,表现在对数据的序列化和反序列化时间快,占用的空间小,还记得之前我们看到的那个pb文件吗,它里面采用的就是protobuf,之前一直是Google内部使用,这也是源于它的缺点,之前这个pb文件我们打开过,里面只能看懂键和值,缺乏描述,因此就影响了可读性,和广泛性,不如Json和XML简单。因此我们目前也只是在DataStore中使用protobuf,下面为了使用,我们需要在项目中装一个插件。


1. 插件安装


这个插件的安装比较的麻烦,首先是添加协议缓冲区插件


① 添加协议缓冲区插件


首先打开工程的build.gradle,在里面添加如下代码:

id "com.google.protobuf" version "0.8.12" apply false

cc682a197b8d410f9bd654ab90f7313d.png


再打开app下的build.gradle,添加如下代码:

  id 'com.google.protobuf'

345813fe2b8c4045a179cce907a31a0b.png


② 添加协议缓冲区和 Proto DataStore 依赖项


在app的dependencies{}闭包中添加如下代码:

  //Proto DataStore
    implementation  'androidx.datastore:datastore-core:1.0.0'
    implementation  'com.google.protobuf:protobuf-javalite:3.10.0'

6769ad9cd087458a8db882bf63dcea75.png


③ 配置协议缓冲区


在app的build.gradle中添加如下代码:

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.10.0"
    }
    // 为该项目中的 Protobufs 生成 java Protobuf-lite 代码。
    generateProtoTasks {
        all().each { task ->
            task.builtins {
                java {
                    option 'lite'
                }
            }
        }
    }
}


注意它添加的位置:

98ace729edb8429e902dcf8ceb97df19.png


点击Sync Now。


2. 创建proto文件


将项目切换到Project,然后在main下面新建一个proto文件夹。

d4efef0025754b738a2465725a7db955.png


在此文件夹下新建study.proto文件,然后AS会发现打开这个格式需要安装一个插件。

6f1c71ba5a1048f7adc26ce5e9eeb126.png


点击Install plugins进行安装。

73bccb2c423f43f997ba217ced8d85b1.png


安装成功之后,重启AS插件生效。

501a81bd165a43b4b3b365e3ad0b7f5c.png


注意看这个文件的图标变了,这说明你的插件安装成功并且配置成功了。


3. 配置proto文件


 里面的代码如下:

// 声明协议, 也支持 prota2,普遍使用proto3
syntax = "proto3";
/**
 * 通过potorbuf 描述对象生成java类。
 */
option java_package = "com.llw.datastore";//设置生成的类所在的包
option java_multiple_files = true;//可能会有多个文件。
message PersonPreferences {
  string name = 1;
  int32 age = 2;
}


这里要按照Protobuf的语言规则去设置,参考protobuf 语言指南


这里我们定了一个对象,然后你可以Make Project,此时通过编译时技术,会生成一个PersonPreferences类,下面我们创建一个序列化器。


4. 创建序列化器


在com.llw.datastore下新建一个data包,包下新建一个PersonSerializer的单例,里面的代码如下:

object PersonSerializer : Serializer<PersonPreferences> {
    override val defaultValue: PersonPreferences = PersonPreferences.getDefaultInstance()
    override suspend fun readFrom(input: InputStream): PersonPreferences {
        try {
            return PersonPreferences.parseFrom(input)
        } catch (exception: InvalidProtocolBufferException) {
            throw CorruptionException("Cannot read proto.", exception)
        }
    }
    override suspend fun writeTo(t: PersonPreferences, output: OutputStream) = t.writeTo(output)
}


这里要注意导包的问题,这个类的作用就是PersonPreferences的序列化和反序列化。


5. 对象写入和取出


这里我们现在xml中增加两个按钮,如下所示:

b8ab80bdcb904afe97f10f749ef4cc9f.png


然后回到MainActivity中,在里面添加如下代码:

  //创建 DataStore
  val Context.studyDataStore: DataStore<PersonPreferences> by dataStore(
        fileName = "study.pb",
        serializer = PersonSerializer
    )

这里就用到了那个序列化器,然后会保存到study.pb下,下面来看存和取的方法,代码如下:

    //proto 存数据
        binding.btnProtoPut.setOnClickListener {
            runBlocking {
                studyDataStore.updateData {
                    it.toBuilder()
                        .setName("刘爱国")
                        .setAge(11)
                        .build()
                }
            }
        }
        //proto 取数据
        binding.btnProtoGet.setOnClickListener {
            runBlocking {
                val person = studyDataStore.data.first()
                binding.textView.text = "name: ${person.name} , age: ${person.age}"
            }
        }


下面我们运行一下:

70b665ee2b064b94a886f3f46219d9c6.gif


六、源码


GitHub:DataStoreDemo

CSDN:DataStoreDemo.rar


相关文章
|
23天前
|
存储 Android开发 开发者
深入理解安卓应用开发的核心组件
【10月更文挑战第8天】探索Android应用开发的精髓,本文带你了解安卓核心组件的奥秘,包括Activity、Service、BroadcastReceiver和ContentProvider。我们将通过代码示例,揭示这些组件如何协同工作,构建出功能强大且响应迅速的应用程序。无论你是初学者还是资深开发者,这篇文章都将为你提供新的视角和深度知识。
|
26天前
|
数据可视化 Android开发 开发者
安卓应用开发中的自定义View组件
【10月更文挑战第5天】在安卓应用开发中,自定义View组件是提升用户交互体验的利器。本篇将深入探讨如何从零开始创建自定义View,包括设计理念、实现步骤以及性能优化技巧,帮助开发者打造流畅且富有创意的用户界面。
58 0
|
26天前
|
XML 前端开发 Java
安卓应用开发中的自定义View组件
【10月更文挑战第5天】自定义View是安卓应用开发的一块基石,它为开发者提供了无限的可能。通过掌握其原理和实现方法,可以创造出既美观又实用的用户界面。本文将引导你了解自定义View的创建过程,包括绘制技巧、事件处理以及性能优化等关键步骤。
|
1月前
|
测试技术 数据库 Android开发
深入解析Android架构组件——Jetpack的使用与实践
本文旨在探讨谷歌推出的Android架构组件——Jetpack,在现代Android开发中的应用。Jetpack作为一系列库和工具的集合,旨在帮助开发者更轻松地编写出健壮、可维护且性能优异的应用。通过详细解析各个组件如Lifecycle、ViewModel、LiveData等,我们将了解其原理和使用场景,并结合实例展示如何在实际项目中应用这些组件,提升开发效率和应用质量。
38 6
|
2月前
|
存储 开发框架 数据可视化
深入解析Android应用开发中的四大核心组件
本文将探讨Android开发中的四大核心组件——Activity、Service、BroadcastReceiver和ContentProvider。我们将深入了解每个组件的定义、作用、使用方法及它们之间的交互方式,以帮助开发者更好地理解和应用这些组件,提升Android应用开发的能力和效率。
142 5
|
2月前
|
缓存 搜索推荐 Android开发
安卓应用开发中的自定义View组件实践
【9月更文挑战第10天】在安卓开发领域,自定义View是提升用户体验和实现界面个性化的重要手段。本文将通过一个实际案例,展示如何在安卓项目中创建和使用自定义View组件,包括设计思路、实现步骤以及可能遇到的问题和解决方案。文章不仅提供了代码示例,还深入探讨了自定义View的性能优化技巧,旨在帮助开发者更好地掌握这一技能。
|
测试技术 API Android开发
retrofit rxjava android 封装 使用
  本人使用 github 现成封装 modules  rxretrofitlibrary 一步步封装 移步 http://blog.csdn.net/wzgiceman/article/details/51939574 专栏 移步 http://blog.
1134 0
|
4天前
|
编解码 Java Android开发
通义灵码:在安卓开发中提升工作效率的真实应用案例
本文介绍了通义灵码在安卓开发中的应用。作为一名97年的聋人开发者,我在2024年Google Gemma竞赛中获得了冠军,拿下了很多项目竞赛奖励,通义灵码成为我的得力助手。文章详细展示了如何安装通义灵码插件,并通过多个实例说明其在适配国际语言、多种分辨率、业务逻辑开发和编程语言转换等方面的应用,显著提高了开发效率和准确性。
|
3天前
|
Android开发 开发者 UED
安卓开发中自定义View的实现与性能优化
【10月更文挑战第28天】在安卓开发领域,自定义View是提升应用界面独特性和用户体验的重要手段。本文将深入探讨如何高效地创建和管理自定义View,以及如何通过代码和性能调优来确保流畅的交互体验。我们将一起学习自定义View的生命周期、绘图基础和事件处理,进而探索内存和布局优化技巧,最终实现既美观又高效的安卓界面。
14 5
|
1天前
|
JSON Java Android开发
探索安卓开发之旅:打造你的第一个天气应用
【10月更文挑战第30天】在这个数字时代,掌握移动应用开发技能无疑是进入IT行业的敲门砖。本文将引导你开启安卓开发的奇妙之旅,通过构建一个简易的天气应用来实践你的编程技能。无论你是初学者还是有一定经验的开发者,这篇文章都将成为你宝贵的学习资源。我们将一步步地深入到安卓开发的世界中,从搭建开发环境到实现核心功能,每个环节都充满了发现和创造的乐趣。让我们开始吧,一起在代码的海洋中航行!