ひとり体制から始める Android アプリ開発

こんにちは。ギフトモールで Android アプリの開発をしている @KeithYokoma です。

はじめに

ギフトモールの Android アプリは開発開始から1年半以上が経過しています。Web サービスとしてのギフトモールと比べると歴史が浅く、今も活発に機能開発を進めています。ユーザーの規模も Web サービスとアプリではまだまだ Web サービスのほうが大きいのですが、ユーザー規模が小さい分アプリの施策では様々なチャレンジを積極的に実施しています。

現在は Android アプリを開発するメンバーが増えチームとして開発を進める体制を整えてきていますが、Android アプリの開発を開始した当初は自分ひとりしか Android アプリ開発者がいませんでした。ひとりでの開発は自分自身の裁量で決断できる部分しかないので自由度は高いように見えますが、一方で別の視点・意見を持つことが難しくなりやすく困りごとの相談もすぐにはできないため、将来の開発が苦しくなるきっかけを作ってしまう可能性もはらんでいます。

この記事では、Android アプリの立ち上げから現在に至るまでの間にたどった設計の変遷やその時々に意図していたことを振り返ります。1年半にわたるアプリ開発の歴史があるため、複数回に分けながら解説していきます。

今回はアプリ開発の一番最初の設計をどう考え実装したかについて振り返っていきます。


アプリ開発を取り巻く状況の整理

アプリ開発をするにあたって最初の目標となるリリースマイルストーンはネイティブの UI でサクサク商品を探して購入できることでした。

ネイティブの UI を作るにはサーバーアプリケーションに API を作り、データを取得可能にする必要があります。幸い Android のアプリ開発を開始した時点で既にいくつかの API が実装済みでした。実装済みの API では、ギフトモールで取り扱っている様々なギフト商品を一覧で表示したり、ギフト商品を検索するためのカテゴリやタグなどのデータが取得できます。

ただしすべての必要な API が揃っていたわけではなく、多くの画面で WebView を使う必要がありました。またギフト商品の購入を可能とするには決済システムとの結合も必要になります。特に決済時の本人確認ステップについては WebView ではない方法を使って本物の本人確認ページであることを示せるようにする必要がありました。

ネイティブ UI について目指すものとしては、先行してリリースしていた iOS アプリの画面構成を踏襲することにしました。大まかな画面構成としてはメインの画面に下タブ(Bottom navigation)があり様々な機能へアクセスできたり、簡単なアプリのオンボーディング画面や設定画面があったりする構成です。これを Android の UI に落とし込んで実装を進めることにしました。

アプリ開発開始時点での Android アプリの設計方針の策定

アプリでできることや現状の技術スタックの整理をしたところで、Android アプリをどうやって作っていくかという設計方針を考えます。

Android アプリの設計方針を考える上で重視したことは次の4点です。

  1. アプリ開始時点では自分ひとりしか Android アプリを開発できる人がいないので、できるだけ Android の標準的な技術や手法を用いて簡単にキャッチアップできる状態で手軽に開発したい
  2. 下タブによる画面の切り替えのほか、WebView の画面とネイティブの UI の画面を交互に行き来することから、Fragment を利用して画面の実装を分離したい
  3. 画面遷移はひとつのモジュールで集中的に管理したいが、各画面はできる限り直接参照しあうことがないようにしたい
  4. 各画面で管理すべき状態を表す状態オブジェクトを1つ定義し、UI はその状態オブジェクトを見て適切な表示を作ることに集中させたい

以上の4点を踏まえて、次の3点を設計の主要な指針としてまとめました。

  1. Android Jetpack を最大限活用したアプリ開発
  2. マルチモジュール構成によるレイヤー・ドメイン分割
  3. Unidirectional なデータフローによる画面の状態の更新

1. Android Jetpack を最大限活用したアプリ開発

Android Jetpack は Google が提供している Android アプリ開発に欠かせないライブラリ群です。Activity や Fragment などの主要コンポーネントの後方互換性を担ってくれるライブラリから、主要コンポーネントの一つである Fragment を用いた画面遷移を実現するライブラリ、端末ごとのハードウェアの差を吸収するライブラリなど多種多様なライブラリがあり、今や Android Jetpack なしでは Android アプリ開発ができなくなりそうなほど重要な機能を持っています。

