Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
プロダクトで安全にDataStore移行する
Search
Go Takahana
October 06, 2022
Technology
1.9k
2
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
プロダクトで安全にDataStore移行する
Go Takahana
October 06, 2022
More Decks by Go Takahana
See All by Go Takahana
ABEMAのRenderScript Intrinsics Replacement Toolkit 導入事例
takahana
0
1.5k
Other Decks in Technology
See All in Technology
On-behalf-of Token exchange with AgentCore Identity
hironobuiga
2
220
小さく始める AI 活用推進 ― 日経電子版 Web チームの事例/nikkei-tech-talk47
nikkei_engineer_recruiting
0
270
Claude Codeをどのように キャッチアップしているか
oikon48
13
8.2k
AAIFに入ってみた ~内から見えるコミュニティ動向~
sato4
0
240
2026TECHFRESH畢業分享會 - 原生還是跨平台? App 開發踩坑實錄
line_developers_tw
PRO
0
1.1k
MCP Appsを作ってみよう
iwamot
PRO
4
660
SONiCの統計情報を取得したい
sonic
0
180
Oracle AI Database@Azure:サービス概要のご紹介
oracle4engineer
PRO
6
2k
プロダクト開発から業務改善コンサルまで。事業全体へ「染み出す」ことで広がるエンジニアの可能性
ham0215
0
130
新しいVibe Codingと”自走”について
watany
6
330
白金鉱業Meetup_Vol.24_「AIエージェントは分けるほど良い」は本当か? / Is it true that “the more you divide AI agents, the better”?
brainpadpr
1
390
あなたの知らないPDFのアクセシビリティ
lycorptech_jp
PRO
0
200
Featured
See All Featured
RailsConf 2023
tenderlove
30
1.5k
Chasing Engaging Ingredients in Design
codingconduct
0
220
Faster Mobile Websites
deanohume
310
31k
Design and Strategy: How to Deal with People Who Don’t "Get" Design
morganepeng
133
19k
Documentation Writing (for coders)
carmenintech
77
5.4k
Making Projects Easy
brettharned
120
6.7k
Java REST API Framework Comparison - PWX 2021
mraible
34
9.4k
Primal Persuasion: How to Engage the Brain for Learning That Lasts
tmiket
0
370
Automating Front-end Workflow
addyosmani
1370
210k
Thoughts on Productivity
jonyablonski
76
5.2k
Facilitating Awesome Meetings
lara
57
7k
Building AI with AI
inesmontani
PRO
1
1.1k
Transcript
プロダクトで安全に DataStore移行する Go Takahana @go_takahana 1
2 Go Takahana 株式会社サイバーエージェント 株式会社AbemaTV 21新卒入社。 ABEMAのAndroidアプリ開発。
Agenda DataStoreの概要 Preferences DataStore移行のモチベーション Preferences DataStoreへの移行計画 リリース前のテスト、リリース後の懸念 まとめ 01 02
03 04 05 3
Agenda DataStoreの概要 Preferences DataStore移行のモチベーション Preferences DataStoreへの移行計画 リリース前のテスト、リリース後の懸念 まとめ 01 02
03 04 05 4
DataStore • ローカルのデータストレージのライブラリ。 (Jetpackの一部) • 小規模で単純なデータ保存に適している。 ◦ アプリのユーザ設定など 5
DataStoreの特徴 • Preferences DataStore と Proto DataStore の2種類がある。 • Kotlin
Coroutines (suspend / Flow) をベースに実装されている。 6
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
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関数
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を 渡すだけでマイグレーションが可能。
Agenda DataStoreの概要 Preferences DataStore移行のモチベーション Preferences DataStoreへの移行計画 リリース前のテスト、リリース後の懸念 まとめ 01 02
03 04 05 10
Agenda DataStoreの概要 Preferences DataStore移行のモチベーション Preferences DataStoreへの移行計画 リリース前のテスト、リリース後の懸念 まとめ 01 02
03 04 05 11
移行していなくてもあまり問題にならない • プロダクトとしては機能(価値)を提供できているので、問題にはならない。 →開発者目線で移行するモチベーションを見つけることが必要。 12
移行するモチベーションはチームごとに違う • まずはSharedPreferencesとPreferences DataStoreの比較をして、 DataStoreの良いところを知るのが第1歩。 13
DataStoreとの機能比較 14 Shared Preferences Preferences DataStore Proto DataStore 非同期API 同期処理
エラーハンドリング タイプセーフ データの整合性 マイグレーション のサポート ✅ ※ ✅ ❌ ❌ ❌ ❌ ✅ ❌ ✅ ❌ ✅ ✅ ✅ ❌ ✅ ✅ ✅ ✅ ※UIスレッドを ブロッキングする 参考:Introduction to Jetpack DataStore https://medium.com/androiddevelopers/introduc tion-to-jetpack-datastore-3dc8d74139e7
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があるかどうか。
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では対応していない。 スケジュールされた書き込み処理が完了するまで待ち合 わせる処理があるかどうか。
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のエラーハンドリングで 例外をキャッチできる
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
移行モチベーションの例 • メインスレッドでEditor#commit()を呼び出しているところがあり、ANRやUIジャン クが起きる可能性があって、それを回避したい。 • 非同期 (Flow) でPreferencesの変更を受け取りやすくしたい。 19
Agenda DataStoreの概要 Preferences DataStore移行のモチベーション Preferences DataStoreへの移行計画 リリース前のテスト、リリース後の懸念 まとめ 01 02
03 04 05 20
Preferences DataStoreへの移行計画 1. 新規でキーを追加する場合は、Preferences DataStoreに追加する 2. SharedPreferencesで保存していたデータをFlowで流したいケースが出てきた ら、DataStoreに移行する。 21
DataStoreのインスタンスの管理 22 注意点 • ファイル(xxx.preferences_pb)に対して、DataStoreのインスタンスは1つである 必要がある。(シングルトン)
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
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 { … } } 実装クラスをシングルトンにする。
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()) }
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> = … デフォルト実装だと一度に全てのキーをマイ グレーションする。 便利ではあるが、影響範囲が大きすぎる可 能性がある😅
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) } } …
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) に対応させる必要がある。
キーは1つずつ移行しよう 29 • Kotlin Coroutines (Flow / suspend) の対応を一度にやらなくていい。 •
マイグレーションするとSharedPreferencesからはデータが消えてしまうので、正 しくマイグレーションできたのか確認しやすいように1つずつ移行した方がいい。
キーを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) ) ) } ) マイグレーションしたいキーを指定する。
Kotlin Coroutines の対応で悩むこと • データの利用側が Flow, suspend 関数を扱う実装になっていない。 • Javaからの呼び出しをどうするか。
31
データの利用側が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に置き換えた時
データ取得時に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
データのプリロード • はじめてデータの取得をするときにファイル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
データの書き込み時はrunBlockingしない方が良いかも 書き込みする時は必ずファイルI/Oが生じるので、runBlockingでメインスレッドをブロッ キングするのは避けた方が良い。 35 class SetUserAgeUseCase( val dataStore: UserPreferencesDataStore ,
val applicationScope : CoroutineScope , ) { operator fun invoke(age: Int) { applicationScope .launch { dataStore.setAge(age) } } } 例)アプリケーションスコープの CoroutineScopeを使 い、コルーチンを起動して書き込む。
Javaからの呼び出しをどうするか JavaからKotlin Coroutinesの機能を使えないので、Flowでデータを公開したり、書き 込む関数をsuspendで公開しても使えない。 →別の関数を用意するアプローチをする 36
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の変換 を利用する
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 } } } }
Agenda DataStoreの概要 Preferences DataStore移行のモチベーション Preferences DataStoreへの移行計画 リリース前のテスト、リリース後の懸念 まとめ 01 02
03 04 05 39
リリース前のテスト • ユーザがアプリをアップデートするシナリオでテストする。 ◦ 前に保存した状態が残っているか ◦ 機能にデグレが生じないか 40
リリース後の懸念 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 マイグレーション済みのキーがあ ると上書きされずに消える。
リリースタイミングに気を付ける • 重要な機能がリリースされるバージョンで一緒にリリースするのは避ける ◦ revertしたり、前のバージョンに切り戻す可能性があるから 42
Agenda DataStoreの概要 Preferences DataStore移行のモチベーション Preferences DataStoreへの移行計画 リリース前のテスト、リリース後の懸念 まとめ 01 02
03 04 05 43
まとめ プロダクトで安全にPreferences DataStore移行するためには • キーを1つずつ移行する。 • ユーザがアプリをアップデートするシナリオでテストする。 • マイグレーションのrevertが起きないようにリリースを調整する。 44