こんにちは、モバイル請求書の Android アプリを担当している nakaji です。 前回の iOS アプリの記事に続いて、今回は Android アプリの技術選定と TalkBack 対応についてご紹介します。
アプリのダウンロードはこちらから:
apps.apple.com技術選定
なぜ Jetpack Compose を選んだのか
一つの画面を除いたすべての画面をJetpack Compose で実装しています。
Jetpack Compose は既存の XML ベースの Android View とは異なり、React や Flutter などと同じように宣言的に UI を記述することができます。
社内の他アプリではまだ Android View を利用しており学習コストがかかる懸念もありましたが、以下の理由から請求書アプリでは Jetpack Compose を採用しました。
- 開発開始時点で Jetpack Compose 1.0 のリリースからすでに 1 年半以上経っており、公式ライブラリが充実してきている
- 開発効率が向上した他社事例が多い
- Android View はすでにメンテナンスモードである
- 完全新規のアプリである
- リストなどの基本的な UI が構築しやすい
実際に請求書アプリは請求書の一覧や作成画面などでリスト表示が多く、RecyclerView での実装に比べてとても楽に実装ができました。
一方でアクセシビリティを対応しようとしたときに、Compose UI ライブラリのバグや内部的な実装の都合で TalkBack のフォーカス順がおかしくなることが多い印象を受けました。 また、Experimental アノテーションがついたコンポーネントがまだまだ多く、基本的な UI コンポーネントであっても API が不安定な部分もあり、API の更新による追従はまだまだ必要です。
デファクトスタンダードなライブラリを選択
ライブラリは将来的な保守の観点から以下の技術から選択しました。
- Kotlin 公式のライブラリ (Kotlin Coroutines, Kotlin Serialization)
- Google が開発、推奨しているライブラリ (Jetpack, Accompanist, Dagger Hilt, etc.)
- Android 開発でデファクトスタンダードになっているライブラリ (Retrofit, OkHttp, Timber, etc.)
freee では社内で複数のアプリを開発しており、公式やデファクトスタンダードな技術を選択することで、各人が担当アプリ以外も理解しやすいようにしています。
Android 公式の推奨アーキテクチャに準拠
アプリのアーキテクチャは Android 公式のアプリアーキテクチャガイドの Domain Layer を省略したものです。
ドメインレイヤは、複雑なビジネス ロジック、または複数の ViewModel で再利用される単純なビジネス ロジックをカプセル化します。 すべてのアプリにこのような要件があるわけではないため、このレイヤはオプションです。複雑さに対処する場合や再利用性を優先する場合など、必要な場合にのみ使用してください。
公式ドキュメントでも上記のように言及されており、請求書アプリは画面数が多くなく共有できるロジックも少ないため Domain Layer を省略しています。 ただ、実際には請求書や見積書の作成画面では管理が多く、それに伴って ViewModel が肥大化しており、複雑性への対処として一部 Domain Layer の実装があってもよかったように感じています。
ライブラリ選定と同様に他アプリの担当者が理解しやすいように、公式ガイドで記載のある一般的なアーキテクチャを採用しています。
Jetpack Compose を用いた複雑な UI 実装と TalkBack 対応
作成、編集画面の行の並び替えと削除 UI
請求書や見積書の作成、編集画面の明細行はタップ、並び替え、スワイプして削除の 3 つの動作を行うことができる複雑な UI です。
RecyclerView で並び替えを実装する場合は ItemTouchHelper を使うことが一般的ですが、Jetpack Compose は公式でそういったライブラリはありません。 当初は ComposeReorderable の利用を検討していましたが、最終更新から時間が経っており、各種ライブラリのアップデートも止まっている様子だったので今回は独自に実装を行いました。
detectVerticalDragGestures のようなジェスチャを処理するメソッドを使えば比較的かんたんに実装ができますが、Talkback のみでも操作ができるようにするにはいくつか工夫が必要だったためご紹介します。
ドラッグハンドルの実装
TalkBack 利用中は 2 本指で画面スクロールできますが、もし指がドラッグハンドルに触れてしまうと意図せず並び替えが発生してしまう可能性があるため、請求書アプリでは TalkBack が有効なときはジェスチャを変更しています。
この有効/無効の判定には AccessibilityManager#isTouchExplorationEnabled を利用しており、有効であればドラッグハンドルを含む行全体に detectDragAfterLongPress を、無効であればドラッグハンドルのみに detectVerticalDragGestures を用いた Modifier に差し替えています。これにより、TalkBack を利用しないユーザはドラッグハンドルをドラッグするだけでよく、利用しているユーザはダブルタップしてそのまま長押し、その後ドラッグすると並び替えができるようになっています。
メソッドのドキュメントにもあるとおり、UI の挙動をこれで行うことは技術的負債になりうるため避けるよう記載がありますが、このコンポーネントに関しては変更頻度が低く、作成、編集画面以外で再利用することがないため、例外的にこのメソッドを利用した判定をしています。
様々な場所で無作為に利用されないように、以下のようなアノテーションを用意して利用時にワンクッション挟まるようにしています。
@RequiresOptIn("アプリから状態を取得して処理や表示を変えるといずれかの更新漏れが起きやすくなるため、その対応が必要かよく検討してください") @Retention(AnnotationRetention.BINARY) annotation class TalkBackStateApi
並び替え中の読み上げ
Modifier の付け替えでドラッグをできるようになりましたが、並び替え中にどこに移動したのかは別途読み上げを設定する必要があります。 状態変化に応じて読み上げさせるには LiveRegion セマンティクスと View#announceForAccessibility の二通りがあります。今回は保持している UIState と LiveRegion を連携して読み上げさせることは難しかったため、後者の announceForAccessibility を利用しました。
Google Keep アプリの並び替えを参考に、それぞれ以下の読み上げを実装しています。
- ドラッグ開始: 「場所 x の項目を並び替えています」
- 位置変更: 「場所 y」
- ドラッグ終了: 「ドラッグを終了しました」
明細行の削除カスタムアクション
明細行の削除は以下の二通りの方法があります。
- 明細行をタップして表示された編集シート上で削除ボタンを押す
- 明細行を横スワイプして表示された削除ボタンを押す
TalkBack では先述の通り、2本指で画面のドラッグ処理になるため、画面が見えない状態では特定の明細行を横スワイプするのは容易ではありません。
そこで TalkBack が有効であれば横スワイプ自体を無効化し、かわりにカスタムアクションで明細行の削除を行えるようにしました。
カスタムアクションは
- Gmail アプリのメール一覧の横スワイプでも利用されている
- 設定すると読み上げの最後にカスタムアクションの利用方法が追加される
ので、普段から TalkBack を利用しているユーザであれば、違和感なく扱えるだろうと判断してこのように実装を行いました。
まとめ
Android アプリの技術選定と複雑な UI とその TalkBack 対応の工夫を紹介しました。今回は主にジェスチャやその切り替えについてでしたが、ほかにも TalkBack 利用者がアプリを利用しやすい工夫がたくさんあります。それらは先日頒布された freee 技術の本でも紹介していますので、ご興味あればそちらもご覧ください!