Ludiras
Ludiras

Reputation: 514

Is there a safer way to get the activity in Jetpack Compose?

I have created a class that acts as a manager to handle the ads in my Jetpack Compose application, the class is as follows:

class InterstitialAdManager(private val context: Context) {

    private var mInterstitialAd: InterstitialAd? = null
    private var lastLoadedUnitAd: String? = null

    init {
        loadInterstitial()
    }

    private fun loadInterstitial() {
        InterstitialAd.load(
            context,
            lastLoadedUnitAd.orEmpty(),
            AdRequest.Builder().build(),
            object : InterstitialAdLoadCallback() {

                override fun onAdFailedToLoad(adError: LoadAdError) {
                    mInterstitialAd = null
                }

                override fun onAdLoaded(interstitialAd: InterstitialAd) {
                    mInterstitialAd = interstitialAd
                }
            }
        )
    }

    fun showInterstitial(
        onAdDismissed: () -> Unit = {},
        onAdShowedFullScreen: () -> Unit = {}
    ) {
        val activity = context.findActivity()

        if (mInterstitialAd != null && activity != null) {

            mInterstitialAd?.fullScreenContentCallback = object : FullScreenContentCallback() {

                override fun onAdFailedToShowFullScreenContent(e: AdError) {
                    mInterstitialAd = null
                    onAdDismissed()
                }

                override fun onAdDismissedFullScreenContent() {
                    mInterstitialAd = null

                    loadInterstitial()
                    onAdDismissed()
                }

                override fun onAdShowedFullScreenContent() {
                    onAdShowedFullScreen()
                }
            }
            mInterstitialAd?.show(activity)

        } else {
            onAdDismissed()
        }
    }

    fun removeInterstitial() {
        mInterstitialAd?.fullScreenContentCallback = null
        mInterstitialAd = null
    }

    fun setAdUnit(unitAd: String) {
        lastLoadedUnitAd = unitAd
        loadInterstitial()
    }

    private fun Context.findActivity(): Activity? = when (this) {
        is Activity -> this
        is ContextWrapper -> baseContext.findActivity()
        else -> null
    }
}

When I was using XML everything was fine, I had no problems using this class. The problem is that to use it with Jetpack Compose and inject it with Hilt I do it in the following way. I have a class that stores the state in a mutableStateOf:

data class DetailState(
    val interstitialAdManager: InterstitialAdManager? = null,
    val currentDesign: Design? = null
)

This state is handled in the ViewModel:

@HiltViewModel
class DetailViewModel @Inject constructor(interstitialAdManager: InterstitialAdManager,   
) : ViewModel() {

    var state by mutableStateOf(DetailState())
        private set

    init {
        state = state.copy(interstitialAdManager = interstitialAdManager)
        state.interstitialAdManager?.setAdUnit("hidden_unit_ad")
    }

I created a Hilt module to create the injection network:

@Module
@InstallIn(SingletonComponent::class)
class AdsModule {

    @Provides
    @Singleton
    fun interstitialAdManagerProvider(@ApplicationContext context: Context) =
        InterstitialAdManager(context)
}

What is the problem? The context I'm injecting is the context of the application that Hilt offers me, so the method I have in my manager class is not able to find the activity in this context. My first thought is to do the following:

@Module
@InstallIn(ActivityComponent::class)
class AdsModule {

    @Provides
    @ActivityScoped
    fun interstitialAdManagerProvider(@ActivityContext context: Context) =
        InterstitialAdManager(context)
}

The problem is that as I have shown above, I have injected the manager in the viewModel, so Hilt gives me a compilation error. The other solution, and it is the only one that has worked for me, is to pass the activity as a parameter in the showInterstitial() method, but the way I pass it in Jetpack Compose is by doing the following cascading:

val activity = LocalContext.current as Activity

From what I have read, this is not the most advisable way to do it and I would like to know if there is a better way to pass the activity or to improve my class in another way that I have not thought of.

Upvotes: 3

Views: 3626

Answers (3)

Teo Coding
Teo Coding

Reputation: 113

From version 1.10 of androidx.activity:

val activity = LocalActivity.current

Android Activity Release Note

Upvotes: 3

Halifax
Halifax

Reputation: 765

Because Activity is inherited from ContextWrapper, and because Activity will trigger Activity#attach method call when it is created, it will execute attachBaseContext(context), It is found that the set context is in ContextWrapper.

And ContextWrapper is inherited from Context, so we can try to get an Activity instance in the following way.

We can get the context of the current context through LocalContext.current, and get the baseContext through a loop. Get the Activity instance by matching the context is Activity condition.

fun Context.findAndroidActivity(): Activity? {
    var context = this
    while (context is ContextWrapper) {
        if (context is Activity) return context
        context = context.baseContext
    }
    return null
}

Upvotes: 3

CommonsWare
CommonsWare

Reputation: 1007474

LocalContext.current is not necessarily an Activity, so that is a risky cast. However, you do not need an Activity. You need a Context that has an Activity somewhere in its hierarchy, so your findActivity() extension function can work. In production code, LocalContext.current should qualify for that (even if it might not in a @Preview function or in an instrumented test). So, if your objective is to simply get InterstitialAdManager working largely as it does now, have showInterstitial() accept a Context, and leverage your existing findActivity() extension function.

Ideally, your ad network SDK would support Compose more cleanly.

Upvotes: 2

Related Questions