初めに
DataStore はCoroutine
やFlow
を使ってデータにアクセスでき、JetpackComposeとも親和性が高いため、SharedPreferences
を使っている場合は移行を推奨されています。コードラボ に基本的な使い方の説明がありますが、できる限りDataStore
以外の知識を必要としない書き方になっていて、余りスマートな使い方ではありません。また、取得箇所や方法も色々あるので、いくつか紹介したいと思います。
DataStoreの設定
DataStore
のインスタンスと状態を保持するAppSettingsState
データクラス、Preferences
からAppSettingsState
へ変換する関数を定義します。
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
でも同じ使い方ができます。
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)
}
}
}
}
@Composable
fun MainPage(
appSettings: AppSettingsState
) {
// TODO
}
ViewModel経由
ViweModel
でDataStore
を参照する方法は色々ありますが、今回はHiltを使ってDIする方法を取ります。また、直接DataStore
を参照するのではなく、リポジトリ層を経由して参照するようにします。
Hiltの導入方法については下記を参照してください。
Hilt を使用した依存関係挿入
hiltViewModel()
を使用するので、上記の手順に加えてモジュールの追加が必要です。
implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
Hiltが参照できるよう、モジュールへの登録を行います。他にもDataStore<Preferences>
型のモジュールを使用する場合もあるので、別名を使って指定できるようにしています。
@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>
に変換します。
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が参照できるよう、モジュールへの登録を行います。
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
companion object {
// ...
@Binds
@Singleton
abstract fun bindAppSettingsRepository(
appSettingsRepository: AppSettingsRepositoryImpl
): AppSettingsRepository
}
}
ViewModel
では、コストラクタインジェクションでリポジトリを取得し、StateFlow<AppSettingsState>
に変換します。
@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
経由で取得することができます。
@Composable
fun MainPage(
mainPageViewModel: MainPageViewModel = hiltViewModel(),
) {
val appSettings by mainPageViewModel.appSettingsState.collectAsState()
// TODO
}
ViewModelを使わない方法
Jetpack ComposeでViewModelを使わずに、Composable関数を使って状態とロジックを切り出す! でも書かれているように、JetpackComposeではViewModelは不要で、状態を返すComposable関数を使いましょうという意見もあります。下記はその例です。
State<AppSettingsState>
を返すComposable関数を作成し、Composable
から呼び出して取得します。
@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)
}
}
@Composable
fun MainPage() {
val appSettings by rememberAppSettingsState()
// TODO
}
終わりに
DataStore
の変更された値を、Composable
で参照する方法をいくつか紹介してみました。Roomを使ったデータの永続化の場合も、Flow
を返すDao
を定義できるので、同じような使い方ができます。それぞれのプロジェクトに合った使い方の参考になれば幸いです。