Tampilan
Bab 35 — Google AdMob
Iklan adalah cara monetisasi paling umum, dan sekaligus yang paling mudah dilakukan dengan buruk. Bab ini membahas cara memasangnya secara teknis — dan sama pentingnya, cara memasangnya tanpa membuat pengguna meninggalkan aplikasimu.
Peringatan sebelum mulai
Sebelum kode, satu hal yang perlu kamu pertimbangkan dengan jujur.
Iklan tidak disukai pengguna. Rata-rata, aplikasi beriklan mendapat ulasan sedikit lebih rendah — dan kalau iklannya agresif, jauh lebih rendah. Ia juga tidak menghasilkan sebanyak yang dibayangkan orang: pendapatan per seribu tampilan sering hanya beberapa ribu rupiah.
Artinya kamu butuh volume besar agar iklan bermakna secara finansial, dan volume besar butuh pengguna yang bertahan — sesuatu yang justru dirusak oleh iklan agresif.
Kapan iklan masuk akal
- Aplikasi dengan sesi panjang dan sering — game, berita, media sosial.
- Sebagai tingkat gratis, dengan opsi berbayar untuk menghilangkannya.
- Ketika basis penggunamu sudah besar.
Kapan tidak masuk akal: aplikasi utilitas yang dipakai sebentar, aplikasi dengan basis pengguna kecil, atau aplikasi yang bisa dijual sebagai langganan. Model langganan dibahas di Bab 36, dan hampir selalu menghasilkan lebih banyak per pengguna.
Menyiapkan AdMob
- Buat akun di admob.google.com.
- Add app — daftarkan aplikasimu untuk Android dan iOS terpisah.
- Salin App ID masing-masing platform.
- Ad units → buat unit untuk setiap format yang akan dipakai.
yaml
# pubspec.yaml
dependencies:
google_mobile_ads: ^5.2.0Konfigurasi Android
xml
<!-- android/app/src/main/AndroidManifest.xml -->
<manifest ...>
<application ...>
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-XXXXXXXXXXXXXXXX~YYYYYYYYYY" />
</application>
</manifest>Konfigurasi iOS
xml
<!-- ios/Runner/Info.plist -->
<key>GADApplicationIdentifier</key>
<string>ca-app-pub-XXXXXXXXXXXXXXXX~YYYYYYYYYY</string>
<!-- Wajib sejak iOS 14 — dialog izin pelacakan -->
<key>NSUserTrackingUsageDescription</key>
<string>Izin ini membantu kami menampilkan iklan yang lebih relevan untukmu.</string>App ID bukan Ad Unit ID
Keduanya mirip dan sering tertukar:
- App ID memakai tilde:
ca-app-pub-XXXX~YYYY→ masuk ke manifest. - Ad Unit ID memakai garis miring:
ca-app-pub-XXXX/ZZZZ→ dipakai di kode.
Menukarnya menyebabkan iklan tidak pernah muncul, tanpa pesan error yang jelas.
ID pengujian
Selama pengembangan, wajib memakai ID pengujian. Mengklik iklan sungguhan di aplikasimu sendiri melanggar kebijakan AdMob dan bisa menyebabkan akunmu diblokir permanen.
dart
// lib/data/services/id_iklan.dart
import 'dart:io';
import 'package:flutter/foundation.dart';
class IdIklan {
IdIklan._();
// ID pengujian resmi dari Google — aman diklik
static const _ujiBannerAndroid = 'ca-app-pub-3940256099942544/6300978111';
static const _ujiBannerIos = 'ca-app-pub-3940256099942544/2934735716';
static const _ujiInterstitialAndroid =
'ca-app-pub-3940256099942544/1033173712';
static const _ujiInterstitialIos = 'ca-app-pub-3940256099942544/4411468910';
static const _ujiBerhadiahAndroid = 'ca-app-pub-3940256099942544/5224354917';
static const _ujiBerhadiahIos = 'ca-app-pub-3940256099942544/1712485313';
static const _ujiAppOpenAndroid = 'ca-app-pub-3940256099942544/9257395921';
static const _ujiAppOpenIos = 'ca-app-pub-3940256099942544/5575463023';
// ID produksimu — ganti dengan milikmu
static const _banner = 'ca-app-pub-XXXX/1111111111';
static const _interstitial = 'ca-app-pub-XXXX/2222222222';
static const _berhadiah = 'ca-app-pub-XXXX/3333333333';
static const _appOpen = 'ca-app-pub-XXXX/4444444444';
static String get banner {
if (kDebugMode) {
return Platform.isAndroid ? _ujiBannerAndroid : _ujiBannerIos;
}
return _banner;
}
static String get interstitial {
if (kDebugMode) {
return Platform.isAndroid
? _ujiInterstitialAndroid
: _ujiInterstitialIos;
}
return _interstitial;
}
static String get berhadiah {
if (kDebugMode) {
return Platform.isAndroid ? _ujiBerhadiahAndroid : _ujiBerhadiahIos;
}
return _berhadiah;
}
static String get appOpen {
if (kDebugMode) {
return Platform.isAndroid ? _ujiAppOpenAndroid : _ujiAppOpenIos;
}
return _appOpen;
}
}Pemeriksaan kDebugMode membuat pergantian otomatis — kamu tidak perlu ingat mengganti ID sebelum rilis.
Inisialisasi
dart
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Tidak perlu await — biarkan berjalan di latar
unawaited(MobileAds.instance.initialize());
// Perangkat pengujian, agar iklan sungguhan tidak tampil di perangkatmu
if (kDebugMode) {
MobileAds.instance.updateRequestConfiguration(
RequestConfiguration(
testDeviceIds: ['ID_PERANGKAT_DARI_LOG'],
),
);
}
runApp(const AplikasiSaya());
}ID perangkat pengujian muncul di log saat kamu pertama kali memuat iklan:
I/Ads: Use RequestConfiguration.Builder().setTestDeviceIds(
Arrays.asList("33BE2250B43518CCDA7DE426D04EE231"))Banner
Format paling sederhana: persegi panjang di bagian atas atau bawah layar.
dart
// lib/presentation/widgets/iklan_banner.dart
import 'package:flutter/material.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import '../../data/services/id_iklan.dart';
class IklanBanner extends StatefulWidget {
const IklanBanner({super.key});
@override
State<IklanBanner> createState() => _IklanBannerState();
}
class _IklanBannerState extends State<IklanBanner> {
BannerAd? _iklan;
bool _siap = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Lebar layar baru tersedia di sini, bukan di initState
if (_iklan == null) _muat();
}
Future<void> _muat() async {
final lebar = MediaQuery.sizeOf(context).width.truncate();
// Ukuran adaptif menyesuaikan lebar layar —
// menghasilkan pendapatan lebih baik daripada ukuran tetap
final ukuran =
await AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize(lebar);
if (ukuran == null || !mounted) return;
final iklan = BannerAd(
adUnitId: IdIklan.banner,
size: ukuran,
request: const AdRequest(),
listener: BannerAdListener(
onAdLoaded: (_) {
if (mounted) setState(() => _siap = true);
},
onAdFailedToLoad: (iklan, kesalahan) {
iklan.dispose();
debugPrint('Banner gagal dimuat: ${kesalahan.message}');
},
),
);
_iklan = iklan;
await iklan.load();
}
@override
void dispose() {
_iklan?.dispose(); // ⚠️ wajib, kalau tidak memori bocor
super.dispose();
}
@override
Widget build(BuildContext context) {
// Jangan tampilkan apa pun sampai iklan benar-benar siap
if (!_siap || _iklan == null) return const SizedBox.shrink();
return SafeArea(
child: SizedBox(
width: _iklan!.size.width.toDouble(),
height: _iklan!.size.height.toDouble(),
child: AdWidget(ad: _iklan!),
),
);
}
}Memakainya:
dart
Scaffold(
appBar: AppBar(title: const Text('Beranda')),
body: const DaftarPoll(),
bottomNavigationBar: const IklanBanner(),
)Jangan pesan ruang sebelum iklan siap
Menampilkan kotak kosong setinggi 50 piksel yang tiba-tiba terisi iklan membuat tata letak melompat — pengalaman yang buruk dan bisa menyebabkan pengguna salah mengetuk.
Mengembalikan SizedBox.shrink() sampai _siap bernilai benar menghindari itu.
Interstitial
Iklan layar penuh yang muncul di antara aktivitas.
dart
// lib/data/services/layanan_interstitial.dart
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:flutter/foundation.dart';
import 'id_iklan.dart';
class LayananInterstitial {
InterstitialAd? _iklan;
bool _sedangMemuat = false;
int _percobaan = 0;
DateTime? _terakhirDitampilkan;
/// Jarak minimal antar iklan — mencegah spam.
static const _jedaMinimal = Duration(minutes: 3);
/// Berapa kali aksi sebelum iklan boleh muncul.
static const _aksiPerIklan = 5;
int _hitungAksi = 0;
void muat() {
if (_iklan != null || _sedangMemuat) return;
_sedangMemuat = true;
InterstitialAd.load(
adUnitId: IdIklan.interstitial,
request: const AdRequest(),
adLoadCallback: InterstitialAdLoadCallback(
onAdLoaded: (iklan) {
_iklan = iklan;
_sedangMemuat = false;
_percobaan = 0;
},
onAdFailedToLoad: (kesalahan) {
_sedangMemuat = false;
_percobaan++;
debugPrint('Interstitial gagal: ${kesalahan.message}');
// Coba lagi dengan jeda yang membesar, maksimal 3 kali
if (_percobaan <= 3) {
Future.delayed(
Duration(seconds: 5 * _percobaan),
muat,
);
}
},
),
);
}
/// Catat satu aksi pengguna. Iklan mungkin muncul setelahnya.
void catatAksi() {
_hitungAksi++;
}
/// Tampilkan kalau syaratnya terpenuhi.
/// [saatSelesai] dipanggil setelah iklan ditutup — atau langsung
/// kalau iklan tidak jadi ditampilkan.
Future<void> tampilkanKalauPantas({
required VoidCallback saatSelesai,
}) async {
if (!_bolehTampil()) {
saatSelesai();
return;
}
final iklan = _iklan!;
iklan.fullScreenContentCallback = FullScreenContentCallback(
onAdDismissedFullScreenContent: (iklan) {
iklan.dispose();
_iklan = null;
_terakhirDitampilkan = DateTime.now();
_hitungAksi = 0;
muat(); // siapkan yang berikutnya
saatSelesai();
},
onAdFailedToShowFullScreenContent: (iklan, kesalahan) {
iklan.dispose();
_iklan = null;
muat();
saatSelesai();
},
);
await iklan.show();
}
bool _bolehTampil() {
if (_iklan == null) return false;
if (_hitungAksi < _aksiPerIklan) return false;
final terakhir = _terakhirDitampilkan;
if (terakhir != null &&
DateTime.now().difference(terakhir) < _jedaMinimal) {
return false;
}
return true;
}
void dispose() {
_iklan?.dispose();
_iklan = null;
}
}Kelas ini menerapkan tiga aturan sekaligus yang membuat iklan terasa wajar:
Jeda minimal antar iklan. Tanpa ini, pengguna bisa melihat interstitial tiga kali dalam satu menit.
Ambang jumlah aksi. Iklan muncul setelah pengguna melakukan lima aksi, bukan setiap kali.
Selalu memuat iklan berikutnya setelah ditutup. Interstitial butuh waktu beberapa detik untuk dimuat; menyiapkannya lebih awal berarti ia siap saat dibutuhkan.
Memakainya:
dart
class _BerandaState extends State<Beranda> {
final _interstitial = LayananInterstitial();
@override
void initState() {
super.initState();
_interstitial.muat();
}
@override
void dispose() {
_interstitial.dispose();
super.dispose();
}
Future<void> _bukaDetail(Poll poll) async {
_interstitial.catatAksi();
// Tampilkan iklan dulu (kalau pantas), baru navigasi
await _interstitial.tampilkanKalauPantas(
saatSelesai: () {
if (!mounted) return;
Navigator.of(context).pushNamed('/poll', arguments: poll.id);
},
);
}
}Kapan JANGAN menampilkan interstitial
Kebijakan AdMob melarang, dan pengguna membenci:
- Saat aplikasi baru dibuka (pakai App Open Ad untuk itu).
- Saat pengguna keluar aplikasi.
- Di tengah tugas yang sedang berjalan — mengisi formulir, membaca artikel.
- Beruntun tanpa jeda.
Waktu yang tepat: di antara aktivitas yang jelas berakhir — setelah level game selesai, setelah kembali dari halaman detail, setelah menyelesaikan alur.
Rewarded Ad
Format yang paling disukai pengguna, karena mereka memilih menontonnya untuk mendapat sesuatu.
dart
// lib/data/services/layanan_berhadiah.dart
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:flutter/foundation.dart';
import 'id_iklan.dart';
class LayananBerhadiah {
RewardedAd? _iklan;
bool _sedangMemuat = false;
bool get siap => _iklan != null;
void muat() {
if (_iklan != null || _sedangMemuat) return;
_sedangMemuat = true;
RewardedAd.load(
adUnitId: IdIklan.berhadiah,
request: const AdRequest(),
rewardedAdLoadCallback: RewardedAdLoadCallback(
onAdLoaded: (iklan) {
_iklan = iklan;
_sedangMemuat = false;
},
onAdFailedToLoad: (kesalahan) {
_sedangMemuat = false;
debugPrint('Rewarded gagal: ${kesalahan.message}');
},
),
);
}
/// Mengembalikan true kalau pengguna menonton sampai selesai.
Future<bool> tampilkan() async {
final iklan = _iklan;
if (iklan == null) return false;
var dapatHadiah = false;
iklan.fullScreenContentCallback = FullScreenContentCallback(
onAdDismissedFullScreenContent: (iklan) {
iklan.dispose();
_iklan = null;
muat();
},
onAdFailedToShowFullScreenContent: (iklan, _) {
iklan.dispose();
_iklan = null;
muat();
},
);
await iklan.show(
onUserEarnedReward: (_, hadiah) {
dapatHadiah = true;
},
);
return dapatHadiah;
}
void dispose() {
_iklan?.dispose();
_iklan = null;
}
}Memakainya untuk memberi manfaat nyata:
dart
Future<void> _tontonUntukUnggulan(Poll poll) async {
if (!_berhadiah.siap) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Iklan belum siap. Coba sebentar lagi.')),
);
return;
}
final setuju = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Jadikan Unggulan Gratis'),
content: const Text(
'Tonton satu iklan singkat untuk menjadikan polling ini '
'unggulan selama 24 jam.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Nanti'),
),
FilledButton.icon(
onPressed: () => Navigator.pop(ctx, true),
icon: const Icon(Icons.play_circle_outline),
label: const Text('Tonton'),
),
],
),
);
if (setuju != true) return;
final selesai = await _berhadiah.tampilkan();
if (!mounted) return;
if (selesai) {
await context.read<PenyediaPoll>().jadikanUnggulan(poll.id, jam: 24);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Polling kamu kini unggulan selama 24 jam!')),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Iklan tidak selesai ditonton.')),
);
}
}Rewarded ad adalah format terbaik
Ia menghasilkan paling banyak per tampilan dari semua format, dan justru meningkatkan kepuasan pengguna karena mereka mendapat sesuatu. Kalau kamu hanya ingin memasang satu format iklan, pilih ini.
App Open Ad
Iklan yang muncul saat aplikasi dibuka atau kembali dari latar belakang.
dart
// lib/data/services/layanan_app_open.dart
import 'package:flutter/widgets.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'id_iklan.dart';
class LayananAppOpen with WidgetsBindingObserver {
AppOpenAd? _iklan;
bool _sedangDitampilkan = false;
DateTime? _dimuatPada;
DateTime? _terakhirDitampilkan;
/// Iklan kedaluwarsa setelah 4 jam — aturan Google.
static const _umurIklan = Duration(hours: 4);
/// Jangan tampilkan lebih sering dari ini.
static const _jedaMinimal = Duration(minutes: 15);
void mulai() {
WidgetsBinding.instance.addObserver(this);
muat();
}
void muat() {
if (_iklan != null) return;
AppOpenAd.load(
adUnitId: IdIklan.appOpen,
request: const AdRequest(),
adLoadCallback: AppOpenAdLoadCallback(
onAdLoaded: (iklan) {
_iklan = iklan;
_dimuatPada = DateTime.now();
},
onAdFailedToLoad: (_) {},
),
);
}
bool get _masihSegar {
final dimuat = _dimuatPada;
if (dimuat == null) return false;
return DateTime.now().difference(dimuat) < _umurIklan;
}
bool get _bolehTampil {
if (_iklan == null || _sedangDitampilkan || !_masihSegar) return false;
final terakhir = _terakhirDitampilkan;
if (terakhir != null &&
DateTime.now().difference(terakhir) < _jedaMinimal) {
return false;
}
return true;
}
void tampilkanKalauPantas() {
if (!_bolehTampil) {
// Iklan kedaluwarsa — buang dan muat yang baru
if (_iklan != null && !_masihSegar) {
_iklan!.dispose();
_iklan = null;
muat();
}
return;
}
_sedangDitampilkan = true;
_iklan!.fullScreenContentCallback = FullScreenContentCallback(
onAdDismissedFullScreenContent: (iklan) {
_sedangDitampilkan = false;
_terakhirDitampilkan = DateTime.now();
iklan.dispose();
_iklan = null;
muat();
},
onAdFailedToShowFullScreenContent: (iklan, _) {
_sedangDitampilkan = false;
iklan.dispose();
_iklan = null;
muat();
},
);
_iklan!.show();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// Aplikasi kembali dari latar belakang
if (state == AppLifecycleState.resumed) {
tampilkanKalauPantas();
}
}
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_iklan?.dispose();
}
}WidgetsBindingObserver adalah cara Flutter memberi tahu perubahan siklus hidup aplikasi. AppLifecycleState.resumed berarti aplikasi kembali ke depan.
App Open Ad paling mudah disalahgunakan
Menampilkannya setiap kali pengguna membuka aplikasi adalah cara tercepat membuat orang menghapus aplikasimu. Jeda 15 menit pada kode di atas adalah minimum yang wajar; beberapa pengembang memakai jeda satu jam.
Jangan juga menampilkannya pada peluncuran pertama — biarkan pengguna melihat aplikasimu dulu.
Menghormati privasi
Sejak GDPR dan aturan serupa, kamu wajib meminta persetujuan sebelum menampilkan iklan yang dipersonalisasi di wilayah tertentu.
dart
import 'package:google_mobile_ads/google_mobile_ads.dart';
Future<void> mintaPersetujuan() async {
final parameter = ConsentRequestParameters();
ConsentInformation.instance.requestConsentInfoUpdate(
parameter,
() async {
final tersedia =
await ConsentInformation.instance.isConsentFormAvailable();
if (tersedia) {
ConsentForm.loadAndShowConsentFormIfRequired((kesalahan) {
if (kesalahan != null) {
debugPrint('Formulir persetujuan gagal: ${kesalahan.message}');
}
// Setelah ini, MobileAds boleh diinisialisasi
MobileAds.instance.initialize();
});
} else {
MobileAds.instance.initialize();
}
},
(kesalahan) {
debugPrint('Info persetujuan gagal: ${kesalahan.message}');
MobileAds.instance.initialize();
},
);
}Untuk iOS 14+, kamu juga butuh dialog App Tracking Transparency:
yaml
dependencies:
app_tracking_transparency: ^2.0.6dart
import 'package:app_tracking_transparency/app_tracking_transparency.dart';
Future<void> mintaIzinPelacakan() async {
final status =
await AppTrackingTransparency.trackingAuthorizationStatus;
if (status == TrackingStatus.notDetermined) {
// Beri jeda agar tidak bentrok dengan dialog lain saat peluncuran
await Future.delayed(const Duration(milliseconds: 500));
await AppTrackingTransparency.requestTrackingAuthorization();
}
}Menghilangkan iklan lewat pembelian
Pola yang sangat baik: iklan sebagai tingkat gratis, dengan opsi berbayar untuk menghilangkannya.
dart
// lib/presentation/widgets/iklan_bersyarat.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../providers/penyedia_langganan.dart';
import 'iklan_banner.dart';
class IklanBersyarat extends StatelessWidget {
const IklanBersyarat({super.key});
@override
Widget build(BuildContext context) {
final bebasIklan = context.select<PenyediaLangganan, bool>(
(p) => p.bebasIklan,
);
if (bebasIklan) return const SizedBox.shrink();
return const IklanBanner();
}
}Bungkus semua penempatan iklan dengan pemeriksaan ini, sehingga menonaktifkannya cukup mengubah satu nilai. Implementasi langganannya dibahas di Bab 36.
Penempatan yang baik
✅ BAIK ❌ BURUK
───────────────────── ─────────────────────
Banner di bawah, Banner menutupi tombol
di luar area konten atau konten
Interstitial setelah Interstitial saat aplikasi
menyelesaikan alur baru dibuka
Rewarded saat pengguna Interstitial di tengah
memilih menontonnya pengguna mengisi formulir
App Open dengan jeda App Open setiap kali
15+ menit aplikasi dibuka
Satu banner per layar Banner di atas DAN bawahAturan praktis yang layak dipegang: kalau kamu sendiri akan terganggu oleh penempatan itu, penggunamu juga.
Mengukur hasil
AdMob menyediakan metrik yang perlu kamu pahami:
| Metrik | Artinya |
|---|---|
| Impressions | Berapa kali iklan tampil |
| Clicks | Berapa kali diklik |
| CTR | Rasio klik terhadap tampilan |
| eCPM | Pendapatan per seribu tampilan |
| Match rate | Persentase permintaan yang terisi iklan |
| Show rate | Persentase iklan termuat yang benar-benar tampil |
Match rate rendah biasanya berarti kamu meminta terlalu sering, atau audiensmu di wilayah dengan permintaan pengiklan rendah.
Show rate rendah berarti kamu memuat iklan tetapi jarang menampilkannya — pemborosan yang bisa diperbaiki dengan mengatur waktu pemuatan lebih baik.
Perhatikan retensi, bukan hanya pendapatan
Pendapatan iklan naik ketika kamu menampilkan lebih banyak iklan — sampai titik di mana pengguna pergi. Pantau retensi hari ke-1 dan hari ke-7 bersamaan dengan pendapatan.
Kalau retensi turun setelah kamu menambah iklan, pendapatan jangka panjangmu justru berkurang meskipun angka harian naik.
Kesalahan yang bisa memblokir akunmu
Ini serius — akun AdMob yang diblokir tidak selalu bisa dipulihkan.
Mengklik iklan sendiri. Termasuk saat menguji. Selalu pakai ID pengujian.
Meminta pengguna mengklik iklan. "Klik iklan untuk mendukung kami" melanggar kebijakan.
Menempatkan iklan dekat tombol. Klik tidak sengaja dianggap manipulasi.
Menampilkan iklan pada layar tanpa konten. Layar pemuatan atau layar kosong tidak boleh berisi iklan.
Aplikasi tanpa nilai nyata. Aplikasi yang hanya berisi iklan akan ditolak.
Latihan Mandiri
Kerjakan salah satu, beberapa, atau semuanya secara berurutan untuk melatih pemahamanmu sampai benar-benar lekat.
Variasi 1: Banner dan Interstitial — ⭐ · 20–30 menit
Tantangan: Pasang banner adaptif dan satu interstitial di aplikasi mana pun yang sudah kamu punya, seluruhnya dengan ID pengujian. Lalu tulis kebijakan penempatan interstitial-mu sendiri sebagai kode: jeda minimal, ambang jumlah aksi, dan daftar momen ketika ia tidak boleh muncul sama sekali.
Bayangan tampilan:
text
╭───────────────────────────────────╮
│ 9:41 ▂▄▆ 🔋 │
│ [ ...konten aplikasi... ] │
│ ┌─────────────────────────────┐ │
│ │ Banner Adaptif │ │
│ └─────────────────────────────┘ │
│ ▬▬▬▬ │
╰───────────────────────────────────╯Banner di atas hanya muncul setelah siap dimuat — tata letak tidak melompat naik-turun. Interstitial muncul setelah satu tugas selesai (mis. tamat satu level), tidak pernah saat aplikasi baru dibuka atau di tengah pengguna mengisi formulir:
text
┌─────────────────────────────────┐
│ [ Iklan Interstitial ] │
│ [ Tutup × ] │
└─────────────────────────────────┘Kriteria selesai:
- Pemilihan ID otomatis lewat pemeriksaan
kDebugMode, sehingga mustahil ID produksi terpakai saat pengembangan. - Banner memakai ukuran adaptif, dan tata letak tidak melompat saat iklan selesai dimuat.
- Interstitial tidak pernah muncul saat aplikasi baru dibuka, dan tidak pernah di tengah tugas yang sedang dikerjakan pengguna.
- Setiap objek iklan di-
dispose(). - Seluruh penempatan iklan dibungkus satu pemeriksaan terpusat, sehingga mematikan iklan cukup dari satu tempat.
Petunjuk: Kriteria terakhir terasa berlebihan sekarang, dan akan terasa sangat masuk akal begitu kamu menambahkan langganan "hapus iklan" di bab berikutnya. Kalau pemeriksaannya tersebar di sepuluh tempat, kamu akan melewatkan satu — dan pelanggan yang membayar melihat iklan adalah keluhan yang mahal. Soal tata letak yang melompat: godaannya adalah memesan ruang setinggi banner sejak awal supaya tidak bergeser, tetapi itu berarti ruang kosong yang mencolok kalau iklannya gagal dimuat. Muat dulu, tampilkan setelah siap. Dan sekali lagi, karena ini tidak bisa terlalu sering diulang: jangan pernah mengklik iklan sungguhan di aplikasimu sendiri.
Variasi 2: Iklan Berhadiah — ⭐⭐ · 30–45 menit
Tantangan: Tambahkan rewarded ad yang memberi sesuatu yang benar-benar berguna: satu kali pemakaian fitur premium, tambahan kuota harian, atau membuka tema tambahan. Hadiah hanya diberikan kalau iklan benar-benar ditonton sampai selesai.
Bayangan tampilan:
text
╭───────────────────────────────────╮
│ 9:41 ▂▄▆ 🔋 │
│ [ 🎬 Tonton Iklan: +5 Nyawa ] │
│ ▬▬▬▬ │
╰───────────────────────────────────╯Tonton sampai selesai → "+5 Nyawa" diberikan. Tutup iklan di tengah-tengah → hadiah tidak diberikan, disertai pesan yang menjelaskan kenapa:
text
┌─────────────────────────────────┐
│ Iklan belum selesai ditonton, │
│ hadiah tidak diberikan. │
└─────────────────────────────────┘Kriteria selesai:
- Hadiah diberikan lewat callback penghargaan, bukan setelah iklan ditutup.
- Menutup iklan di tengah tidak memberi hadiah apa pun, dan pengguna diberi tahu alasannya.
- Iklan berikutnya dimuat lebih awal, sehingga tombol tidak perlu menunggu saat ditekan.
- Kegagalan memuat iklan menampilkan pesan yang jujur, bukan tombol yang diam saja.
Petunjuk: Rewarded ad adalah format yang paling disukai pengguna justru karena mereka memilih menontonnya, dan itu tercermin di pendapatannya. Kuncinya adalah membuat hadiahnya benar-benar bernilai — hadiah yang terasa sepele menghasilkan penonton yang sedikit. Soal memuat lebih awal: objek iklan hanya bisa dipakai sekali, jadi muat penggantinya segera setelah yang sekarang ditampilkan.
Variasi 3: Persetujuan dan Privasi — ⭐⭐⭐ · 45–60 menit
Tantangan: Tambahkan alur persetujuan privasi yang benar sebelum iklan diinisialisasi: formulir persetujuan untuk pengguna di wilayah yang membutuhkan, dan permintaan App Tracking Transparency di iOS. Pengguna yang menolak tetap melihat iklan, tetapi iklan yang tidak dipersonalisasi.
Bayangan tampilan:
text
┌─────────────────────────────────┐
│ Persetujuan Privasi │
│ Kami & mitra memakai data untuk │
│ personalisasi iklan. │
│ │
│ [ Setuju ] [Tolak Personalisasi]│
└─────────────────────────────────┘Iklan (SDK AdMob) baru diinisialisasi setelah salah satu tombol di atas ditekan — tidak sebelumnya.
Kriteria selesai:
- Iklan tidak diinisialisasi sebelum status persetujuan diketahui.
- Penolakan menghasilkan iklan non-personalisasi, bukan tidak ada iklan sama sekali.
- Permintaan ATT di iOS muncul pada momen yang masuk akal, disertai penjelasan sebelumnya.
- Ada cara bagi pengguna mengubah pilihannya kemudian di layar pengaturan.
Petunjuk: Urutan di kriteria pertama sering terbalik dan itu masalah kepatuhan, bukan sekadar gaya: menginisialisasi SDK iklan sebelum persetujuan berarti pengumpulan data sudah dimulai. Untuk ATT, Apple menolak aplikasi yang memunculkan dialog sistem tanpa konteks — layar penjelasan sebelumnya bukan hanya sopan, ia praktis wajib, dan ia juga menaikkan tingkat persetujuan secara signifikan.
Variasi 4: Kebijakan dan Pengukuran — ⭐⭐⭐⭐ · 60–90 menit
Tantangan: Rancang kebijakan iklan menyeluruh untuk satu aplikasi hipotetismu: format apa di layar mana, frekuensi berapa, dan momen apa yang dilarang. Terapkan sebagai kode terpusat, lalu tambahkan pencatatan supaya kamu bisa mengukur dampaknya terhadap retensi.
Kriteria selesai:
- Seluruh kebijakan berada di satu berkas, dan mengubah frekuensi cukup mengubah satu angka.
- Ada pencatatan berapa iklan yang ditampilkan per sesi dan pada momen apa.
- Kamu menulis daftar momen terlarang dan alasannya, minimal lima butir.
- Kamu bisa menjelaskan bagaimana kamu akan mengukur apakah iklan membuat pengguna berhenti memakai aplikasi.
Petunjuk: Kriteria terakhir adalah yang paling jarang dipikirkan dan paling menentukan: pendapatan iklan mudah diukur, sementara pengguna yang pergi karenanya tidak muncul di laporan mana pun. Cara paling praktis adalah membandingkan retensi antara dua kelompok pengguna dengan frekuensi iklan berbeda. Tanpa pengukuran itu, menaikkan frekuensi selalu terlihat menguntungkan di jangka pendek.
Ikhtisar
- Iklan menurunkan kepuasan pengguna dan menghasilkan relatif sedikit per tampilan. Pertimbangkan langganan sebelum memilihnya.
- App ID (dengan
~) masuk ke manifest; Ad Unit ID (dengan/) dipakai di kode. Menukarnya membuat iklan tidak pernah muncul. - Selalu pakai ID pengujian saat pengembangan. Mengklik iklan sungguhan di aplikasimu sendiri bisa memblokir akunmu permanen.
- Pemeriksaan
kDebugModemembuat pergantian ID otomatis. - Setiap objek iklan wajib di-
dispose(). - Banner: pakai ukuran adaptif, dan jangan pesan ruang sebelum iklan siap — tata letak yang melompat merusak pengalaman.
- Interstitial: terapkan jeda minimal dan ambang jumlah aksi. Jangan tampilkan saat aplikasi dibuka atau di tengah tugas.
- Rewarded ad adalah format terbaik — menghasilkan paling banyak dan justru meningkatkan kepuasan karena pengguna memilih menontonnya.
- App Open Ad kedaluwarsa setelah 4 jam dan paling mudah disalahgunakan. Beri jeda minimal 15 menit.
- Minta persetujuan privasi (GDPR) dan App Tracking Transparency (iOS 14+) sebelum menginisialisasi iklan.
- Bungkus semua penempatan iklan dengan pemeriksaan langganan, agar mudah dinonaktifkan.
- Pantau retensi bersama pendapatan. Iklan berlebih menaikkan pendapatan harian tetapi menurunkan pendapatan jangka panjang.
Berikutnya: Bab 36 — Langganan: Stripe & PayPal.
Transkrip asli
Disintesis dari 4_flutter_ai-chatbot-n-firebase/13_monetizing-with-google-admob.md (6 video: Introduction to Google AdMob, Displaying Banner Ads, Banner Ad Enhancement, Interstitial Ad, Integrating App Open Ads, Controlling App Open Ad Frequency) dan 3_flutter-monetize/8_how-to-make-money-with-your-app.md. Lihat PDF Firebase & AI dan PDF Monetize.