こんにちは。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
の内部実装なんかを見てみると、colorScheme
、typography
、shapes
はそれぞれ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の中に書いていく際は、プロジェクトごとにメリットとデメリットを比較して、一定のルール決めなどをした上で導入いただくのがいいのかなと思います。
参考
- Locally scoped data with CompositionLocal | Jetpack Compose | Android Developers
- GitHub - android/nowinandroid: A fully functional Android app built entirely with Kotlin and Jetpack Compose
- Y.A.M の 雑記帳: Jetpack Compose の Composition Local ってなんなの?
- Jetpack ComposeのCompositionLocalを使って下位階層にデータを渡す - Kenji Abe - Medium