dip Engineer Blog

Engineer Blog
ディップ株式会社のエンジニアによる技術ブログです。
弊社はバイトル・はたらこねっとなど様々なサービスを運営しています。

プロダクトにJetpack Composeを導入する際に考えたことと、実際に使ってみた感想

はじめに

こんにちは!ディップ株式会社でAndroidエンジニアをしている吉田です。主にバイトルNEXTアプリの開発を担当しています。

バイトルNEXTは主に正社員案件を取り扱う求人広告サービスです。 私自身dipに正社員として転職して働いているので、求職者であるユーザーや雇用主であるクライアントの課題に当事者の気持ちで共感しながら、課題解決のため日々エンジニアリングに向き合っています!

前置きはさておき、みなさんはJetpack Composeの導入を進めていますか? バイトルNEXTでは一部の画面のリニューアルに伴って、Jetpack Composeの導入を行いました。

入社1年目の新卒や、初めてJetpack Composeを触るメンバーもいる中で安定した運用をするために工夫したポイントと、実際に使ってみての効果や感想を記事として書き起こしてみましたので、ぜひ最後までお付き合いください🙇

対象の読者

  • Androidアプリエンジニアの方
  • Jetpack Composeの導入について興味がある方
  • 宣言型UIについて興味がある方

Jetpack Composeについて

AndroidのUIを宣言的に記述できるUIツールキットです。 ”Compose”と略されることが多いです。

2021 年 7 月 28 日にStable版がリリースされました🚀

Compose以前からある旧来のUIツールキットは”Android View”と呼ばれていて、Androidアプリ開発ではAndroid ViewからComposeに移行することの関心が非常に高まっていると感じています。実際に商用プロダクトへ導入された事例も増えています。

導入の動機

優れたものとしてトレンドになっていて、その恩恵を受けたかった

Composeのリリース以前から、宣言型UIはGUIアプリケーション開発のトレンドとして受け入れられていました。

レイアウトと振る舞いの記述を半ば強制的に分断される命令型UIよりも、

  • 分断が少ないので人間がコードを記述しやすい
  • UIの振る舞いや状態の表現が、より明確になる
  • 結果的に開発効率や開発体験の向上に繋がっている

というところが大きな要因だと解釈しています👀

バイトルNEXTにおいても、旧来のAndroid Viewから移行することで保守性と開発速度を上げられるのでは、という期待感がありました。

プロダクトの性質上、投資効果が高そうだった

Android Viewにおけるリスト表示はかなり複雑でした。

メモリ効率の問題を解消するためにUIを使い回す思想があり、それを実現するRecyclerViewというUIコンポーネントが用意されています。

これを用いたリスト表示には、LayoutManager、Adapter、ViewHolderなどの実装が必要です。実装ファイルが多く使用言語もバラつくため、認知負荷が高い実装になりやすいです。

他所からAndroid開発にスライドしたエンジニアは、リストUIの実装でRecyclerViewと邂逅して一度衝撃を受けるのではないかなと思います…😇

弊社のプロダクトは、求人リストを表示した上で、そこに広告枠やレコメンドを差し込む機能を持っています。 要するに、リスト表示の制御がソフトウェアの重要な仕事の一つになっています。これが複雑で難しいAndroid Viewの性質は、サービスのボトルネックと言って差し支えないと考えています。

RecyclerViewのコンポーネント構成図

一方、Composeでリスト表示するLazyColumnは、ほぼKotlinのみで完結して実装ファイルが膨らみづらいので、従来と比べて複雑さを抑えやすいです。

Android ViewとJetpack Composeの比較w650

Composeへ移行することで、求人の表示機能の保守運用がしやすくなり、新規施策も実装コストを抑えて実現しやすくなることが想像できます。

プロダクトチームのアジリティや持続可能性を高める上で有効な技術投資になるのではないかという期待も、導入の動機として大きかったです。

プロダクトへの導入

以降のセクションでは、バイトルNEXTでのCompose導入の取り組みの一部として、UIの分割指針を紹介します。

前提

技術スタックとソフトウェアアーキテクチャについては

  • Jetpack Compose + AACのViewModel + Dagger Hilt
  • Google公式が推奨している MVVM + レイヤードアーキテクチャ

の構成を前提とした解説になります。

また、画面を丸々リニューアルするプロジェクトだったため、Viewを部分的かつ徐々に置き換える話は本記事では出てきません。ご了承ください🙇

UIの分割指針について

Composeの導入にあたって、FatなUI実装が量産されたり、UIの組み方に個人差が出て保守しづらくなる問題を防ぐために、UIの分割指針を定めたいと考えました。

Atomic Designが真っ先に浮かびましたが、以前Webのフロントエンドで実践した際に「分割ルールが難しい」「エンジニア間で分割粒度が揃いにくい」という具合で、うまく扱い切ることができませんでした。そのため、Atomic Designの採用には抵抗がありました。

これに関してはDroidKaigi 2023の構成と、リポジトリに上がっていたIssueが大変参考になりました。

バイトルNEXTではこれに倣って、

  • Screen/Content
  • Section
  • Component

という、大きく3層に分ける分割指針を取ることにしました。

バイトルNEXTアプリにおける、それぞれの層の指針について解説します。

Screen/Contentについて

最も大きいコンポーネントです。1画面全体に相当するものです。

