こんにちは。Sansan事業部でSansanのAndroidアプリ開発を担当しています北村です。今回はSansanの中でもビッグプロジェクトとなる「オンライン名刺機能」のAndroid開発の裏側として、オンライン名刺開発で得た知見の一部を紹介します。iOS版の記事はこちらとなります。
オンライン名刺機能の開発
オンライン名刺機能は、Sansanに取り込んだ自分の名刺をオンライン名刺として登録し、そのオンライン名刺のURLを発行して渡したい相手に送信し、同時にSansanユーザからも、それ以外の方からでも名刺を受けとることができるオンラインでの名刺交換機能です。コロナ禍の中でオンラインミーティングが増えている事を考慮してのSansanの新機能となります。
自分の名刺をオンライン名刺として登録する機能、相手に名刺を受けとってもらうためのURL発行機能、オンライン名刺が送られてきた際の受け取り機能など大きな機能が多く、画面数も多くなっています。

高速開発の工夫を2点紹介
オンライン名刺機能はリリース日が決まっており、またその開発スケジュールは非常にタイトなものでした。通常の開発ではAPIの開発が先行しますが、今回はAPIの開発は待たずにアプリの開発を開始し、APIの開発が完了したらすぐに結合試験をしてバグを修正後リリースするというスケジュールでした。そこでAPIのIF案が作成されるとRepository層をモックで作成し、そのモックを使って開発を進めることができるようにしました。レイヤードアーキテクチャの利点を体感できたので共有したいと思います。
また、オンライン名刺は画面数が多く状態によって複雑な画面遷移を行うため、Navigationライブラリを用いて表現するようにしました。従来の画面遷移ではそれぞれの画面で定義する必要がありましたが、Navigationによって一元管理し、仕様に詳しいメンバーが担当することでそれぞれの画面を担当するメンバーの仕様キャッチアップの負荷を軽減しました。そちらも共有したいと思います。
実際には通信しないRepositoryを用い開発を加速
オンライン名刺開発では、サーバの開発と同時並行して開発するためにAPI案のIFを実装したRepositoryのモックを作成しました。このモックは実際にはサーバと通信しませんが、ランダムなタイムラグでダミーの結果を返すよう実装し、また一定の確率で通信失敗を発生させるなど通信中画面や通信失敗画面の動作確認ができるようにしています。そして、オンライン名刺の登録処理を実行するとその内容をメモリ上に保持し、次からそのオンライン名刺の登録情報を返すようにして新規登録からの導線の動作確認ができるようにしています。これによりAPIの開発が完了していなくても、モバイルアプリ側の実装や動作確認を行う事ができスムーズな開発をすることができました。
例えば次のコード例はサーバからオンライン名刺の状態を取得するメソッドのモックの実装です。遅延を入れてローディング画面を表示し、一定の確率でエラー表示となります。そして、オンライン名刺登録処理で入力した情報を返すようにしています。
override suspend fun getVirtualCardBarStatus(): VirtualCardBarStatus { randomDelay() // オンライン名刺状態の取得はsuspend関数なのでここでランダムな遅延を入れてローディング画面の確認をする when (Random.nextInt(8)) { 1 -> { return VirtualCardBarStatus.Forbidden // 一定の確率でエラー表示 } 2 -> { return VirtualCardBarStatus.Error // 一定の確率でエラー表示 } } val url = bizCardExchangeUrl return VirtualCardBarStatus.Shared(url) // 登録時のモックで登録したインスタンスを返し、登録後の導線に進めるようにする }
さらに開発ビルドをするとデバッグ用画面からRepositoryを実際のサーバ通信版とモック版を切り替えることができるようにしています。実際の実装とモック実装の切り替えは、Daggerを用い、Moduleでそれぞれのインスタンスを切り替えることで実現しています。
次はDaggerのModuleで注入するインスタンスを切り替えるコード例です。
@Provides fun provideRepository( mockRepository: RepositoryMockImpl, repository: RepositoryImpl, sharedPreferences: SharedPreferences ): Repository { return if (sharedPreferences.getBoolean("USE_MOCK")) { mockRepository } else { repository } }
このように、Android開発でもレイヤードアーキテクチャを採用することは多いですが、Daggerで注入するインスタンスを切り替えるだけで実装の切り替えが容易にできて柔軟性が高いのが良いですね。

Navigationを用いた画面遷移の仮作成で開発を加速
日頃の開発では一つのプロジェクトに数人で取り組みますが、オンライン名刺ではリリース日が決まっておりスケジュールがタイトなため、SansanのAndroidチーム9名の全員で開発を行いました。そして、オンライン名刺は機能が多く、また同じボタンでも状態によって遷移する画面が複数あるなど複雑な画面遷移を行います。9人全員で並行して開発を進めるのですが、途中からオンライン名刺のプロジェクトに参加したメンバーも多いため、その複雑な画面遷移の仕様を把握するのはコストが高くなります。そこで、Navigationライブラリを導入し、オンライン名刺に関わるすべての画面とそれらの複雑な画面遷移の情報のみをXMLに抽出し、仕様を把握しているメンバーがその管理を行うことでそれぞれの画面担当者はその画面のみに集中して開発することができました。
そして、仕様を十分把握しているメンバーが全画面に対しボタンと画面遷移だけ実装されている空実装を最初に作成しました。続いて各画面の実装を担当するメンバーは渡ってくるすでに定義済みのパラメータを処理し、仮実装されているボタンの画面遷移に正しいパラメータを設定することで画面遷移の仕様を満たすことができます。これにより、画面間のメンバーのコミュニケーション負荷を小さくすることができました。開発期間はコロナ禍の真っ最中ですので、チームは全員リモートでした。ドキュメントに画面間の仕様を書かなくても、チームメンバーに聞かなくてもXMLやコードに書かれている情報で開発を進める事ができるというのはメリットとなりました。
ある画面のNavigationの定義の例は次のようになります。この画面はどのFragmentを使い、どのようなパラメータを受けとり、この画面からどの画面に遷移するかをこのXMLに記述します。
<!-- Fragment 画面名やレイアウトを指定 --> <fragment android:id="@+id/destination_confirm" android:name="com.sansan.feature.hoge.ConfirmFragment" android:label="@string/confirm_toolbar_title" tools:layout="@layout/fragment_confirm"> <!-- Fragmentのパラメータ --> <argument android:name="bizCard" app:argType="com.sansan.domain.entity.BizCard" /> <!-- この画面から遷移する先を設定 --> <action android:id="@+id/action_digitization_requested" app:destination="@id/destination_digitization_in_progress" /> <action android:id="@+id/action_preview" app:destination="@id/destination_preview" /> </fragment>
Navigationを使い便利だった点
- 画面遷移に関する情報だけをXMLに抽出することで、どんな画面が存在するか、どの画面からどの画面に遷移するか、その際のパラメータは何かがわかりやすくなります
- 画面のパラメータを安全に扱うことができるSafe Argsで、不完全なパラメータがそもそもコンパイルエラーとなるようにして開発効率を上げることができます
- Android StudioのNavigation Editorで画面と画面遷移をグラフィカルに確認することができ、オンライン名刺機能全体の把握が容易となります

Navigationを使った上で躓いた点
Navigationは便利ではありますが、いくつか躓いた点もありましたのでその知見を共有します。なお、使用しているNavigationのバージョンは2.0.0です。
ボタン同時押しで遷移先が無いというクラッシュが発生する問題
一つのFragmentから複数の遷移先がある場合にそれらに遷移するボタンを同時押しすると、NavController.navigateでクラッシュする事があります。これは、複数ボタン同時押しすると、後に処理した画面遷移処理を実行した際にはすでに元いた画面から次の画面に遷移しているため、今の画面からはそんな遷移先はありませんと言うエラーのIllegalArgumentException: navigation destination [destination名] is unknown to this NavController が発生しクラッシュします。今回は、現在の画面とその遷移先をチェックする拡張関数を用いて画面遷移後に重複して画面遷移しようとしないようにチェックするようにしました。
fun NavController.navigateSafe( @IdRes destinationId: Int, navDirection: NavDirections) { if (currentDestination?.id == destinationId) { navigate(navDirection) } }
Activityの再生成時に期待しないnavController.graph.startDestinationの値になる問題
開発者オプションのActivityを保持しない設定をした場合などActivityが破棄された後の再表示時に、実際に表示されているFragmentとnavController.graph.startDestinationの値が予期せず異なる値となってしまう問題がありました。実際に表示されているFragmentは正しいですが、startDestinationがgraph.xmlの初期値であるstartDestinationの値になってしまいます。Navigationライブラリのバグのように思えますが、今回はonSaveInstanceStateでstartDestinationを保存するようにしてFragmentとstartDestinationが予期せず異なってしまう問題を回避しました。
Navigationを使ってみて
NavigationはAndroid開発における複雑な画面遷移をより扱いやすくすることができます。オンライン名刺機能は他の画面から独立しているため、今回はオンライン名刺機能に関する部分だけにNavigationを導入しました。その結果、オンライン名刺の画面遷移に関する情報を一括で管理できるようになり、Safe Argsで画面遷移のパラメータを安全に扱うことができるようになりました。しかし、Navigationを導入することで新しいバグを生むこともありました。
ここで学んだのは、やはり画面遷移というのは本質的に難しく、たとえアプリ内の画面遷移が安全そして簡単に書けたとしても、Activityの再生成やActivityとFragmentのライフサイクルの違いなどAndroidのフレームワーク自体の複雑性を隠すことはできないという事です。
まとめ
オンライン名刺のAndroid開発では、短い期間に9人で一つの機能を開発する事になりました。ここまで並列性の高い開発をした経験は初めてでしたが、レイヤードアーキテクチャやNavigation、また1画面に大きな機能が集中すること無くそれぞれの画面で並行開発できたこともあり、特に大きな問題はありませんでした。
短い開発期間、サーバと並行してのアプリ開発、一度に多くの画面を作成する大きなプロジェクトで工夫した点がありました。Repositoryのモック化はモダンなアーキテクチャがもたらす疎結合の利点を再確認し、Navigationは新しいAndroidの画面遷移のあり方を見ることができました。
Android版オンライン名刺の初回リリースの裏側は以上ですが、もちろんオンライン名刺の機能拡張は継続して行っています。オンライン名刺関連を含め、実装したい機能はまだまだあります。そこで、Androidチームはすでに9人というなかなか大きなチームですが、まだまだメンバーを探しています!詳しくはこちらをどうぞ!