PairsのAndroidアプリにおける開発効率向上のための取り組み

Yuya Kaido
Eureka Engineering

--

これは「Eureka Advent Calendar 2021」の3日目の記事です。

こんにちは!Androidエンジニアのyuyakaidoです。

業務ではPairsのAndroidアプリを開発しており、新機能の開発を担当することもありますが、それ以外にも開発効率を向上させるための仕組み開発を行うこともあり、この記事ではここ数年でAndroidチーム全体で進めてきた取り組みを紹介しようと思います。

開発効率の向上は様々な観点があるかと思いますが、この記事ではAndroidアプリを開発する際に生じる時間的なロスを解消することで開発作業をよりスムーズに進めるための取り組みと定義します。

また、技術ブログということで、開発効率向上のための取り組みそのものに加えて、Pairsでの実装アプローチも合わせて紹介しようと思います。本来はコード全体を公開できれば一番良いのですが、プロダクト本体や内部ツールという関係上、実装の要点だけを抜き出して紹介する形になります。

この記事では以下のような内容を紹介します。

  • モジュール分割
  • 接続環境の切り替え
  • Storybook
  • 開発者メニュー
  • 今後取り組みたいこと

モジュール分割

Pairsはサービスとしての歴史が長く、コードベースもそれなりに大きなものとなっています。コードベースが大きくなってくると、アプリの全容が分かりにくくなったりビルド時間が長くなったりといった悩みが生じてきます。

Androidアプリではこういった状況下においてはGradleの機能を利用してモジュールを分割することが推奨されており、Pairsもこの考え方に沿ってモジュール分割をしています。

モジュールを分割する際は主に以下2つの方法があります。

  • アーキテクチャのレイヤー単位での分割
  • サービスの機能単位での分割

レイヤー単位での分割については、パッケージを使ってレイヤー構造を表現する方法もありますが、パッケージにはレイヤー間の参照を制限する機能がなく、本来参照すべきではないものも参照できてしまいますが、モジュールを使ってレイヤー構造を表現する方法の場合は参照方向に制限をかけることができます。

機能単位での分割については、アプリの全容を素早く把握したい、Gradleの並列ビルドの恩恵を最大限享受したいという目論見があります。

Pairsの場合はレイヤー単位での分割と機能単位での分割のハイブリッド構造となっています。

Coreモジュール

サービスの基盤となるクラスを配置するためのモジュールです。

モデルクラスや複数のFeatureモジュールから参照する必要があるクラスを配置しています。

Featureモジュール

1つの機能を実現するために必要になるクラスを配置するためのモジュールです。

Pairsにはお相手を探すための機能や同じ趣味や価値観を持った人と繋がるためのコミュニティ機能、マッチングしたお相手とやり取りするためのメッセージ機能などが存在し、そういった粒度でFeatureモジュールを定義しています。

Appモジュール

ビルドキャッシュをより効果的に効かせるために原則Featureモジュール同士は参照しないというポリシーで実装しています。ただ、そうは言っても機能同士は繋がりを持っているため概念的には相互にやり取りする必要があるため、AppモジュールはFeatureモジュール同士が相互にやり取りするのを助ける目的があります。

接続環境の切り替えで言及したように、PairsではDIライブラリとしてDaggerを採用しており、機能同士が相互にやり取りする場合はDaggerを使ってFeatureモジュール同士は直接依存しないが、Coreモジュールを介することで間接的に相互にやり取りを行います。

例えば、探す機能からコミュニティ機能に遷移するケースを考えます。

Androidで画面遷移を実装する場合は遷移先のActivityを参照する必要がありますが、Featureモジュール同士が相互に参照できない状況においては遷移先のActivityを参照することができません。

そこで画面遷移を抽象化したAppRouterTypeというインターフェイスをCoreモジュールに配置します。

interface AppRouterType {
fun newCommunityIntent(context: Context): Intent
}

次に、Appモジュールは全てのFeatureモジュールを参照できるという性質を利用してAppRouterTypeの実装クラスを定義します。

