You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Jetpack Compose中预加载激励广告的正确方式:跨屏幕加载展示的可行性与优化方案咨询

Hey there! Let's break down your rewarded ad implementation step by step—covering compliance, safety, and cleaner, more maintainable approaches.

1. Is Your Current Implementation Compliant & Safe?

Good news: your core logic aligns with AdMob's requirements, but there are a few things to watch out for:

  • Reward Triggering: You're granting rewards inside the RewardedAd.show callback, which is exactly what Google recommends. This callback is the official signal that a user has completed watching the ad, so this part is fully compliant and safe.
  • Memory Leak Risk: Your mRewardedAd is a global variable. Make sure you nullify it when the host activity/fragment is destroyed (e.g., in onDestroy())—otherwise, it can hold references to the context and cause memory leaks.
  • State Consistency: Using LiveData to pass reward states works, but be cautious of configuration changes (like screen rotation). LiveData resends the latest value after a config change, which could lead to duplicate reward processing. Your current code resets isAdRewardGranted to false after handling the reward, which mitigates this, so that's solid.
  • Ad Loading Limits: You mentioned loading ads on app start—just avoid spamming load requests in a short period, as this can trigger AdMob's rate limits.
2. Pros & Cons of Your Current Setup

Pros

  • Correct core reward logic (follows AdMob guidelines)
  • Separates state management into ViewModel/LiveData, which fits MVVM best practices

Cons

  • Global ad instance (mRewardedAd) creates tight coupling and memory leak risks
  • Exposing a MutableLiveData (getIsAdRewardGranted) from your ViewModel lets external code accidentally modify state—you should expose an immutable LiveData instead
  • Ad loading logic is tied to an Activity, which makes it harder to reuse across screens
3. Better Implementation Options

Let's look at a more modular, maintainable approach:

Option 1: Encapsulate Ads in a Singleton Manager

Create a dedicated class to handle all ad operations, decoupling them from UI components:

class AdManager private constructor() {
    private var mRewardedAd: RewardedAd? = null
    private val _rewardEvent = MutableSharedFlow<RewardModel>()
    val rewardEvent: SharedFlow<RewardModel> = _rewardEvent // Expose immutable flow

    fun loadRewardedAd(context: Context, adUnitId: String) {
        val adRequest = AdRequest.Builder().build()
        RewardedAd.load(context, adUnitId, adRequest) { callback ->
            callback.result?.let { ad ->
                mRewardedAd = ad
                Timber.i("AdMob | Rewarded ad loaded successfully")
            } ?: run {
                Timber.i("AdMob | Failed to load ad: ${callback.message}")
            }
        }
    }

    fun showRewardedAd(activity: Activity, rewardType: String) {
        mRewardedAd?.show(activity) {
            // Trigger reward via flow (one-time event, no config change resends)
            _rewardEvent.tryEmit(RewardModel(true, rewardType))
            Timber.i("AdMob | User earned reward: $rewardType")
            // Pre-load next ad immediately for future requests
            loadRewardedAd(activity, YOUR_AD_UNIT_ID)
        } ?: run {
            Timber.i("AdMob | Ad not ready—loading now")
            loadRewardedAd(activity, YOUR_AD_UNIT_ID)
            // Optional: Show a toast to user saying "Ad loading, try again soon"
        }
    }

    fun clearAdReferences() {
        mRewardedAd = null // Prevent memory leaks when app/activity closes
    }

    companion object {
        val instance: AdManager by lazy { AdManager() }
    }
}

data class RewardModel(
    val isRewardGranted: Boolean,
    val rewardType: String
)

Update Your ViewModel

Use a SharedFlow to listen for reward events (better than LiveData for one-time actions):

class ShopViewModel : ViewModel() {
    private var _uiState by mutableStateOf(ShopState())
    val uiState: ShopState get() = _uiState

    init {
        // Listen for reward events in the ViewModel
        viewModelScope.launch {
            AdManager.instance.rewardEvent.collect { reward ->
                if (reward.isRewardGranted) {
                    processReward(reward.rewardType)
                }
            }
        }
    }

    private fun processReward(rewardType: String) {
        Timber.i("Processing reward: $rewardType")
        // Add your reward logic here (e.g., unlock clues, update user data)
    }

    fun requestRewardedAd(activity: Activity) {
        AdManager.instance.showRewardedAd(activity, "clue")
    }

    fun loadInitialAd(context: Context) {
        AdManager.instance.loadRewardedAd(context, YOUR_AD_UNIT_ID)
    }
}

Update Your Compose Screen

Simplify the UI logic by moving ad handling to the ViewModel:

// Load ad when the screen initializes
LaunchedEffect(Unit) {
    shopViewModel.loadInitialAd(context)
}

// Button to trigger ad
Button(
    onClick = {
        (context.findActivity() as? AppCompatActivity)?.let { activity ->
            shopViewModel.requestRewardedAd(activity)
        }
    }
) {
    Text("Watch Ad to Unlock Clue")
}

Key Improvements Here

  • Decoupling: Ads are managed in a dedicated class, so you can reuse this logic across any screen
  • Safety: SharedFlow avoids duplicate reward triggers from config changes
  • Maintainability: Clear separation of concerns (UI handles user input, ViewModel manages business logic, AdManager handles ads)
4. Final Compliance Reminders
  • Never grant rewards before the ad's completion callback fires—this will get your account flagged
  • Don't simulate ad completion events (e.g., manually calling reward logic without the user watching the ad)
  • Always inform users what reward they'll get before they start watching the ad (no bait-and-switch!)

内容的提问来源于stack exchange,提问作者AloneWalker

火山引擎 最新活动