Android Jetpack は API ドキュメントだけでなく様々なチュートリアルやサンプルコード、Codelabs が充実しており、なにか困ったときに頼れるノウハウの検索も簡単です。 最近はライブラリだけでなく、Android Jetpack を利用した推奨される設計パターンも Google が提供してくれているので、Android Jetpack を利用すれば大まかな設計指針が策定できるようになっています。特に Guide to app architecture にあるようなレイヤー分割は Android Jetpack を使って簡単に実現可能になっています。

ギフトモールの Android アプリでは極力 Android の標準的な技術や手法を用いて手軽に開発を進められるよう、特に次に示すライブラリを中心に各機能の設計を構築し機能開発を行っています。

  1. ViewModelSaved State module
  2. FragmentNavigation

アプリの画面は Fragment をベースに実装し、Fragment ごとに 1 つの ViewModel を定義し画面の振る舞いを実装します。主要画面の UI に Bottom navigation を利用していることが Fragment を採用する大きな理由のひとつですが、それ以外にも、Fragment を利用して画面ごとに実装を分離・独立させる設計とすることで後述するマルチモジュールの構成を作りやすくなることも Fragment を採用する理由となりました。 また ViewModel に関しては Saved State module と組み合わせることで簡単に画面回転などの Configuration Chages に対応可能になります。現状では横画面専用の UI や大画面用の UI は作っていませんが、Android 端末の多様化や Android OS の進化に伴って Configuration Changes の機会(例えば言語設定の変更やダークモード)が増えつつあることから Saved State module も初めから導入することにしました。

一方で、アプリ開発開始時点では導入を見送ったライブラリもあります。

その中でも最も大きなライブラリとして Jetpack Compose があります。Jetpack Compose は最初の安定版リリースが 2021 年 7 月で、アプリ開発開始時点ではまだβ版でした。既に導入し始めた事例があったり、Jetpack Compose の使い方や考え方を紹介する文献ができていたりとかなり盛り上がっていた時期ではありましたが、ギフトモールアプリに必要なコンポーネントがなかったりバグが多かったりしたときに工数や手戻りが増える懸念があったため、一旦は Jetpack Compose を導入せず従来の View をベースとした UI 開発を採用しました。ただ将来的には Jetpack Compose の導入も視野にあり、いつ Jetpack Compose に乗り換えてもいいような作りにはしておきたかったため、後述する Unidirectional なデータフローによる画面の状態の更新が可能な設計を導入しておきました1

2. マルチモジュール構成によるレイヤー・ドメイン分割

Android アプリプロジェクトでは Gradle を用いてビルドをするため、Gradle の仕組みで複数のモジュールを作ることができます。モジュールを分割することの利点にはモジュール単位でのコンパイルによるビルドを並列化とビルド時間の短縮ができること、言語仕様のパッケージでは対応できない各種リソース(画像やレイアウトなど)の分離が可能なこと、モジュール単位で依存関係が定義できることなどが挙げられます。これらの利点を踏まえて、モジュールをどのように分割したらよいかを考えることにしました。

ギフトモールの Android アプリでは Guide to app architecture に基づいて、UI レイヤーと Data レイヤーの 2 層構造のアーキテクチャを採用することにしました。それぞれ feature layerrepository layer と命名しています。

2つのレイヤー構造とアプリ本体モジュールの構成

Guide to app architecture にはもうひとつ Domain レイヤーも含まれていますが、アプリ開発開始時点では導入せず 2 層構造にしました。これは開発開始当初のアプリの機能としては Domain レイヤーが非常に薄く、Domain レイヤー固有の知識や処理があまり必要なかったためです。

またモジュール分割によるレイヤー構造の実現の他にも、画面や機能の実装もモジュール単位で管理するためのモジュール分割も導入しました。この分割の主な目的は画面や機能ごとに独立してリソースを管理することにあります。他にも Kotlin の言語仕様上 package private なスコープが作れないことから、Fragment や ViewModel などのコードを分離させるためにモジュールの分割をしています。

同一レイヤー内でのモジュール分割

