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

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

Go Takahana
October 06, 2022

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

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 マイグレーション済みのキーがあ ると上書きされずに消える。