Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Android Architecture & Unidirectional Data Flow...

Android Architecture & Unidirectional Data Flow Guidance & Essence

集まれKotlin好き!Kotlin愛好会 Vol.60 @ クラシル株式会社での登壇資料になります。

【概要】
Android開発におけるUnidirectional Data Flow(UDF)の実践的なガイダンスとエッセンスをまとめた資料です。従来のMVVMアーキテクチャの課題を見極め、ReduxやMVIといった単方向データフローを活用した状態管理の手法を解説します。

【主な内容】
MVVMの課題と限界:複雑な画面における双方向依存の問題点と対処法
Redux実装パターン:Store/Action/Reducer/Middlewareの詳細とKotlinでの実装例
MVI実装パターン:Intent/State/Reducer/SideEffectを用いた宣言的UI構築
実践例の紹介:DroidKaigi 2024/2025公式アプリのアーキテクチャ解説
アーキテクチャ選択の指針:プロジェクトの複雑度に応じた適切な選択方法

【キーポイント】
✅ 予測可能性・デバッグ性・テスタビリティ・保守性の向上
✅ 副作用の明示的な分離によるコード品質の改善
✅ Jetpack ComposeとReactive Programmingとの親和性
✅ 段階的なアーキテクチャ進化の戦略

【対象者】
Android開発における状態管理に課題を感じている方
MVVMからUDFへの移行を検討している方
Redux/MVIの実装パターンを学びたい方
大規模Androidアプリの設計に興味がある

Avatar for Fumiya Sakai

Fumiya Sakai

October 21, 2025
Tweet

More Decks by Fumiya Sakai

Other Decks in Technology

