fix(ads): banner retries load when SDK becomes ready
On a cold start the consent flow is still running when home first builds, so createBanner returns null and the slot stayed empty until a rebuild. AdService now exposes an isReady ValueNotifier; BannerAdSlot listens and retries its load once MobileAds finishes initializing. Verified: analyze clean, 169 tests green.
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
// lib/services/ad_service.dart
|
// lib/services/ad_service.dart
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:google_mobile_ads/google_mobile_ads.dart';
|
import 'package:google_mobile_ads/google_mobile_ads.dart';
|
||||||
|
|
||||||
import 'ad_config.dart';
|
import 'ad_config.dart';
|
||||||
@@ -29,11 +30,18 @@ class AdService {
|
|||||||
RewardedAd? _rewarded;
|
RewardedAd? _rewarded;
|
||||||
bool _initialized = false;
|
bool _initialized = false;
|
||||||
|
|
||||||
/// Called by ConsentService once the SDK is initialized. Preloads ads.
|
/// Flips true once the SDK is initialized. Banner slots that were built
|
||||||
|
/// before consent finished listen to this and retry their load — otherwise
|
||||||
|
/// the banner would stay empty until the screen is rebuilt.
|
||||||
|
final ValueNotifier<bool> isReady = ValueNotifier<bool>(false);
|
||||||
|
|
||||||
|
/// Called by ConsentService once the SDK is initialized. Preloads ads and
|
||||||
|
/// notifies any waiting banner slots.
|
||||||
void onSdkReady() {
|
void onSdkReady() {
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
_loadInterstitial();
|
_loadInterstitial();
|
||||||
_loadRewarded();
|
_loadRewarded();
|
||||||
|
isReady.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- lifecycle hooks the game calls ----
|
// ---- lifecycle hooks the game calls ----
|
||||||
@@ -137,5 +145,6 @@ class AdService {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
_interstitial?.dispose();
|
_interstitial?.dispose();
|
||||||
_rewarded?.dispose();
|
_rewarded?.dispose();
|
||||||
|
isReady.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,16 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:google_mobile_ads/google_mobile_ads.dart';
|
import 'package:google_mobile_ads/google_mobile_ads.dart';
|
||||||
|
|
||||||
|
import '../../services/ad_service.dart';
|
||||||
import '../../state/providers.dart';
|
import '../../state/providers.dart';
|
||||||
|
|
||||||
/// A self-loading 320x50 banner for the home/map screens. Renders nothing
|
/// A self-loading 320x50 banner for the home/map screens. Renders nothing
|
||||||
/// when ads are removed or the banner has not loaded — never reserves blank
|
/// when ads are removed or the banner has not loaded — never reserves blank
|
||||||
/// space and never appears on the game screen.
|
/// space and never appears on the game screen.
|
||||||
|
///
|
||||||
|
/// If the slot is built before the AdMob SDK finished initializing (common on
|
||||||
|
/// a cold start while the consent flow is still running), the first create
|
||||||
|
/// returns null; it then retries once the [AdService.isReady] notifier flips.
|
||||||
class BannerAdSlot extends ConsumerStatefulWidget {
|
class BannerAdSlot extends ConsumerStatefulWidget {
|
||||||
const BannerAdSlot({super.key});
|
const BannerAdSlot({super.key});
|
||||||
|
|
||||||
@@ -18,13 +23,22 @@ class BannerAdSlot extends ConsumerStatefulWidget {
|
|||||||
class _BannerAdSlotState extends ConsumerState<BannerAdSlot> {
|
class _BannerAdSlotState extends ConsumerState<BannerAdSlot> {
|
||||||
BannerAd? _ad;
|
BannerAd? _ad;
|
||||||
bool _loaded = false;
|
bool _loaded = false;
|
||||||
|
AdService? _adService;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeDependencies() {
|
void didChangeDependencies() {
|
||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
if (_ad != null) return;
|
if (_adService == null) {
|
||||||
|
_adService = ref.read(adServiceProvider);
|
||||||
|
_adService!.isReady.addListener(_tryCreate);
|
||||||
|
}
|
||||||
|
_tryCreate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _tryCreate() {
|
||||||
|
if (_ad != null || !mounted) return;
|
||||||
if (ref.read(adsRemovedProvider)) return;
|
if (ref.read(adsRemovedProvider)) return;
|
||||||
final ad = ref.read(adServiceProvider).createBanner(
|
final ad = _adService!.createBanner(
|
||||||
listener: BannerAdListener(
|
listener: BannerAdListener(
|
||||||
onAdLoaded: (_) {
|
onAdLoaded: (_) {
|
||||||
if (mounted) setState(() => _loaded = true);
|
if (mounted) setState(() => _loaded = true);
|
||||||
@@ -32,12 +46,13 @@ class _BannerAdSlotState extends ConsumerState<BannerAdSlot> {
|
|||||||
onAdFailedToLoad: (ad, _) => ad.dispose(),
|
onAdFailedToLoad: (ad, _) => ad.dispose(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (ad == null) return;
|
if (ad == null) return; // SDK not ready yet; isReady will retry.
|
||||||
_ad = ad;
|
_ad = ad;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_adService?.isReady.removeListener(_tryCreate);
|
||||||
_ad?.dispose();
|
_ad?.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user