class AppRouter @Inject constructor() : AppRouterType {
override fun newCommunityIntent(context: Context): Intent {
return CommunityActivity.createIntent(context)
}
}

最後に、Daggerを使ってAppRouterTypeを配ることでFeatureモジュール同士が直接参照することなく画面遷移を実現することが可能になります。

余談

Pairsは2021年12月時点ではJetpack Navigationを使っていない画面も多く、原始的なActivityでの遷移を前提とした実装を紹介しましたが、Jetpack Navigationを使う場合も同じ考え方で実装可能です。Jetpack Navigationを前提とした実装については以下の記事で紹介していますので、興味のある方はご覧ください。

接続環境の切り替え

サービス開発の現場では開発環境や本番環境、QA環境といった複数の環境が存在することが多いかと思います。Pairsもユースケースごとにいくつかの環境が用意されており、必要に応じて接続先を切り替える必要があるため、以下のように接続環境の切り替え機能を実装しています。

Androidアプリで接続環境の切り替えを実装する場合、大まかに以下の実装アプローチがあります。

  • ユースケースごとにProduct Flavorを定義する
  • アプリの起動時などで動的に接続先を決定する

どちらの実装アプローチでもやりたいことは実現可能で、Pairsの場合、以前は前者のProduct Flavorを使った実装を採用していました。しかし、普段は開発環境で開発をしながらも時折本番環境やQA環境に接続したいケースもあり、そういった場合にビルドをし直すのが手間だったという理由から、後者のアプリ起動時に動的に接続先を決定するような実装に変更しました。

前者は接続環境が静的に決まるので実装はシンプルなものになりますが、後者は接続環境が動的に決まることから、実装時にちょっとした工夫が必要になります。具体的には、サーバーとのやり取りにはRetrofitを利用しており、動的に接続先が決まる場合はRetrofitのインスタンスも動的に生成する必要があります。

PairsではDIライブラリとしてDaggerを利用しており、RetrofitのインスタンスはDaggerによって管理されることとなるため、DaggerのScope機能を使って接続環境が決まる前と後で大きくフェーズを分割することで、実質的に接続環境が静的に決まる形に落とし込むことができます。

余談

ここでは接続先のサーバーを切り替えるケースだけを取り上げていますが、実はローカルのDBやSharedPreferencesといった永続化機構自体も動的に決定できるようになっています。これによって開発環境を途中で切り替えたとしてもそれぞれのデータが混ざることはなく、複数の環境に同時にログインした状態でそれぞれの環境を行き来することが可能になっています。

この観点の詳しい実装については、DroidKaigi 2018にて「マルチログインの実装方法」という内容で発表していますので、興味のある方は合わせてご覧ください。Pairsは利用規約上複数アカウントを保有するのが禁止されていますが、SNSアプリを開発するケースなどを想定すると、複数カウントで同時にログインするような機能を実装する場合も多いので参考になる部分があるかもしれません。

Storybook

Androidアプリの開発過程では数多くのUIコンポーネントを作ることになります。PairsのAndroidアプリは2021年12月時点ではJetpack Composeを利用しておらず、従来のViewを使ってUIコンポーネントを実装しています。

Jetpack Composeを利用していないのは単純に導入の時間が確保できていない以上の深い理由はありませんが、従来のViewを使ってUIコンポーネントを実装する場合、Jetpack Composeのようなプレビュー機能が利用できないため、何も考えずに実装するとアプリ内で正規のフローで確認を行う必要が生じます。正規のフローで動作確認を行うのは重要ですが、ちょっとしたUI変更のたびに毎回正規のフローで動作確認を行うのは少し大変です。

そこで従来のViewを使ってUIコンポーネントを実装する場合でもJetpack Composeのようなプレビュー機能を実現するための仕組みをStorybookという形で自前で用意しています。このStorybookは元々はWebフロントエンドの開発で使われているアプローチですが、Androidアプリの開発にも同様のアプローチが流用可能です。

PairsにおけるStorybookの活用例をいくつか紹介します。

