[Android] Fragment〜DataStoreまでの設計サンプルを作ってみた

スポンサーリンク

導入:レイヤー分け、結局どう実装するのが正解か

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に「保存先」を教えない。
それだけで設計はかなり安定します。

コメント

タイトルとURLをコピーしました