Kotlin学习笔记(十二)DataStore的封装

本封装是基于1.0.0版本。

出现DataStore是因为SharePeference存在一些问题。

SharePeference可能会导致以下问题:

1. SharePeference第一次加载数据时需要全量加载,当数据量大时可能会阻塞UI线程造成卡顿。

2. SharePeference读写文件不是类型安全的,且没有发出错误信号的机制,缺少事务性API。

3. commit()是同步提交,会在UI主线程中直接执行IO操作,当写入操作耗时比较长时就会导致UI线程被阻塞,进而产生ANR;apply()虽然是异步提交,但异步写入磁盘时,如果执行了Activity / Service中的onStop()方法,那么一样会同步等待SP写入完毕,等待时间过长时也会引起ANR问题。

其实这个导致问题的概率是非常低的!“数据量大时可能会阻塞UI线程”对于中小型app不会有什么问题。从SharePeference到DataStore可能是Google内部的KPI原因,因为他们后台还是可以检测到的。

但是DataStore的功能确实比SharePeference更好。因为基于协程和Flow, 所以可以是异步的,而且可以监听值的变化。比如,用户购买了去广告内容,设置值后,前端就能接到通知了。这是SharePeference不具有的功能。

然而直接使用DataStore并不方便。 封装一下,就和SharePeference一样便捷了。

下面开始封装:

1. 创建DataStore

需求上,默认只有一个DataStore, 但是也可以创建多个。

所以做一下缓存。这一块代码借鉴了WanAndroid的源码。

object DataStoreFactory {

    private const val USER_PREFERENCES = "default_user_preferences"
    private lateinit var defaultDataStore: DataStore<Preferences>
    private val dataStoreMaps = ConcurrentHashMap<String, DataStore<Preferences>>()

    private val applicationScope =
        CoroutineScope(SupervisorJob() + Dispatchers.Default + CoroutineExceptionHandler { _, throwable ->
            Log.e("",
                "applicationScope:\n${throwable.message.toString()}", throwable
            )
        })

    fun init(appContext: Context) {
        getDefaultPreferencesDataStore(appContext)
    }

    private fun getDefaultPreferencesDataStore(appContext: Context): DataStore<Preferences> {
        if (this::defaultDataStore.isInitialized.not()) {
            defaultDataStore = createPreferencesDataStore(appContext, USER_PREFERENCES)
        }
        return defaultDataStore
    }

    fun getDefaultPreferencesDataStore() = defaultDataStore

    fun getPreferencesDataStore(appContext: Context, name: String): DataStore<Preferences> =
        dataStoreMaps.getOrPut(name) {
            createPreferencesDataStore(appContext, name)
        }

    private fun createPreferencesDataStore(
        appContext: Context,
        name: String
    ): DataStore<Preferences> {
        return PreferenceDataStoreFactory.create(
            corruptionHandler = ReplaceFileCorruptionHandler(
                produceNewData = { emptyPreferences() }
            ),
            migrations = listOf(
                SharedPreferencesMigration(
                    appContext,
                    name
                )
            ),
            applicationScope,
            produceFile = { appContext.preferencesDataStoreFile(name) }
        )
    }
}

使用上首先要初始化一个默认叫“default_user_preferences”的Datastore。

open class BaseApp : Application() {

    override fun onCreate() {
        super.onCreate()
        DataStoreFactory.init(this) 
    }

}

2. put/get接口封装

在构造函数里一定要通过DataStoreFactory传入DataStore对象。因为DataStoreFactory会把所有的DataStore管理起来。不至于出现相同名字导致的异常。

下面接口的封装,以读写Int类型为例:

putInt对外封装了一种,通常put只需要异步,极少需要同步的情况。

getInt对外封装了四种,分别是:getIntSync/getIntFlow/getInt。

getIntSync是同步的,会阻塞当前线程.

getIntFlow返回的是Flow<Int>, 用户可以根据需求用不同的方式接受新到的数据。

getInt其实是对getIntFlow的封装,通过内含协程作用域,封装了Flow, 使用更直观更方便。


class UserPreferences(private val dataSore: DataStore<Preferences> = DataStoreFactory.getDefaultPreferencesDataStore()) {
    private val dataSoreData = dataSore.data

