Androidプロジェクトのアーキテクチャ選定 + マルチモジュール対応した話

こんにちは！
今回はAndroidアプリのリアーキテクチャの提案をし、実際に対応するまでにやったこと・考えたことついてまとめます。
私自身の振り返りの側面が強いですが、何かの参考になれば幸いです！

## きっかけ

新規でAndroidアプリを開発することになったのですが、直近で携わっていた別のプロダクトではあまり満足のいく開発ができませんでした。
その原因が「アーキテクチャが欠如していた」ことでした。
これによって、「認知負荷が非常に大きい」、「改修コストの増加」、「パフォーマンス低下」などの問題を引き起こしていました。

そこで、新規開発では問題の再発を防ぐべく、「アーキテクチャを見直しませんか？」とメンバーに提案したことがきっかけです。

リードエンジニアさんからも「本格的な開発に入る前にぜひ取り組んでほしい！」と前向きに快諾していただけました！

## 目標

今回のリアーキテクチャ対応では、以下の3つを目標に設定しました。

- メンバー全員が良い開発体験を得られるようにする
- 効率的に分担して開発を進められるようにする
- 今後の開発にも活用できる形にする

リアーキテクチャ対応をしたことによって得られる技術的なメリットを通じて、最終的にはプロダクト、開発メンバーに貢献できることを目標としました。

その為、対応中はメンバーから積極的な意見をいただくようにしました。

## 課題と方針

はじめに、現状のプロダクト及び開発メンバーが抱えている課題を洗い出しました。

### モジュールの分割

これまではシングルモジュールで開発していました。
しかし、今回の開発は少し特殊であり、一般的なアプリケーションとしての機能を実装すると同時に、Httpサーバーとしての機能を同じアプリ内に持たせる必要がありました。
イメージとしては、UIからの入力と外部からのリクエストをアプリが処理するような内容です。
これをシングルモジュールで管理してしまうと、実装のコンフリクトが発生する可能性が考えられました。
また、感覚的に「アプリとしての機能とサーバーとしての機能が共存するのは良くないのでは？」とも思いました。
メンバー内で相談したところ、「少なくともサーバー関係の実装は別のモジュールに切り出したい」という結論になった為、マルチモジュール対応を行う方針にしました。

### アーキテクチャ知識の共有

前提として、全メンバーが同じレベルの技術知識を持っている訳ではありません。
既存のメンバーに加え、新規メンバーが参画した際にもギャップは生まれてしまいます。
このギャップを可能な限り狭くすることが開発メンバー内での課題でした。
そこで、今回の対応ではよりアーキテクチャに対する理解を深めてもらう為の施策をすることにしました。
具体的には、対面レビューを増やすことと、ドキュメント化で解決していく方針にしました。

## アーキテクチャの選定

新規開発では、Android公式が推奨するレイヤードアーキテクチャを採用しました。
アーキテクチャの詳細につきましては、以下ドキュメントがとても参考になります。

https://developer.android.com/topic/architecture/recommendations?hl=ja

また、UI部分の実装のみMVVMを採用しました。

今回、採用の決め手となったのが、**ドメインレイヤが必須ではなくオプショナル**であることです。
前述した通り、今回の開発ではアプリ+Httpサーバーという特殊な構成ではあります。
が、機能自体は非常にシンプルなアプリケーションなので、ビジネスロジックをわざわざViewModelから分離する必要性がそんなに感じることができませんでした。
また、必要になったらドメインレイヤを実装すればいいとも考えました。
ただ、ドメインレイヤを採用しないことでビジネスロジックの誇大化リスクはありますが、UIレイヤがデータレイヤに依存するだけの**シンプルな構成**にでき、非常に分かりやすいモデルにできました。

## マルチモジュール対応

### モジュール構成
今回、以下のようなモジュール構成にしました。
```
root/
├── :app # エントリポイント
├── :core # 共通ロジック
├── :data # APIアクセスやDB処理など
├── :feature:service # HTTPサーバー機能
└── :feature:view # ActivityやViewModelなど
```

特徴的なのが、featureモジュールの構成だと思います。
serviceとviewモジュールを作り、それぞれサーバー機能と画面機能で分割しました。
これにより、サーバー関連の実装とUI関連の実装のコンフリクトを防止できます。
また、今回の開発では、実装する画面(Activity)数が2画面しかない為、各画面の実装はモジュール単位で分割せず、viewモジュール内のディレクトリで分割するようにしました。

```
(イメージ)
view/
├── home/
|   ├── ViewModel
|   ├── Activity
|   └── ...
└── settings/
```

