FLINTERS Engineer's Blog

FLINTERSのエンジニアによる技術ブログ

Jetpack ComposeのCompositionLocalを使ってイベント計測を実装する

こんにちは。GANMA!のAndroidアプリの開発をしています大塚です。

株式会社FLINTERSは2024年1月に10周年を迎えます。それを記念して全社員でブログリレーする企画を行なっています。こちらはその60日目のブログになります。

GANMA!ではGA4(Google Analytics 4)を使ってアプリからのイベントベースのデータを収集しており、機能の開発や改善に役立てています。そのGA4のイベント計測でJetpack ComposeのCompositionLocalを使ってViewの中で実装する方法を試してみたので、今回はその方法を紹介したいと思います。

CompositionLocalとは

Jetpack Composeを使った開発で、UIの下の階層にデータを渡す時はComposableのパラメータに明示的に値を渡す方法が一般的です。しかし、深い階層構造の下部にあるComposableまで値を渡したいときに、この方法をとってしまうとその値を使わない中間のComposableに対してもその値をパラメータとして渡さなくてはいけなくなり、値のバケツリレーが発生してしまうことがよくあります。

CompositionLocalはパラメータでデータを明示的に渡すのではなく、Compositionを通じて暗黙的データを伝えることができるツールです。

例えば、Jetpack Composeでcontextが必要な場合は、val context = LocalContext.currentのような形でcontextにアクセスできますが、そのLocalContextはCompositionLocalですし、MaterialThemeの内部実装なんかを見てみると、colorSchemetypographyshapesはそれぞれCompositionLocalから値が取得されていたりします。

object MaterialTheme {
    /**
     * Retrieves the current [ColorScheme] at the call site's position in the hierarchy.
     */
    val colorScheme: ColorScheme
        @Composable
        @ReadOnlyComposable
        get() = LocalColorScheme.current

    /**
     * Retrieves the current [Typography] at the call site's position in the hierarchy.
     */
    val typography: Typography
        @Composable
        @ReadOnlyComposable
        get() = LocalTypography.current

    /**
     * Retrieves the current [Shapes] at the call site's position in the hierarchy.
     */
    val shapes: Shapes
        @Composable
        @ReadOnlyComposable
        get() = LocalShapes.current
}

CompositionLocalを使ってイベント計測をする

GANMA!のイベント計測は、基本的にViewModelに計測の処理をまとめて記述しています。例えば、アプリ内で単行本の購入が完了した時に送信するイベントは、ViewModelに単行本購入の処理があるので、それと同じタイミングでイベントも送信されるようになっています。しかし、例えばインプレッション計測(表示計測)などは、ViewModelに関連する処理があるわけではないので、計測のためのonViewXXXSectionItem()のようなコールバック関数を、計測対象のComposableからViewModelまでパラメータで渡していく必要があります。

画面内の計測対象が少なかったり、Viewの階層が浅かったりすれば特に気にすることはないですが、一つの画面にインプレッション計測をしたいUIコンポーネントがいくつもあったり、深い階層構造のViewが組まれていたりする場合は厄介です。実際にGANMA!のホーム画面などはインプレッション計測をしているUIコンポーネントが数十種類あり、Viewの階層構造も比較的深いので、ViewModelにイベント計測の処理を記述しようとすると、多くのonViewXXXSectionItem()onClickXXXSectionItem()のようなコールバック関数をViewModelまでバケツリレーする形になってしまいます。

上記の課題感から、GANMA!のホーム画面のような画面では、ViewModelではなくViewの中でイベント計測を完結させてしまおうと考え、CompositionLocalを使ってそれを実現しました。具体的には、Viewから直接イベント計測の処理がまとまっているクラスにCompositionLocalを使ってアクセスができるようにして、Viewの内部でイベントを発火できるようにしました。

この方法は、Now in Android Appでも、スクリーンイベントの計測のために採用されている方法なので、そちらも参考にしてみてください。

実装方法

以下のようなイベント計測用のInterfaceとクラスがあると仮定します。

interface GoogleAnalytics {

   fun logEvent(eventName: String)
}

class GoogleAnalyticsImpl: GoogleAnalytics {

   override fun logEvent(eventName: String) {
      firebaseAnalytics.logEvent(eventName)
   }
}

次にCompositionLocalを作成します。CompositionLocalを作成する方法はcompositionLocalOfを使う方法と、staticCompositionLocalを使う方法がありますが、今回はstaticCompositionLocalOf()を使ってCompositionLocalを作成します。

val LocalGoogleAnalytics = staticCompositionLocalOf<GoogleAnalytics> {
    NoOpGoogleAnalyticsImpl()
}

CompositionLocalの定義ができたら、CompositionLocalProviderを使って、CompositionLocalに値を設定します。GANMA!はまだSingle Activityの構成になっていないため、CompositionLocalを使ったイベント計測をしたい画面ごとにCompositionLocalProviderを使ってCompositionLocalに値を設定しています。Single ActivityのアプリであればMainActivityで対応してあげれば問題ありません。

@AndroidEntryPoint
class HomeFragment : Fragment() {

  @Inject
  lateinit var googleAnalytics: GoogleAnalytics

  override fun onCreateView(
     inflater: LayoutInflater,
     container: ViewGroup?,
     savedInstanceState: Bundle?
   ): View? {
       return ComposeView(requireContext()).apply {
           setContent {
              CompositionLocalProvider(LocalGoogleAnalytics provides googleAnalytics) {
                 HomeScreen()
            }
       }
     }
}

以上の設定が完了すると、GoogleAnalyticsがCompositionLocal経由で呼び出すことができるようになり、Viewの内部でイベントの計測ができるようになります。

@Composable
fun HomeScreen() {
   ...

   val analytics = LocalAnalytics.current
   analytics.logEvent("HomeScreen")
}

おわりに

いかがでしたでしょうか?個人的にはとても便利な方法だなと思いつつも、実際に運用していくと、どこにイベント計測の処理が書いてあるのかが分かりづらくなったり、UIコンポーネントを他の画面でも使おうとしたときに、計測するイベントが異なるので、ちょっとリファクタリングしないと使いまわせないといった小さな問題がちょこちょこ発生したりなどしています。このようにCompositionLocalを使ってイベント計測をViewの中に書いていく際は、プロジェクトごとにメリットとデメリットを比較して、一定のルール決めなどをした上で導入いただくのがいいのかなと思います。

参考