    companion object {
        val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO);
    }


    fun putInt(key: String, value: Int) = putData(intPreferencesKey(key), value)


    fun getIntSync(key: String, defValue: Int): Int = readNonNullData(intPreferencesKey(key), defValue)


    suspend fun getIntFlow(key: String, defValue: Int): Flow<Int> =
        readNonNullFlowData(intPreferencesKey(key), defValue)

    fun getInt(key: String, defValue: Int, block: (Int) -> Unit) =
        readEmitData(intPreferencesKey(key), defValue, block)

    private fun <T> readNonNullData(key: Preferences.Key<T>, defValue: T): T {
        return runBlocking {
            dataSoreData.map {
                it[key] ?: defValue
            }.first()
        }
    }

    private fun <T> putData(key: Preferences.Key<T>, value: T?) {
        scope.launch {
            dataSore.edit {
                if (value != null) it[key] = value else it.remove(key)
            }
        }
    }

    private suspend fun <T> readNonNullFlowData(key: Preferences.Key<T>, defValue: T): Flow<T> {
        return Companion.scope.async {
            dataSoreData.map {
                it[key] ?: defValue
            }
        }.await();
    }
}

下面是完整的UserPreferences类:

//call DataStoreFactory.init(appContext) in application onCreated first.
class UserPreferences(private val dataSore: DataStore<Preferences> = DataStoreFactory.getDefaultPreferencesDataStore()) {
    private val dataSoreData = dataSore.data

    companion object {
        val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO);
    }

    fun putString(key: String, value: String?) = putData(stringPreferencesKey(key), value)

    fun putStringSet(key: String, values: Set<String>?) =
        putData(stringSetPreferencesKey(key), values)

    fun putInt(key: String, value: Int) = putData(intPreferencesKey(key), value)

    fun putLong(key: String, value: Long) = putData(longPreferencesKey(key), value)

    fun putFloat(key: String, value: Float) = putData(floatPreferencesKey(key), value)

    fun putBoolean(key: String, value: Boolean) = putData(booleanPreferencesKey(key), value)

    fun getString(key: String, defValue: String?): String? =
        readNullableData(stringPreferencesKey(key), defValue)

    fun getStringSet(key: String, defValues: Set<String>?): Set<String>? =
        readNullableData(stringSetPreferencesKey(key), defValues)

    fun getIntSync(key: String, defValue: Int): Int =
        readNonNullData(intPreferencesKey(key), defValue)

    fun getLong(key: String, defValue: Long): Long =
        readNonNullData(longPreferencesKey(key), defValue)

    fun getFloat(key: String, defValue: Float): Float =
        readNonNullData(floatPreferencesKey(key), defValue)

    fun getBooleanSync(key: String, defValue: Boolean): Boolean =
        readNonNullData(booleanPreferencesKey(key), defValue)

    suspend fun getStringFlow(key: String, defValue: String): Flow<String> =
        readNonNullFlowData(stringPreferencesKey(key), defValue)

    suspend fun getStringSetFlow(key: String, defValues: Set<String>): Flow<Set<String>> =
        readNonNullFlowData(stringSetPreferencesKey(key), defValues)

    suspend fun getIntFlow(key: String, defValue: Int): Flow<Int> =
        readNonNullFlowData(intPreferencesKey(key), defValue)

    suspend fun getLongFlow(key: String, defValue: Long): Flow<Long> =
        readNonNullFlowData(longPreferencesKey(key), defValue)

    suspend fun getFloatFlow(key: String, defValue: Float): Flow<Float> =
        readNonNullFlowData(floatPreferencesKey(key), defValue)

    suspend fun getBooleanFlow(key: String, defValue: Boolean): Flow<Boolean> =
        readNonNullFlowData(booleanPreferencesKey(key), defValue)

    fun getInt(key: String, defValue: Int, block: (Int) -> Unit) =
        readEmitData(intPreferencesKey(key), defValue, block)

    fun getBoolean(key: String, defValue: Boolean, block: (Boolean) -> Unit) =
        readEmitData(booleanPreferencesKey(key), defValue, block)

    fun getString(key: String, defValue: String?, block: (String?) -> Unit) =
        readEmitNullData(stringPreferencesKey(key), defValue, block)

    fun getStringSet(key: String, defValues: Set<String>?, block: (Set<String>?) -> Unit) =
        readEmitNullData(stringSetPreferencesKey(key), defValues, block)

    fun getLong(key: String, defValue: Long, block: (Long) -> Unit) =
        readEmitData(longPreferencesKey(key), defValue, block)


    fun getFloat(key: String, defValue: Float, block: (Float) -> Unit) =
        readEmitData(floatPreferencesKey(key), defValue, block)

    private fun <T> readNonNullData(key: Preferences.Key<T>, defValue: T): T {
        return runBlocking {
            dataSoreData.map {
                it[key] ?: defValue
            }.first()
        }
    }

    private fun <T> readNullableData(key: Preferences.Key<T>, defValue: T?): T? {
        return runBlocking {
            dataSoreData.map {
                it[key] ?: defValue
            }.firstOrNull()
        }
    }

    private suspend fun <T> readNullableEmitFlowData(key: Preferences.Key<T>, defValue: T?): Flow<T?> {
        return dataSoreData.catch {
            if (it is IOException) {
                it.printStackTrace()
                emit(emptyPreferences())
            } else {
                throw it
            }
        }.map {
            it[key] ?: defValue
        }
    }

    private fun <T> putData(key: Preferences.Key<T>, value: T?) {
        scope.launch {
            dataSore.edit {
                if (value != null) it[key] = value else it.remove(key)
            }
        }
    }

    private suspend fun <T> readNonNullFlowData(key: Preferences.Key<T>, defValue: T): Flow<T> {
        return dataSoreData.catch {
            if (it is IOException) {
                it.printStackTrace()
                emit(emptyPreferences())
            } else {
                throw it
            }
        }.map {
            it[key] ?: defValue
        }
    }

    private fun <T> readEmitData(key: Preferences.Key<T>, defValue: T, block: (T) -> Unit) {
        scope.launch {
            readNonNullFlowData(key, defValue).collectLatest {
                block(it)
            }
        }
    }

    private fun <T> readEmitNullData(key: Preferences.Key<T>, defValue: T?, block: (T?) -> Unit) {
        scope.launch {
            readNullableEmitFlowData(key, defValue).collectLatest {
                block(it)
            }
        }
    }
}

