Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
プロダクトで安全にDataStore移行する
Search
Go Takahana
October 06, 2022
Technology
2k
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
Kiro Crew 入門 (手を離しても進む仕事) | 目指せ Kiro マスター 11
inariku
PRO
4
640
DBTS2026 盛り上がってますか!?Databricksが実現するAI時代のデータ基盤
akuwano
1
200
Kiro 入門 (1 つのエージェントを、どの画面からでも) | 目指せ Kiro マスター 01
inariku
PRO
6
1.3k
Gifu WordPress Meetup #200 に向けて、私たちには何ができるだろうか?/ 2026-11-07 Gifu WordPress Meetup
torounit
0
240
2026 Apache Flink Community Checkpoint
rmetzger
0
200
Ebitengine製iOSアプリ公開ボタンを押す会
eiei114
0
110
`go get`したらELFが降ってきた件
taiyow
0
190
HQ駆動開発で最高のAI-Contextを作ろう
sansantech
PRO
1
120
Claude Codeで作ったアプリを安全に公開する/Built with Claude Japan 2026
yoshidashingo
0
210
続・ソフトウェアアーキテクトのための意思決定術: Decision Readiness, Revisited — When AI Changes the Economics of Software Architecture
snoozer05
PRO
9
2.8k
In-Context Learning再入門。ICLの勾配降下的解釈の研究動向を追ってみる。
dai08srhg
1
750
Copilot Coworkで「みのるん式」やってみた
minorun365
PRO
2
1.6k
Featured
See All Featured
Deep Space Network (abreviated)
tonyrice
0
410
Building the Perfect Custom Keyboard
takai
2
950
Exploring anti-patterns in Rails
aemeredith
4
560
The Illustrated Children's Guide to Kubernetes
chrisshort
51
53k
Leveraging Curiosity to Care for An Aging Population
cassininazir
1
560
Tell your own story through comics
letsgokoyo
1
1.1k
How to Create Impact in a Changing Tech Landscape [PerfNow 2023]
tammyeverts
56
3.5k
Building a Scalable Design System with Sketch
lauravandoore
464
34k
How To Speak Unicorn (iThemes Webinar)
marktimemedia
1
630
"I'm Feeling Lucky" - Building Great Search Experiences for Today's Users (#IAC19)
danielanewman
230
23k
How To Stay Up To Date on Web Technology
chriscoyier
790
250k
How to Grow Your eCommerce with AI & Automation
katarinadahlin
PRO
2
350
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