Preferences DataStore(旧SharedPreferences)の使い方あれこれ

# 初めに
[DataStore](https://developer.android.com/topic/libraries/architecture/datastore?hl=ja) は`Coroutine`や`Flow`を使ってデータにアクセスでき、JetpackComposeとも親和性が高いため、`SharedPreferences`を使っている場合は移行を推奨されています。[コードラボ](https://developer.android.com/codelabs/basic-android-kotlin-compose-datastore?hl=ja) に基本的な使い方の説明がありますが、できる限り`DataStore`以外の知識を必要としない書き方になっていて、余りスマートな使い方ではありません。また、取得箇所や方法も色々あるので、いくつか紹介したいと思います。

# DataStoreの設定
`DataStore`のインスタンスと状態を保持する`AppSettingsState`データクラス、`Preferences`から`AppSettingsState`へ変換する関数を定義します。
```kotlin:AppSettingsState.kt
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "app_settings")

data class AppSettingsState(
    val count: Int
)

private val COUNT = intPreferencesKey("count")

fun Preferences.mapAppSettingsState(): AppSettingsState {
    return AppSettingsState(
        count = this[COUNT] ?: 0
    )
}
```

## Activityで監視
`onCreate`内で`lifecycleScope`を使って`Coroutine`を起動、内部で`DataStore`を監視し、`AppSettingsState`を取得、`Composable`へ引き渡す方法です。
`Activity`だけでなく、`LifecycleService`を継承した`Service`でも同じ使い方ができます。
```kotlin:MainActivity.kt
private var appSettings by mutableStateOf(AppSettingsState(0))

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    lifecycleScope.launch {
        lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
            dataStore.data.onEach {
                appSettings = it.mapAppSettingsState()
            }.collect()
        }
    }

    setContent {
        DataStoreSampleTheme {
            Surface(
                modifier = Modifier.fillMaxSize(),
                color = MaterialTheme.colorScheme.background
            ) {
                MainPage(appSettings)
            }
        }
    }
}
```
```kotlin:MainPage.kt
@Composable
fun MainPage(
    appSettings: AppSettingsState
) {
    // TODO
}
```

## ViewModel経由
`ViweModel`で`DataStore`を参照する方法は色々ありますが、今回はHiltを使ってDIする方法を取ります。また、直接`DataStore`を参照するのではなく、リポジトリ層を経由して参照するようにします。

Hiltの導入方法については下記を参照してください。
[Hilt を使用した依存関係挿入](https://developer.android.com/training/dependency-injection/hilt-android?hl=ja)
`hiltViewModel()`を使用するので、上記の手順に加えてモジュールの追加が必要です。
```
implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
```
Hiltが参照できるよう、モジュールへの登録を行います。他にも`DataStore<Preferences>`型のモジュールを使用する場合もあるので、別名を使って指定できるようにしています。

```kotlin:AppSettingsState.kt
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AppSettingsDataStore

@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
    companion object {
        @AppSettingsDataStore
        @Provides
        @Singleton
        fun provideAppSettingsDataStore(
            @ApplicationContext applicationContext: Context
        ): DataStore<Preferences> {
            return applicationContext.dataStore
        }
    }
}
```

リポジトリでは、コストラクタインジェクションで`DataStore`を取得し、`Flow<AppSettingsState>`に変換します。

```kotlin:AppSettingsRepository.kt
interface AppSettingsRepository {
    val appSettingsFlow: Flow<AppSettingsState>
}

class AppSettingsRepositoryImpl 
    @Inject constructor(@AppSettingsDataStore private val dataStore: DataStore<Preferences>)
    : AppSettingsRepository {
    
    override val appSettingsFlow =
        dataStore.data.map {
            it.mapAppSettingsState()
        }
}
```

Hiltが参照できるよう、モジュールへの登録を行います。

```kotlin:AppSettingsState.kt
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
    companion object {

        // ...
    
        @Binds
        @Singleton
        abstract fun bindAppSettingsRepository(
            appSettingsRepository: AppSettingsRepositoryImpl
        ): AppSettingsRepository
    }
}
```

`ViewModel`では、コストラクタインジェクションでリポジトリを取得し、`StateFlow<AppSettingsState>`に変換します。

```kotlin:MainPageViewModel.kt
@HiltViewModel
class MainPageViewModel 
    @Inject constructor(private val appSettingsRepository: AppSettingsRepository)
    : ViewModel() {
    
    private val _appSettingsState =
        appSettingsRepository.appSettingsFlow.stateIn(viewModelScope, SharingStarted.Eagerly, AppSettingsState(0))

    val appSettingsState: StateFlow<AppSettingsState>
        get() {
            return _appSettingsState
        }
}
```
`Composable`では`ViewModel`経由で取得することができます。
```kotlin:MainPage.kt
@Composable
fun MainPage(
    mainPageViewModel: MainPageViewModel = hiltViewModel(),
) {
    val appSettings by mainPageViewModel.appSettingsState.collectAsState()

    // TODO
}
```
## ViewModelを使わない方法
[Jetpack ComposeでViewModelを使わずに、Composable関数を使って状態とロジックを切り出す!](https://qiita.com/karamage/items/9b2b5a79c364b72836d4) でも書かれているように、JetpackComposeではViewModelは不要で、状態を返すComposable関数を使いましょうという意見もあります。下記はその例です。

`State<AppSettingsState>`を返すComposable関数を作成し、`Composable`から呼び出して取得します。

```kotlin:AppSettingsState.kt
@Composable
fun rememberAppSettingsState(): State<AppSettingsState> {
    var appSettings by remember { mutableStateOf(AppSettingsState(0)) }
    val context  = LocalContext.current
    val composableScope = rememberCoroutineScope()

    LaunchedEffect(Unit) {
        context.dataStore.data.onEach {
            appSettings = it.mapAppSettingsState()
        }.stateIn(composableScope)
    }

    return remember (appSettings) {
        mutableStateOf(appSettings)
    }
}
```
```kotlin:MainPage.kt
@Composable
fun MainPage() {
    val appSettings by rememberAppSettingsState()

    // TODO
}
```

# 終わりに
`DataStore`の変更された値を、`Composable`で参照する方法をいくつか紹介してみました。[Room](https://developer.android.com/training/data-storage/room?hl=ja)を使ったデータの永続化の場合も、`Flow`を返す`Dao`を定義できるので、同じような使い方ができます。それぞれのプロジェクトに合った使い方の参考になれば幸いです。