我正在参加「启航计划」

前语

Jetpack DataStore 是一种数据存储处理计划,由于运用了 Kotlin 协程或许 RxJava 以异步、共同的事务方法存储数据,用法相较于其它存储计划 (SharedPreferences、MMKV) 会愈加特别,所以现在网上都没有什么比较好的 DataStore 封装。

个人了解了用法后觉得运用起来挺费事的,会和许多人相同,觉得无脑用 MMKV 就完事了,个人也对 MMKV 做了十分好用的封装,感觉没必要用 DataStore。直到我看到了扔物线的文章,才明白不能无脑用 MMKV,可是 DataStore 用起来有点繁琐,仍是有必要封装一下的。

在做了许多探索和测验后,总算封装出了一套个人十分满足的用法,希望能协助到咱们 ~ 文章会比较长,主张耐心看完。

根底用法

DataStore 供给两种不同的完成:Preferences DataStore 和 Proto DataStore。Preferences DataStore 是运用键值的方法进行存储,而 Proto DataStore 是将数据作为自界说数据类型的实例进行存储,简略来说便是存取什么样的数据都由一个 Protopuf 文件决议,所以 Proto DataStore 能确保类型是安全的,可是学习成本会高许多,由于要学习多一门新的言语。

Preferences DataStore 的键值用法相对来说会愈加符合多数人的运用习惯,所以个人挑选运用 Preferences DataStore,而且经过个人封装后的用法其实也是能确保类型是安全的。

下面介绍一下 Preferences DataStore 的用法。

Kotlin 用法

增加 Preferences DataStore 的依靠:

dependencies {
  implementation "androidx.datastore:datastore-preferences:1.0.0"
}

创立 DataStore

运用特点托付来创立Datastore<Preferences>实例,这行代码要写在 Kotlin 文件的顶层,这样能够更轻松地将Datastore<Preferences>目标保留为单例。比方:

// At the top level of your kotlin file:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

前面说了 DataStore 用法相较于其它存储计划会愈加特别,从创立的代码就体现出来了。这儿声明的是一个 Context 的扩展特点,用法不是很惯例。你能够理解为给 Context 类额外声明晰一个名为 dataStore 的特点,而且这个特点是个单例。

读取内容

读取数据时要运用相应的键类型函数为需求存储在DataStore<Preferences>实例中的每个值界说一个键。例如,如需为 int 值界说一个键,请运用intPreferencesKey()特点,然后经过Flow供给恰当的存储值。比方:

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

写入内容

修改数据需求运用edit()函数,在代码块中用前面界说的 key 目标去更新值。

suspend fun incrementCounter() {
 context.dataStore.edit { settings ->
  val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
  settings[EXAMPLE_COUNTER] = currentCounterValue + 1
 }
}

由于 Preferences.Key<T> 目标不仅有键名的信息,还有回来值类型的信息,只要存取用了同一个 key 目标,就能确保存取的类型是共同的。这种规划相较于其它键值存储计划 (SharedPreferences、MMKV) 会更好一点。

Java 用法

需求增加额外的 RxJava 依靠,有 RxJava2 和 RxJava3 可选。

dependencies {
  // optional - RxJava2 support
  implementation "androidx.datastore:datastore-preferences-rxjava2:1.0.0"
  // optional - RxJava3 support
  implementation "androidx.datastore:datastore-preferences-rxjava3:1.0.0"
}

创立 RxDataStore

创立一个 RxDatastore<Preferences>实例。

RxDataStore<Preferences> dataStore =
 new RxPreferenceDataStoreBuilder(context, "settings").build();

留意这儿仅仅创立了目标,咱们还要自己将其完成为单例,不能每次想存取数据的时分都创立一个新的 RxDatastore<Preferences>实例。前面的 Kotlin 用法是用了特点托付的语法特性完成了单例。

读取内容

读取数据时相同要创立一个 Preferences.Key<Integer> 目标,然后调用 dataStore.data().map(...) 函数,用法和 Kotlin 的类似,可是回来一个 RxJava 的 Flowable 目标,这样咱们就能在 Java 代码中运用了。