実装では、一つの画面のをScreenとして定義して、同一ファイル内にprivateな関数でContentを書いています。

Screenが”枠”で、Contentが実際にコンテンツの表示を受け持つ”中身”のような関係です。

バイトルNEXTでは以下のような実装パターンに落ち着いています。

@Composable
fun XXScreen( // SuffixにScreenをつける
    viewModel: XXViewModel = hiltViewModel()
) {
    // ViewModelからUiStateを取得
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    XXContent(
        uiState = uiState,
        onClick = { viewModel.XXX() } // イベントホイスティング
    )
}

@Composable
private fun XXContent( // SuffixにContentをつける
    uiState: HogeUiState,
    onClick: () -> Unit
) {
    Scaffold(
        topBar = { ... },
        .
        .
    ) {
        Column(
            modifier = Modifier.clickable{ onClick(...) }
        ) {
            XXSection(uiState = uiState)
            Spacer()
            YYComponent(
                modifier = Modifier.clickable{ onClick(...) }, 
                name = uiState.name
            )
            Spacer()
            .
            .
            .
        }
    }
}

@Preview
@Composable
fun XXScreenPreview() { // エラーでプレビュー不可
    XXScreen()
}

@Preview
@Composable
fun XXContentPreview() { // プレビュー可能
    XXContent(
        uiState = XXUiState(),
        onClick = {}
    )
}

ScreenとContentを分けた構造についてですが、これは実装中のUIをプレビューで動かすためのプラクティスとなっています。

ComposableのプレビューはHiltによってDIされるViewModelを解決できません。

そこで、ScreenのレイヤーでViewModelからuiStateを取り出してContentに渡し、ContentとViewModelを切り離すことでプレビューを機能させ開発効率を上げています。

上記の例の場合、XXScreenはプレビューできませんが、XXContentはプレビュー可能です。

Google公式のアーキテクチャガイドに則り、UIイベントをScreenまで吸い上げてViewModelを触るようなイベントホイスティングを意識しています。

Sectionについて

画面を構成する中程度のUI要素です。

「ヘッダーやフッター、リスト表示、何らかのまとまった情報を持つUI」を期待していますが、見た目の大きさを基に判断することがほとんどです。

図の緑枠がSectionに相当するUIとなっています。

定義が曖昧ですが、チームでは曖昧さと実装ブレを一定許容する方針でSectionを取り扱っています。

前述したDroidKaigi/conference-app-2023のissue内でもブレを許容する方針で議論が着地していて、その影響も受けていますが、この程度の曖昧さならば、必要な保守性を保ちながら実装者がルールに縛られて手が止まるケースも回避できるだろうとチームで判断しました。

Componentについて

Screen/Content、Section以外のもので、Sectionよりも小さいUI要素です。

実装するUIとしては最小の単位で、「加工表示するテキスト」「タップ可能な画像」「テキスト + アイコン」のようなケースでよく作られています。

導入してみて

Atomic Designの分割指針ですと、「パーツが機能や意味を持っているか」の観点でも分割が行われる一方で、バイトルNEXTチームでは「ほぼパーツの大きさしか見ない」という、言ってしまえばかなり大雑把な区切り方となっています。

しかし、冒頭で挙げた

  • FatなUI実装の量産を防ぐ
  • UI実装の個人差を抑える

という観点では十分に機能しており、保守性が向上した実感があります。

また、Android View時代よりも作成するファイルや記述量が減ったことで、開発効率と開発体験の向上も感じています。主機能であるリスト表示でRecyclerViewから脱却できたことは、やはり効果が大きかったと手応えを感じています。

分割統治が機能したことでPRレビューもやりやすくなり、ジュニアレベルのメンバーもレビュアーに挑戦し、質の良いフィードバックを行えるようになりました。これも嬉しい効果でした。

総じて、Composeへの移行を経て開発効率と開発体験が向上していて、やって良かったと感じています。

Compose導入以外の取り組みについて

今回はComposeに絞ったお話がメインでしたが、アプリケーションアーキテクチャ全体の見直しや、LiveDataからFlowへの移行、ユニットテストの実装、CI/CDの整備なども行いました。(盛りだくさんで大変でしたが充実していました✨)

View以外のコード品質の向上や環境の整備も、Composeの導入が上手くいった要因として大きいと考えています。

特にアーキテクチャの見直しに関しては、ソフトウェアが規則的な構造を持てたことで生成AIによるコード出力の精度が向上しました! 生成AI自体の進化スピードも相まって、想像以上の開発効率向上を実感しています🤖✨

今回スコープ外としたCompose以外のトピックについては、また機会があったらテックブログの記事にしようと思います!

まとめ

今回ご紹介したお話は、2023年のDroidKaigiの話がリファレンスであったり、あまり真新しいものではなかったかもしれません。

ですが、商用アプリをComposeで開発している現場の活きた知見が、導入を検討している方の助けになったらいいなという想いで記事を書かせていただきました!

Composeの適切な導入の流れが社外にも伝播することで、効率や体験の改善が連鎖したら嬉しいです!

さいごに

ディップ株式会社ではエンジニアを募集中です。もちろんAndroidエンジニアも絶賛募集中です! 興味がある方は下記リンクからぜひ応募をよろしくお願いします。カジュアル面談も大歓迎です!🙌

最後まで読んでいただき、ありがとうございました!