Android Jetpack DataStore

发布时间 2023-04-26 15:32:37作者: cps666

原文地址 www.jianshu.com

导语

Jetpack简介及其它组件文章
DataStore就是SharedPreferences(简称SP)的替代品,Google为什么要用DataStore来替代SP呢,因为SP存在着很多问题,我之前在Android SharedPreferences转为MMKV中有详细说明了SP的不足,但是当时的有些观点还有些浅薄,所以使用了MMKV来替代SP,现在我更推荐大家使用DataStore替代SP,下面会详细讲出。

主要内容

  • DataStore的基本概念
  • DataStore、SP、MMKV对比
  • DataStore的基本使用
  • DataStore的封装

DataStore的基本概念

Jetpack DataStore 是一种数据存储解决方案,允许您使用协议缓冲区存储键值对或类型化对象。DataStore 使用 Kotlin 协程和 Flow 以异步、一致的事务方式存储数据。
所以如果想要使用DataStore,就必须使用Kotlin,因为DataStore用到了flow,flow用到了协程,协程是Kotlin的特性。
但是,如果将DataStore封装起来,那么直接使用Java调用的话,也是可以正常使用的,所以大家也不用担心。
DataStore 提供两种不同的实现:Preferences DataStore 和 Proto DataStore。

  • Preferences DataStore 使用键存储和访问数据。此实现不需要预定义的架构,也不确保类型安全。
  • Proto DataStore 将数据作为自定义数据类型的实例进行存储。此实现要求您使用协议缓冲区来定义架构,但可以确保类型安全。
    这里我们重点讲Preferences DataStore,这足以够大多数用户使用。

DataStore、SP、MMKV对比

大量文章指出MMKV的性能是多么多么高,但其实MMKV在存大数据的时候,因为MMKV是自行管理一块内存,性能是反而更低的。
我们先来看一下三种方案进行1000次Int值的连续写入耗时:

连续写入1000次Int值耗时

这么一看MMKV快到离谱,但是SP是可以异步写入的,而DataStore是基于协程的,我们关心的卡顿是主流程的流畅,所以我们只需要考虑主线程的耗时,所以会变成这样:

连续写入1000次Int值耗时(异步)
可以看到耗时大大缩减,但是还是比MMKV慢很多,这是Int数据类型的写入,Int数据是很小的,我们换成长字符串,再来看一下效果:
连续写入1000次长字符串耗时(异步)
这时我们会发现,MMKV反而成为那个最慢的,DataStore遥遥领先。

所以我们要注意的是主线程的耗时,而这个数据其实也并不重要,因为在项目中也很少有1000次大数据的写入,而1000次大数据耗时也不过不到1秒,所以选合适的更重要。

DataStore的基本使用

创建 Preferences DataStore
 private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
        name = "settings"
    ) 

name参数是 Preferences DataStore 的名称。类似SP中的name。

将内容写入 Preferences DataStore
 val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
    suspend fun putIntData() {
        context.dataStore.edit { settings ->
            val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
            settings[EXAMPLE_COUNTER] = currentCounterValue + 1
        }
    } 

我们通过intPreferencesKey来定义我们的Key的name以及存储的数据类型,如果是存放String类型就是stringPreferencesKey。
通过settings[EXAMPLE_COUNTER]是可以取到数据的,通过赋值可以用来存数据。

从 Preferences DataStore 读取内容
 val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
    val exampleCounterFlow: Flow<Int> = context.dataStore.data
        .map { preferences ->
            // No type safety.
            preferences[EXAMPLE_COUNTER] ?: 0
        } 

通过这种方式取到的是一个被Flow,通过exampleCounterFlow.first()就可以拿到真实的数据。

在同步代码中使用 DataStore

DataStore 的主要优势之一是异步 API,但可能不一定始终能将周围的代码更改为异步代码。
Kotlin 协程提供 runBlocking() 协程构建器,以帮助消除同步与异步代码之间的差异。您可以使用 runBlocking() 从 DataStore 同步读取数据。

val exampleData = runBlocking { context.dataStore.data.first() } 

对界面线程执行同步 I/O 操作可能会导致 ANR 或界面卡顿。您可以通过从 DataStore 异步预加载数据来减少这些问题:

override fun onCreate(savedInstanceState: Bundle?) {
    lifecycleScope.launch {
        context.dataStore.data.first()
        // You should also handle IOExceptions here.
    }
} 

这样,DataStore 可以异步读取数据并将其缓存在内存中。以后使用 runBlocking() 进行同步读取的速度可能会更快,或者如果初始读取已经完成,可能也可以完全避免磁盘 I/O 操作。

DataStore的封装

封装

既然我们都使用Kotlin了,那就使用Kotlin的特性扩展函数来实现吧。

import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.*
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking

/**
 * Created by 郭士超 on 2022/11/6 15:48
 * Describe:DataStoreExt.kt
 */

/**
 * 存放数据
 */