ただし、将来的にモジュール分割の必要性が発生した場合には、モジュールを新しく作って実装コードをコピペするだけで完結するようにし、移行コストを減らせるようにしています。
その為にMVVMを採用するなど、疎結合な実装ができるようにしています。

### マルチモジュール対応時のポイント

はじめからマルチモジュール化していた訳ではなく、最初はシングルモジュールで開発を進めてました。(リアーキテクチャの調査や検証と並行で進めたかった為)
シングルモジュールで実装した機能を切り出してモジュール分割するのですが、このとき、**末端のモジュールから着手**した方がやりやすかったです。
というのも、マルチモジュール環境では循環参照ができない為です。
なので、どこにも依存しない実装、今回の例だとデータレイヤの実装をdataモジュールに移行するところから始めました。
結果、スムーズに移行することができました。

### 共有インスタンスの取り扱い

元々シングルモジュールで開発していた時は、SharedPreferencesなどのアプリ全体で共有したいインスタンスをApplicationクラスで取得するようにしていました。
しかし、マルチモジュール対応をしたことで、Applicationクラスを含むappモジュールがSharedPreferencesクラスを持つdataモジュールを参照することができなくなりました。
そうなってしまうと、dataモジュールに移行したSharedPreferencesのインスタンスを取得することができません。

これを解決する為に、DIコンテナを導入しました。
今回のプロジェクトでは、Hiltを採用しました。

https://developer.android.com/training/dependency-injection/hilt-android?hl=ja

これにより、DIモジュール内でSharedPreferencesのインスタンスを取得することができ、依存関係にあるモジュールで共有インスタンスを扱うことができました。

``` kotlin
@Module
@InstallIn(SingletonComponent::class)
object PreferencesModule {

    @Provides
    @Singleton
    fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences {
        // インスタンスを取得
        return context.getSharedPreferences("pref", Context.MODE_PRIVATE)
    }
}
```

### 外部ライブラリの管理

複数のモジュールを作成すると、モジュール毎にbuild.gradleのバージョン定義が必要になります。
そうなると、アプリ全体で使用したいライブラリは各モジュールのgradleファイルに定義しないといけず、一手間かかってしまいます。

これを解決する為、version catalogを用いて、ライブラリバージョンを全モジュールで共有できるようにしました。

今だとAndroid Studioでプロジェクトを立ち上げるとデフォルトで使用されているので、深く気にする必要はないかもしれないです。
が、マルチモジュール化に伴い、build.gradleファイルへバージョンの直書きをコミットするのはやめるようにしました。

## その他取り組んだこと

### モジュール構成のドキュメント化

プロダクトで管理しているドキュメントツールがありますが、モジュール構成など技術的なものはプロジェクトのReadmeに記載しました。
何故なら、環境構築等もReadmeに記載している為、新規メンバーは必ず目を通すことになるからです。
また、Readmeに開発ルールをまとめて記載することで、開発者が困った時にすぐ見れるようにしました。

### 情報共有する場を用意した

メンバー全員がはじめからマルチモジュールやアーキテクチャに対する理解があるかと言われると、そうではありませんでした。
実際、私も調査するまでは"なんとなく"な状態でした。
そこで、ある程度方向性や理解が深まった段階で対面でのコードレビューを行い、情報共有的な機会を作りました！
これにより、メンバー全員がリアーキテクチャに対する理解を深められるほか、私自身も理解の整理や意見をいただける場として有意義な時間にできました。
ポイントとして、調べたことは全てドキュメントにまとめ、読み返しができるようにすると綺麗に進められました。
その方が意見を出しやすい！との声もいただけました！
また、公式ドキュメントの読み合わせも効果的でした。
なので、メンバー内で不定期にこれからもやっていきたい！という方向になりました。

## これからの課題

### 開発分担による効率化の効果を最大化する

実際にリアーキテクチャをしてから日が浅く、目標にした「効率的に分担して開発を進められるようにする」の効果があまり受けられていません。
が、メンバーからは「前よりは開発しやすくなった」と前向きなコメントをいただけています。
その為、フィードバックを受けながら、これからもアーキテクチャの改善や運用方法の最適化を進める必要があります。

## あとがき

結果的に早期段階でリアーキテクチャを実行できた為、移行にかかる工数はそんなにかかりませんでした。
その一方で、既に大きくなったプロジェクトで今回と同じような方法で移行できるかというと、多分できません。
何故なら、機能の切り出しに時間がかかるからです。
多数の機能を適切な粒度で分割しないといけず、今回のように提案がスルッと通らないからだと思います。

実際にやってみて、今後は、初めからマルチモジュールや今回の課題を意識した開発をすることで、実際に移行にかかるコストを少しでも減らしたいと思っています！