i Cubed Systems Engineering blog

株式会社アイキューブドシステムズの製品開発メンバーが、日頃のCLOMO開発の様子などを紹介します。

モダンなAndroidアプリ開発への取り組み

はじめに

アイキューブドシステムズで製品開発運用本部の製品開発部に所属している kazuya-oota です。 現在私は新規アプリケーション開発に携わっており、Androidアプリケーションにおけるモダンなアーキテクチャについて調べる機会がありました。 同じチームで開発をするt-yusakuさんからも多大な情報提供をいただきながら、機能の一部を切り取ったプロトタイプを作成しました。 今回はそのプロトタイプ作成からモダンなアプリケーション開発の取り組みについて紹介したいと思います。

アーキテクチャ

Androidアプリケーションの骨子となるアーキテクチャですが、これはGoogleよりベストプラクティスが公開されており、制約のない新規のアプリケーションではこれに従うことがよいと考えました。 アプリ アーキテクチャ ガイド

Android開発者の方であれば、ご存知の通りかもしれませんが簡単に解説すると、 アプリケーションを少なくとも2層に分割して、関心の分離を行います。 ドメインレイヤーは必須ではありませんが、こちらがあることでUIレイヤとデータレイヤの間のやり取りが簡素化します。

コーディングでありがちな、ActivityやFragmentにコードが集約してしまう問題を排除することができ、アプリの拡張、テストがしやすくなります。

UI構成

アプリケーションのUI実装において、画面のレンダリングには大きく2種類の方法があります。

  • res/layoutにレイアウトリソースを定義、またはコードでViewを生成する
  • Jetpack ComposeでViewを生成する

従来の方法では前者を採用することが一般的でしたが、2021年7月にJetpack Composeのメジャーリリースされたことでこちらも選択肢となりました。 Jetpack Composeは、従来の実装とは大きく変化したことにより、習得にはそれなりの学習コストが必要になります。

これらの学習には、Googleから多数の学習方法が提供されています。 チュートリアルパスウェイから基本を学習でき、 よりリッチなサンプルがGithubに公開されています。

また、Accompanistライブラリにより、一般的に開発者が利用するもサポートされています。 例としては以下のようなものが挙げられます。

  • ページング(ViewPager)
  • カメラや位置情報などの権限を取得
  • 画面遷移のアニメーションをカスタマイズ
  • ボトムシートを表示する
  • リストのPullToRefresh
  • Webを表示する
  • etc...

今回は、新規のアプリケーションということもありJetpack Composeを全面的に採用し、全ての画面をJetpack Composeで構成することとしました。

実際にやってみると、従来の方法から大きく変わっていることもあり苦労する点もありましたが開発者にとって利点となる部分も多いと感じました。

UIを構成する各部品にテストデータを与えることで画面全体のプレビューを作成することもできます。

例えば、ProgressDialogを表示するテストデータに変更するだけで、以下のように画面にダイアログを表示した場合のプレビューを確認できます。

今までは、実機やエミュレータにデプロイする必要がありましたが、Android Studio上で確認できるようになりました。

HiltによるDI(依存性注入)

Androidのアーキテクチャに従って実装を行うと、各クラスが適切に動作するためには他のクラスへの依存が発生します。 これらの依存関係の管理するために、AndroidではHiltを利用することが推奨されています。

Androidアプリケーションの開発にあたって、DI(依存性注入)は必須ではないため利用したことが無いという方もいらっしゃるのではないでしょうか。かくいう私はその一人でした。

依存性注入についての詳細な解説や実装方法は省かせていただきますが、以下のようなメリットが得られます。

  • コードを再利用できる
  • リファクタリングが容易になる
  • テストが容易になる

Hiltは依存性注入を行うJetpackの推奨されたライブラリで、よく知られたDIライブラリである Daggerの上に構築されています。これにより、コンパイル時の正確性、実行時のパフォーマンス、スケーラビリティ、Android Studio のサポートといったDaggerの恩恵を受けられます。

具体的な導入方法は、公式の解説と同時に紹介されるサンプルプロジェクトが参考になります。

今回プロジェクトに導入したことで、テストが容易になるというメリットを実感できました。