Preferences.Key<Integer> EXAMPLE_COUNTER = PreferencesKeys.int("example_counter");
Flowable<Integer> exampleCounterFlow =
 dataStore.data().map(prefs -> prefs.get(EXAMPLE_COUNTER));

写入内容

修改数据需求运用dataStore.updateDataAsync()函数。

Single<Preferences> updateResult = dataStore.updateDataAsync(prefsIn -> {
 MutablePreferences mutablePreferences = prefsIn.toMutablePreferences();
 Integer currentInt = prefsIn.get(EXAMPLE_COUNTER);
 mutablePreferences.set(EXAMPLE_COUNTER, currentInt != null ? currentInt + 1 : 1);
 return Single.just(mutablePreferences);
});
// The update is completed once updateResult is completed.

回来的 Single 目标不需求订阅,需求在更新完成后做什么事才进行订阅。

小结

能够看到 Preferences DataStore 用起来比 SharedPreferences、MMKV 费事许多,所以有必要封装一下简化用法。

封装思路

Proto DataStore 怎样确保类型安全

原本不计划讲 Proto DataStore 的,可是了解官方的规划思维能更好的协助咱们去封装 DataStore。

Proto DataStore 用法会愈加地不惯例,如果没用过 Protobuf,估计连官方文档都看不懂。而许多文章都是直接摘抄官方文档,导致个人早期学习的时分都没搞懂到底是怎样来用。所以个人会尽量详细点把全体的工作机制和用法讲清楚。

首要要用 Protopuf 言语写一个文件,比方 settings.pb,并放到 app/src/main/proto/ 文件夹中。

syntax = "proto3";
option java_package = "com.example.application";
option java_multiple_files = true;
message Settings {
  int32 example_counter = 1;
}

Protobuf 言语和 Java 很像,即便咱们没学过也能读懂上面的代码,这儿界说了一个 Settings 类,有个名为 example_counter 的 int 变量。

之后咱们能经过该文件去创立一个对应的 DataStore 单例。

val Context.settingsDataStore: DataStore<Settings> by dataStore(
 fileName = "settings.pb",
 serializer = SettingsSerializer
)

重点来了,这儿的 DataStore 泛型是 Settings,这是哪来的呢?莫非是 settings.pb 文件声明的? Kotlin 和 Java 不可能会跨言语访问到其它言语的类呀。这便是令人最疑惑的地方,其实 settings.pb 文件会编译生成一个对应 Java 类,所以咱们得重新编译一下项目,这样就能得到一个 Settings 类了。

知道会编译生成 Java 类的要害信息后,Proto DataStore 的工作机制就能好理解了。存储的数据必定仍是会写到一个文件中,那么就需求将文件数据序列化成一个 Java 目标,那要怎样序列化呢?上面创立 DataStore 还有个参数是 SettingsSerializer,这个类还需求咱们自己写。

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

这段序列化代码会让人很懵是怎样写出来的,其实不必管,照抄就行了,这是一套模板代码。简略说一下,这儿的序列化需求做到三件事:取默认值、从文件流中得到 Java 目标、把 Java 目标写到文件中。这三个功用具体怎样完成不必咱们写,编译生成的 Java 类会供给 getDefaultInstance()parseFrom()writeTo() 函数给咱们调用。

到这儿咱们总算把 Proto DataStore 的整个工作机制讲清楚了,总算能讲下为什么能确保类型安全了,来看下读写的用法。

读取内容:

val exampleCounterFlow: Flow<Int> = context.settingsDataStore.data
 .map { settings ->
  // The exampleCounter property is generated from the proto schema.
  settings.exampleCounter
 }

写入内容:

suspend fun incrementCounter() {
 context.settingsDataStore.updateData { currentSettings ->
  currentSettings.toBuilder()
   .setExampleCounter(currentSettings.exampleCounter + 1)
   .build()
  }
}

和 Preferences DataStore 最大的不同是,在代码块中得到的是一个 Settings 类型的目标,类型是由 DataStore 的泛型决议的。该目标就限定了咱们能存取什么类型的数据,确保读写的类型是共同的。

