Skip to content

Commit 0ba275e

Browse files
Nicholas Ventimigliacopybara-github
authored andcommitted
Added Jetpack Compose to next-gen sample.
PiperOrigin-RevId: 855922809
1 parent eab6f6b commit 0ba275e

5 files changed

Lines changed: 629 additions & 0 deletions

File tree

kotlin/NextGenExample/app/src/main/java/com/example/next_gen_example/MenuFragment.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ class MenuFragment : Fragment() {
5858
ExampleData(R.string.inline_banner, R.id.action_MenuFragment_to_InlineBannerFragment),
5959
ExampleData(R.string.interstitial, R.id.action_MenuFragment_to_InterstitialFragment),
6060
ExampleData(R.string.native_ad, R.id.action_MenuFragment_to_NativeFragment),
61+
ExampleData(R.string.compose_native_ad, R.id.action_MenuFragment_to_NativeComposeFragment),
6162
ExampleData(R.string.preloading, R.id.action_MenuFragment_to_PreloadingFragment),
6263
ExampleData(
6364
R.string.full_screen_native,
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.example.next_gen_example.native
17+
18+
import android.os.Bundle
19+
import android.util.Log
20+
import android.view.LayoutInflater
21+
import android.view.View
22+
import android.view.ViewGroup
23+
import android.widget.Toast
24+
import androidx.compose.foundation.Image
25+
import androidx.compose.foundation.layout.Column
26+
import androidx.compose.foundation.layout.Row
27+
import androidx.compose.foundation.layout.aspectRatio
28+
import androidx.compose.foundation.layout.fillMaxWidth
29+
import androidx.compose.foundation.layout.heightIn
30+
import androidx.compose.foundation.layout.padding
31+
import androidx.compose.material3.Button
32+
import androidx.compose.material3.Checkbox
33+
import androidx.compose.material3.Text
34+
import androidx.compose.runtime.Composable
35+
import androidx.compose.runtime.DisposableEffect
36+
import androidx.compose.runtime.getValue
37+
import androidx.compose.runtime.mutableStateOf
38+
import androidx.compose.runtime.remember
39+
import androidx.compose.runtime.setValue
40+
import androidx.compose.ui.Alignment
41+
import androidx.compose.ui.Modifier
42+
import androidx.compose.ui.graphics.asImageBitmap
43+
import androidx.compose.ui.platform.ComposeView
44+
import androidx.compose.ui.unit.dp
45+
import androidx.core.graphics.drawable.toBitmap
46+
import androidx.fragment.app.Fragment
47+
import com.example.next_gen_example.Constant
48+
import com.google.android.libraries.ads.mobile.sdk.common.LoadAdError
49+
import com.google.android.libraries.ads.mobile.sdk.common.VideoOptions
50+
import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAd
51+
import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdLoader
52+
import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdLoaderCallback
53+
import com.google.android.libraries.ads.mobile.sdk.nativead.NativeAdRequest
54+
55+
/** A Fragment that hosts a Jetpack Compose screen to display a native ad. */
56+
class NativeComposeFragment : Fragment() {
57+
58+
/**
59+
* Sets up a ComposeView to host the Jetpack Compose UI, allowing Compose to be used within a
60+
* Fragment-based navigation structure.
61+
*/
62+
override fun onCreateView(
63+
inflater: LayoutInflater,
64+
container: ViewGroup?,
65+
savedInstanceState: Bundle?,
66+
): View {
67+
return ComposeView(requireContext()).apply {
68+
// `setContent` is the entry point for building a Compose UI in a ComposeView.
69+
setContent { NativeScreen() }
70+
}
71+
}
72+
73+
/**
74+
* The main composable for the screen. The `@Composable` annotation marks a function as a
75+
* UI-building block.
76+
*/
77+
@Composable
78+
fun NativeScreen() {
79+
// `remember` stores state across recompositions. `mutableStateOf` creates an observable
80+
// state that triggers UI updates when its value changes.
81+
var nativeAdState by remember { mutableStateOf<NativeAd?>(null) }
82+
var nativeAdIsDisposed by remember { mutableStateOf(false) }
83+
84+
// State for ad loading options.
85+
var requestVideo by remember { mutableStateOf(false) }
86+
var startMuted by remember { mutableStateOf(true) }
87+
88+
/** Loads a native ad based on the current configuration. */
89+
fun loadAd() {
90+
val adUnitId = if (requestVideo) VIDEO_AD_UNIT_ID else IMAGE_AD_UNIT_ID
91+
val videoOptions: VideoOptions = VideoOptions.Builder().setStartMuted(startMuted).build()
92+
val adRequest =
93+
NativeAdRequest.Builder(adUnitId, listOf(NativeAd.NativeAdType.NATIVE))
94+
.setVideoOptions(videoOptions)
95+
.build()
96+
val adCallback =
97+
object : NativeAdLoaderCallback {
98+
override fun onNativeAdLoaded(nativeAd: NativeAd) {
99+
Log.d(Constant.TAG, "Native ad loaded.")
100+
// Update the state with the loaded ad, triggering a recomposition to display it.
101+
if (!nativeAdIsDisposed) {
102+
nativeAdState = nativeAd
103+
} else {
104+
// If the view is already gone, destroy the ad to prevent memory leaks.
105+
nativeAd.destroy()
106+
}
107+
}
108+
109+
override fun onAdFailedToLoad(adError: LoadAdError) {
110+
Log.e(Constant.TAG, "Native ad failed to load: $adError")
111+
Toast.makeText(requireContext(), "Ad failed to load.", Toast.LENGTH_SHORT).show()
112+
}
113+
}
114+
NativeAdLoader.load(adRequest, adCallback)
115+
}
116+
117+
// `DisposableEffect` cleans up resources when the composable leaves the screen.
118+
DisposableEffect(Unit) {
119+
onDispose {
120+
// This `onDispose` block runs on cleanup, destroying the ad to prevent memory leaks.
121+
nativeAdIsDisposed = true
122+
nativeAdState?.destroy()
123+
nativeAdState = null
124+
}
125+
}
126+
127+
// `Column` arranges its children vertically. `Modifier` adds properties like size and padding.
128+
Column(modifier = Modifier.fillMaxWidth()) {
129+
// Renders the ad if it's loaded.
130+
nativeAdState?.let { DisplayNativeAdView(ad = it) }
131+
132+
// Ad configuration controls.
133+
Row(verticalAlignment = Alignment.CenterVertically) {
134+
Checkbox(checked = requestVideo, onCheckedChange = { requestVideo = it })
135+
Text(text = "Request video")
136+
}
137+
Row(verticalAlignment = Alignment.CenterVertically) {
138+
Checkbox(checked = startMuted, onCheckedChange = { startMuted = it })
139+
Text(text = "Start muted")
140+
}
141+
Button(onClick = { loadAd() }) { Text(text = "Refresh Ad") }
142+
}
143+
}
144+
145+
/** Displays the native ad content. */
146+
@Composable
147+
fun DisplayNativeAdView(ad: NativeAd) {
148+
// `NativeAdView` is a custom composable (in NativeComposableUtility.kt) that wraps the
149+
// SDK's `NativeAdView` for use in Compose.
150+
NativeAdView(ad) {
151+
Column(modifier = Modifier.fillMaxWidth()) {
152+
Row(modifier = Modifier.fillMaxWidth()) {
153+
// Check if ad assets are available before displaying them.
154+
ad.icon?.let { icon ->
155+
NativeAdIconView(Modifier.padding(5.dp)) {
156+
// Convert the drawable to a bitmap for the `Image` composable.
157+
icon.drawable?.toBitmap()?.let { Image(bitmap = it.asImageBitmap(), "Icon") }
158+
}
159+
}
160+
Column {
161+
ad.headline?.let { NativeAdHeadlineView { Text(text = it) } }
162+
ad.starRating?.let { NativeAdStarRatingView { Text(text = "Rated $it") } }
163+
}
164+
}
165+
166+
// The MediaView must be at least 120x120 dp.
167+
// To ensure the MediaView consistently meets the size requirements for video ads,
168+
// use the aspectRatio modifier from Compose. This will force the composable to adhere to
169+
// the video's aspect ratio while respecting the width constraint.
170+
val mediaModifier =
171+
if (ad.mediaContent.hasVideoContent) {
172+
val aspectRatio = ad.mediaContent.aspectRatio
173+
Modifier.fillMaxWidth()
174+
.let {
175+
// Enforce the aspect ratio if it's available, otherwise default to a 16:9 ratio
176+
// to ensure a valid size.
177+
if (aspectRatio > 0) it.aspectRatio(aspectRatio) else it.aspectRatio(16f / 9f)
178+
}
179+
.heightIn(min = 120.dp) // Keep heightIn as a safeguard
180+
.padding(vertical = 5.dp)
181+
} else {
182+
Modifier.fillMaxWidth().padding(vertical = 5.dp)
183+
}
184+
NativeAdMediaView(modifier = mediaModifier)
185+
186+
ad.body?.let { NativeAdBodyView(modifier = Modifier.padding(5.dp)) { Text(text = it) } }
187+
188+
Row(
189+
Modifier.align(Alignment.End).padding(5.dp),
190+
verticalAlignment = Alignment.CenterVertically,
191+
) {
192+
ad.price?.let { NativeAdPriceView(Modifier.padding(5.dp)) { Text(text = it) } }
193+
ad.store?.let { NativeAdStoreView(Modifier.padding(5.dp)) { Text(text = it) } }
194+
ad.callToAction?.let {
195+
NativeAdCallToActionView(Modifier.padding(5.dp)) { NativeAdButton(text = it) }
196+
}
197+
}
198+
}
199+
}
200+
}
201+
202+
/** Defines constants for ad unit IDs. */
203+
private companion object {
204+
const val IMAGE_AD_UNIT_ID = "ca-app-pub-3940256099942544/2247696110"
205+
const val VIDEO_AD_UNIT_ID = "ca-app-pub-3940256099942544/1044960115"
206+
}
207+
}

0 commit comments

Comments
 (0)