On this page14 sections
Lecture 006 — App Open Ads & the AdState Flag
| Category | 2 — Banner & Full-Screen Ads |
| Lecture | 006 of 010 |
| Estimated video length | 18 min |
| Target | Flutter 3.44 · Dart 3.12 · google_mobile_ads 9 |
| Level | Beginner |
| Source code | GitHub — urraan_admob_final |
An app-open ad appears when the user comes back to the app. It is easy to annoy people with it, so we use a small shared flag that keeps it quiet for 30 seconds after any other full-screen ad.
Contents
Prerequisites · Connection · Objectives · Lesson · Code Examples · Common Mistakes · Best Practices · Mini Challenge · Practice Questions · Answers · Takeaways
Prerequisites
Lectures 003–005; WidgetsBindingObserver.
Connection With Previous Lectures
Lecture 005 introduced AdState. Now the app-open ad reads it.
Learning Objectives
By the end of this lecture you will be able to:
- Load and show an
AppOpenAd - Detect the app coming back with
didChangeAppLifecycleState - Avoid showing two full-screen ads back to back
- Reload after each show
Lesson
1. The Helper
// lib/admob/app_open_ad_helper.dart
import 'package:flutter/foundation.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'admob_manager.dart';
class AppOpenAdHelper {
AppOpenAd? _appOpenAd;
bool _isAdShowing = false;
void loadAppOpenAd() {
AppOpenAd.load(
adUnitId: AdmobManager.appOpenId,
request: const AdRequest(),
adLoadCallback: AppOpenAdLoadCallback(
onAdLoaded: (ad) {
_appOpenAd = ad;
ad.fullScreenContentCallback = FullScreenContentCallback(
onAdDismissedFullScreenContent: (ad) {
ad.dispose();
_appOpenAd = null;
_isAdShowing = false;
loadAppOpenAd();
},
onAdFailedToShowFullScreenContent: (ad, error) {
ad.dispose();
_appOpenAd = null;
_isAdShowing = false;
},
);
},
onAdFailedToLoad: (error) {
debugPrint('App open failed: ${error.code} ${error.message}');
_appOpenAd = null;
},
),
);
}
void showAdIfAvailable() {
if (_appOpenAd == null || _isAdShowing) {
if (_appOpenAd == null) loadAppOpenAd();
return;
}
_isAdShowing = true;
_appOpenAd!.show();
}
void dispose() => _appOpenAd?.dispose();
}showAdIfAvailableshows the ad if one is loaded. If none is loaded it starts loading one for next time._isAdShowingprevents the same ad being shown twice.
2. Watching the App Lifecycle
WidgetsBindingObserver tells a widget when the app goes to the background and returns:
// lib/screen_dashboard.dart
import 'package:flutter/material.dart';
import 'admob/ad_state.dart';
import 'admob/app_open_ad_helper.dart';
import 'screen_banner_ads.dart';
import 'screen_interstitials.dart';
import 'screen_native_ads.dart';
import 'screen_rewarded_ads.dart';
import 'screen_rewarded_interstitial_ads.dart';
class ScreenDashboard extends StatefulWidget {
const ScreenDashboard({super.key});
@override
State<ScreenDashboard> createState() => _ScreenDashboardState();
}
class _ScreenDashboardState extends State<ScreenDashboard>
with WidgetsBindingObserver {
final appOpenAdHelper = AppOpenAdHelper();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
appOpenAdHelper.loadAppOpenAd();
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
appOpenAdHelper.dispose();
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed &&
!AdState().isOtherAdsDisabled) {
appOpenAdHelper.showAdIfAvailable();
}
}
void _open(Widget screen) {
Navigator.push(context, MaterialPageRoute(builder: (_) => screen));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Dashboard')),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton(
onPressed: () => _open(const ScreenBannerAds()),
child: const Text('Banner Ads'),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => _open(const ScreenInterstitials()),
child: const Text('Interstitial Ads'),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => _open(const ScreenNativeAds()),
child: const Text('Native Ads'),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => _open(const ScreenRewardedAds()),
child: const Text('Rewarded Ads'),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => _open(const ScreenRewardedInterstitialAds()),
child: const Text('Rewarded Interstitial Ads'),
),
],
),
),
);
}
}addObserverininitState,removeObserverindispose.- When the state is
resumedandAdState().isOtherAdsDisabledisfalse, we show the app-open ad.
3. The 30-Second Rule
Interstitial and rewarded ads disable app-open ads when they show and enable them 30
seconds after they close (enableAppOpenAdsAfterCooldown). Without this, closing a full-screen
ad can bring the app "back", triggering an app-open ad immediately after.
Code Examples
Example 1 — Test It
Run the app, press the Home button, then reopen the app. The test app-open ad appears. Open an interstitial screen, finish the ad, and background/reopen within 30 seconds — no app-open ad should show.
Common Beginner Mistakes
1. Never removing the observer — the dashboard keeps receiving events after it is gone.
2. Showing the ad over the splash screen — the user has not seen your app yet. This demo shows it only when the app comes back (resumed).
3. No cooldown — two full-screen ads back to back.
Best Practices
- Do not show an app-open ad right after another full-screen ad
- Do not show it before the user has seen your app's first screen
- Always remove observers in
dispose
Mini Challenge
Change the cooldown from 30 to 60 seconds in AdState and explain who calls it.
Practice Questions
- Which lifecycle state triggers the ad?
- Why does
AdStateexist? - What does
_isAdShowingprevent? - When do we remove the observer?
Practice Answers
AppLifecycleState.resumed.- To stop two full-screen ads appearing together.
- Showing the same ad twice.
- In
dispose.
Key Takeaways
- App open = lifecycle
resumed+ available ad AdStatecoordinates full-screen ads- Add and remove the observer