Upgrade to Pro — share decks privately, control downloads, hide ads and more …

プロダクトで安全にDataStore移行する

Avatar for Go Takahana Go Takahana
October 06, 2022

 プロダクトで安全にDataStore移行する

Avatar for Go Takahana

Go Takahana

October 06, 2022
Tweet

More Decks by Go Takahana

Other Decks in Technology

Transcript

  1. DataStoreの特徴 • Preferences DataStore と Proto DataStore の2種類がある。 • Kotlin

    Coroutines (suspend / Flow) をベースに実装されている。 6
  2. Preferences DataStore と Proto DataStore 特徴的な違いは「データ格納方法」と「タイプセーフかどうか」の2点。 7 Preferences DataStore Proto

    DataStore データ格納方法 Key-Valueでデータ格納 Protocol Buffersを利用して型付きオ ブジェクトを格納 タイプセーフかどうか タイプセーフではない タイプセーフ 参考:Preferences vs Proto DataStore - Introduction to Jetpack DataStore https://medium.com/androiddevelopers/introduction-to-jetpack-datastore-3dc8d74139e7
  3. DataStore と Kotlin Coroutines DataStoreはKotlin Coroutines (suspned / Flow) をベースに実装されている。

    8 public interface DataStore<T> { … public val data: Flow<T> … public suspend fun updateData(transform: suspend (t: T) -> T): T } データの取得はFlow データの更新はsuspend関数
  4. Preferences DataStore はSharedPreferencesからの 移行が可能。 9 private val USER_PREFERENCES_NAME = "user_preferences"

    private val sharedPreferences : SharedPreferences = context.applicationContext.getSharedPreferences( USER_PREFERENCES_NAME , Context.MODE_PRIVATE ) SharedPreferencesのインスタンス取得 private const val USER_PREFERENCES_NAME = "user_preferences" private val Context.dataStore by preferencesDataStore( name = USER_PREFERENCES_NAME, produceMigrations = { context -> listOf(SharedPreferencesMigration(context, USER_PREFERENCES_NAME)) } ) DataStoreのインスタンス取得 SharedPreferencesMigrationに同じnameを 渡すだけでマイグレーションが可能。
  5. DataStoreとの機能比較 14 Shared Preferences Preferences DataStore Proto DataStore 非同期API 同期処理

    エラーハンドリング タイプセーフ データの整合性 マイグレーション のサポート ✅ ※ ✅ ❌ ❌ ❌ ❌ ✅ ❌ ✅ ❌ ✅ ✅ ✅ ❌ ✅ ✅ ✅ ✅ ※UIスレッドを ブロッキングする 参考:Introduction to Jetpack DataStore https://medium.com/androiddevelopers/introduc tion-to-jetpack-datastore-3dc8d74139e7
  6. DataStoreとの機能比較 15 Shared Preferences Preferences DataStore Proto DataStore 非同期API 同期処理

    エラーハンドリング タイプセーフ データの整合性 マイグレーション のサポート ✅ ※ ✅ ❌ ❌ ❌ ❌ ✅ ❌ ✅ ❌ ✅ ✅ ✅ ❌ ✅ ✅ ✅ ✅ ※UIスレッドを ブロッキングする 参考:Introduction to Jetpack DataStore https://medium.com/androiddevelopers/introduc tion-to-jetpack-datastore-3dc8d74139e7 Editor#apply(), OnSharedPreference ChangeListener※ Kotlin coroutines (suspend / Flow) 非同期でデータを読み書きする APIがあるかどうか。
  7. DataStoreとの機能比較 16 Shared Preferences Preferences DataStore Proto DataStore 非同期API 同期処理

    エラーハンドリング タイプセーフ データの整合性 マイグレーション のサポート ✅ ※ ✅ ❌ ❌ ❌ ❌ ✅ ❌ ✅ ❌ ✅ ✅ ✅ ❌ ✅ ✅ ✅ ✅ ※UIスレッドを ブロッキングする 参考:Introduction to Jetpack DataStore https://medium.com/androiddevelopers/introduc tion-to-jetpack-datastore-3dc8d74139e7 Editor#commit() UIスレッドで呼び出すと ANRやUIジャンクの原因になる。 →だからDataStoreでは対応していない。 スケジュールされた書き込み処理が完了するまで待ち合 わせる処理があるかどうか。
  8. DataStoreとの機能比較 17 Shared Preferences Preferences DataStore Proto DataStore 非同期API 同期処理

    エラーハンドリング タイプセーフ データの整合性 マイグレーション のサポート ✅ ※ ✅ ❌ ❌ ❌ ❌ ✅ ❌ ✅ ❌ ✅ ✅ ✅ ❌ ✅ ✅ ✅ ✅ ※UIスレッドを ブロッキングする 参考:Introduction to Jetpack DataStore https://medium.com/androiddevelopers/introduc tion-to-jetpack-datastore-3dc8d74139e7 RuntimeExceptionをス ローすることがある Flowのエラーハンドリングで 例外をキャッチできる
  9. OnSharedPreferenceChangeListenerの懸念点 18 class UserPreferencesRepository private constructor (context: Context) { private

    val sharedPreferences : SharedPreferences = … private val mutableUserPreferencesStateFlow = MutableStateFlow<UserPreferences?>( null) val userPreferencesStateFlow : StateFlow<UserPreferences?> get() = mutableUserPreferencesStateFlow .asStateFlow() init { sharedPreferences .registerOnSharedPreferenceChangeListener { sharedPreferences , key -> when (key) { Key. AGE -> { mutableUserPreferencesStateFlow .value = UserPreferences( age = sharedPreferences.getInt(Key. AGE, -1) ) } } } } } callbackはmainスレッドで実行される。 RuntimeExceptionが起こる可能性がある。 https://cs.android.com/android/platform/superpr oject/+/master:frameworks/base/core/java/andr oid/content/SharedPreferences.java;l=301
  10. DataStoreのインスタンスの管理 23 private const val USER_PREFERENCES = "user_preferences" @InstallIn(SingletonComponent:: class)

    @Module object DataStoreModule { @Singleton @Provides fun providePreferencesDataStore (@ApplicationContext appContext: Context): DataStore<Preferences> { return PreferenceDataStoreFactory.create( scope = CoroutineScope(Dispatchers.IO + SupervisorJob()), produceFile = { appContext.preferencesDataStoreFile(USER_PREFERENCES) } ) } } class UserPreferencesRepository @Inject constructor( private val dataStore: DataStore<Preferences> ) { … } Repositoryのテストは書きやすい ...?🤔 Inject
  11. DataStoreのインスタンスの管理 24 テストを書きやすいようにFakeを差し込めるように実装するのも良い。 class UserPreferencesRepository @Inject constructor( private val dataStore:

    UserPreferencesDataStore ) { … } interface UserPreferencesDataStore { fun getUserPreferencesFlow (): Flow<UserPreferences> } @Singleton class UserPreferencesDataStoreImpl @Inject constructor( private val context: Context ) : UserPreferencesDataStore { private val Context.dataStore by preferencesDataStore( name = USER_PREFERENCES , ) override fun getUserPreferencesFlow (): Flow<UserPreferences> { return context.dataStore.data.map { … } } 実装クラスをシングルトンにする。
  12. DataStoreのキーの管理 25 テストでキーの重複をチェックできるようにsealed classなどにまとめておく。 @VisibleForTesting sealed class UserPreferencesKeys< T>(val key:

    Preferences.Key< T>) { object Age : UserPreferencesKeys<Boolean>( booleanPreferencesKey("age")) } @Test fun checkDuplicatedKeys () { val subClasses = Key:: class.sealedSubclasses val nonDuplicatedKeys = subClasses. map { it.objectInstance ?.key?.name }.toSet() assertThat(nonDuplicatedKeys. count()).isEqualTo(subClasses. count()) }
  13. 26 マイグレーションの方法 private const val USER_PREFERENCES_NAME = "user_preferences" private val

    Context.dataStore by preferencesDataStore( name = USER_PREFERENCES_NAME, produceMigrations = { context -> listOf(SharedPreferencesMigration(context, USER_PREFERENCES_NAME)) } ) public fun SharedPreferencesMigration ( context: Context , sharedPreferencesName: String , keysToMigrate: Set<String> = MIGRATE_ALL_KEYS, ): SharedPreferencesMigration<Preferences> = … デフォルト実装だと一度に全てのキーをマイ グレーションする。 便利ではあるが、影響範囲が大きすぎる可 能性がある😅
  14. 27 1度に全てのキーを移行する場合に起こること class UserPreferencesSharedPreferences( private val context: Context ) {

    private val sharedPreferences : SharedPreferences = … fun getAge(): Int { return sharedPreferences .getInt(Key.Age, -1) } fun getHeight(): Int { return sharedPreferences .getInt(Key.Height, -1) } fun setAge(age: Int) { sharedPreferences .edit { putInt(Key.Age, age) } } fun setHeight(height: Int) { sharedPreferences .edit { putInt(Key.Height, height) } } …
  15. 28 1度に全てのキーを移行する場合に起こること class UserPreferencesSharedPreferences( private val context: Context ) {

    private val sharedPreferences : SharedPreferences = … fun getAge(): Int { return sharedPreferences .getInt(Key.Age, -1) } fun getHeight(): Int { return sharedPreferences .getInt(Key.Height, -1) } fun setAge(age: Int) { sharedPreferences .edit { putInt(Key.Age, age) } } fun setHeight(height: Int) { sharedPreferences .edit { putInt(Key.Height, height) } } … class UserPreferencesDataStoreImpl( private val context: Context ) { private val Context.dataStore by preferencesDataStore( … ) fun getAge(): Flow<Int> { return context.dataStore.data .map { preferences -> preferences[DataStoreKey. Age] ?: -1 } } … suspend fun setAge(age: Int) { context.dataStore.edit { preferences -> preferences[DataStoreKey. Age] = age } } … } 全てKotlin Courutines (suspend / Flow) に対応させる必要がある。
  16. キーは1つずつ移行しよう 29 • Kotlin Coroutines (Flow / suspend) の対応を一度にやらなくていい。 •

    マイグレーションするとSharedPreferencesからはデータが消えてしまうので、正 しくマイグレーションできたのか確認しやすいように1つずつ移行した方がいい。
  17. キーを1つずつマイグレーションする方法 30 private val Context.dataStore by preferencesDataStore( name = USER_PREFERENCES_NAME,

    produceMigrations = { context -> listOf( SharedPreferencesMigration( context = context, sharedPreferencesName = USER_PREFERENCES_NAME, keysToMigrate = setOf(Key.Age) ) ) } ) マイグレーションしたいキーを指定する。
  18. データの利用側がFlowを扱う実装になっていない 対応方法 • 戻り値をFlowにする。 • suspend関数にして、Flow#first()でデータを取得する。 32 class GetUserAgeUseCase( val

    sharedPreferences : UserPreferencesSharedPreferences , ) { operator fun invoke(): Int { return sharedPreferences .getAge() } } class GetUserAgeUseCase( val dataStore: UserPreferencesDataStore , ) { operator fun invoke(): Int { // 戻り値がFlowなのでビルドエラー return dataStore.getAge() } } SharedPreferencesを使っていた時 DataStoreに置き換えた時
  19. データ取得時にrunBlockingを使っていいのか Flowにするのもsuspned関数にするのも大変なケースがある。 (修正範囲が広すぎる場合など) 33 class GetUserAgeUseCase( val dataStore: UserPreferencesDataStore ,

    ) { operator fun invoke(): Int { return runBlocking { dataStore.getAge().first() } } } DataStoreを使う時(runBlockingを使う時) 推奨はされないが毎回ファイル I/Oが生じるわけではない ので、許容して使うこともできる。 (※UIスレッドをブロックすると UIジャンクやANRの可能性はある) 参考:同期コードで DataStore を使用する https://developer.android.com/topic/libraries/architecture/datasto re#synchronous
  20. データのプリロード • はじめてデータの取得をするときにファイルI/Oは生じる。 • 次回以降はファイルI/Oをスキップしてメモリキャッシュしているデータを取得できる のがほとんど。 → データ取得時にrunBlockingを使うなら、プリロードをしておくのも良い。 34 override

    fun onCreate(savedInstanceState: Bundle? , persistentState: PersistableBundle?) { super.onCreate(savedInstanceState , persistentState) lifecycleScope.launchWhenCreated { try { dataStore.data.first() } catch (e: IOException) { // handle IOException } } } 参考:同期コードで DataStore を使用する https://developer.android.com/topic/librari es/architecture/datastore#synchronous
  21. データの書き込み時はrunBlockingしない方が良いかも 書き込みする時は必ずファイルI/Oが生じるので、runBlockingでメインスレッドをブロッ キングするのは避けた方が良い。 35 class SetUserAgeUseCase( val dataStore: UserPreferencesDataStore ,

    val applicationScope : CoroutineScope , ) { operator fun invoke(age: Int) { applicationScope .launch { dataStore.setAge(age) } } } 例)アプリケーションスコープの CoroutineScopeを使 い、コルーチンを起動して書き込む。
  22. Javaから呼べるメソッドを実装する(取得時) 37 import kotlinx.coroutines.rx3.asObservable class UserPreferencesDataStoreImpl( private val context: Context,

    private val applicationScope: CoroutineScope , ) : UserPreferencesDataStore { … override fun getAgeAsObservable (): Observable<Int> { return context.dataStore.data .map { preferences -> preferences[DataStoreKey. Age] ?: -1 } .asObservable() } override fun getAgeSync(): Int { return runBlocking { context.dataStore.data.map { preferences -> preferences[DataStoreKey. Age] ?: -1 }.first() } } } Flow → RxのObservableの変換 を利用する
  23. Javaから呼べるメソッドを実装する(更新時) 38 import kotlinx.coroutines.rx3.rxCompletable class UserPreferencesDataStoreImpl( private val context: Context,

    private val applicationScope: CoroutineScope , ) : UserPreferencesDataStore { … override fun setAgeAsCompletable (age: Int): Completable { return rxCompletable { context.dataStore.edit { preferences -> preferences[DataStoreKey. Age] = age } } } override fun setAgeAsync(age: Int) { applicationScope .launch { context.dataStore.edit { preferences -> preferences[DataStoreKey. Age] = age } } } }
  24. リリース後の懸念 DataStoreにマイグレーションしたコミットをrevertすると、SharedPreferencesの値が DataStoreに反映されない場合がある。 41 v1.0.0 Shared Preferences Preferences DataStore age

    = 24 v1.1.0 age = 24 マイグレーション実 装追加 v1.2.0 revertして コミットはv1.0.0と同じ age = 24 age = 25 v1.3.0 マイグレーション実 装追加 age = 24 マイグレーション済みのキーがあ ると上書きされずに消える。