可能有人会说如果还有另一个 Protopuf 文件也声明晰个 example_counter 变量,但类型是 String,那存取的时分不就可能出现类型错误了?其实并不会,由于新的 Protopuf 文件是会创立另一个 DataStore 目标来运用的,即便有同名目标,也同名不同源,不会相互影响的。

现在只有 Proto DataStore 要求把数据类型界说出来,以此确保类型安全。而 Preferences DataStore、MMKV、SharedPreferences 都没这样的要求,都存在着类型安全隐患。那么有没什么方法能让键值存储计划也能确保类型安全?其实也有,能够运用 Koltin 特点托付封装一下。

MMKV 的特点托付计划

先讲一下个人的另一个库 MMKV-KTX 的封装思路,这个库甚至被 ChatGPT 推荐了。

如何利用 Kotlin 特性封装 DataStore

来看下新版别的用法,需求让一个类承继 MMKVOwner 类并传入 mmapID 参数,然后在该类里能运用by mmkvXXXX()函数将特点托付给MMKV

object Settings : MMKVOwner(mmapID = "settings") {
  var exampleCounter by mmkvInt(default = 1)
}

设置或获取特点的值会调用对应的 encode()decode() 函数,用特点名作为 key 值。比方:

val counter = Settings.exampleCounter
Settings.exampleCounter = 100

咱们这么来运用的话相同能够确保类型安全,由于这和 Proto DataStore 确保类型安全的思路是类似的。咱们用 Kotlin 特点托付写的 Settings 类就包含了前面 settings.pb 文件所声明的信息,界说了一个 Settings 类,有个名为 exampleCounter 的 Int 变量。该类约束了能存取什么类型的数据,能确保类型安全。

这儿运用了 Kotlin 特点托付进行封装,简略讲下特点托付,其实是一种托付 (署理) 形式的运用。一般咱们是把一个接口署理给一个具体的完成类,而特点托付是把赋值和取值操作署理给托付类。该托付类需求有固定模板的 getValue() 和 setValue() 函数,而且能拿到特点名,那就能用特点名作为 MMKV 存取的键名。比方:

val kv = MMKV.defaultMMKV()
class MMKVIntProperty(private val default: Int = 0) : ReadWriteProperty<Any, Int> {
  override fun getValue(thisRef: Any, property: KProperty<*>): Int =
    kv.decodeInt(property.name, default)
  override fun setValue(thisRef: Any, property: KProperty<*>, value: Int) {
    kv.encode(property.name, value)
  }
}

这样就能把一个特点经过 by 要害字署理给咱们写的托付类,赋值会调用 setValue() 函数,取值会调用 getValue() 函数。比方:

var counter: Int by MMKVIntProperty()

尽管运用了 Kotlin 特点托付进行封装,可是特点托付并不是精华。让 MMKV 和特点托付相结合许多人都想得到,可是 MMKVOwner 的规划思路大多数人想不到,这个类才是个人库的精华地点。

MMKVOwner 顾名思义便是 MMKV 目标的具有者,代码十分少,可是效果十分大。

open class MMKVOwner(override val mmapID: String) : IMMKVOwner {
  override val kv: MMKV by lazy { MMKV.mmkvWithID(mmapID) }
}
// 该接口用于兼容不能多承继的场景
interface IMMKVOwner {
  val mmapID: String
  val kv: MMKV
}

个人约束了必需求承继了该类才干运用 MMKV 的特点托付,这样来规划有两个要害的效果:

第一个要害效果是能引导用户把特点托付都会集写到一个类中,这样写才干确保在该类里边是类型安全的。如果没有 MMKVOwner 的约束能够随意托付,那么可能会有人想存就存、想读就读,写出下面的代码:

class InputWifiActivity : AppCompatActivity() {
  private var psd by mmkvString()
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_input_wifi)
    //...
    btnConfirm.setOnClickListener {
      psd = etPassword.text.toString()
      //...
    }
  }
}
class QRCodeActivity : AppCompatActivity() {
  private val pwd by mmkvString()
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_wifi)
    //...
    createQRCode(ssid, pwd)
  }
}

