I am trying to implement InterstitialAd
in Jetpack Compose. This is my AdsHelpder.kt
class:
class AdsHelper(private val context: Context) : FullScreenContentCallback() {
private var onAdDismissed: (() -> Unit)? = null
private var interstitialAd: InterstitialAd? = null
private val adRequest = AdRequest.Builder().build()
fun onDestroy() {
interstitialAd?.fullScreenContentCallback = null
interstitialAd = null
}
fun onLoadInterstitialAd(canLoad: Boolean) {
val id = context.onString(R.string.interstitial_ad_id)
if (canLoad) {
InterstitialAd.load(context, id, adRequest, object : InterstitialAdLoadCallback() {
override fun onAdLoaded(p0: InterstitialAd) {
interstitialAd = p0
}
override fun onAdFailedToLoad(p0: LoadAdError) {
interstitialAd = null
}
})
}
}
override fun onAdDismissedFullScreenContent() {
interstitialAd = null
onAdDismissed?.invoke()
}
fun onShowInterstitialAd(onAdDismissed: () -> Unit) {
interstitialAd?.apply {
show(context.onMainActivity())
fullScreenContentCallback = this@AdsHelper
}
this.onAdDismissed = onAdDismissed
}
override fun onAdFailedToShowFullScreenContent(e: AdError) {
interstitialAd = null
}
}
Since I need to show the InterstitialAd in several screens, I think this implementation way looks optimal for MainActivity.kt
:
class MainActivity : ComponentActivity() {
private lateinit var data: Data
private var adsHelper: AdsHelper? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
data = ViewModelProvider(this)[Data::class.java]
adsHelper = AdsHelper(this)
setContent {
AppTheme {
val controller = rememberAnimatedNavController()
controller.addOnDestinationChangedListener { _, route, _ ->
adsHelper?.onLoadInterstitialAd(route != HOME)
}
LaunchedEffect(data.interstitial) {
when {
data.interstitial -> adsHelper?.onShowInterstitialAd {
data.interstitial = false
}
}
}
Column {
Scaffold(
Modifier.weight(1f)
) {
NavGraph(controller, data)
}
}
}
}
}
override fun onDestroy() {
adsHelper?.onDestroy()
super.onDestroy()
}
}
Here the Data.kt
:
class Data(app: Application) : AndroidViewModel(app) {
var interstitial by mutableStateOf(false)
}
I could avoide these lines in the MainActivity.kt
...
controller.addOnDestinationChangedListener { _, route, _ ->
adsHelper?.onLoadInterstitialAd(route != HOME)
}
...
and directly call adsHelper?.onLoadInterstitialAd(true)
, but it causes endless logs:
...
D/CCodecConfig: c2 config diff is c2::u32 default.color.matrix = 1
c2::u32 default.color.primaries = 1
c2::u32 default.color.range = 2
c2::u32 default.color.transfer = 3
c2::u32 raw.max-size.height = 360
c2::u32 raw.max-size.width = 640
c2::u32 raw.size.height = 360
c2::u32 raw.size.width = 640
W/Codec2Client: query -- param skipped: index = 1107298332.
D/CCodec: client requested max input size 86400, which is smaller than what component recommended (6291456); overriding with component recommendation.
W/CCodec: This behavior is subject to change. It is recommended that app developers double check whether the requested max input size is in reasonable range.
D/CCodec: encoding statistics level = 0
D/CCodec: setup formats input: AMessage(what = 0x00000000) = {
int32_t height = 360
int32_t level = 256
int32_t max-input-size = 6291456
string mime = "video/x-vnd.on2.vp9"
int32_t profile = 1
int32_t width = 640
Rect crop(0, 0, 639, 359)
}
D/CCodec: setup formats output: AMessage(what = 0x00000000) = {
int32_t android._color-format = 2135033992
int32_t android._video-scaling = 1
Rect crop(0, 0, 639, 359)
int32_t color-standard = 1
int32_t color-range = 2
int32_t color-transfer = 3
Buffer hdr10-plus-info = {
}
int32_t height = 360
int32_t max-height = 360
int32_t max-width = 640
string mime = "video/raw"
int32_t rotation-degrees = 0
int32_t sar-height = 1
int32_t sar-width = 1
int32_t width = 640
int32_t android._dataspace = 260
int32_t color-format = 2130708361
}
I/CCodecConfig: query failed after returning 11 values (BAD_INDEX)
W/Codec2Client: query -- param skipped: index = 1107298332.
D/CCodec: client requested max input size 86400, which is smaller than what component recommended (6291456); overriding with component recommendation.
W/CCodec: This behavior is subject to change. It is recommended that app developers double check whether the requested max input size is in reasonable range.
D/CCodec: encoding statistics level = 0
D/CCodec: setup formats input: AMessage(what = 0x00000000) = {
int32_t height = 360
int32_t level = 256
int32_t max-input-size = 6291456
string mime = "video/x-vnd.on2.vp9"
int32_t profile = 1
int32_t width = 640
Rect crop(0, 0, 639, 359)
}
D/CCodec: setup formats output: AMessage(what = 0x00000000) = {
int32_t android._color-format = 2135033992
int32_t android._video-scaling = 1
Rect crop(0, 0, 639, 359)
int32_t color-standard = 1
int32_t color-range = 2
int32_t color-transfer = 3
Buffer hdr10-plus-info = {
}
int32_t height = 360
int32_t max-height = 360
int32_t max-width = 640
string mime = "video/raw"
int32_t rotation-degrees = 0
int32_t sar-height = 1
int32_t sar-width = 1
int32_t width = 640
int32_t android._dataspace = 260
int32_t color-format = 2130708361
}
I/CCodecConfig: query failed after returning 11 values (BAD_INDEX)
D/MediaCodec: keep callback message for reclaim
D/MediaCodec: keep callback message for reclaim
W/Codec2Client: query -- param skipped: index = 1342179345.
W/Codec2Client: query -- param skipped: index = 2415921170.
W/Codec2Client: query -- param skipped: index = 1342179345.
W/Codec2Client: query -- param skipped: index = 2415921170.
W/Codec2Client: query -- param skipped: index = 2684356609.
W/Codec2Client: query -- param skipped: index = 2684356609.
D/CCodecBufferChannel: [c2.goldfish.vp9.decoder#948] Created input block pool with allocatorID 16 => poolID 60 - OK (0)
D/CCodecBufferChannel: [c2.goldfish.vp9.decoder#415] Created input block pool with allocatorID 16 => poolID 59 - OK (0)
D/CCodecBufferChannel: [c2.goldfish.vp9.decoder#948] Query output surface allocator returned 0 params => BAD_INDEX (6)
D/CCodecBufferChannel: [c2.goldfish.vp9.decoder#415] Query output surface allocator returned 0 params => BAD_INDEX (6)
I/CCodecBufferChannel: [c2.goldfish.vp9.decoder#415] Created output block pool with allocatorID 18 => poolID 151 - OK
I/CCodecBufferChannel: [c2.goldfish.vp9.decoder#948] Created output block pool with allocatorID 18 => poolID 152 - OK
D/CCodecBufferChannel: [c2.goldfish.vp9.decoder#415] Configured output block pool ids 151 => OK
D/CCodecBufferChannel: [c2.goldfish.vp9.decoder#948] Configured output block pool ids 152 => OK
D/Codec2-OutputBufferQueue: remote graphic buffer migration 0/0
D/Codec2-OutputBufferQueue: remote graphic buffer migration 0/0
D/Codec2Client: setOutputSurface -- failed to set consumer usage (6/BAD_INDEX)
D/Codec2Client: setOutputSurface -- generation=9693208 consumer usage=0x900
D/Codec2Client: setOutputSurface -- failed to set consumer usage (6/BAD_INDEX)
D/Codec2Client: setOutputSurface -- generation=9693207 consumer usage=0x900
D/Codec2Client: Surface configure completed
D/Codec2Client: Surface configure completed
I/DynamiteModule: Considering local module com.google.android.gms.ads.dynamite:0 and remote module com.google.android.gms.ads.dynamite:230500000
I/DynamiteModule: Selected remote version of com.google.android.gms.ads.dynamite, version >= 230500000
D/CCodecConfig: c2 config diff is c2::u32 raw.channel-mask.value = 12
D/CCodecBuffers: [c2.android.aac.decoder#912:Output[N]] popFromStashAndRegister: at 1000000000000us, output format changed to AMessage(what = 0x00000000) = {
int32_t aac-drc-album-mode = 0
int32_t aac-drc-boost-level = 127
int32_t aac-drc-cut-level = 127
int32_t aac-drc-effect-type = 3
int32_t aac-drc-output-loudness = -1
int32_t aac-encoded-target-level = -1
int32_t aac-max-output-channel_count = 8
int32_t aac-target-ref-level = 64
int32_t channel-count = 2
int32_t channel-mask = 12
int32_t max-output-channel-count = 8
string mime = "audio/raw"
int32_t sample-rate = 44100
int32_t android._config-pcm-encoding = 2
}
D/CCodecConfig: c2 config diff is c2::u32 raw.crop.height = 360
c2::u32 raw.crop.left = 0
c2::u32 raw.crop.top = 0
c2::u32 raw.crop.width = 640
D/CCodecConfig: c2 config diff is c2::u32 raw.channel-mask.value = 12
D/CCodecBuffers: [c2.android.aac.decoder#806:Output[N]] popFromStashAndRegister: at 1000000000000us, output format changed to AMessage(what = 0x00000000) = {
int32_t aac-drc-album-mode = 0
int32_t aac-drc-boost-level = 127
int32_t aac-drc-cut-level = 127
int32_t aac-drc-effect-type = 3
int32_t aac-drc-output-loudness = -1
int32_t aac-encoded-target-level = -1
int32_t aac-max-output-channel_count = 8
int32_t aac-target-ref-level = 64
int32_t channel-count = 2
int32_t channel-mask = 12
int32_t max-output-channel-count = 8
string mime = "audio/raw"
int32_t sample-rate = 44100
int32_t android._config-pcm-encoding = 2
}
I/DynamiteModule: Considering local module com.google.android.gms.ads.dynamite:0 and remote module com.google.android.gms.ads.dynamite:230500000
I/DynamiteModule: Selected remote version of com.google.android.gms.ads.dynamite, version >= 230500000
V/FA: Inactivity, disconnecting from the service
D/BufferPoolAccessor2.0: evictor expired: 4, evicted: 4
I/DynamiteModule: Considering local module com.google.android.gms.ads.dynamite:0 and remote module com.google.android.gms.ads.dynamite:230500000
I/DynamiteModule: Selected remote version of com.google.android.gms.ads.dynamite, version >= 230500000
I/DynamiteModule: Considering local module com.google.android.gms.ads.dynamite:0 and remote module com.google.android.gms.ads.dynamite:230500000
I/DynamiteModule: Selected remote version of com.google.android.gms.ads.dynamite, version >= 230500000
D/CCodecConfig: c2 config diff is c2::u32 raw.crop.height = 360
c2::u32 raw.crop.left = 0
c2::u32 raw.crop.top = 0
c2::u32 raw.crop.width = 640
D/BufferPoolAccessor2.0: evictor expired: 1, evicted: 1
I/Ads: This request is sent from a test device.
...
furthermore, while generating those logs and especially showing up a InterstitialAd
(when data.interstitial = true
), the the app slows down/freezes.
That's why I decided to load the InterstitialAd
only in target screens, in this case all screens beside the HOME
. But this does not solve the problem and the logs are kept loading endless.
Moreover, according to Crashlytics
and App Quality Insights
there occurs a TransactionTooLargeException
.
But the app does not crash, only freezes until data.interstitial = false
. After that the app runs ok.
Obviously, loading and showing up the InterstitialAd
consumes a lot of resources/memory, especially, if there is a video ad.
I recently started working with Jetpack Compose and many things are new to me. Therefore, my implementation could be wrong and not really optimal. What might be wrong here?
Firstly, I would recommend that you consider separating your concerns in your classes.
class holding your InterstitialAd
and AdaptiveBannerAd
implementation.
@Composable
fun AdMobBanner() {
val currentWidth = LocalConfiguration.current.screenWidthDp
AndroidView(factory = { context ->
AdView(context).apply {
setAdSize(AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize(context, currentWidth))
adUnitId = context.getString(R.string.test_app_banner_id)
loadAd(AdRequest.Builder().build())
}
})
}
// Remove @Inject annotation if you're not using Hilt
class AdMobInterstitial @Inject constructor(
private val context : Context
){
var mInterstitialAd : InterstitialAd? = null
fun loadAd() {
val adUnitId = context.getString(R.string.test_app_interstitial_id)
val adRequest = AdRequest.Builder().build()
InterstitialAd.load(
context, adUnitId, adRequest,
object : InterstitialAdLoadCallback() {
override fun onAdLoaded(ad: InterstitialAd) {
mInterstitialAd = ad
}
override fun onAdFailedToLoad(error: LoadAdError) {
mInterstitialAd = null
}
}
)
}
fun showAd(activity: Activity) {
mInterstitialAd?.let {
it.fullScreenContentCallback = object : FullScreenContentCallback() {
override fun onAdDismissedFullScreenContent() {
super.onAdDismissedFullScreenContent()
mInterstitialAd = null
loadAd()
logger("Ad Dismissed")
}
override fun onAdShowedFullScreenContent() {
super.onAdShowedFullScreenContent()
logger("Ad Showed Full Screen Content")
}
override fun onAdFailedToShowFullScreenContent(p0: AdError) {
super.onAdFailedToShowFullScreenContent(p0)
mInterstitialAd = null
logger("Ad failed to show fullScreenContent")
}
}
it.show(activity)
} ?: loadAd()
}
private fun logger(msg : String) {
Log.e("AdComponents",msg)
}
}
Feel free to change the above Interstitial class according to your need.
I suggest creating a separate NavGraph class to contain your composable.
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AppNavGraph(showAd: () -> Unit) {
val navController = rememberAnimatedNavController()
AnimatedNavHost(
navController = navController,
startDestination = Screens.HomeScreen.name
) {
composable(
route = "Your screen name",
enterTransition = { slideInHorizontally (tween(400)) { 1000 } },
exitTransition = { slideOutHorizontally (tween(400)) { -1000 } },
popEnterTransition = { slideInHorizontally (tween(400)) { -1000 } },
popExitTransition = { slideOutHorizontally (tween(400)) { 1000 } },
) {
LaunchedEffect(key1 = Unit) {
it.whenCreated { showAd() }
}
// call your screen composable here
}
}
}
with Hilt Dependency Injection.
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject lateinit var adMobInterstitial : AdMobInterstitial
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
adMobInterstitial.loadAd()
setContent {
AppTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
AppNavigation {
adMobInterstitial.showAd(this)
}
}
}
}
}
}
Without Dependency Injection.
class MainActivity : ComponentActivity() {
private lateinit var adMobInterstitial : AdMobInterstitial
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
adMobInterstitial = AdMobInterstitial(context = this)
adMobInterstitial.loadAd()
setContent {
AppTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
AppNavigation {
adMobInterstitial.showAd(this)
}
}
}
}
}
}