※ この記事はエキサイトAdvent Calendar 2018 1日目の記事です。
今年もアドベントカレンダーやりたい!ということで半ば強引にカレンダーを立ち上げた@rmakiyamaです。
業務ではflagmeのAndroidやエキサイト婚活全般、Radiotalk(10月リリース)のAndroidを担当しています!
今回は、2年連続でAndroidのフルスクラッチ開発に関わることになり、エイヤア!と
Radiotalkアプリをマルチモジュールでの実装を進めていったお話をします。
設計方針
5月某日、設計方針をどうするかのチーム会議が行われました。
まずは、そこで決まった方針について簡単に説明します。
ガンガンいこうぜ!
なかなかド新規でAndroid開発!というのは弊社では多くなく
せっかくだし使いたい技術、やりたい設計、ガンガンいこうぜ!の作戦が議決されました。
というわけで
- Kotlinは問答無用だよね
- 前回のMVVMの反省を活かしてMVVMで!
- Repositoryパターンみなおしたい(独断と偏見)
- Instant Appと相性良さそう
- ↑もあるし完全にマルチモジュールの機運だよね
- DDDぽいの入れていきたい
- AACもやろうぜ
という欲に満ちた感じになりました。正直、反省しています。
こうなった
- フルKotlin
- UseCaseでローカルまたはリモートからデータを取得する
- ViewModelでObservableをLiveDataに変換して扱う
- マルチモジュール
- 別モジュールからのデータ取得はリモートから取得と同じような扱いとしClientを経由
- DBはRoomを採用
- ActivityのBaseクラスを作らずやってみよう
モジュールの分割
モジュールの分割では「水平方向」と「垂直方向」でのわけかたがありますが、Radiotalkでは垂直方向での分割を採用しました。
水平方向での分割も同時に入れることも検討しましたが、単純にモジュール数がレイヤー数倍に膨らむといった懸念と、別プロジェクトではレイヤーわけした設計を、コードレビューを通してその原則を守るように実装できていた、などの意見から今回は採用を見送りました。
その結果、以下のような分割になりました。
- app
- common
- talk
- program
- user
- talkdetail
- programdetail
- userdetail
- record
- player
- mypage
- etc...
※ モジュールの分割は、当時もベストプラクティスがわかっておらず、1番後悔している部分です。これを参考に、はならないと思います。
UseCase
例として、Radiotalkでトーク詳細で表示するデータを取得するUseCaseを見ていきます。(一部実コードと違う部分もアあります)
internal class GetDisplayedTalkUseCase @Inject constructor(
private val api: DetailsApiClient,
private val programClient: ProgramClient,
private val talkClient: TalkClient,
private val userClient: UserClient
) : RxCommandUseCase<TalkId, DisplayedTalk>() {
override fun execute(parameters: TalkId): Completable {
return api.getDisplayedTalk(parameters.value)
.flatMap { it.body()?.cache() ?: throw TalkNotFoundException(parameters) }
.flatMapCompletable {
Completable.fromAction { onNextResult(it) }
}
}
private fun GetDisplayedTalkResponse.cache(): Single<DisplayedTalk> {
return Singles.zip(
programClient.convertWithCache(program),
talkClient.convertWithCache(talk),
userClient.convertWithCache(user)
) { program, talk, user ->
DisplayedTalk(program, talk, user, clipped)
}
}
}
このようにProgram
, Talk
, user
それぞれのコンバートやキャッシュ処理は、各モジュールで行うため、Client
を経由して処理をしています。
所感
よかったこと
責務の分離ができた
Kotlinのinternal
修飾子を使うことで、制約を強くできたのは大きかったです。あとは、コードレビューで、「これはここにかくべきではない」「こっちのモジュールの責務では」みたいな議論ができたのも良かったです!
機能開発の分担が楽だった
リリースまでの開発は2名で行いました。当時、お互いに主務は別にあったため、手の空いたときにという状況でした。
今回は機能ごとにモジュールを分けていたので、自分の開発に集中することができました。
つらかったこと
開発期間が短い中、かなり手探りだったのでたくさんありました…
ざっと列挙すると
- 循環参照
- 画面遷移の循環
- Responseのモデルを使い回せない
- DI(Dagger)
- 雰囲気でDIを使っている
- マルチモジュールでのDIの知見が少ない
- ボイラーテンプレートが多い
- モジュール毎にDaggerの諸々を書かなきゃ(仕方ないよね)
- 毎回、DataBindingとViewModelの初期化とか
- Baseクラスを導入しても良いかも
とくに困った循環参照問題について特筆してみます。
循環参照
マルチモジュールならではの問題で、モジュールAがモジュールBに依存している場合、モジュールBはモジュールAに依存できません。
ここで画面遷移の循環が発生します。例えば、Radiotalkの場合、番組詳細からトーク画面へ、トーク画面から番組詳細への画面遷移があります。
当初、それぞれprogramdetail
/talkdetail
のモジュールに分けていたため、循環依存が発生していました。
悩んだ結果、この場合に関しては、「詳細を表示する」という意味合いのモジュールとしてモジュールの統合をして解決しました。
DIを用いてインターフェースを使って、という方法も検討しましたが、開発期間の問題もあり、スピード重視の判断だったので最善ではなさそうです。
わからないこと
本業もありながら10月のリリースに向けてスピード重視という部分もあり、不安も疑問も抱えながらの実装でした。
徐々にマルチモジュールの知見も多く見られるようになった今、まだ悩んでいる、わからない部分も多々あります。
- マルチモジュール開発にしたけどビルドかなり遅い
- 特にcommonを変えたとき。そんなものなのかな。設定の問題、?
- APIモジュールを作るべきだったのか
- 分割の考え方にもよるけど、作っておくとスムーズだったかも
- ioschedくらい少ない分割でも良かったかも。
- 開発メンバーも多くない中で、分割数も結構あり、制約が多すぎたかもなと思っている
今後
リリースも完了した今、大きくやりたいことは以下の2つです。
- モジュールの整理
- DI整理
今回は、大げさに言うと「画面単位」くらいのモジュール分割になっています。
ここはサービスを俯瞰して、しっかりしたコンテキストマップを作り、モジュールの整理をしたいです。
DIについても、雰囲気でDaggerを使っている感じが否めないので、スコープを意識して設計しなおしたいです。