Repositoryクラスをモック化することができるので、データを外部から得る方法がない場合であっても単体テストを容易に実施することができます。 そのためには、@TestInstallInを使って擬似的な依存性注入を行います。

本番コードでは、NetworkRepositoryを利用するクラスはRemoteNetworkRepositoryを指定します。

@Module
@InstallIn(SingletonComponent::class)
interface DataModule {
    @Binds
    fun bindNetworkRepository(
        networkRepository: RemoteNetworkRepository
    ) : NetworkRepository
}

単体テスト用に@TestInstallInを使って、単体テストコードはTestNetworkRepositoryを利用するよう指定します。

@Module
@TestInstallIn(
    components = [SingletonComponent::class],
    replaces = [DataModule::class]
)
interface TestDataModule {
    @Binds
    fun bindNetworkRepository(
        networkRepository: TestNetworkRepository
    ) : NetworkRepository
}

このようにすることで、NetworkRepositoryに依存するクラスをテストする場合に、モックで単体テストをすることが可能です。

Room

アプリケーションの永続データを扱うにあたっては、いくつかの方法が考えられます。

  • SharedPreference
  • Datastore
  • SQLite データベース

簡易的なデータであれば、SharedPreferenceやDatastoreで扱うと思います。 しかし、データ量が多く、複雑であればSQLiteを利用することが一般的です。 AndroidでSQLiteデータベースを扱う際にSQLite APIを扱うことはすでに非推奨とされています。

これに変わる方法として、Jetpack ライブラリスイートのRoomが推奨されています。 SQLite APIを扱うよりも、以下のようなメリットが得られます。

  • SQLクエリのコンパイル時検証
  • 繰り返しが多く間違いを犯しやすいボイラープレートコードを最小限に抑える便利なアノテーション
  • 効率的なデータベース移行パス

SQLite APIを直接扱う場合には、構文の間違いを検知する方法はなかったため単体テストコードや実機、エミュレータで動作させる必要がありました。 RoomではDAOオブジェクトの@Queryアノテーションの構文がチェックされるため早期に間違いを発見できるようになりました。 また、SQLite APIでは、クエリオブジェクトからデータオブジェクトに変換するために大量のボイラープレートを作成する必要がありました。Roomではエンティティで定義したデータオブジェクトに格納されるためコード量を削減することができるようになっています。

WorkManager

ユーザがアプリケーションを操作しない間にも定期的にデータを更新するためには、永続処理は必須です。

これを実現するためには、従来ではAlarmManager+IntentServiceやJobSchedulerが一般的でした。 現在では、JetpackのWorkManagerは永続処理において推奨のAPIとなっています。

WorkManagerでは3種類のタスクを処理することが可能です。

  • 即時
    • すぐに開始してすぐに完了する必要があるタスク。優先処理が可能。
  • 長時間実行
    • 10分以上の長時間にわたって実行される可能性のあるタスク。
  • 延期可能
    • 遅延実行と定期的な実行できる、スケジュール設定されたタスク。

また、これらの特長を持っており、メリットになります。

  • 処理の制約
    • 定額ネットワーク、バッテリー残量などによる制約
  • 強力なスケジュール設定
    • 1回、繰り返し処理や、デバイスの再起動後のスケジューリング、Dozeなどの省電力設定に従った動作
  • 優先処理
    • 即時処理の優先
  • 柔軟な再試行ポリシー
    • 指数バックオフなどの柔軟な再試行
  • 処理の連結
    • ワーカーの直列、並列、チェーン実行

デバイスの再起動後であってもスケジューリングが継続するのは従来の方法ではできませんでしたが、WorkManagerを利用することで容易に実現することができます。

プロジェクト紹介

これまでに挙げたモダンな開発手法の導入にあたって参考とさせていただいたプロジェクトをご紹介します。

終わりに

今回挙げたもの以外にも開発効率の向上、最適化につながる技術はまだまだ存在します。 Androidのアプリケーション開発はJetpackを中心に進化が続けられており、常にリファクタリングする必要があると感じました。

弊社では、モダンな開発を推進したいという方を募集しています。 ご興味がありましたらお気軽にお問い合わせください。