这儿别离写了两个特点托付,存的是 psd,可是别人读数据敲成了 pwd,这样存取的键名就不共同了,甚至可能出现类型不共同的状况。正确的用法是都把 MMKV 的特点托付写到一个类中,运用同一个特点托付进行存取就必定不会犯错。而且同一个类中不能声明同名不同类型的目标,就更能确保类型是安全的。

MMKVOwner 类的存在会让分开写特点托付的成本变高,引导咱们更标准地会集在 Repository 或 Model 等数据类中运用 MMKV 特点托付。而且关于一些不太懂的搭档,想随便在别的类里写by mmkvXXXX()是不可的,只能仿照已有的代码写到数据类中。

第二个要害效果是硬性要求运用 MMKV.mmkvWithID(mmapID) 进行分区存储,这是确保类型安全的第二重保障。

前面所说把托付会集写到一个类中,其实只能确保在该类里边是类型安全的,而咱们实际开发可能存在多个数据存储类。比方在组件化项目,咱们不知道别人会存取怎样样的数据,就可能会不同的组件界说了同名的特点托付。比方:

// 在视频组件
object VideoRepository {
  var counter by mmkvInt()
}
// 在消息组件
object MessageRepository {
  var counter by mmkvInt()
}

如果用的都是 MMKV.defaultMMKV(),就出现了数据相互覆盖的状况,甚至可能会类型不共同,存在类型安全隐患。咱们回头想一下 Proto DataStore 也可能有多个 protopuf 文件存在同名变量, 可是每个 protopuf 文件都创立了一个对应的 DataStore 目标,这样数据才互不搅扰,确保了类型安全。那咱们也给每个 Kotlin 存储类都创立一个对应的 MMKV 目标不就处理了。

MMKVOwner 有个 mmapID 结构参数,强制要求了用 MMKV.mmkvWithID(mmapID) 创立 MMKV,使其支撑分区存储,这样就 100% 确保类型安全了。

// 在视频组件
object VideoRepository : MMKVOwner(mmapID = "video") {
  var counter by mmkvInt()
}
// 在消息组件
object MessageRepository : MMKVOwner(mmapID = "message") {
  var counter by mmkvInt()
}

只用特点托付封装 MMKV 是不够好的,由于特点托付更多的效果仅仅免除声明很多的键名常量,而 MMKVOwner 的存在能让咱们更标准地去写类型安全的托付代码。

那是不是把这套封装计划的底层完成换成 DataStore 就能够了?其实没那么简略,DataStore 的用法相关于 MMKV 特别许多。个人做过了许多探索和测验后,才把类似的用法封装出来了。下面给咱们分享一下个人是怎样封装的。

DataStore 的特点托付计划

根据前面 Owner + 特点托付的封装思路,咱们规划出以下的用法:

object Settings : DataStoreOwner(name = "settings") {
  var exampleCounter by intPreference(default = 1)
}

这个 Kotlin 文件就对应着前面示例的 Protobuf 文件,这样咱们就用了 Preferences DataStore 结合 Kotlin 特性来到达 Proto DataStore 确保类型安全的效果。下面就开端来完成。

界说 DataStoreOwner

首要要界说一个 DataStoreOwner 类,该类能获取一个 DataStore 目标,结构函数需求传入个 name 参数去创立 DataStore 目标。

可是创立 DataStore 目标是需求 Context 的,咱们还需求界说一个 application 静态变量用于初始化,这样就能得到一个 Context 目标。别的能够再抽取一个 IDataStoreOwner 接口,得到以下代码。

open class DataStoreOwner(name: String) : IDataStoreOwner {
  private val Context.dataStore by preferencesDataStore(name)
  override val dataStore get() = context.dataStore
}
interface IDataStoreOwner {
  val context: Context get() = application
  val dataStore: DataStore<Preferences>
  companion object {
    internal lateinit var application: Application
  }
}

为什么要抽取一个接口?如果自身就有个父类,就无法再承继 DataStoreOwner 类了。抽取一个 IDataStoreOwner 接口便是为了能用 Kotlin 托付的特性去处理多承继的问题,用法如下:

object SettingsRepository : BaseRepository(), IDataStoreOwner by DataStoreOwner("settings") {
  // ...
}

能够用 App Startup 主动初始化 application 静态变量。

<application>
  <provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    android:exported="false"
    tools:node="merge">
    <meta-data
      android:name="com.dylanc.datastore.DataStoreInitializer"
      android:value="androidx.startup" />
  </provider>
</application>
class DataStoreInitializer : Initializer<Unit> {
  override fun create(context: Context) {
    IDataStoreOwner.application = context as Application
  }
  override fun dependencies() = emptyList<Class<Initializer<*>>>()
}

怎样完成特点托付

这是该计划完成起来的一大难点,由于 DataStore 必定要用 Kotlin 协程或许 RxJava 异步存取数据。而特点托付的 get、set 函数只能是同步的,那该怎样办?用 runBlocking {...} 能够把协程的异步履行改成同步履行,那么特点的托付类就有方法写出来了。比方:

class IntPreferenceProperty(private val default: Int?) : ReadWriteProperty<IDataStoreOwner, Int?> {
  override fun getValue(thisRef: IDataStoreOwner, property: KProperty<*>): Int? =
    runBlocking {
      val key = intPreferencesKey(property.name)
      thisRef.dataStore.data.map { it[key] }.firstOrNull() ?: default
    }
  override fun setValue(thisRef: IDataStoreOwner, property: KProperty<*>, value: Int?) {
    runBlocking {
      thisRef.dataStore.edit { preferences ->
        val key = intPreferencesKey(property.name)
        if (value == null) {
          preferences.remove(key)
        } else {
          preferences[key] = value
        }
      }
    }
  }
}

给接口增加个托付函数简化托付用法。

interface IDataStoreOwner {
  // ... 
  fun intPreference(default: Int? = null) = IntPreferenceProperty(default)
}

这样把咱们前面所想的用法给完成出来了。

object Settings : DataStoreOwner(name = "settings") {
  var exampleCounter by intPreference(default = 1)
}

其完成在的封装仅仅把 MMKV 计划的底层完成换成了 DataStore,获取特点值或许给特点赋值会调用 DataStore 读取或许保存数据。可是调用方法和 MMKV 计划不相同,由于 DataStore 只支撑异步的用法,改成同步调用是会堵塞线程的。所以咱们用的时分需求另起个线程,比方:

thread {
  val counter = Settings.exampleCounter
  handler.post {
    tvCounter.text = counter.toString()
  }
}

尽管这么也能用,可是老是要自己去切线程很费事,还完全摒弃了 Kotlin 协程的用法,用起来十分不便利。可是特点的 get、set 只能是同步调用,好像很难把协程用法给保留。

个人后边又做了测验和探索,总算找到了个处理计划,便是不调用特点自身的 get、set 函数,而是调用咱们自己别的完成的用了 suspend 润饰的 get、set 函数。

那要怎样做呢?首要仍是得用到特点托付,用特点托付才干拿到特点名,用特点名作为 key 能省去声明很多的键名常量,而且能确保在类里的键名不会重复。

可是咱们不能像前面声明一个读写 Int 类型的特点托付类,而是声明一个只读的特点托付类,回来的类型是咱们自界说的, 具有 suspend 润饰的 get、set 函数。咱们先界说一个 DataStorePreference 类:

class DataStorePreference<V>(
  private val dataStore: DataStore<Preferences>,
  val key: Preferences.Key<V>,
  val default: V?
) {
  suspend fun set(value: V?): Preferences =
    dataStore.edit { preferences ->
      if (value == null) {
        preferences.remove(key)
      } else {
        preferences[key] = value
      }
    }
  suspend fun get(): V? = asFlow().first()
  fun asFlow(): Flow<V?> =
    dataStore.data.map { it[key] ?: default }
}

这样咱们就能完成一个获取 DataStorePreference 特点的只读托付类,留意这儿要做个缓存,否则每次获取特点的时分都会创立个新目标。