fun <T> DataStore<Preferences>.putData(key: String, value: T) {
    runBlocking {
        when(value) {
            is String -> {
                putString(key, value)
            }
            is Int -> {
                putInt(key, value)
            }
            is Long -> {
                putLong(key, value)
            }
            is Float -> {
                putFloat(key, value)
            }
            is Double -> {
                putDouble(key, value)
            }
            is Boolean -> {
                putBoolean(key, value)
            }
        }
    }
}

/**
 * 取出数据
 */
fun <T> DataStore<Preferences>.getData(key: String, defaultValue: T): T {
     val data = when(defaultValue) {
        is String -> {
            getString(key, defaultValue)
        }
        is Int -> {
            getInt(key, defaultValue)
        }
        is Long -> {
            getLong(key, defaultValue)
        }
        is Float -> {
            getFloat(key, defaultValue)
        }
        is Double -> {
            getDouble(key, defaultValue)
        }
        is Boolean -> {
            getBoolean(key, defaultValue)
        }
        else -> {
            throw IllegalArgumentException("This type cannot be saved to the Data Store")
        }
    }
    return data as T
}


/**
 * 清空数据
 */
fun DataStore<Preferences>.clear() = runBlocking { edit { it.clear() } }


/**
 * 存放String数据
 */
private suspend fun DataStore<Preferences>.putString(key: String, value: String) {
    edit {
        it[stringPreferencesKey(key)] = value
    }
}

/**
 * 存放Int数据
 */
private suspend fun DataStore<Preferences>.putInt(key: String, value: Int) {
    edit {
        it[intPreferencesKey(key)] = value
    }
}

/**
 * 存放Long数据
 */
private suspend fun DataStore<Preferences>.putLong(key: String, value: Long) {
    edit {
        it[longPreferencesKey(key)] = value
    }
}

/**
 * 存放Float数据
 */
private suspend fun DataStore<Preferences>.putFloat(key: String, value: Float) {
    edit {
        it[floatPreferencesKey(key)] = value
    }
}

/**
 * 存放Double数据
 */
private suspend fun DataStore<Preferences>.putDouble(key: String, value: Double) {
    edit {
        it[doublePreferencesKey(key)] = value
    }
}

/**
 * 存放Boolean数据
 */
private suspend fun DataStore<Preferences>.putBoolean(key: String, value: Boolean) {
    edit {
        it[booleanPreferencesKey(key)] = value
    }
}


/**
 * 取出String数据
 */
private fun DataStore<Preferences>.getString(key: String, default: String? = null): String = runBlocking {
    return@runBlocking data.map {
        it[stringPreferencesKey(key)] ?: default
    }.first()!!
}

/**
 * 取出Int数据
 */
private fun DataStore<Preferences>.getInt(key: String, default: Int = 0): Int = runBlocking {
    return@runBlocking data.map {
        it[intPreferencesKey(key)] ?: default
    }.first()
}

/**
 * 取出Long数据
 */
private fun DataStore<Preferences>.getLong(key: String, default: Long = 0): Long = runBlocking {
    return@runBlocking data.map {
        it[longPreferencesKey(key)] ?: default
    }.first()
}

/**
 * 取出Float数据
 */
private fun DataStore<Preferences>.getFloat(key: String, default: Float = 0.0f): Float = runBlocking {
    return@runBlocking data.map {
        it[floatPreferencesKey(key)] ?: default
    }.first()
}

/**
 * 取出Double数据
 */
private fun DataStore<Preferences>.getDouble(key: String, default: Double = 0.00): Double = runBlocking {
    return@runBlocking data.map {
        it[doublePreferencesKey(key)] ?: default
    }.first()
}

/**
 * 取出Boolean数据
 */
private fun DataStore<Preferences>.getBoolean(key: String, default: Boolean = false): Boolean = runBlocking {
    return@runBlocking data.map {
        it[booleanPreferencesKey(key)] ?: default
    }.first()
} 
使用

封装完成我们应该如何使用呢?

//第一步先创建一个自己的Application
class MyApplication: Application() {

    companion object {
        lateinit var instance : MyApplication
    }

    override fun onCreate() {
        super.onCreate()
        instance = this
    }

} 
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore

/**
 * Created by 郭士超 on 2022/11/6 16:38
 * Describe:AppDataStore.kt
 */
object AppDataStore {

    // 创建DataStore
    private val MyApplication.appDataStore: DataStore<Preferences> by preferencesDataStore(
        name = "App"
    )

    // DataStore变量
    private val dataStore = MyApplication.instance.appDataStore

    private fun <T> putData(key: String, value: T) {
        dataStore.putData(key, value)
    }

    private fun <T> gutData(key: String, value: T): T {
        return dataStore.getData(key, value)
    }

    fun clear() {
        dataStore.clear()
    }

    private const val NUMBER = "number"
    fun putNumber(number: Int) {
        dataStore.putData(NUMBER, number)
    }
    fun getNumber(): Int {
        return dataStore.getData(NUMBER, 0)
    }

} 

封装好之后,我们在使用的时候,不同的模块使用不同的XxxDataStore,这样可以解耦合,我们将put和get方法放到XxxDataStore中统一管理,方便我们快速定位这个方法都在哪里调用,从而更快定位到问题或者加快开发速度。

更多内容戳这里(整理好的各种文集)