Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Mobileアプリのアーキテクチャ設計法
Search
Keita Kagurazaka
April 21, 2022
Programming
2
1.4k
Mobileアプリのアーキテクチャ設計法
2022/04/21 (木) に開催されたKyash Tech Talk #3の登壇資料です。
Keita Kagurazaka
April 21, 2022
Tweet
Share
More Decks by Keita Kagurazaka
See All by Keita Kagurazaka
SELECT FOR UPDATEの話
kkagurazaka
0
370
原理から完全理解するDagger Hilt Migration
kkagurazaka
1
1.8k
今後のJetpackでAndroid開発はこう変わる!
kkagurazaka
16
6.1k
外部SDKのViewにマスク処理をする方法と罠
kkagurazaka
0
920
AWAのフルリニューアルを支えたアーキテクチャ
kkagurazaka
1
850
CQRS Architecture on Android
kkagurazaka
7
2.9k
suspending functionの裏側
kkagurazaka
3
430
coroutinesで非同期ページネーション
kkagurazaka
1
630
async/awaitで快適非同期ライフ
kkagurazaka
4
1.8k
Other Decks in Programming
See All in Programming
「影響が少ない」を自分の目でみてみる
o0h
PRO
2
1.2k
実践Webフロントパフォーマンスチューニング
cp20
14
3.4k
「”誤った使い方をすることが困難”な設計」で良いコードの基礎を固めよう / phpcon-odawara-2025
taniguhey
0
170
Youtube Lofier - Chrome拡張開発
ninikoko
0
2.5k
RuboCop: Modularity and AST Insights
koic
2
1.6k
Vibe Coding の話をしよう
schroneko
12
2.9k
Compose Hot Reload is here, stop re-launching your apps! (Android Makers 2025)
zsmb
1
550
[NG India] Event-Based State Management with NgRx SignalStore
markostanimirovic
1
170
Road to RubyKaigi: Making Tinny Chiptunes with Ruby
makicamel
4
450
REALITY コマンド作成チュートリアル
nishiuriraku
0
110
Optimizing JRuby 10
headius
0
430
新しいPHP拡張モジュールインストール方法「PHP Installer for Extensions (PIE)」を使ってみよう!
cocoeyes02
0
410
Featured
See All Featured
Building a Modern Day E-commerce SEO Strategy
aleyda
40
7.2k
The Straight Up "How To Draw Better" Workshop
denniskardys
232
140k
Building a Scalable Design System with Sketch
lauravandoore
462
33k
[RailsConf 2023] Rails as a piece of cake
palkan
54
5.4k
The Power of CSS Pseudo Elements
geoffreycrofte
75
5.8k
Adopting Sorbet at Scale
ufuk
76
9.3k
Why Our Code Smells
bkeepers
PRO
336
57k
Optimising Largest Contentful Paint
csswizardry
36
3.2k
Learning to Love Humans: Emotional Interface Design
aarron
273
40k
Designing Dashboards & Data Visualisations in Web Apps
destraynor
231
53k
Save Time (by Creating Custom Rails Generators)
garrettdimon
PRO
31
1.1k
Designing Experiences People Love
moore
141
24k
Transcript
Mobileアプリの アーキテクチャ設計法 〜KMMを例に〜 Keita Kagurazaka
課題と制約
課題と制約 • 現状と理想のギャップが課題 • 選択肢を制限するものが制約 ◦ 技術的なもの ◦ チーム由来のもの ◦
歴史的経緯によるもの • 制約を守りつつ、課題を解決しなければならない • アーキテクチャは課題の解決方法の1つ
Kyashでの課題
Kyashでの課題 • [課題1] 両OSで同じロジックを2度実装することによる問題 ◦ リソースの無駄遣い ◦ 同メンバーが対応することによる虚無感 (特にテスト) •
[課題2] OSでロジックが異なったことによるインシデント ◦ 画面遷移可能かの判断で非常に複雑な場所があった ◦ その判定ロジックがOS間で異なり、片方のOSでだけインシデントが発 生
制約条件 • [制約1] 各OSごとにUIを最適化し、UXを最大化する方針 • [制約2] フルリニューアルを実施することができない ◦ 事業の状態を考えたときのリソースに起因 ◦
すなわち、部分的な導入がしやすく、それによって既存部分に与える 影響を最小化したい
制約条件 • [制約1] 各OSごとにUIを最適化し、UXを最大化する方針 • [制約2] フルリニューアルを実施することができない ◦ 事業の状態を考えたときのリソースに起因 ◦
すなわち、部分的な導入がしやすく、それによって既存部分に与える 影響を最小化したい [選択1] Kotlin Multiplatform Mobile
共通化するレイヤー • KMMは部分導入の自由度が高い ◦ APIクライアントだけ ◦ UseCaseまで ◦ etc.
共通化するレイヤー • KMMは部分導入の自由度が高い ◦ APIクライアントだけ ◦ UseCaseまで ◦ etc. •
[課題1] 両OSで同じロジックを2度実装することによる問題 の解決のため には [選択2] UI以外は原則すべて共通化する
選択による課題と制約
KMM採用の課題と制約 • [制約3] iOSメンバーはKotlinの経験がなく、これから学ぶ必要あり • [制約4] 過去のPRにしかないストック情報がある ◦ [選択3] KMMのリポジトリを分ける
◦ アプリ側のリポジトリとのインテグレーションが必要 ◦ [課題3] インテグレーションのオーバーヘッド • [課題4] Kotlin Nativeにおけるmulti-thread問題 ◦ 以下native-mt問題
native-mt問題 • Kotlin Nativeでは原則としてオブジェクトがスレッドを跨げない • 例外はfreezeしたfrozen object ◦ frozen objectを変更するとランタイムでクラッシュ
• 基本的な対応策 ◦ スレッドを跨ぐオブジェクトはImmutableにする ◦ mutable stateを持つクラスがfreezeされないようにする
native-mt問題 • [制約3] iOSメンバーはKotlinの経験がなく、これから学ぶ必要あり ◦ 注意して開発しよう、は選択できない • 課題を制約に変換して解消 ◦ [制約5]
frozenをアーキテクチャ基盤に隠蔽し、ロジックやUIの実装時 に意識させない
アーキテクチャの選定 • [選択4] MVIベースのアーキテクチャを採用する • ImmutableなIntentをUI側から入力し、ImmutableなUI state / eventを 出力してもらう
• 各レイヤーで受け渡されるデータがすべてImmutableであれば、基盤側 でfreezeしちゃえばnative-mt問題は発生しない
UI層の技術選定 • [選択5] UI toolkitには宣言的UIを採用 • [制約1] 各OSごとにUIを最適化し、UXを最大化する方針 ◦ OS標準のUI
toolkitを選択したい • [選択4] MVIベースのアーキテクチャを採用する ◦ ImmutableなUI stateが差分ではなく全体として更新通知される
アーキテクチャ概観 UI StateHolder UseCase Repository ApiClient state Intent event
アーキテクチャ概観 UI StateHolder UseCase Repository ApiClient state Intent event Intent
→ UI state or event
アーキテクチャ概観 UI StateHolder UseCase Repository ApiClient state Intent event
アーキテクチャ概観 UI StateHolder UseCase Repository ApiClient state Intent event [課題5]
IntentがAndroidの既 存概念とコンフリクトする
アーキテクチャ概観 UI StateHolder UseCase Repository ApiClient state Action event [選択6]
UI層からの入力は Actionと命名
自動テスト戦略 • [課題2] OSでロジックが異なったことによるインシデント
自動テスト戦略 • [課題2] OSでロジックが異なったことによるインシデント • [選択7] 自動結合テスト ◦ HttpClientにおけるレスポンスのみをmockする ▪
KMMではKtor clientのMockEngineを活用 ◦ 全Actionに対し、期待となるUI state / eventを検証 ◦ expect / actualを活用し、JVM / iOS両方の環境でのテストをワン ソースで実施
テストコードのイメージ @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) /* 省略 */ } }
テストコードのイメージ @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) /* 省略 */ } }
テストコードのイメージ @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) /* 省略 */ } }
テストコードのイメージ @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) /* 省略 */ } }
CIによるインテグレーション省力化 • [課題3] インテグレーションのオーバーヘッド • [選択8] 徹底的なインテグレーションの自動化 ◦ maven repository
/ XCFrameworkを置く成果物リポジトリ ◦ KMM側のPRに対応して成果物リポジトリにbranchが生える ◦ SNAPSHOT運用されるので、そのbranchをrefする ◦ KMM側のPRが閉じたら成果物リポジトリのbranchも消える ◦ その他tagを切って過去バージョンをアーカイブしたり、QA期間用のフ ローも自動化
PoC実装
Cache strategy • [課題6] Cache DBの変更通知をUI stateに反映するフローを自然に表現 できない Model User
Action state fun update( prev: State a: Action ): State Server
Cache strategy • [課題6] Cache DBの変更通知をUI stateに反映するフローを自然に表現 できない Server DB
? Model User Action state fun update( prev: State a: Action ): State
Cache strategy • [課題6] Cache DBの変更通知をUI stateに反映するフローを自然に表現 できない Server DB
Model User Action state fun update( prev: State a: Action ): State
Cache strategy • [課題6] Cache DBの変更通知をUI stateに反映するフローを自然に表現 できない Server DB
Model ? state fun update( prev: State a: Action ): State User Action
調査 • KMM + MVIで調査したところ、Wantedlyさんの記事を発見 • https://www.wantedly.com/companies/wantedly/post_articles/300999 ◦ 2020年のKotlin Advent
Calendar最終日の記事
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
結果 • スタートポイントの課題は解決 ◦ これまでより品質が担保できる状態 • iOSメンバーの感想 ◦ Kotlinの書き方は学ぶ必要があったが、アーキテクチャやiOSだから という理由で困ったことはなかった
• 単純にコード量が減ったというだけでなく、書ける人・レビューできる人がこ れまでより増えた面も
Why not • 既存のアーキテクチャフレームワークを使わず、自前で基盤を実装 • 解くべき課題と制約は事業・チームが異なれば変わる • 何なら時間でも変化する
native-mt問題の完全解決 • Kotlin NativeにおけるNew Memory Manager ◦ インスタンスがスレッドを跨げないという制約がなくなる ◦ まだexperimentalでnot
production ready • これからKMMにtryしたい人は、これがreadyになるのを待ちましょう • 非常に大きな制約が消えるので、必然的にアーキテクチャ設計に影響する でしょう
まとめ • 世のアーキテクチャがシンプル or オーバーエンジニアリングに見えるの は、解くべき課題や制約がそれぞれで異なるから • 課題を解決するアイデアとしてのアーキテクチャの知識は必要 • 事業やチームの課題と制約を明確に意識して設計しましょう
アーキテクチャは 選定するのではなく 設計せよ
Thanks!