Search code examples
androidxmlviewandroid-jetpack-composeadmob

Admob native ads in Jetpack Compose?


I am trying to implement a basic Native ad from adMob to fit the design of my app in Jetpack Compose, but I don't understand how the onClick is supposed to be handled.

I know that it is possible using Viewbinding, but there has to be a Jetpack Compose way as well, as this is supposed to be the future of android development.

My full composable:

object Advertising {
        var currentImageAd: NativeAd? by mutableStateOf(null)
    }
    
    @Composable
    fun ImageAd(modifier: Modifier = Modifier, adId: String, nativeAd: NativeAd?, setNativeAd: (NativeAd?) -> Unit) {
        val context = LocalContext.current
        var iconPainter = rememberAsyncImagePainter(
            model = ImageRequest.Builder(LocalContext.current)
                .data(nativeAd?.icon)
                .size(coil.size.Size.ORIGINAL)
                .build()
        )
    
        DisposableEffect(Unit) {
            val adLoader = AdLoader.Builder(context, adId)
                .forNativeAd { ad ->
                    if (nativeAd == null) {
                        setNativeAd(ad)
                    }
                }
                .withAdListener(object : AdListener() {
                    override fun onAdFailedToLoad(loadAdError: LoadAdError) {
                    }
                })
                .withNativeAdOptions(NativeAdOptions.Builder().build())
                .build()
    
            adLoader.loadAd(AdRequest.Builder().build())
    
            onDispose {
    
            }
        }
    
        nativeAd?.let { adData ->
            Box.SimpleBox(modifier,
                icon = R.drawable.advertisement,
                iconColor = colors.ad,
                headline = adData.headline,
                body = adData.body
            ) {
                Row(modifier = Modifier.height(IntrinsicSize.Max), horizontalArrangement = Arrangement.spacedBy(Padding.ContentSeparation.standard)) {
                    Image(painter = iconPainter,
                                contentDescription = null,
                                modifier = Modifier
                                    .fillMaxHeight()
                                    .aspectRatio(1f)
                            )
                    Button_Basic(shape = Shapes.tight, 
                         color = colors.ad, 
                         text = adData.callToAction ?:"Mehr", 
                         icon = R.drawable.web_exit, 
                         onClick = { 
                               //What to call here? 
                         })
                }
            }
        }
}

Box.SimpleBox is just a Box Container with rounded corners and Button_Basic a clickable Box with text. The Advertisements headline, body and callToAction text all all loaded and displayed correctly, but I do not understand how you are supposed to get the onClick to work.

It seems like the AdMob Sdk handles onClick for traditional views in Xml by using the ads assets automatically, but it don't know how to get it to work in Compose. Also it seems weird to not be able to create a custom Button for a Native ad which is supposed to be customisable.

Is this just not possible in Compose yet and I need to write the ad code in XML or how do I have to approach this?


Solution

  • Yes it is possible.

    Use below code to implement Native Ad using Jetpack Compose.

    Make a remember of NativeAd object for compose function or screen

     var nativeAd by remember { mutableStateOf<NativeAd?>(null) }
    

    Also intialiaze NativeAd object using LaunchEffect as shown below

    LaunchedEffect(null) {
        loadNativeAd(context, context.resources.getString(R.string.native_ad_id)) {
            nativeAd = it
        }
    }
    
    fun loadNativeAd(context: Context, adUnitId: String, callback: (NativeAd?) -> Unit) {
        val builder = AdLoader.Builder(context, adUnitId)
            .forNativeAd { nativeAd ->
                callback(nativeAd)
            }
        
        val adLoader = builder
            .withAdListener(object : AdListener() {
                override fun onAdFailedToLoad(adError: LoadAdError) {
                    callback(null)
                }
            })
            .withNativeAdOptions(NativeAdOptions.Builder().build())
            .build()
        
        adLoader.loadAd(AdRequest.Builder().build())
    }
    

    Use below functions to fetch the UI and load the Ad

    @Composable
    fun CallNativeAd(nativeAd: NativeAd) {
        NativeAdView(ad = nativeAd) { ad, view ->
            LoadAdContent(ad, view)
        }
    }
    
    @Composable
    fun NativeAdView(
        ad: NativeAd,
        adContent: @Composable (ad: NativeAd, contentView: View) -> Unit,
    ) {
        val contentViewId by remember { mutableIntStateOf(View.generateViewId()) }
        val adViewId by remember { mutableIntStateOf(View.generateViewId()) }
        AndroidView(
            factory = { context ->
                val contentView = ComposeView(context).apply {
                    id = contentViewId
                }
                NativeAdView(context).apply {
                    id = adViewId
                    addView(contentView)
                }
            },
            update = { view ->
                val adView = view.findViewById<NativeAdView>(adViewId)
                val contentView = view.findViewById<ComposeView>(contentViewId)
    
                adView.setNativeAd(ad)
                adView.callToActionView = contentView
                contentView.setContent { adContent(ad, contentView) }
            }
        )
    }
    
    @OptIn(ExperimentalFoundationApi::class)
    @Composable
    private fun LoadAdContent(nativeAd: NativeAd?, composeView: View) {
        Card(
            modifier = maxWithModifier
                .padding(horizontal = padding16, vertical = padding4)
                .clip(CardDefaults.shape)
                .combinedClickable {
                    composeView.performClick()
                },
            colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
        ) {
            nativeAd?.let {
                Column(
                    modifier = maxWithModifier
                        .padding(padding8)
                ) {
                    Row(
                        modifier = maxWithModifier,
                        horizontalArrangement = Arrangement.Start
                    ) {
                        val icon: Drawable? = it.icon?.drawable
                        icon?.let { drawable ->
                            Image(
                                painter = rememberAsyncImagePainter(model = drawable),
                                contentDescription = "Ad"/*it.icon?.contentDescription*/,
                                modifier = Modifier
                                    .size(padding40)
                                    .padding(end = padding8),
                                contentScale = ContentScale.Crop
                            )
                        }
    
                        Column {
                            Text(
                                text = it.headline ?: "",
                                style = MaterialTheme.typography.bodyLarge,
                                color = MaterialTheme.colorScheme.onSurface,
                            )
                            Text(
                                text = it.body ?: "",
                                style = MaterialTheme.typography.bodySmall,
                                color = MaterialTheme.colorScheme.onSurface,
                            )
                        }
                    }
    
                    it.callToAction?.let { cta ->
                        CornerButton(
                            modifier = maxWithModifier,
                            onClick = {
                                composeView.performClick()
                            },
                            content = {
                                Text(
                                    text = cta.uppercase(),
                                    color = MaterialTheme.colorScheme.onSurfaceVariant,
                                    style = MaterialTheme.typography.titleMedium
                                )
                            }
                        )
                    }
                }
            } ?: run {
                // Placeholder for loading state or error state
                Text("Loading ad...")
            }
        }
    }
    

    Use compose coil for load Async Image (rememberAsyncImagePainter)

    implementation("io.coil-kt:coil-compose:2.6.0")
    

    Now just call the below function from your compose UI to load the Ad

    if (nativeAd != null) {
        CallNativeAd(nativeAd!!)
    }