Transcript

  1. Android Architecture & Unidirectional Data Flow Guidance & Essence Fumiya

    Sakai 集まれKotlin好き!Kotlin愛好会 Vol.60 @ クラシル株式会社 2025/10/23
  2. 自己紹介 ・Fumiya Sakai ・Mobile Application Engineer アカウント: ・Twitter: https://twitter.com/fumiyasac ・Facebook:

    https://www.facebook.com/fumiya.sakai.37 ・Github: https://github.com/fumiyasac ・Qiita: https://qiita.com/fumiyasac@github 発表者: ・Born on September 21, 1984 これまでの歩み: Web Designer 2008 ~ 2010 Web Engineer 2012 ~ 2016 App Engineer 2017 ~ Now iOS / Android / sometimes Flutter
  3. 技術書同人誌博覧会スタッフ&デザイン協力もしてます (告知)第12回技術書同人誌博覧会が10/26に大宮ソニックシティにて開催! 🍀 #10 一般参加募集チラシ 🍀 #11 次回案内チラシ 🍀 #12

    サークル参加募集 🍀 #13 次回案内チラシ Conpassから一般参加者を募集していますのでぜひ参加登録をお願いします🙇 https://gishohaku.connpass.com/event/352601/
  4. 従来のMVVMにおいて課題になりやすい部分はどこか? 特に複雑な画面における双方向の依存関係を考えながら作る場合等 1. 状態の予測不可能性: 複数のコンポーネントが状態を変更でき、データの流れを追いづらい ViewとViewModelの相互依存により、バグの原因特定が困難 2. 双方向データバインディングの複雑さ: MVVMは成熟した優れたアーキテクチャである MVVMは悪いアーキテクチャでは決してない点

    状態変化のパターンが多すぎて、すべてのケースをカバーするのが困難 3. テストの難しさ: View ※前提条件※ Googleが公式に推奨し、豊富な実績とエコシステムを持つ Unidirectional Data Flowが力を発揮するか否かの見極め 現状でMVVMで問題なく開発が出来ているなら、無理に移行する必要はない ViewModel Model ⚠ ⚠ ⚠ ⚠
  5. 再度MVVMが優れている点を改めて整理してみる 公式に推奨されて豊富な実績やエコシステムの力が活きる点はどこにあるか 1. 学習コストが低い: 2. 素早い開発: 3. 柔軟性が高い: MVVMが最適と考えられるかもしれないケース 概念がシンプルで理解しやすい

    豊富な学習リソースと事例 Androidの公式ドキュメントが充実 チームメンバーの教育が容易 ボイラープレートが少ない プロトタイプを迅速に作成 シンプルな画面は直感的に実装 小規模チームでも採用しやすい 段階的にアーキテクチャを進化 させられる 既存コードとの統合が容易 厳密なルールに縛られない プロジェクトに応じてカスタマ イズ可能 📱 シンプルなアプリ 🚀 MVP・プロトタイプ 👥 小規模チーム 📊 CRUD中心 🔧 レガシーコードベース 画面数が少なく、状態管理が複雑でない場合 素早く市場検証したい初期フェーズ 学習コストを抑えたい少人数開発 データの表示・編集がメインの業務アプリ 既存のMVVMが十分機能している場合
  6. MVVMから単方向データフローによる状態管理へ 複雑な双方向の依存関係から明確な単方向データフローを意識した状態管理 1. Redux: Reactコミュニティで生まれた状態管理パターン 2. MVI(Model-View-Intent): 単一のStoreで全アプリ状態を管理 Action →

    Reducer → Stateの一方向フロー Android向けに最適化された単方向パターン Intent(ユーザー意図)から始まるフロー 不変なStateで予測可能性を担保 Intent / Action View State Reducer / Processor 単方向 サイクル ♻
  7. UDFのAndroid開発における魅力と課題解決のメカニズム Modern Android Developmentの中核概念ともなり得る可能性を秘めている 1. 開発体験の向上: コードの可読性が劇的に向上 2. 保守性の向上: チーム内での認識統一が容易

    オンボーディングが速くなる 状態管理ロジックが一箇所に集約 リファクタリングが安全に行える バグの混入リスクが低減 3. スケーラビリティ 機能追加時の影響範囲が明確 大規模アプリでも破綻しない設計 モジュール分割がしやすい https://developer.android.com/develop/ui/compose/architecture
  8. 例としてMVIの実装における要点をコードから見てみる Stateの更新はIntentを渡す事でしか実行できない様な形となっている sealed class LoginIntent { data class EmailChanged(val email:

    String) : LoginIntent() object SubmitClicked : LoginIntent() } data class LoginState( val email: String = "", val isLoading: Boolean = false, val error: String? = null ) class LoginViewModel : ViewModel() { private val _state = MutableStateFlow(LoginState()) val state: StateFlow<LoginState> = _state.asStateFlow() fun processIntent(intent: LoginIntent) { when (intent) { is LoginIntent.EmailChanged -> { _state.value = _state.value.copy(email = intent.email) } is LoginIntent.SubmitClicked -> { _state.value = _state.value.copy(isLoading = true) // Process login... } } } } Intent State ViewModel 💡 メリットを感じられる点 予測可能性の向上 状態変更は必ずReducerを経由 デバッグの容易さ 時系列でActionを追跡可能 テスタビリティ 純粋関数でテストが簡単
  9. Reduxにおける登場人物と各登場人物が担う処理 画面やComposableに対応するStateを受け取って構築される点がポイント 1. Store: アプリケーション全体の状態(複数の画面表示用State)を一枚岩の様な形で保持する。 2. Action: Storeが保持している状態(対象の画面表示用State)を更新するための唯一の手段でsealed classで定義する。 (ポイント)Actionの発行はStoreが提供しているdispatchを実行する形となります。

    3. Reducer: 現在の状態(対象の画面表示用State)とActionの内容から新しい状態を作成する部分で純粋関数として定義する。 4. Middleware: Reducerの実行前後で処理を差し込むための部分で純粋関数として定義する。 (ポイント)画面表示に必要なMiddleware内部で、API非同期通信処理や内部データ登録処理等を実施する。
  10. AndroidにおけるReduxアーキテクチャの詳細(1) Reduxの思想をAndroidアプリに取り入れた宣言的なUIとも非常に相性が良い形 // 典型的なRedux実装の流れ UI → dispatch(Action) → Middleware(副作用処理) →

    Reducer(新しいStateを生成) → Store(Stateを更新) → UI(再レンダリング) data class AppState( val todos: List<Todo> = emptyList(), val isLoading: Boolean = false, val error: String? = null ) sealed interface Action { data class AddTodo(val title: String) : Action data object LoadTodos : Action } fun reduce(state: AppState, action: Action): AppState { return when (action) { is Action.AddTodo -> state.copy(todos = state.todos + newTodo) } } ① 不変なdata classで状態を定義 ② すべてのUI状態を1つのクラスに集約 ③ デフォルト値を設定し、初期化を簡潔に ① sealed interfaceにより、すべてのActionが型安全にコンパイル時にチェック可能 ② すべてのUI状態を1つのクラスに集約 ③ デフォルト値を設定し、初期化を簡潔に ① 元の状態を変更せず、新しい状態をcopyで生成 ② 副作用なし = テストが容易 ③ 同じ入力には常に同じ出力 = 予測可能
  11. AndroidにおけるReduxアーキテクチャの詳細(2) ComposeとReactiveな連携をする形にして状態変更はActionの発行のみで実施 class TodoViewModel( private val store: Store ) :

    ViewModel() { val state: StateFlow<AppState> = store.state fun dispatch(action: Action) { store.dispatch(action) } override fun onCleared() { super.onCleared() store.dispose() } } private val _state = MutableStateFlow(initialState) val state: StateFlow<AppState> = _state.asStateFlow() private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main) fun dispatch(action: Action) { scope.launch { middlewares.forEach { middleware -> middleware.process(action, _state.value, ::dispatch) } val currentState = _state.value val newState = reducer(currentState, action) _state.value = newState } } fun dispose() { scope.cancel() } Store内処理の抜粋 @Composable fun TodoScreen(viewModel: TodoViewModel) { val state by viewModel.state.collectAsState() // TODOを追加するActionを発行する viewModel.dispatch(Action.AddTodo(title)) } ✅ 非同期処理等をReducerから分離する StoreとStateFlowの統合 ✅ Actionに応じたReducer処理を実行する ※ComposeとReactiveな連携 ✅ ViewModel経由でdispatchを発行
  12. AndroidにおけるReduxアーキテクチャの詳細(3) 非同期処理やデータ永続化等の処理はMiddlewareに分離して管理する class AsyncMiddleware( private val todoRepository: TodoRepository ) :

    Middleware { override suspend fun process( action: Action, currentState: AppState, dispatch: (Action) -> Unit ) { when (action) { is Action.LoadTodos -> { try { val todos = todoRepository.fetchTodos() dispatch(Action.LoadTodosSuccess(todos)) } catch (e: Exception) { dispatch(Action.LoadTodosFailure(e.message ?: "Unknown error")) } } else -> { /* 他のアクションは処理しない */ } } } } ① Coroutineを活用した自然な非同期処理 ② 新しいActionをdispatchして結果を反映 非同期処理を実行するMiddleware例 interface TodoRepository { suspend fun fetchTodos(): List<Todo> } class FakeTodoRepositoryImpl : TodoRepository { override suspend fun fetchTodos(): List<Todo> { delay(1000) return listOf( Todo("1", "Reduxアーキテクチャを学ぶ", completed = true), Todo("2", "Jetpack Composeでアプリを作る", completed = false) ) } }
  13. 画面やComposableに対応するStateを受け取って構築される点がポイント 1. 単一不変State(Single Immutable State): アプリケーションの状態を一つの不変なStateオブジェクトで管理する。 状態が一度設定されると変更されず、新しい状態を作成することで更新する。 2. Intent: すべてのユーザーインタラクションをIntentとしてデータストリームで表現する。通常はsealed

    classで定義する。 3. Reducer(純粋関数): 以前の状態とIntentを受け取り、新しい状態を返す純粋関数。 ネットワークやデータベース操作を行わず、既存の状態を直接変更しない。 4. Side Effect : ナビゲーション、ネットワーク呼び出し、メッセージ表示などの副作用を状態更新から分離。 MVIにおける登場人物と各登場人物が担う処理
  14. MVI的なアプローチはMVVM的なアプローチと何が違うか 一見すると似ている様に見えてもコード内の処理における思想は大きく異なる // 複数のStateを個別管理 val todos = MutableStateFlow<List<TodoItem>>(emptyList()) val isLoading

    = MutableStateFlow(false) val filter = MutableStateFlow(TodoFilter.ALL) // 状態を直接変更 fun addTodo(title: String) { todos.value = todos.value + TodoItem(title) } // 単一State val state = MutableStateFlow(TodoViewState()) // Intentを送信してReducerで処理 fun sendIntent(intent: TodoIntent) { val (newState, effect) = reducer.reduce(state.value, intent) state.value = newState } MVVM的なアプローチ例 MVI的なアプローチ例 ① Stateは常に不変にする ② Reducerを純粋関数に保つ ③ 副作用は明示的に分離する ④ IntentはすべてのUIアクションをカバーする ✅ DO: ❌ DO NOT: ① Reducer内でAPI呼び出しをしない ② Stateを直接変更しない ③ 複数のStateオブジェクトを作らない ④ 副作用をStateに含めない 💡 MVIの利点: 予測可能性 / テスタビリティ / デバッグ性 / スケーラビリティ / 保守性
  15. AndroidにおけるMVIアーキテクチャの詳細(1) 画面やComposableに対応する単一不変のStateとユーザーの意図を示すIntent 単一不変State Intentパターン // Intentの送信 viewModel.sendIntent(TodoIntent.AddTodo(newTodoText)) sealed class TodoIntent

    { data class AddTodo(val title: String) : TodoIntent() data class ToggleTodo(val id: String) : TodoIntent() data class DeleteTodo(val id: String) : TodoIntent() data class ChangeFilter(val filter: TodoFilter) : TodoIntent() object LoadTodos : TodoIntent() } data class TodoViewState( val todos: List<TodoItem> = emptyList(), val isLoading: Boolean = false, val filter: TodoFilter = TodoFilter.ALL ) ① すべてのUI状態を1つのdata classで管理 ② data classなので不変(immutable)である ③ 状態の全体像が一目でわかる ④ タイムトラベルデバッグが可能 💡 役割: アプリケーション全体で状態を表す1つのObjectで管理 👀 特徴: 不変(immutable)で予測可能 ① sealed classですべてのアクションを型安全に表現 ② when式で漏れなく処理できる ③ ユーザーの意図(Intent)を明示的にモデル化 ④ ログやアナリティクスに最適 💡 役割: 全てのUserActionをデータとして表現 👀 特徴: sealed classで型安全に定義。ログ・分析に最適 状態変更
  16. AndroidにおけるMVIアーキテクチャの詳細(2) Reducer(純粋関数)の処理が実質上のビジネスロジックとして振る舞う Reducer (純粋関数) fun reduce( previousState: TodoViewState, intent: TodoIntent

    ): Pair<TodoViewState, TodoSideEffect?> { return when (intent) { is TodoIntent.AddTodo -> { val newTodo = TodoItem(id = generateId(), title = intent.title) val newState = previousState.copy(todos = previousState.todos + newTodo) newState to TodoSideEffect.ShowMessage("TODOを追加しました") } // ... } } @Test fun `AddTodo intent should add new todo to state`() { val reducer = TodoReducer() val initialState = TodoViewState(todos = emptyList()) val intent = TodoIntent.AddTodo("新しいタスク") val (newState, _) = reducer.reduce(initialState, intent) assertEquals(1, newState.todos.size) assertEquals("新しいタスク", newState.todos[0].title) } ① 純粋関数: 同じ入力には常に同じ出力 ② 既存の状態を変更せず、copy()で新しい状態を作成 ③ 副作用(Side Effect)を明示的に返す ④ ユニットテストが極めて容易 💡 役割: 前の状態 + Intent → 新しい状態への変換 👀 特徴: 副作用なし / 同じ入力には常に同じ出力 ✅ 既存ToDoの末尾に新たなToDoを追加 ✅ 新たなStateと共にSideEffectを一緒に返却する ※この例におけるSideEffectはSnackbar等に1度限りのMessageを表示する想定
  17. AndroidにおけるMVIアーキテクチャの詳細(3) ViewModelを準備しているがあくまで調整役に徹する受け身な形となっている ViewModel内の処理 private val reducer = TodoReducer() // State:

    単一の不変なStateをFlowで公開 private val _state = MutableStateFlow(TodoViewState(isLoading = true)) val state: StateFlow<TodoViewState> = _state.asStateFlow() // Side Effect: 一度だけ処理される副作用をChannelで管理 private val _sideEffect = Channel<TodoSideEffect>(Channel.BUFFERED) val sideEffect: Flow<TodoSideEffect> = _sideEffect.receiveAsFlow() /** * IntentをReducerに送信し、新しいStateと副作用を処理する * これがMVIの中心的なメカニズム */ fun sendIntent(intent: TodoIntent) { viewModelScope.launch { val currentState = _state.value val (newState, sideEffect) = reducer.reduce(currentState, intent) // 新しい状態を反映 _state.value = newState // 副作用があれば送信 sideEffect?.let { effect -> _sideEffect.trySend(effect).onFailure { println("副作用の送信に失敗: ${it?.message}") } } } } ① ViewModelはReducerへの橋渡し ② 現在のStateとIntentをReducerに渡す ③ Reducerから返された新たなStateと副作用を処理 ④ ビジネスロジックはReducerに委譲 ✨ ViewModelの役割
  18. MVIとMVVMやReduxとの違いやメリットを改めて整理 MVI自体はMVCとFluxの影響を受けたReactive Programmingの原則に基づいた設計 1. MVVMとの相違点: 2. Reduxとの相違点: MVIアーキテクチャが持つメリット5選 複数のLiveData/StateFlow 双方向バインディング可能

    🔮 予測可能性 🐛 デバッグ性 🔧 保守性 🧪 テスタビリティ 📈 スケーラビリティ 状態遷移が明確で、デバッグが容易。何が起きているか常に把握可能。 単方向フローにより、状態変化を追跡しやすい。ログで完全再現可能。 関心の分離が徹底。各層の責務が明確で、変更の影響範囲が限定的。 Reducerが純粋関数なので、ユニットテストが簡単。モックも不要。 複雑な状態管理にも対応。大規模アプリでも破綻しない設計。 単一State 厳密な単方向 状態を直接変更 Reducerで新規作成 副作用が混在 明示的に分離 グローバルStoreなし Dispatcher不要 ViewModelスコープ シンプルな構造 より軽量 Android向けに最適化 Kotlin Coroutines活用 非同期処理が簡単
  19. DriodKaigi2024公式アプリのArchitectureを紐解く ライブラリ「Rin」を利用してUnidirectional Data Flowを実現している Data Flow Diagram (Bookmark処理) ① Event

    TimetableScreen(uiState, onBookmarkClick) ✨ UI Layer UserDataStore / SessionCacheDataStore ✨ Data Layer timetableScreenPresenter() ✨ Presenter Layer - EventEffectでイベント処理 - Repositoryから状態を購読 - UiStateを構築して返す SessionRepository ✨ Repository Layer - Composable関数でデータを購読 - safeCollectAsRetainedStateで状態保持 ⑤ UI State ② toggleBookmark() ④ Timetable toggleFavorite() ③ Flow<Data> ・eventFlowでUserEventを管理 ・timetableScreenPresenterがUiStateを生成 ※UI層は状態を受け取り表示するのみ 💡 Dataの流れ: 1. UserがBookmarkアイコンをタップ 2. onBookmarkClick Callbackが発火 3. eventFlow.tryEmiy()でEvent発行 4. EventEffectでEventをキャッチ 5. Repositoryのメソッドを実行 safeCollectAsRetainedState() 🌱 状態の購読: RepositoryではDataStoreからのFlow を購読しBookmark情報と組み合わせて Timetableを生成 🏃 UI〜Presenter Layerのポイント 🌀 ライブラリ「Rin」の重要な機能: Flowを安全に収集してリコンポジションやナビ ゲーションをまたいで状態を保持する
  20. DriodKaigi2025公式アプリのArchitectureを紐解く UDFの流れは同じだがRepository・ViewModelがない比較的シンプルな構成 (TimetableScreen) ✨ UI Layer EventFlow ✨ Presenter Layer

    MutationKey (受信) uiStateを表示&イベントを発行 EventEffect (処理) (データ更新) UiState (情報を返す) ✨ SoilDataBoundary QueryKey (データ取得) SubscriptionKey (データ監視) Loading / Error処理 or データ取得完了 Data Flow Diagram (Bookmark処理) ✨ ScreenContext 依存性の提供 QueryKey MutationKey 等 … ① UserがBookmarkボタンをタップ ② UILayer: onBookMarkClick(sessionId) ③ eventFlow.tryEmit(TimetableScreenEvent.Bookmark(sessionId)) ④ Presenter: EventEffectでイベント受信 ⑤ favoriteTimetableItemIdMutation.mutate(itemId) ⑥ DataStore / API: データ更新 ⑦ SubsctiptionKey: 変更を自動検知 ⑧ SoilBoundary: 新しいデータを受信 ⑨ Presenter: 新しいUiStateを構築 ⑩ UILayer: 再構築
  21. まとめ Android開発におけるUnidirectional Data Flowのまとめ UDFの有効活用によって複雑な状態管理を実現可能。プロジェクトの特性や開発チームの状況に応じた適切な選択が大事。 1. 従来MVVMの課題を見極める: 複雑な画面における双方向の依存関係では、状態の予測不可能性やテストの難しさが課題に。最小限だけ取得し、State・ ViewModelに閉じ込め、描画やレイアウト調整は別View/Modifierに委譲する発想が重要。 2.

    Redux/MVIで単方向データフローを主役に状態を宣言的に扱う: Intent/Action → Reducer → State の一方向フローで位置管理。予測可能性・デバッグ性・テスタビリティを大幅に向上。 Middleware/SideEffectで副作用を明示的に分離し、命令的制御を抑えて統合を避ける。 3. プロジェクトの複雑度で適切なアーキテクチャを「シンプル→MVVM→UDF」に整理: 小規模・CRUD中心ならMVVMで十分。複雑な状態管理が必要な場合はRedux/MVIを採用。学習コスト・保守性・スケーラビリティの バランスを考慮し、段階的に進化させる戦略が有効。