ただし、1つのモジュールに対して複数の Fragment や ViewModel を持つことは許容しました。例えば、お知らせの一覧を表示する Fragment / ViewModel とお知らせの詳細を表示する Fragment / ViewModel は同一のモジュールで管理しても良いことにしています。

レイヤーによるモジュール分割と画面・機能単位によるモジュール分割を組み合わせたことで、次の図のような全体像となりました。レイヤーによるモジュール分割では、同一レイヤー内でモジュールの依存を作らないようにしています。もし共通して使いたいものが出来た場合は、別途共通モジュールに切り出すことにしています。

モジュール構成の全体像

このモジュール構成を維持するにあたっては次のような規則を設けています。

  • app モジュールがすべてのモジュールを知っている状態にする
    • 最終的に aab を生成するのは app モジュールであるため。Play Feature Delivery は当面利用しないので Dynamic Feature Module のような構成にはしない。
  • レイヤーごとにディレクトリを分けて、そのディレクトリ内にモジュールを作る
    • モジュール数の増加にともなって project root に大量のディレクトリができるのを避けるため。
    • どのモジュールがどのレイヤーに対応するのかわかりやすくするため。
  • 同一レイヤー内でのモジュール同士の依存はつくらない
    • 依存関係が複雑化しやすくモジュール同士の循環参照が起こり得るため。
  • feature layer のモジュールは repository layer のモジュールに複数の依存を持って良い
    • 適宜 feature layer のモジュール自身に必要なドメインの知識・処理を選択させるため。

3. Unidirectional なデータフローによる画面の状態の更新

Unidirectional なデータフローによる画面の状態の更新を実現するフレームワークや実装は既に FluxRedux などが存在します。元は JavaScript を用いた開発で使われ始めたフレームワークですが、同じ考え方を Android アプリ開発に持ち込むことが可能で Java / Kotlin による実装も存在します。自分自身は過去 Redux 2 の経験があり Unidirectional なデータフローの利点もある程度理解していましたが、Redux では登場するコンポーネントが多く初見で理解するには大変なフレームワークだとも感じていました。

過去の経験から Unidirectional なデータフローを導入した画面(より正確には View の振る舞い)の実装自体には慣れており、後々アプリを Jetpack Compose 対応していく上でも Unidirectional なデータフローは重要な考え方になってくるため、Redux よりも簡易な実装を導入することにしました。端的には、あるひとつの画面が依存する ViewModel がその画面に必要な View の状態を保持するオブジェクトを管理し、RxJava を用いて更新を View に通知するような実装になります。

次のコード例はその ViewModel から一部抜粋したコードと、具体的な実装例を示しています。各画面の ViewModel の具象クラスはこの StateSavingViewModel を継承して実装することになります。中身は非常にシンプルで、RxJava の BehaviorSubject で画面の状態を示すオブジェクト ViewState を保持し、更新があれば Observable で通知するものです。ViewState の実装クラスは data class で、状態の更新は copy 関数で新たな ViewState インスタンスを作って BehaviorSubject にわたすことにしています。

// 共通のベースとなるインタフェースと ViewModel の定義
interface ViewState : Parcelable

abstract class StateSavingViewModel<S : ViewState>(
  defaultState: S,
  private val savedStateHandle: SavedStateHandle
) : ViewModel() {
  private val stateMutation: BehaviorSubject<S> = BehaviorSubject.createDefault(
    savedStateHandle[KEY_SAVE_VIEW_STATE] ?: defaultState
  )
  val states: Observable<S> = stateMutation.hide()
}

// ViewState と ViewModel の実装例。
data class HomeViewState(
  val homeSections: List<HomeSection> = listOf(),
) : ViewState

class HomeViewModel(
  private val fooDataSource: FooDataSource,
  savedStateHandle: SavedStateHandle,
) : StateSavingViewModel(defaultState = HomeViewState(), savedStateHandle = savedStateHandle) {
  fun load() {
    // fooDataSource からデータを読み込んで HomeViewState の homeSections を更新する処理
  }
}

アプリ開発開始時点では Kotlin coroutines を利用する選択肢もありました。しかし自分自身がまだ Kotlin coroutines の経験が浅かったこと、Kotlin Coroutines を利用して実装する上では Flow API の StateFlow が必要になるもののまだ Stable ではなかったことから RxJava を利用して実装することにしました3

