Help us understand the problem. What is going on with this article?

Android開発でマルチモジュールを採用してみて

More than 1 year has passed since last update.

※ この記事はエキサイトAdvent Calendar 2018 1日目の記事です。

今年もアドベントカレンダーやりたい!ということで半ば強引にカレンダーを立ち上げた@rmakiyamaです。
業務ではflagmeのAndroidやエキサイト婚活全般、Radiotalk(10月リリース)のAndroidを担当しています!

今回は、2年連続でAndroidのフルスクラッチ開発に関わることになり、エイヤア!と
Radiotalkアプリをマルチモジュールでの実装を進めていったお話をします。

設計方針

5月某日、設計方針をどうするかのチーム会議が行われました。
まずは、そこで決まった方針について簡単に説明します。

ガンガンいこうぜ!

なかなかド新規でAndroid開発!というのは弊社では多くなく
せっかくだし使いたい技術、やりたい設計、ガンガンいこうぜ!の作戦が議決されました。

というわけで

  • Kotlinは問答無用だよね
  • 前回のMVVMの反省を活かしてMVVMで!
  • Repositoryパターンみなおしたい(独断と偏見)
  • Instant Appと相性良さそう
  • ↑もあるし完全にマルチモジュールの機運だよね
  • DDDぽいの入れていきたい
  • AACもやろうぜ

という欲に満ちた感じになりました。正直、反省しています。

こうなった

archtecture.png

  • フル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を見ていきます。(一部実コードと違う部分もアあります)

GetDisplayedTalkUseCase.kt
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の場合、番組詳細からトーク画面へ、トーク画面から番組詳細への画面遷移があります。

スクリーンショット 2018-12-01 17.21.27.png

当初、それぞれprogramdetail/talkdetailのモジュールに分けていたため、循環依存が発生していました。

悩んだ結果、この場合に関しては、「詳細を表示する」という意味合いのモジュールとしてモジュールの統合をして解決しました。
DIを用いてインターフェースを使って、という方法も検討しましたが、開発期間の問題もあり、スピード重視の判断だったので最善ではなさそうです。

わからないこと

本業もありながら10月のリリースに向けてスピード重視という部分もあり、不安も疑問も抱えながらの実装でした。
徐々にマルチモジュールの知見も多く見られるようになった今、まだ悩んでいる、わからない部分も多々あります。

  • マルチモジュール開発にしたけどビルドかなり遅い
    • 特にcommonを変えたとき。そんなものなのかな。設定の問題、?
  • APIモジュールを作るべきだったのか
    • 分割の考え方にもよるけど、作っておくとスムーズだったかも
  • ioschedくらい少ない分割でも良かったかも。
    • 開発メンバーも多くない中で、分割数も結構あり、制約が多すぎたかもなと思っている

今後

リリースも完了した今、大きくやりたいことは以下の2つです。

  • モジュールの整理
  • DI整理

今回は、大げさに言うと「画面単位」くらいのモジュール分割になっています。
ここはサービスを俯瞰して、しっかりしたコンテキストマップを作り、モジュールの整理をしたいです。

DIについても、雰囲気でDaggerを使っている感じが否めないので、スコープを意識して設計しなおしたいです。

参考

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away