Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

課題と制約

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

Kyashでの課題

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

選択による課題と制約

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

テストコードのイメージ @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) /* 省略 */ } }

Slide 24

Slide 24 text

テストコードのイメージ @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) /* 省略 */ } }

Slide 25

Slide 25 text

テストコードのイメージ @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) /* 省略 */ } }

Slide 26

Slide 26 text

テストコードのイメージ @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) /* 省略 */ } }

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

PoC実装

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

Thanks!