Skip to content

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

  1. Buat akun di admob.google.com.
  2. Add app — daftarkan aplikasimu untuk Android dan iOS terpisah.
  3. Salin App ID masing-masing platform.
  4. Ad units → buat unit untuk setiap format yang akan dipakai.
yaml
# pubspec.yaml
dependencies:
  google_mobile_ads: ^5.2.0

Konfigurasi 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"))

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.6
dart
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 bawah

Aturan praktis yang layak dipegang: kalau kamu sendiri akan terganggu oleh penempatan itu, penggunamu juga.

Mengukur hasil

AdMob menyediakan metrik yang perlu kamu pahami:

MetrikArtinya
ImpressionsBerapa kali iklan tampil
ClicksBerapa kali diklik
CTRRasio klik terhadap tampilan
eCPMPendapatan per seribu tampilan
Match ratePersentase permintaan yang terisi iklan
Show ratePersentase 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 kDebugMode membuat 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.

Rangkuman pembelajaran pribadi, disusun ulang dari beberapa kursus Flutter.