class PreferenceProperty<V>(
  private val key: (String) -> Preferences.Key<V>,
  private val default: V? = null,
) : ReadOnlyProperty<IDataStoreOwner, DataStorePreference<V>> {
  private var cache: DataStorePreference<V>? = null
  override fun getValue(thisRef: IDataStoreOwner, property: KProperty<*>): DataStorePreference<V> =
    cache ?: DataStorePreference(thisRef.dataStore, key(property.name), default).also { cache = it }
}

咱们再封装一下托付函数:

interface IDataStoreOwner {
  fun intPreference(default: Int? = null) = 
    PreferenceProperty(::intPreferencesKey, default)
  fun doublePreference(default: Double? = null) = 
    PreferenceProperty(::doublePreferencesKey, default)
  fun longPreference(default: Long? = null) = 
    PreferenceProperty(::longPreferencesKey, default)
  fun floatPreference(default: Float? = null) =
    PreferenceProperty(::floatPreferencesKey, default)
  fun booleanPreference(default: Boolean? = null) =
    PreferenceProperty(::booleanPreferencesKey, default)
  fun stringPreference(default: String? = null) =
    PreferenceProperty(::stringPreferencesKey, default)
  fun stringSetPreference(default: Set<String>? = null) =
    PreferenceProperty(::stringSetPreferencesKey, default)
}

这样咱们就能用特点托付了,和咱们最开端规划的用法一有点点不同是,特点用 val 而不是 var,由于托付类是只读的。

object Settings : DataStoreOwner(name = "settings") {
  val exampleCounter by intPreference(default = 1)
}

这就在能协程里运用该特点供给的 get、set 函数,比咱们刚开端用同步方法封装的特点托付好用多了。

lifecycleScope.launch {
  tvCounter.text = Settings.exampleCounter.get().toString()
}

怎样支撑 RxJava

尽管现在 Kotlin 用得越来越多,可是在 Java 代码运用的场景仍是要考虑的,比方可能接手了个老项目,要根据已有的 RxJava 代码进行开发。

那要怎样兼顾呢?个人想到了一个用法,便是把 DataStoreOwner 改成 RxDataStoreOwner,原有的特点托付就能增加回来 RxJava 观察者目标的函数,这样就能在 Java 环境下调用了。

在封装的时分发现了一个很大的问题,便是 DataStore 和 RxDataStore 不同源。比方咱们用同一个 name 来创立 DataStore 和 RxDataStore 单例:

// At the top level of your kotlin file:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
val Context.rxDataStore: RxDataStore<Preferences> by rxPreferencesDataStore(name = "settings")

这两个单例目标即运用了同一个 key 目标去存取数据,也都是存在两份数据互不搅扰。那要怎样让 DataStore 和 RxDataStore 存取的数据都是来自同一个文件?这个尽管在官方文档上没有写,可是理论上应该是能够做到的,那就只能在源码上找答案了。

个人在翻源码的时分找到了一段要害代码:

如何利用 Kotlin 特性封装 DataStore

RxDataStore 类有个 create() 静态函数,参数中有个 DataStore 目标,那么咱们经过这个函数去创立的 RxDataStore 目标应该会和传入的 DataStore 目标是同源的。这个猜想在 RxPreferenceDataStoreBuilder 的源码得到了验证。

如何利用 Kotlin 特性封装 DataStore

能够看到 RxPreferenceDataStoreBuilder 的 build() 函数终究也是用 RxDataStore.create() 创立目标的,可是在函数里别的创立了个 DataStore 目标。这样就说清楚为什么前面用同一个 name 创立的 DataStore 和 RxDataStore 不同源了,由于用了两个不同的 DataStore 目标。

咱们封装一个扩展函数,用已有的 DataStore 目标去创立 RxDataStore 单例。

fun DataStore<Preferences>.toRxDataStore(scheduler: Scheduler = Schedulers.io()) = lazy {
  RxDataStore.create(this, CoroutineScope(scheduler.asCoroutineDispatcher() + Job()))
}
val rxDataStore: RxDataStore<Preferences> by dataStore.toRxDataStore()

这样就能让 DataStore 和 RxDataStore 目标存取同一份数据了。

