🐾

【RevenueCat】Jetpack Compose で CompositionLocal を使ったサブスク状態管理 Tips

に公開

RevenueCat、便利ですよね。

Jetpack Compose 製の個人アプリにサブスクを導入する際、RevenueCat のおかげでスムーズに実装できました。今回は、その際に利用した CompositionLocal を使った サブスク状態の管理Tips をご紹介したいと思います。


なぜCompositionLocalを使うのか?

サブスクの情報は、アプリの様々な場所で必要になります。例えば、サブスク購読者限定の機能を有効にしたり、非購読者には購読を促す UI を表示したり。

通常、Jetpack Compose でデータを引き回すには State Hoisting などを使いますが、アプリの規模が大きくなると、必要な場所にバケツリレーのように渡していくのは手間がかかります。コードの見通しも悪くなりがちです。

お行儀よくするなら Repository でサブスク情報を取得して…、などがありそうですが、そこまでする元気もない…。(笑)

そこで、CompositionLocal 。これを使えば、Compose ツリーの特定の部分で共通のデータを提供できます。一度設定してしまえば、そのツリー内のどの Composable からも直接購読情報にアクセスできるようになるので、個人開発アプリではうってつけでした。


CompositionLocalで購読状態をハンドリングする

まずは、購読の状態を表す enum と、その情報を保持するための CompositionLocal を定義しました。この enum には、例えば購読の有無に応じて異なる制限値(カテゴリ作成上限数など)を持たせると、より柔軟に機能の出し分けが可能になります。

// サブスク状態管理用種別
enum class SubscriptionStatus(
    val categoryLimit: Int,
) {
    ACTIVE(
        categoryLimit = Int.MAX_VALUE,
    ),
    INACTIVE(
        categoryLimit = 5,
    );

    val isActivated: Boolean
        get() = this == ACTIVE

    val isDeactivated: Boolean
        get() = this == INACTIVE
}
// SubscriptionLocal.kt
val LocalSubscriptionStatus = compositionLocalOf { SubscriptionStatus.INACTIVE }

次に、アプリのルートとなる Composable 関数( MainActivitysetContent ブロック内)で、RevenueCat から取得したサブスク情報を基に CompositionLocalProvider を設定します。LaunchedEffect を使って RevenueCat の購読情報が更新されるたびに SubscriptionStatus を更新するフローを組み込むのがポイントです。こうすることで、ユーザーの購読状態が変更された際も、アプリ全体にリアルタイムに反映されます。

https://sdk.revenuecat.com/android/5.2.1/purchases/com.revenuecat.purchases.interfaces/-updated-customer-info-listener/index.html

// MainActivity.kt (一部抜粋)
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            val subscriptionStatusFlow = remember { MutableStateFlow(SubscriptionStatus.INACTIVE) }
            val subscriptionStatus by subscriptionStatusFlow.collectAsState()

            // Composable でサブスク購読状態の更新
            LaunchedEffect(Unit) {
                Purchases.sharedInstance.updatedCustomerInfoListener =
                    UpdatedCustomerInfoListener { customerInfo ->
                        subscriptionStatusFlow.update {
                            // 有効なサブスクがあるかどうか
                            if (customerInfo.activeSubscriptions.any()) {
                                SubscriptionStatus.ACTIVE
                            } else {
                                SubscriptionStatus.INACTIVE
                            }
                        }
                    }
                // アプリ起動時の初回購読情報取得
                Purchases.sharedInstance.getCustomerInfo { customerInfo, _ ->
                    if (customerInfo != null) {
                        subscriptionStatusFlow.update {
                            if (customerInfo.activeSubscriptions.any()) {
                                SubscriptionStatus.ACTIVE
                            } else {
                                SubscriptionStatus.INACTIVE
                            }
                        }
                    }
                }
            }

            // LocalSubscriptionStatusを提供
            CompositionLocalProvider(
                LocalSubscriptionStatus provides subscriptionStatus,
            ) {
                YourAppNavHost() // アプリのナビゲーションやメインコンテンツ
            }
        }
    }
}

上記のコードでは、MutableStateFlow でサブスク状態を管理し、その最新の値である subscriptionStatusCompositionLocalProvider に渡しています。これで、YourAppNavHost() 以下のすべての Composable 関数が LocalSubscriptionStatus を参照できるようになります。


好きな箇所でCompositionLocalを参照し、サブスク購読者だけの判定を楽にする

CompositionLocalProvider で情報が提供されていれば、あとはどの Composable 関数からでも、current プロパティを使って購読情報を参照するだけとなります。

例えば、購読者のみに表示したいUIがある場合、以下のようにシンプルに条件分岐できます。SubscriptionStatus enum で定義した categoryLimit のような情報も同時に利用できるので、UIの出し分けや機能制限の実装が楽になります。

// SomeScreen.kt
@Composable
fun SomeScreen() {
    // 実際に使う箇所
    val subscriptionStatus = LocalSubscriptionStatus.current

    Column {
        if (subscriptionStatus.isActivated) {
            // サブスク購読者のみに表示されるコンテンツ
            Text("サブスク購読者限定コンテンツ")
            Button(onClick = { /* ... */ }) {
                Text("プレミアム機能へアクセス")
            }
        } else {
            // 非購読者向けのコンテンツ
            Text("プレミアムコンテンツをアンロックするには購読が必要です。")
            Button(onClick = { /* ... */ }) {
                Text("今すぐ購読する")
            }
        }
        // カテゴリの制限値も利用できる
        Text("カテゴリ作成可能数: ${subscriptionStatus.categoryLimit}")
    }
}

まとめ

RevenueCat と Jetpack Compose を組み合わせる際に CompositionLocal を活用することで、サブスク状態の管理を楽に実装することができました。

  • CompositionLocal でサブスク状態をアプリの上位 Composable から一括提供
  • 必要な場所で CompositionLocal.current を使って簡単にサブスク状態を参照
  • LaunchedEffectMutableStateFlow を組み合わせることで、RevenueCat からのサブスク情報更新にもリアルタイムに対応

この記事が、どこかの誰かのアプリ開発の参考になれば幸いです。

Discussion