$30 off During Our Annual Pro Sale. View Details »

Mobileアプリのアーキテクチャ設計法

 Mobileアプリのアーキテクチャ設計法

2022/04/21 (木) に開催されたKyash Tech Talk #3の登壇資料です。

Keita Kagurazaka

April 21, 2022
Tweet

More Decks by Keita Kagurazaka

Other Decks in Programming

Transcript

  1. Mobileアプリの
    アーキテクチャ設計法
    〜KMMを例に〜
    Keita Kagurazaka

    View Slide

  2. 課題と制約

    View Slide

  3. 課題と制約
    ● 現状と理想のギャップが課題
    ● 選択肢を制限するものが制約
    ○ 技術的なもの
    ○ チーム由来のもの
    ○ 歴史的経緯によるもの
    ● 制約を守りつつ、課題を解決しなければならない
    ● アーキテクチャは課題の解決方法の1つ

    View Slide

  4. Kyashでの課題

    View Slide

  5. Kyashでの課題
    ● [課題1] 両OSで同じロジックを2度実装することによる問題
    ○ リソースの無駄遣い
    ○ 同メンバーが対応することによる虚無感 (特にテスト)
    ● [課題2] OSでロジックが異なったことによるインシデント
    ○ 画面遷移可能かの判断で非常に複雑な場所があった
    ○ その判定ロジックがOS間で異なり、片方のOSでだけインシデントが発

    View Slide

  6. 制約条件
    ● [制約1] 各OSごとにUIを最適化し、UXを最大化する方針
    ● [制約2] フルリニューアルを実施することができない
    ○ 事業の状態を考えたときのリソースに起因
    ○ すなわち、部分的な導入がしやすく、それによって既存部分に与える
    影響を最小化したい

    View Slide

  7. 制約条件
    ● [制約1] 各OSごとにUIを最適化し、UXを最大化する方針
    ● [制約2] フルリニューアルを実施することができない
    ○ 事業の状態を考えたときのリソースに起因
    ○ すなわち、部分的な導入がしやすく、それによって既存部分に与える
    影響を最小化したい
    [選択1] Kotlin Multiplatform Mobile

    View Slide

  8. 共通化するレイヤー
    ● KMMは部分導入の自由度が高い
    ○ APIクライアントだけ
    ○ UseCaseまで
    ○ etc.

    View Slide

  9. 共通化するレイヤー
    ● KMMは部分導入の自由度が高い
    ○ APIクライアントだけ
    ○ UseCaseまで
    ○ etc.
    ● [課題1] 両OSで同じロジックを2度実装することによる問題 の解決のため
    には
    [選択2] UI以外は原則すべて共通化する

    View Slide

  10. 選択による課題と制約

    View Slide

  11. KMM採用の課題と制約
    ● [制約3] iOSメンバーはKotlinの経験がなく、これから学ぶ必要あり
    ● [制約4] 過去のPRにしかないストック情報がある
    ○ [選択3] KMMのリポジトリを分ける
    ○ アプリ側のリポジトリとのインテグレーションが必要
    ○ [課題3] インテグレーションのオーバーヘッド
    ● [課題4] Kotlin Nativeにおけるmulti-thread問題
    ○ 以下native-mt問題

    View Slide

  12. native-mt問題
    ● Kotlin Nativeでは原則としてオブジェクトがスレッドを跨げない
    ● 例外はfreezeしたfrozen object
    ○ frozen objectを変更するとランタイムでクラッシュ
    ● 基本的な対応策
    ○ スレッドを跨ぐオブジェクトはImmutableにする
    ○ mutable stateを持つクラスがfreezeされないようにする

    View Slide

  13. native-mt問題
    ● [制約3] iOSメンバーはKotlinの経験がなく、これから学ぶ必要あり
    ○ 注意して開発しよう、は選択できない
    ● 課題を制約に変換して解消
    ○ [制約5] frozenをアーキテクチャ基盤に隠蔽し、ロジックやUIの実装時
    に意識させない

    View Slide

  14. アーキテクチャの選定
    ● [選択4] MVIベースのアーキテクチャを採用する
    ● ImmutableなIntentをUI側から入力し、ImmutableなUI state / eventを
    出力してもらう
    ● 各レイヤーで受け渡されるデータがすべてImmutableであれば、基盤側
    でfreezeしちゃえばnative-mt問題は発生しない

    View Slide

  15. UI層の技術選定
    ● [選択5] UI toolkitには宣言的UIを採用
    ● [制約1] 各OSごとにUIを最適化し、UXを最大化する方針
    ○ OS標準のUI toolkitを選択したい
    ● [選択4] MVIベースのアーキテクチャを採用する
    ○ ImmutableなUI stateが差分ではなく全体として更新通知される

    View Slide

  16. アーキテクチャ概観
    UI
    StateHolder
    UseCase
    Repository
    ApiClient
    state Intent
    event

    View Slide

  17. アーキテクチャ概観
    UI
    StateHolder
    UseCase
    Repository
    ApiClient
    state Intent
    event
    Intent → UI state or event

    View Slide

  18. アーキテクチャ概観
    UI
    StateHolder
    UseCase
    Repository
    ApiClient
    state Intent
    event

    View Slide

  19. アーキテクチャ概観
    UI
    StateHolder
    UseCase
    Repository
    ApiClient
    state Intent
    event
    [課題5] IntentがAndroidの既
    存概念とコンフリクトする

    View Slide

  20. アーキテクチャ概観
    UI
    StateHolder
    UseCase
    Repository
    ApiClient
    state Action
    event
    [選択6] UI層からの入力は
    Actionと命名

    View Slide

  21. 自動テスト戦略
    ● [課題2] OSでロジックが異なったことによるインシデント

    View Slide

  22. 自動テスト戦略
    ● [課題2] OSでロジックが異なったことによるインシデント
    ● [選択7] 自動結合テスト
    ○ HttpClientにおけるレスポンスのみをmockする
    ■ KMMではKtor clientのMockEngineを活用
    ○ 全Actionに対し、期待となるUI state / eventを検証
    ○ expect / actualを活用し、JVM / iOS両方の環境でのテストをワン
    ソースで実施

    View Slide

  23. テストコードのイメージ
    @Test
    fun executeAction_loadInitially_success() = reactorSuspendTest(
    reactorFactory = ::CouponListReactorFactory,
    responses = {
    every { "/v1/coupons/list?limit=10" } returns SUCCESS_RESPONSE_1_3
    }
    ) { reactor ->
    reactor.test {
    reactor.execute(Action.LoadInitially)
    assertTrue(awaitState() is LoadState.Loading)
    /* 省略 */
    }
    }

    View Slide

  24. テストコードのイメージ
    @Test
    fun executeAction_loadInitially_success() = reactorSuspendTest(
    reactorFactory = ::CouponListReactorFactory,
    responses = {
    every { "/v1/coupons/list?limit=10" } returns SUCCESS_RESPONSE_1_3
    }
    ) { reactor ->
    reactor.test {
    reactor.execute(Action.LoadInitially)
    assertTrue(awaitState() is LoadState.Loading)
    /* 省略 */
    }
    }

    View Slide

  25. テストコードのイメージ
    @Test
    fun executeAction_loadInitially_success() = reactorSuspendTest(
    reactorFactory = ::CouponListReactorFactory,
    responses = {
    every { "/v1/coupons/list?limit=10" } returns SUCCESS_RESPONSE_1_3
    }
    ) { reactor ->
    reactor.test {
    reactor.execute(Action.LoadInitially)
    assertTrue(awaitState() is LoadState.Loading)
    /* 省略 */
    }
    }

    View Slide

  26. テストコードのイメージ
    @Test
    fun executeAction_loadInitially_success() = reactorSuspendTest(
    reactorFactory = ::CouponListReactorFactory,
    responses = {
    every { "/v1/coupons/list?limit=10" } returns SUCCESS_RESPONSE_1_3
    }
    ) { reactor ->
    reactor.test {
    reactor.execute(Action.LoadInitially)
    assertTrue(awaitState() is LoadState.Loading)
    /* 省略 */
    }
    }

    View Slide

  27. CIによるインテグレーション省力化
    ● [課題3] インテグレーションのオーバーヘッド
    ● [選択8] 徹底的なインテグレーションの自動化
    ○ maven repository / XCFrameworkを置く成果物リポジトリ
    ○ KMM側のPRに対応して成果物リポジトリにbranchが生える
    ○ SNAPSHOT運用されるので、そのbranchをrefする
    ○ KMM側のPRが閉じたら成果物リポジトリのbranchも消える
    ○ その他tagを切って過去バージョンをアーカイブしたり、QA期間用のフ
    ローも自動化

    View Slide

  28. PoC実装

    View Slide

  29. Cache strategy
    ● [課題6] Cache DBの変更通知をUI stateに反映するフローを自然に表現
    できない
    Model
    User Action state
    fun update(
    prev: State
    a: Action
    ): State
    Server

    View Slide

  30. Cache strategy
    ● [課題6] Cache DBの変更通知をUI stateに反映するフローを自然に表現
    できない
    Server DB
    ?
    Model
    User Action state
    fun update(
    prev: State
    a: Action
    ): State

    View Slide

  31. Cache strategy
    ● [課題6] Cache DBの変更通知をUI stateに反映するフローを自然に表現
    できない
    Server DB
    Model
    User Action state
    fun update(
    prev: State
    a: Action
    ): State

    View Slide

  32. Cache strategy
    ● [課題6] Cache DBの変更通知をUI stateに反映するフローを自然に表現
    できない
    Server DB
    Model
    ?
    state
    fun update(
    prev: State
    a: Action
    ): State
    User Action

    View Slide

  33. 調査
    ● KMM + MVIで調査したところ、Wantedlyさんの記事を発見
    ● https://www.wantedly.com/companies/wantedly/post_articles/300999
    ○ 2020年のKotlin Advent Calendar最終日の記事

    View Slide

  34. ReactorKitから借用する
    ● [選択9] Action -> UI stateの変換を2 stepsに分ける
    Model
    User Action state
    fun mutate(
    a: Action
    ): Mutation
    fun reduce(
    prev: State,
    m: Mutation
    ): State
    Mutation
    Server DB

    View Slide

  35. 結果
    ● スタートポイントの課題は解決
    ○ これまでより品質が担保できる状態
    ● iOSメンバーの感想
    ○ Kotlinの書き方は学ぶ必要があったが、アーキテクチャやiOSだから
    という理由で困ったことはなかった
    ● 単純にコード量が減ったというだけでなく、書ける人・レビューできる人がこ
    れまでより増えた面も

    View Slide

  36. Why not
    ● 既存のアーキテクチャフレームワークを使わず、自前で基盤を実装
    ● 解くべき課題と制約は事業・チームが異なれば変わる
    ● 何なら時間でも変化する

    View Slide

  37. native-mt問題の完全解決
    ● Kotlin NativeにおけるNew Memory Manager
    ○ インスタンスがスレッドを跨げないという制約がなくなる
    ○ まだexperimentalでnot production ready
    ● これからKMMにtryしたい人は、これがreadyになるのを待ちましょう
    ● 非常に大きな制約が消えるので、必然的にアーキテクチャ設計に影響する
    でしょう

    View Slide

  38. まとめ
    ● 世のアーキテクチャがシンプル or オーバーエンジニアリングに見えるの
    は、解くべき課題や制約がそれぞれで異なるから
    ● 課題を解決するアイデアとしてのアーキテクチャの知識は必要
    ● 事業やチームの課題と制約を明確に意識して設計しましょう

    View Slide

  39. アーキテクチャは
    選定するのではなく
    設計せよ

    View Slide

  40. Thanks!

    View Slide