わたしの大切な価値観

Pairsのプロフィールでは自分が大切にしている価値観を表現できる機能があります。

また、お相手と共通している価値観があるかどうかを確認することもできます。

機能の紹介はこのくらいにして、自分が大切にしている価値観やお相手との共通点はプロフィール画面に表示されることになりますが、かなり多くの表示パターンがあり、正規のフローで動作確認を行うのはかなり大変なため、以下のようにUIコンポーネントだけがプレビューできるようにしています。

コミュニティチャット

Pairsにはコミュニティと呼ばれる趣味や価値観を表現できる機能があり、その中でマッチング前に不特定多数の人と会話することができるようになっています。

このコミュニティチャットも様々な表示パターンがあったり、お相手からリアクションをもらわないと動作確認ができないケースもあり、やはり正規のフローでは動作確認が大変なため、以下のようにUIコンポーネントだけがプレビューできるようにしています。

次に、AndroidアプリにおけるStorybookの実装を紹介します。

StorybookはUIコンポーネントの開発を高速に行うための仕組みなので、AndroidでStorybookを実装する場合、出来るだけ高速にビルドが完了するようなモジュール構造を意識することをオススメします。

具体的には、モジュール分割で言及したようなアプローチでモジュール分割を行い、Storybookアプリをビルドする際はUIコンポーネントが含まれているcore-widgetモジュール以下だけをビルド対象とすることでStorybookの実現に必要最小限なものだけをビルドします。

開発者メニュー

Pairsはサービスとしての歴史が長く、それに伴って画面や機能もそれなりの数になっており、必然的にPairsアカウントに紐付くステータスもそれなりの数になっています。

主要なステータスを挙げると以下のようなものがあります。

  • お相手にアプローチするための手段として「いいね!」と呼ばれる機能があり、そのいいね!が送信可能な回数
  • サービス内でいいね!を含む様々なものに交換可能なPairsポイントの残高
  • 有料会員やプレミアム会員といったアカウント種別

こういった各種ステータスによって見た目やフローが変化する画面が数多くあるため、開発過程では様々なステータスを考慮して実装を進める必要があります。

ステータスを変更する手段としてGoogle Playのテスト決済などを使って実際に有料会員になるための商品を購入するというのが正規のフローに近いですが、開発過程で毎回正規のフローでステータス変更を行うのは時間的なロスが大きいため、Pairsは管理画面と呼ばれるものが用意されており、そこでアカウントのステータスを自由に変更することが可能になっています。

この管理画面はWebサービスとして提供されており、基本的にはWebブラウザから利用することになりますが、ブラウザから管理画面にログインし、自分の開発アカウントを探し、必要なステータス変更を行う、というプロセスを毎回踏むのは少し手間だと考え、アプリ内に管理画面相当の機能を実装することでよりスムーズにステータス変更を行うことができるようにしています。

ここで紹介したような開発者メニューはサーバーのデータも変更する必要がある関係上、Androidアプリで完結しません。Pairsの場合、Webサービスとして提供されている管理画面が裏側で利用している管理画面APIをAndroidアプリから利用することで開発者メニューを実現しています。

今後取り組みたいこと

ここまではすでに取り組んだ内容とその実装を紹介してきましたが、ここからは開発効率の向上という文脈で今後取り組みたいと考えていることです。

Jetpack Composeの導入

Storybookの部分で言及したように、Pairsは2021年12月時点ではJetpack Composeを利用しておらず、従来のViewを用いてUIコンポーネントを実装しています。また、UIコンポーネントのプレビュー機能として自前でStorybookを実装しています。

Storybookの構想を考え始めた時点ではJetpack Composeがリリースされていなかったため自前でプレビュー機能を持ったStorybookを実装するというアプローチに着地しましたが、Jetpack ComposeのStable版がリリースされた状況においては、自前でStorybookをメンテナンスし続けるメリットは薄れてきています。