3. 项目使用

以用户同意隐私政策为例。通常我们不会直接用getBoolean/putBoolean方式,而是会封装成一个有业务意义的名字,isAgree/setAgree. 使用哪种方式的get,同步还是异步,还是返回Flow,取决于自己的业务。

open class BaseApp : Application() {

    private lateinit var userPreferences: UserPreferences

    companion object {
        public const val KEY_IS_USER_AGREE_PRIVACY = "is_user_agree_privacy"
    }

    override fun onCreate() {
        super.onCreate()
        DataStoreFactory.init(this)
        userPreferences = UserPreferences()

        //同步的方式。用户点击同意/不同意的button,不会回调到这里。同意后的初始逻辑需要在其他地方处理。
        userPreferences.getBooleanSync(KEY_IS_USER_AGREE_PRIVACY, false).let {
            if (it) {
                // do init
            }
        }

        //用户点击同意/不同意的button后,就会回调到这里。
        userPreferences.getBoolean(KEY_IS_USER_AGREE_PRIVACY, false) {
            if (it) {
                // do init
            }
        }
    }

    fun isAgree() = userPreferences.getBooleanSync(KEY_IS_USER_AGREE_PRIVACY, false)

    fun setAgree(isAgree: Boolean) = userPreferences.putBoolean(KEY_IS_USER_AGREE_PRIVACY, isAgree)
}

封装就到这里。不足之处,请大家指正!  觉得不错,留下你的赞!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
DataStore是一个新的异步API,用于在Kotlin中存储和读取数据。它使用Kotlin协程和Flow来实现异步操作,并在单独的线程上运行,从而保证了线程安全性。DataStore提供了结构化的错误处理、类型安全以及存储自定义复杂或大型数据类对象的支持。 要使用DataStore,首先需要获取DataStore对象。可以通过使用Kotlin委托来实现,具体如下所示: ```kotlin private val settingsDataStore by preferencesDataStore(name = "app_settings") ``` 在上述代码中,`settingsDataStore`是一个DataStore对象,它使用了`preferencesDataStore`委托来获取。`name`参数指定了DataStore的名称,可以是任何字符串,例如"app_settings"或包名称等。 一旦获取了DataStore对象,就可以使用它来读取和写入数据。以下是一些示例代码: ```kotlin // 写入数据 settingsDataStore.edit { settings -> settings[KEY_NAME] = "John" settings[KEY_AGE] = 25 } // 读取数据 val nameFlow: Flow<String?> = settingsDataStore.data.map { settings -> settings[KEY_NAME] } // 监听数据变化 settingsDataStore.data .map { settings -> settings[KEY_AGE] } .distinctUntilChanged() .onEach { age -> // 处理数据变化 } .launchIn(lifecycleScope) ``` 上述代码中,`edit`函数用于写入数据,`data`属性用于读取数据。可以使用`map`和`distinctUntilChanged`等函数对数据进行转换和过滤。`launchIn`函数用于在协程作用域中启动数据监听。 总结一下,DataStore是一个用于存储和读取数据的异步API,它提供了线程安全、结构化的错误处理、类型安全以及存储自定义复杂或大型数据类对象的支持。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值