エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

7年間運用している主力アプリをリファクタリングしている話 Android編

こんにちは、エンジニアリンググループ マルチデバイスチーム 新卒2年目の小林です。

先日、iOS版m3.comアプリのリファクタリングに関する記事が公開されましたが、Android版もリファクタリングを行っているため、Androidの方で起きていた問題とリファクタリングをどのように行っているのかについて紹介します。

m3.com Androidアプリについて

f:id:kobasato34:20220314145810p:plain
m3.com Androidアプリ

m3.comアプリは医療従事者専用なので中身はお見せできないのですが、一言で説明すると、トップ画面に様々なサービスのタブが並べられたポータルアプリです。サービスは 20個以上あります。

主力のアプリであるため、今後もサービスが増えたり保守運用を続けていく必要があります。 他に開発しているアプリはモダンな構成になっているのですが、このアプリは運用歴が長いこともあり、様々な課題がありました。

m3.com Androidアプリの課題

ActivityやFragmentにビジネスロジックが書かれている

Viewのロジックとビジネスロジックが混ざっており、複雑になっています。

必要以上にコードが共通化されている、ActivityやFragmentが多段階に継承されている

機能が似た画面が複数あって、それらは共通部分を処理するための親Fragmentを継承していました(いわゆるBaseFragment、BaseActivity)。 しかし、運用を続けていくにつれてその親Fragmentに各画面固有のロジックが入り込んでしまってきており、非常に扱いづらくなっていました。

そしてこのような無理のある継承が多段になっているケースもあり(多いものだと5段!)、機能の追加や修正が非常に困難になっていました。

責務が不明なクラスがある

◯◯Service、○○Helperという名前のクラスがViewの処理やビジネスロジックの処理を行なっており、責務が適切に分けられていませんでした。

多くのコードがJavaで書かれている

リファクタリング開始前のJavaのコードの割合は80%でした(GitLabのリポジトリの情報より)*1。Andoirdアプリ開発はKotlinファーストになってきているため、全然Kotlin化できていないことも課題の一つです。

リファクタリングの検討

一からアプリを作り直す案もありましたが、その場合だと「通常の開発が止まってしまう」「アプリの規模が大きいのでリリースまでに時間がかかってしまう」といった問題があるため、現実的ではありませんでした。最終的には既存のコードを部分的にリファクタリングしながらリリースしていくことができそうだという判断になり、リファクタリングが始まりました。

リファクタリングの大まかな設計やルール

アーキテクチャをFluxに

私が所属しているマルチデバイスチームでは、一人のエンジニアが両OSの実装をすることがたびたびあるため、アーキテクチャを両OSで揃えることで実装をしやすくしています。

チーム内で話し合った結果、データの流れが分かりやすく、共有も行いやすいFlux*2を採用することにしました。また、UIレイヤに関しては無理に合わせる必要はないということにしています。

f:id:kobasato34:20220316184707p:plain
m3.com Androidのアーキテクチャ図

FluxとAAC ViewModelの使い分け

画面間で共有する必要がある状態はFluxのStoreで管理しています。また、データ取得等が行われず画面内で完結するような状態はAndroid Architecture Components (AAC)のViewModelで保持しています。

また、ActivityやFragmentはFluxのStoreやActionCreatorを直接参照せず、ViewModelを経由するようにしています。

StateFlowによるViewの状態の管理

一画面の状態をStateというdata classにまとめてViewModelがStateFlowで公開し、FragmentやActivityがそれを監視してViewを操作する形にしています。 UIのイベントは、FragmentやActivityがViewModelのメソッドを呼ぶことで通知し、ViewModelがStateを操作しています。 Viewが見るべき状態が1箇所にまとまっているため、見通しが良くなります。

UIレイヤに関しては公式のガイドが参考になりました。 developer.android.com

データの流れをまとめるとこんな感じです。

f:id:kobasato34:20220316184710p:plain
m3.com Androidのデータフロー

マルチモジュール化

依存関係を整理・強制するためにマルチモジュール構成にしています。

細かいモジュールを省くと、

:app
  - MainActivity
  - Application

:domain
  - Domain model
  - Repositoryのinterface
  - FluxのAction/ActionCreator/Store/Dispatcher

:infra
  - Repositoryの実装クラス
  - DB関連
  - API関連