プレビュー機能以前にまずはJetpack Composeを使ってUIコンポーネントを実装する必要がありますが、すでにAndroidチーム内でJetpack Composeをプロダクトで活用するための実験を進めているメンバーが複数おり、近々プロダクトにも適用してもいいんじゃないかという話も出ています。

開発者メニューの拡充

この記事で紹介した開発者メニューは主にアカウントのステータスを変更するためのものでしたが、開発者メニューはそれ以外の観点でも活用可能だと考えています。

開発者メニュー界隈で有名なライブラリとしてHyperionがあり、このライブラリは開発効率を上げるために非常に有用だと考えています。開発効率の向上を語るのにHyperionを導入していないってどういうこと?というツッコミがありそうですが、、単純に優先順位の問題で導入していないだけで特に深い理由はありません。

モジュール分割のさらなる推進

Pairsのコードベースはそれなりの歴史があり、モジュール分割を開始してから修正が加えられた機能や新しく実装された機能についてはモジュールを分割するというポリシーで進めていますが、あまり手を入れる機会のない機能たちは1つのモジュールにまとまっている状態です。

Pairsは現在も既存機能の改修や新機能の追加を積極的に行っているため、そういった開発を行う際にスキを見つけてモジュール分割を推進していきたいと考えています。

デファクトスタンダードへの追従

Android界隈のここ数年の動きとして、GoogleがAndroid Jetpackという形で色々な観点でデファクトスタンダートとなるであろうライブラリを公開してくれています。もちろん無条件で全て採用するのが正解とは思いませんが、Android界隈で長年議論されてきた結果が反映されたライブラリとなっていることが多く、Googleによる継続的なメンテナンスが期待できるという点も考慮すると、主要なライブラリは積極的に採用していくメリットは大きいと考えています。

数あるライブラリの中でも直近は以下のライブラリに注目して個人レベルで実験を進めています。

  • Navigation

Navigationはすでに導入済みですが、十分には活用できていない状況です。例えば、Navigationは通常の画面遷移に加えてディープリンクによる画面遷移もサポートしていますが、Pairsはディープリンクからの画面遷移はNavigationとは違う独自の実装が存在します。独自実装は新しいエンジニアがジョインした場合のキャッチアップコストが高いなどのデメリットもあるため、ディープリンクからの画面遷移も通常の画面遷移と同様にNavigationを使った実装に移行していきたいと考えています。

  • Dagger Hilt

この記事でも何度か言及しているように、PairsではDIライブラリとしてDaggerを利用していますが、Dagger HiltではなくDaggerのAndroidサポートを利用している状況です。特段何かが実装できないというレベルの困りごとはありませんが、Dagger Hiltと比較すると随分ボイラープレートコードが多いため、長期的にはDagget Hiltに移行していきたいと考えています。

  • DataStore

開発効率の向上という文脈から逸れてきている気がしますが続けます。

歴史のあるAndroidアプリあるあるだと思いますが、現在のPairsにはSharedPreferencesのラッパーが複数存在しています。Dagger Hiltと同様に何か困っているというわけではありませんが、1つの目的に対して複数の実装が存在している状況は望ましくはないため、DataStoreへの移行を目論んでいます。

  • CameraX

Pairsは自分のプロフィールに使う写真を撮影する機能や本人確認のために運転免許証などの各種証明書を撮影する機能などが存在し、現在はサードパーティ製のライブラリを使っていますが、特段そのライブラリでないといけない理由はないため、Googleによる継続的なメンテナンスが期待できるCameraXへの移行を検討しています。

まとめ

この記事ではPairsのAndroidアプリにおける開発効率を向上させるための取り組みとその実装を紹介しました。

  • 接続環境の切り替え
  • Storybook
  • 開発者メニュー
  • モジュール分割

また、今後開発効率の向上という文脈で取り組みたいと考えていることも紹介しました。

  • Jetpack Composeの導入
  • 開発者メニューの拡充
  • モジュール分割のさらなる推進
  • デファクトスタンダードへの追従

最後に、Pairsの開発に興味のある方がいれば、まずは選考云々は抜きにしてカジュアルにお話ししましょう!

--

--