有了 RxDataStore 目标就能去完成特点托付的自界说类型了,写个 RxDataStorePreference 类承继 DataStorePreference 并增加 getAsync()setAsync()asFlowable() 函数,这几个函数都是经过 RxDataStore 目标来完成。

class RxDataStorePreference<V>(
  dataStore: DataStore<Preferences>,
  key: Preferences.Key<V>,
  override val default: V,
  private val rxDataStore: RxDataStore<Preferences>
) : DataStorePreference<V>(dataStore, key, default) {
  fun asFlowable(): Flowable<V> =
    rxDataStore.data().map { it[key] ?: default }
  fun getAsync(): Single<V> = asFlowable().first(default)
  fun setAsync(value: V?): Single<Preferences> =
    rxDataStore.updateDataAsync {
      val preferences = it.toMutablePreferences()
      if (value == null) {
        preferences.remove(key)
      } else {
        preferences[key] = value
      }
      Single.just(preferences)
    }
}

这儿有个细节,咱们重写了 default 特点,把原本的可空类型修改为非空类型。这么做是由于 RxJava 的 Flowable 发出了 null 数据就会履行 onError(),后续不会再回调 onNext()。为了确保订阅联系不被中断,咱们需求给个非空的默认值。

剩下便是完成特点托付了,思路也是类似的,写个 RxDataStoreOwner 类承继 DataStoreOwner 类,重写托付函数,将托付的类型改为 RxDataStorePreference,篇幅联系就不带着咱们写代码了。

这样封装之后,只需在原有的用法上把 DataStoreOwner 改成 RxDataStoreOwner 就支撑了 RxJava。

object Settings : RxDataStoreOwner(name = "settings") {
  val exampleCounter by intPreference(default = 1)
}

终究计划

个人根据以上的思路封装好了 DataStoreKTX 开源库便利咱们运用,咱们觉得不错的话希望点个 star 支撑一下~

Features

  • 无需创立 DataStore、RxDataStore、Preferences.Key 目标;
  • 支撑 Kotlin 协程和 RxJava 用法;
  • 用特点名作为键名,无需声明很多的键名常量;
  • 能够确保类型安全,防止类型或许键名不共同导致的反常;

根底用法

在根目录的 build.gradle 增加:

allprojects {
    repositories {
        //...
        maven { url 'https://www.jitpack.io' }
    }
}

在模块的 build.gradle 增加依靠:

dependencies {
    implementation 'com.github.DylanCaiCoding.DataStoreKTX:datastore-ktx:1.0.0'
}

让一个类承继 DataStoreOwner 类,即可在该类运用 by xxxxPreference() 函数将特点托付给 DataStore,比方:

object SettingsRepository : DataStoreOwner(name = "settings") {
  val counter by intPreference()
  val language by stringPreference(default = "zh")
}

如果已经有了父类无法承继,那就完成 IDataStoreOwner by DataStoreOwner(name),比方:

object SettingsRepository : BaseRepository(), IDataStoreOwner by DataStoreOwner(name = "settings") {
  // ...
}

要确保运用过的 name 不重复,只有这样才干 100% 确保类型安全!!!

支撑运用以下类型的托付函数,会用特点名作为存取的 key 值:

  • intPreference()
  • longPreference()
  • booleanPreference()
  • floatPreference()
  • doublePreference()
  • stringPreference()
  • stringSetPreference()

调用该特点的 get() 函数会履行 dataStore.data.map {...} 的读取数据,比方:

// 需求在协程中调用
val language = SettingsRepository.language.get()
// val language = SettingsRepository.language.getOrDefault()

调用该特点的 set() 函数会履行 dataStore.edit {...} 的保存数据,比方:

// 需求在协程中调用
SettingsRepository.counter.set(100)
SettingsRepository.counter.set { (this ?: 0) + 1 }

也能够作为 FlowLiveData 运用,这样每逢数据发生变化都会有告诉回调,能够更新 UI 或流式编程。比方:

SettingsRepository.counter.asLiveData()
  .observe(this) {
    tvCount.text = (it ?: 0).toString()
  }
SettingsRepository.counter.asFlow()
  .map { ... }