:features:{service-name}
  - 各サービスのUI

:legacy-app
  - リファクタリング前のコード

といった感じに分かれています。

また、既存のコードを一時的に:legacy-appに入れています。

UIの部分はサービス毎にモジュールを分けていますが、:domainや:infraはサービス間を跨ぐケースがあるため、サービス毎には分けませんでした。

ライブラリのモダン化

  • AndroidAnnotationsの廃止
  • Dagger Hiltの導入
  • RxJava->Kotlin Coroutinesへ置き換え
  • 直接OkHttpClientを利用していたのをRetrofitに置き換え

等を行なっています。

ID型を定義

m3.comアプリには様々なサービスが入っているため、

  • サービスAの記事ID
  • サービスAのコメントID
  • サービスBの記事ID
  • サービスBのカテゴリID

というように様々なデータのIDが登場します、リファクタリング前のコードでは、これらのIDは全てIntやStringで定義されていました。

しかしこれだとIDの受け渡しミスが起こる恐れがあるため、リファクタリング時に必ず専用の型を作るようにしています。

Kotlinのvalue classを使うとバイトコードではプロパティの型で扱われるため、オーバーヘッドもありません。

@JvmInline
value class HogeId(val value: Int)

過剰な継承の禁止

先述の通り、単なる共通化目的のBaseFragment/Activityは後で保守運用が辛くなってくるため、FragmentやAppCompatActivity等を直接継承するルールにしました。

○○Helperや○○Serviceという名前のクラスを作らない

リファクタリング前のコードでは、これらのような名前のクラスがたくさんありました(サービス名+Service等)。この中にViewに関するロジックやビジネスロジックが色々詰め込まれていたため、責務がよく分かりませんでした。

そのためこのようななんでもやるクラスは禁止とし、責務を明確に決めてそれが分かる名前をつけるようにしています。

リファクタリングの進め方

通常の開発も並行して行えるように、サービス毎に分けてリファクタリングを行なっていく方針にしました。

まずは一人で基礎の部分を修正し、サービス毎にリファクタリングを進められる状態にして一旦リリースしました。 次に規模の小さいサービスをリファクタリングしてリリースし、お手本のようなものを作成しました。 その後は、通常の開発と並行して各サービスのリファクタリングを順番に行なっていき、リリースしています。

また、iOSの方のリファクタリングが先行して始まっていたため、iOSでリファクタリングが完了したサービスを追っていく形で進めています。設計をiOSとなるべく揃えているため、iOSのコードも参考にしてリファクタリングを進められています。

現在は私ともう一人のメンバーで作業しています。

UIの実装について

Groupieによるリスト画面の描画

リファクタリング前の実装ではListViewやRecyclerViewが使われていたのですが、簡潔にリスト画面を構築したり差分更新ができるGroupie*3を導入しました。他のプロジェクトで既に使用しており、実績があったためです。これにより、複数タイプのアイテムを並べたり動的に切り替えたりするのが楽になりました。

Jetpack Composeの導入

Jetpack Composeの安定版がリリースされ実績も増えてきたため、途中からJetpack Composeを導入しました。現在は相互運用APIのComposeView*4を用いてFragment単位で使用しています。まず小さなサービスで検証し問題なく実装・リリースできたため、新規開発では積極的にJetpack Composeを使っていく方針になりました。

最後に

リファクタリングを終えた部分は、開発が以前よりも格段にやりやすくなったと思います。またUIのレイヤーとビジネスロジックを分離したことにより、単体テストも書きやすくなりました。

他の開発と並行して作業しているため、まだAndroidは6割ほどの進捗ですが着々と進んでいます。現在、Kotlinの割合が20%->50%になりました*5。Kotlinのコードの割合が増えていくのも楽しみです!

We are hiring!

マルチデバイスチームでは複数のアプリを開発しており、ネイティブだったりFlutterだったり様々です! KMMも少し使っています。 興味がありましたらぜひご応募ください!

jobs.m3.com

*1:新規開発は100% Kotlinです。

*2:https://github.com/facebook/flux/tree/main/examples/flux-concepts

*3:https://github.com/lisawray/groupie

*4:https://developer.android.com/jetpack/compose/interop/interop-apis?hl=ja

*5:まだQA中でマージされてないコードが結構あるので、実際にはもっと割合が高いはずです!