いずれにしても、この ViewModel を利用して画面を構築する実装は ViewState の更新を観測し、新しい ViewState の持つプロパティに従った View の構築あるいは Composable の呼び出しをするだけになります。この点で Jetpack Compose への移行はほぼ View をどのように置き換えていくかだけに集中すればよい状態にできました。

初リリース後しばらく運用して見えてきた課題

これまで解説した設計に基づいてアプリを開発し、無事リリースを迎えました。その後も徐々にネイティブ UI による実装を増やしていきましたが、これに伴って徐々に複雑化しやすいポイント、辛くなりやすいポイントも見えてきました。

1. ViewModel が思ったよりも早く太ってしまった

画面によって太り方はまちまちですが、思った以上に処理が多くなり ViewState を更新するのに必要な処理が ViewModel の中で肥大化する箇所があらわれてきました。特にアプリ起動直後に開くホーム画面は表示する内容が多岐にわたるため、新しいセクションを増やしたり A/B テストの設定を増やしたりするごとに ViewModel の処理が増えていきました。

これは Domain レイヤーが存在していないことが原因で、様々な処理を ViewModel でベタに書く必要があったことに起因しています。

ViewModel の肥大化に伴って ViewModel のテストも長大になり、IDE がもたつき始めるくらいに成長してしまったことも地味に辛いポイントでした。

2. Fragment から Activity に対して指示を出したくなってきた

Bottom navigation による画面構成では、BottomNavigationViewNavHostFragment をもった Activity が必要です。各機能は別途 Fragment を作成するため、基本的に BottomNavigationView のことは参照できない構造になります。

しかしアプリの機能として、スクロールに合わせて Bottom navigation の表示を切り替えたいという要求が生まれました。これを実現するには Fragment から何かしらの形で BottomNavigationView を操作可能にしなければなりませんが、直接参照はできないため一工夫必要です。

3. 素のままの RecyclerView ではコードが複雑になってきた

アプリ開発開始時点では RecylerView を素のままに使って画面を作っていました。この時点でも RecyclerView で扱う View Type が 10 を超える画面もありましたが、表示を作るだけでユーザインタラクションも最低限クリックくらいしかなかったためまだなんとか扱いきれる範疇でした。

リリース後の開発でこれまで以上にユーザインタラクションがあり表示にもいろいろなギミックが存在する画面を開発することになったとき、流石に素のままの RecyclerView では複雑すぎて厳しいだろうとの判断から Groupie を導入することにしました。

おわりに

この記事ではアプリ開発の一番最初の設計をどう考え実装したかについて振り返りました。 先行して開発していた iOS アプリやそもそものモバイルアプリを開発する目的、その時点で実装済みになっている技術要素をもとにアプリ開発の方針を決め、設計を考える中での取捨選択についても深堀りしています。

基本的にひとりで開発を進める必要があったことから、できるだけ Android で標準的な技術を使って開発するため Android Jetpack を最大限活用したり、設計も Google が示しているガイドに沿った形にするなど Android アプリ開発における共通の概念をベースに、これまでの自分自身の経験をかけ合わせた設計を導入しました。

さいごに、最初の設計を元にした開発をしばらく続けることで見えてきた課題についてもいくつかの事例を紹介しました。次回はその課題についてどう対応していったかを書いてみようと思います。


ギフトモールでは、一緒に働く仲間を募集しています!

まずは一度話してみるだけなどのカジュアル面談も歓迎ですので、少しでも興味を持った方はお気軽に連絡ください!

ギフトモールCulture Deck

speakerdeck.com

募集職種一覧

open.talentio.com


  1. ギフトモールアプリでは 2022 年 1 月から Jetpack Compose を導入し、いくつかの画面を除き Jetpack Compose で作られた画面が多くの割合を占めるまでになりました。

  2. https://github.com/mercari/RxReduxK を利用したもの。正確には Redux というより Flux に近いかたちで利用していた。

  3. 2022 年 6 月現在は StateFlow を使った Kotlin coroutines 用の ViewModel の実装も稼働していて、徐々に Kotlin coroutines の導入・移行が進んでいます。