适配 RxJava

默认只支撑协程用法,能够做一些简略地适配扩展出 RxJava 用法。首要要在 build.gradle 增加 datastore-rxjava2datastore-rxjava3 依靠。

dependencies {
    // 可选
    implementation 'com.github.DylanCaiCoding.DataStoreKTX:datastore-rxjava2:1.0.0'
    implementation 'com.github.DylanCaiCoding.DataStoreKTX:datastore-rxjava3:1.0.0'
}

然后把 DataStoreOwner 类改为 RxDataStoreOwner 类,这样就适配好了。主张给特点增加 @JvmStatic 注解,能够让调用该特点的 Java 代码会愈加简练。

object SettingsRepository : RxDataStoreOwner(name = "settings") {
  @JvmStatic
  val counter by intPreference()
}

调用该特点新增的 getAsync() 函数会履行 rxDataStore.updateDataAsync(prefsIn -> ...) 的读取数据,回来值是 Single<T>,比方:

SettingsRepository.getCounter().getAsync()
    .subscribe(counter -> {
      // ...
    });

调用该特点新增的 setAsync() 函数会履行 rxDataStore.data().map(prefs -> ...) 的读取数据,比方:

SettingsRepository.getCounter().setAsync(100);
    SettingsRepository.getCounter().setAsync((counter, prefsIn) -> counter + 1);

也能够将作为 Flowable 运用,这样每逢数据发生变化都会有告诉回调,能够更新 UI 或流式编程。比方:

SettingsRepository.getCounter().asFlowable()
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(counter -> tvCounter.setText(String.valueOf(counter)));

协程用法和 RxJava 用法能够混用,只要是同一个特点,存取函数的都是操作同一个数据源。

关于 MMKV-KTX

最近看了下个人的 MMKV-KTX 库已经有 600 左右月下载量了,那至少有几千人在运用,感觉仍是有必要讲下最新 1.2.16 版别的留意事项。

最新版把 MMKVOwner 接口改成了类,有个 mmapID 结构参数,这样运用起来才愈加标准,能确保类型安全,主张都晋级一下~

晋级后通常给 MMKVOwner 加个括号mmapID 就行了。可是如果原本就有个父类,无法再承继 MMKVOwner 类,那就改成完成 IMMKVOwner by MMKVOwner(mmapID),比方:

object SettingsRepository : BaseRepository(), IMMKVOwner by MMKVOwner(mmapID = "settings") {
  // ...
}

别的新增了mmkvXXXX().asLiveData()用法,将特点托付给LiveData,存储数据时能够直连续 UI 一起更新了,有需求的能够运用一下。例如:

object SettingRepository : MMKVOwner(mmapID = "settings") {
  val isNightMode by mmkvBool().asLiveData()
}
SettingRepository.isNightMode.observe(this) {
  checkBox.isChecked = it
}
SettingRepository.isNightMode.value = true

总结

本文介绍 DataStore 的协程用法和 RxJava 用法,尽量讲清楚了 Proto DataStore 的工作机制和用法,以及 Proto DataStore 为什么能确保类型安全。然后讲了 MMKV 的特点托付计划,用 Owner + 特点托付的方法完成的 Kotlin 类与 Protopuf 文件有着类似的效果,相同能确保类型安全。

所以个人根据这个计划对 Preferences DataStore 封装,尽管遇到有不少问题,可是逐个霸占了。终究封装出了一个十分好用的库 DataStoreKTX,觉得有协助的话希望能点个 star 支撑一下 ~

别的如果挑选运用 MMKV,主张用一下个人的另一个库 MMKV-KTX,相同简练好用,而且能确保类型安全。

关于我

一个爱好使然的程序“工匠” 。有代码洁癖,喜欢封装,对封装有必定的个人见解,有不少个人共同或原创的封装思路。GitHub 有分享一些协助建立开发结构的开源库,推荐咱们用一用。有任何运用上的问题或许需求都能够提 issues 或许加我微信直接反馈。

  • :DylanCai
  • GitHub:DylanCaiCoding
  • 微信号:DylanCaiCoding