導入:レイヤー分け、結局どう実装するのが正解か
Android開発でよく見る構成として、
- Fragment
- ViewModel
- UseCase
- Repository
- DataStore
といったレイヤー分割があります。
ただ、「分けたことが目的」になってしまい、
- 処理の流れが追えない
- どこに何を書くべきか迷う
- 修正が怖くなる
という状態に陥るケースも少なくありません。
この記事では、設定値をDataStoreで保存・取得するというシンプルな題材を使って、
Fragment → ViewModel → UseCase → Repository → DataStore
を一気通貫で実装してみます。
先に結論です。
- 各レイヤーは「責務」で分ける
- 依存の向きは必ず一方向
- データ取得元をViewModelに漏らさない
今回作るサンプルの前提
今回のサンプルでは、以下のような機能を想定します。
- ユーザー設定(Boolean)を保持する
- 画面表示時に現在の設定値を表示
- ボタン操作で設定値を更新
ネットワークやDBは使わず、DataStoreのみを永続化層とします。
全体構成と依存関係
まず全体像です。
Fragment
↓
ViewModel
↓
UseCase
↓
Repository
↓
DataStore
重要なのは、
- 上位レイヤーは下位レイヤーの実装詳細を知らない
- 逆方向の依存は絶対に作らない
という点です。
DataStore:永続化の責務
まず最下層の DataStore です。
Preferences DataStore 定義
val Context.settingsDataStore by preferencesDataStore(
name = "settings"
)
Key 定義
object SettingsKeys {
val SAMPLE_FLAG = booleanPreferencesKey("sample_flag")
}
DataStore 操作クラス
class SettingsDataStore(
private val context: Context
) {
val sampleFlagFlow: Flow =
context.settingsDataStore.data
.map { preferences ->
preferences[SettingsKeys.SAMPLE_FLAG] ?: false
}
suspend fun setSampleFlag(value: Boolean) {
context.settingsDataStore.edit { preferences ->
preferences[SettingsKeys.SAMPLE_FLAG] = value
}
}
}
ここでは「保存と取得」だけに責務を限定しています。
Repository:データ取得元の隠蔽
Repository の役割は、
「どこからデータを取っているかを隠す」ことです。
Repository インターフェース
interface SettingsRepository {
fun observeSampleFlag(): Flow
suspend fun updateSampleFlag(value: Boolean)
}
Repository 実装
class SettingsRepositoryImpl(
private val dataStore: SettingsDataStore
) : SettingsRepository {
override fun observeSampleFlag(): Flow {
return dataStore.sampleFlagFlow
}
override suspend fun updateSampleFlag(value: Boolean) {
dataStore.setSampleFlag(value)
}
}
ViewModel や UseCase は、DataStore の存在を一切知りません。
UseCase:アプリのルールを置く場所
UseCase は「アプリとしてどう振る舞うか」を表現する層です。
UseCase 定義
class ObserveSampleFlagUseCase(
private val repository: SettingsRepository
) {
operator fun invoke(): Flow {
return repository.observeSampleFlag()
}
}
class UpdateSampleFlagUseCase(
private val repository: SettingsRepository
) {
suspend operator fun invoke(value: Boolean) {
repository.updateSampleFlag(value)
}
}
ここにバリデーションや条件分岐が入ってきます。
ViewModel:状態を管理する
ViewModel は、
UIが必要とする状態だけを持ちます。
ViewModel 実装
class SampleViewModel(
observeSampleFlagUseCase: ObserveSampleFlagUseCase,
private val updateSampleFlagUseCase: UpdateSampleFlagUseCase
) : ViewModel() {
val sampleFlag: StateFlow =
observeSampleFlagUseCase()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = false
)
fun onToggleClicked() {
viewModelScope.launch {
updateSampleFlagUseCase(!sampleFlag.value)
}
}
}
DataStore や Repository を直接触らない点が重要です。
Fragment:表示とイベント通知のみ
Fragment は最も薄く保つのが正解です。
Fragment 実装
class SampleFragment : Fragment(R.layout.fragment_sample) {
private val viewModel: SampleViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val textView = view.findViewById(R.id.textView)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.sampleFlag.collect { value ->
textView.text = value.toString()
}
}
}
toggle.setOnClickListener {
viewModel.onToggleClicked()
}
}
}
Fragment は状態を保持しません。
この構成のメリット
- データ取得元を差し替えやすい
- UseCase単体でテストしやすい
- UI変更がロジックに影響しにくい
よくあるアンチパターン
- ViewModelからDataStoreを直接触る
- RepositoryにUIロジックを書く
- UseCaseが巨大化する
まとめ
- レイヤーは「責務」で分ける
- 依存は必ず下向き
- Fragmentは薄く、UseCaseにルールを集める
迷ったら:ViewModelに「保存先」を教えない。
それだけで設計はかなり安定します。


コメント