Boost Revenue with the Next Gen Rewarded Interstitial Ads in Jetpack Compose

In our previous article we provided a more in-depth description of what interstitial ads are and the benefits of using them.
Traditional interstitials vs rewarded interstitials
Interstitial ads interrupt the experience without offering anything in return, while rewarded interstitials do reward the user for their attention. Users are presented with an ad between game levels or during app transitions (just like normal interstitial ads) in return for some game currency, extra lives or premium content. It is important to reward the user on completion since unlike traditional interstitials the user has the option to dismiss it.
This type of interstitial offers value to both users and developers, making it a more engaging and user-friendly monetization method. Other key reasons why this type is more attractive are:
1. Higher engagement
The voluntary participation for something in return leads to higher completion and less frustration. Unlike traditional interstitials that interrupt the user experience without any benefit, leading to annoyance or even app uninstalls.
2. Improve retention and user satisfaction
Users feel like their time is valuable from the ad experience. This leads to positive brand perception and users are more likely to keep using the app.
3. Higher eCPM and Revenue potentials (higher than traditional interstitials)
Due to the nature of rewarded interstitials advertisers are willing to pay more. Since users are more likely to watch the full ad which translate to better monetization for developers than typical interstitials.
4. User are more in control
Rewarded interstitials give the user the option to choose to watch and not force them. This put the user in full control.
5. Easy integration in User Flow
Such interstitials can be offer during natural pauses, making them feel like part of the experience.
Implementation
In this article we are going to demonstrate how to implement such interstitial ads in an Android app. The implementation of such an ad is straightforward since the Next Gen Ad SDK takes care of everything for you.
For the full details regarding the implementation read the Google Next Gen Ads documentation.
For our demo app we will be using the following adUnit:
ca-app-pub-3940256099942544/5354046379
The structure of the demo app will be identical to the one presented in this article. Only this time the screen flow will be adjusted to the recommended flow for the rewarded interstitial ads.
View state data model
@Parcelize
data class RewardedInterstitialAdViewState(
val isLoading: Boolean = true,
val navigateToDetail: Boolean = false,
val premiumArticleData: RewardedAdListDisplayItem? = null,
@IgnoredOnParcel
val isLoadingAd: Boolean = false,
@IgnoredOnParcel
val showRewardedInterstitialDialog: Boolean = false,
@IgnoredOnParcel
val showRewardedInterstitial: Boolean = false,
@IgnoredOnParcel
val rewardedInterstitialAdView: RewardedInterstitialAd? = null,
val newsList: List<RewardedAdListDisplayItem> = emptyList()
): Parcelable
View Action sealed interface
sealed interface RewardedInterstitialAdViewAction {
data object ResetViewState : RewardedInterstitialAdViewAction
data object ShowRewardedInterstitialAd : RewardedInterstitialAdViewAction
data class DismissRewardedInterstitialAd(val rewardClaimed: Boolean = false) : RewardedInterstitialAdViewAction
data class ShowRewardedInterstitialAdDialog(val premiumArticleData: RewardedAdListDisplayItem) : RewardedInterstitialAdViewAction
data object DismissRewardedInterstitialAdDialog : RewardedInterstitialAdViewAction
}
Rewarded interstitial ad news repository interface
interface RewardedInterstitialAdNewsRepository {
suspend fun loadRewardedInterstitialDummyNews(): List<RewardedAdListDisplayItem>
}
Rewarded interstitial ad news repository implementation
class RewardedInterstitialAdNewsRepositoryImpl: RewardedInterstitialAdNewsRepository {
override suspend fun loadRewardedInterstitialDummyNews(): List<RewardedAdListDisplayItem> {
delay(2.seconds) // Simulate remote server api call
return getNewsListData()
}
private fun getNewsListData(): List<RewardedAdListDisplayItem> {
return ArrayList<RewardedAdListDisplayItem>().apply {
for (item in 1..50) {
add(
RewardedAdListDisplayItem(
imageID = "$item",
image = getRandomFeatureImage(),
title = "News article $item",
titleID = UUID.randomUUID().toString(),
description = "The description for article $item"
)
)
if (item % 5 == 0) {
add(
RewardedAdListDisplayItem(
imageID = "${UUID.randomUUID()}$item",
image = getRandomPremiumFeatureImage(),
title = "Premium article",
titleID = "${UUID.randomUUID()}$item",
description = "Requires 1 credit to read premium article $item",
premium = true
)
)
}
}
}
}
private fun getRandomFeatureImage(): Int {
val imageIDs = listOf(
R.mipmap.image1,
R.mipmap.image2,
R.mipmap.image3,
R.mipmap.image4
)
val randomIndex = Random.Default.nextInt(imageIDs.size)
return imageIDs[randomIndex]
}
private fun getRandomPremiumFeatureImage(): Int {
val imageIDs = listOf(
R.mipmap.image5,
R.mipmap.image6,
)
val randomIndex = Random.Default.nextInt(imageIDs.size)
return imageIDs[randomIndex]
}
}
Rewarded interstitial ad view model
class RewardedInterstitialViewModel(
private val savedStateHandle: SavedStateHandle,
private val adFetcher: AdFetcher,
private val repository: RewardedInterstitialAdNewsRepository
) : ViewModel() {
companion object {
val savedStateHandleKey = "${this::class.java.simpleName}"
}
private var _rewardedInterstitialAdViewState = MutableStateFlow(
savedStateHandle.get<RewardedInterstitialAdViewState>(savedStateHandleKey)
?: RewardedInterstitialAdViewState()
)
val rewardedInterstitialAdViewState: StateFlow<RewardedInterstitialAdViewState>
get() = _rewardedInterstitialAdViewState.asStateFlow()
init {
CoroutineScope(Dispatchers.Main).launch {
preFetchRewardedAd()
loadNews()
}
}
fun performAction(action: RewardedInterstitialAdViewAction) {
when (action) {
is RewardedInterstitialAdViewAction.DismissRewardedInterstitialAd -> {
if (action.rewardClaimed) {
_rewardedInterstitialAdViewState.update {
it.copy(
navigateToDetail = true,
rewardedInterstitialAdView = null
)
}
} else {
_rewardedInterstitialAdViewState.update {
it.copy(
isLoadingAd = false,
premiumArticleData = null,
rewardedInterstitialAdView = null,
showRewardedInterstitial = false,
navigateToDetail = false
)
}
}
preFetchRewardedAd()
}
is RewardedInterstitialAdViewAction.ShowRewardedInterstitialAdDialog -> {
_rewardedInterstitialAdViewState.update {
it.copy(
premiumArticleData = action.premiumArticleData,
showRewardedInterstitialDialog = true
)
}
}
is RewardedInterstitialAdViewAction.DismissRewardedInterstitialAdDialog -> {
_rewardedInterstitialAdViewState.update {
it.copy(
showRewardedInterstitialDialog = false
)
}
}
is RewardedInterstitialAdViewAction.ShowRewardedInterstitialAd -> {
if (rewardedInterstitialAdViewState.value.isLoadingAd || rewardedInterstitialAdViewState.value.rewardedInterstitialAdView == null)
return
viewModelScope.launch {
_rewardedInterstitialAdViewState.update {
it.copy(
showRewardedInterstitialDialog = false
)
}
delay(500.milliseconds)
_rewardedInterstitialAdViewState.update {
it.copy(
showRewardedInterstitial = true
)
}
}
}
is RewardedInterstitialAdViewAction.ResetViewState -> {
_rewardedInterstitialAdViewState.update {
it.copy(
isLoadingAd = false,
premiumArticleData = null,
showRewardedInterstitial = false,
navigateToDetail = false,
rewardedInterstitialAdView = null
)
}
}
}
}
private fun preFetchRewardedAd() {
adFetcher.fetchRewardedInterstitialAd(adUnit = NextGenAdUnit.RewardedInterstitialAd) { rewardedInterstitialAd ->
println("[Rewarded interstitial ads] - $rewardedInterstitialAd")
viewModelScope.launch {
rewardedInterstitialAd?.let { _rewardedInterstitialAd ->
_rewardedInterstitialAdViewState.update {
it.copy(
isLoadingAd = false,
rewardedInterstitialAdView = _rewardedInterstitialAd
)
}
} ?: run {
_rewardedInterstitialAdViewState.update {
it.copy(
isLoadingAd = false,
premiumArticleData = null,
rewardedInterstitialAdView = null
)
}
}
}
}
}
private suspend fun loadNews() {
_rewardedInterstitialAdViewState.update { RewardedInterstitialAdViewState() }
val newsList = repository.loadRewardedInterstitialDummyNews()
_rewardedInterstitialAdViewState.update { it.copy(isLoading = false, newsList = newsList) }
saveState()
}
private fun saveState() {
savedStateHandle[savedStateHandleKey] = _rewardedInterstitialAdViewState.value
}
}
Rewarded interstitial ad view model factory
lass RewardedInterstitialAdViewModelFactory(private val repository: RewardedInterstitialAdNewsRepository, private val adFetcher: AdFetcher): ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
if (modelClass.isAssignableFrom(RewardedInterstitialViewModel::class.java)) {
return RewardedInterstitialViewModel(
savedStateHandle = extras.createSavedStateHandle(),
adFetcher = adFetcher,
repository = repository
) as T
}
throw IllegalArgumentException("Not ${RewardedInterstitialViewModel::class.simpleName} class")
}
}
Rewarded interstitial ad view list item (composable)
@Composable
fun RewardedInterstitialAdsListView(
viewState: State<RewardedInterstitialAdViewState>,
action: (RewardedInterstitialAdViewAction) -> Unit,
onItemClicked: (RewardedAdListDisplayItem) -> Unit,
modifier: Modifier = Modifier
) {
val lazyColumnListState = rememberLazyListState()
Box(modifier = modifier.fillMaxSize()) {
ShimmerLoadingView(visible = viewState.value.isLoading)
LazyColumn(
state = lazyColumnListState,
modifier = Modifier
.fillMaxSize()
.align(Alignment.Center)
) {
itemsIndexed(viewState.value.newsList) { index, listItem ->
RewardedAdListItem(displayItem = listItem, onClick = {
if (listItem.premium) {
action(
RewardedInterstitialAdViewAction.ShowRewardedInterstitialAdDialog(premiumArticleData = it)
)
} else {
onItemClicked(it)
}
})
if (index < viewState.value.newsList.lastIndex) {
HorizontalDivider()
} else {
Spacer(
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
)
}
}
}
if (viewState.value.showRewardedInterstitialDialog) {
RewardedAdDialog(
onAccept = {
action(RewardedInterstitialAdViewAction.ShowRewardedInterstitialAd)
},
onDismiss = {
action(RewardedInterstitialAdViewAction.DismissRewardedInterstitialAdDialog)
}
)
}
if (viewState.value.showRewardedInterstitial && viewState.value.rewardedInterstitialAdView != null) {
viewState.value.rewardedInterstitialAdView?.let {
RewardedInterstitialAdView(rewardedInterstitial = it, onCompletion = {
action(RewardedInterstitialAdViewAction.DismissRewardedInterstitialAd(rewardClaimed = true))
}, onDismiss = {
action(RewardedInterstitialAdViewAction.DismissRewardedInterstitialAd())
})
}?:run {
action(RewardedInterstitialAdViewAction.DismissRewardedInterstitialAd())
}
}
if (viewState.value.navigateToDetail) {
viewState.value.premiumArticleData?.let {
onItemClicked(it)
}
action(RewardedInterstitialAdViewAction.ResetViewState)
}
}
}
Rewarded interstitial ad dialog (composable)
@Composable
private fun RewardedAdDialog(
onDismiss: () -> Unit,
onAccept: () -> Unit
) {
Dialog(onDismissRequest = onDismiss) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(
color = MaterialTheme.colorScheme.surfaceContainer,
shape = RoundedCornerShape(16.dp)
)
.padding(all = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = stringResource(id = AppString.premium_article),
style = MaterialTheme.typography.titleLarge.copy(
fontWeight = FontWeight.Bold,
color = if (isSystemInDarkTheme()) Color.White else Color.Black
)
)
Text(
text = stringResource(id = AppString.premium_article_ad_message),
style = MaterialTheme.typography.bodyMedium.copy(color = if (isSystemInDarkTheme()) Color.White else Color.Black)
)
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
TextButton(onClick = {
onAccept()
}) {
Text(
stringResource(id = AppString.yes),
style = MaterialTheme.typography.bodyMedium.copy(
color = if (isSystemInDarkTheme()) Color.White else Color.Black,
fontWeight = FontWeight.Bold
)
)
}
TextButton(onClick = {
onDismiss()
}) {
Text(
stringResource(id = AppString.no),
style = MaterialTheme.typography.bodyMedium.copy(
color = if (isSystemInDarkTheme()) Color.White else Color.Black,
fontWeight = FontWeight.Bold
)
)
}
}
}
}
}
Rewarded interstitial ad view (composable)
@Composable
private fun RewardedInterstitialAdView(
rewardedInterstitial: RewardedInterstitialAd,
onCompletion: () -> Unit,
onDismiss: () -> Unit,
) {
LocalActivity.current?.let { _activity ->
rewardedInterstitial.apply {
adEventCallback = object : RewardedInterstitialAdEventCallback {
override fun onAdShowedFullScreenContent() {
super.onAdShowedFullScreenContent()
println("[Rewarded Interstitial Ad] - Ad Showed FullScreen Content")
}
override fun onAdDismissedFullScreenContent() {
super.onAdDismissedFullScreenContent()
println("[Rewarded Interstitial Ad] - On Ad Dismissed FullScreen Content")
onDismiss()
}
override fun onAdFailedToShowFullScreenContent(fullScreenContentError: FullScreenContentError) {
super.onAdFailedToShowFullScreenContent(fullScreenContentError)
println("[Rewarded Interstitial Ad] - On Ad Failed to show FullScreen, cause: ${fullScreenContentError.message}")
}
override fun onAdImpression() {
super.onAdImpression()
println("[Rewarded Interstitial Ad] - On Ad Impression")
}
override fun onAdClicked() {
super.onAdClicked()
println("[Rewarded Interstitial Ad] - On Ad Clicked")
}
}
show(
activity = _activity,
onUserEarnedRewardListener = object : OnUserEarnedRewardListener {
override fun onUserEarnedReward(reward: RewardItem) {
onCompletion()
}
}
)
}
}
}
Rewarded interstitial ad (composable)
@Composable
fun RewardedInterstitial(modifier: Modifier = Modifier) {
val navController = rememberNavController()
val dataStateKey = "data"
Box(modifier = modifier.fillMaxSize()) {
NavHost(
navController = navController,
startDestination = RewardedAdViewRoute.RewardedAdsArticleList.route
) {
composable(route = RewardedAdViewRoute.RewardedAdsArticleList.route) {
val rewardedAdViewModel: RewardedInterstitialViewModel =
viewModel(
factory = RewardedInterstitialAdViewModelFactory(
repository = RewardedInterstitialAdNewsRepositoryImpl(),
adFetcher = MobileAdsManager
)
)
val viewState =
rewardedAdViewModel.rewardedInterstitialAdViewState.collectAsStateWithLifecycle()
RewardedInterstitialAdsListView(
viewState = viewState,
action = rewardedAdViewModel::performAction,
onItemClicked = {
if (viewState.value.isLoadingAd)
return@RewardedInterstitialAdsListView
navController.currentBackStackEntry?.savedStateHandle?.set(dataStateKey, it)
navController.navigateToView(RewardedAdViewRoute.RewardedAdsArticleDetail.route)
})
}
composable(
route = RewardedAdViewRoute.RewardedAdsArticleDetail.route
) {
val data =
navController.previousBackStackEntry?.savedStateHandle?.get<RewardedAdListDisplayItem>(
dataStateKey
)
ArticleDetailScreen(
data = data
)
}
}
}
}
Final result
Rewarded interstitial ads strike a better balance between monetization and user experience.
They’re less intrusive, more engaging, and more profitable, which makes them a preferred ad format for modern mobile apps and games.