Skip to content

Instantly share code, notes, and snippets.

@imnnquy
Created September 9, 2025 05:19
Show Gist options
  • Select an option

  • Save imnnquy/44867cd43c668e16f60a486800b4ca2c to your computer and use it in GitHub Desktop.

Select an option

Save imnnquy/44867cd43c668e16f60a486800b4ca2c to your computer and use it in GitHub Desktop.
import 'dart:async';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:loader_overlay/loader_overlay.dart';
import 'package:provider/provider.dart';
import 'package:tapcraft_hair_clipper/providers/subscription_provider.dart';
import 'package:tapcraft_hair_clipper/services/logger_service/logger_constants.dart';
import 'package:tapcraft_hair_clipper/services/logger_service/logger_service.dart';
import 'package:tapcraft_hair_clipper/utils/navigation_service.dart';
class PreloadAdManager {
static final PreloadAdManager _instance = PreloadAdManager._internal();
factory PreloadAdManager() => _instance;
PreloadAdManager._internal();
InterstitialAd? _preloadedInterstitialAd;
bool _isInterstitialLoading = false;
DateTime? _interstitialLoadTime;
static const Duration _maxCacheTime = Duration(minutes: 30);
/// SỬA LỖI: Khởi tạo là 'true' để logic bỏ qua lần đầu hoạt động.
bool firstLaunch = true;
/// Preload Interstitial Ad
Future<bool> preloadInterstitialAd({
required String adUnitId,
Function? onPreloadSuccess,
Function? onPreloadFailed,
}) async {
// Logic kiểm tra này BÂY GIỜ ĐÃ ĐÚNG, vì quảng cáo sẽ bị gán null ngay khi BẮT ĐẦU hiển thị,
// cho phép việc tải trước quảng cáo 'tiếp theo' được gọi ngay sau đó.
if (_isInterstitialLoading || _isAdStillValid(_interstitialLoadTime)) {
return _preloadedInterstitialAd != null;
}
if (!await _canLoadAds()) {
onPreloadFailed?.call();
return false;
}
_isInterstitialLoading = true;
debugPrint('🔄 Preloading Interstitial Ad: $adUnitId');
try {
await InterstitialAd.load(
adUnitId: adUnitId,
request: const AdRequest(),
adLoadCallback: InterstitialAdLoadCallback(
onAdLoaded: (ad) {
LoggerService.log(
eventName: LoggerConstant.adLoaded,
parameters: {
'adId': ad.adUnitId,
'type': 'preloaded_interstitial',
'responseInfo': ad.responseInfo.toString(),
},
);
_preloadedInterstitialAd = ad;
_interstitialLoadTime = DateTime.now();
_isInterstitialLoading = false;
debugPrint('✅ Interstitial Ad preloaded successfully');
onPreloadSuccess?.call();
},
onAdFailedToLoad: (error) {
LoggerService.log(
eventName: LoggerConstant.adFailedToLoad,
parameters: {
'code': error.code,
'domain': error.domain,
'message': error.message,
'type': 'preloaded_interstitial',
'responseInfo': error.responseInfo.toString(),
},
);
_isInterstitialLoading = false;
debugPrint('❌ Failed to preload Interstitial Ad: ${error.message}');
onPreloadFailed?.call();
},
),
);
} catch (e) {
_isInterstitialLoading = false;
debugPrint('❌ Exception while preloading Interstitial Ad: $e');
onPreloadFailed?.call();
}
return _preloadedInterstitialAd != null;
}
/// Show preloaded Interstitial Ad
/// HÀM NÀY CHỈ HIỂN THỊ, KHÔNG TẢI TRƯỚC QUẢNG CÁO TIẾP THEO.
Future<bool> showPreloadedInterstitialAd({
Function? onAdShown,
Function? onAdDismissed,
Function? onAdFailed,
bool showLoadingOverlay = true,
}) async {
final context = NavigationService.rootNavigatorKey.currentContext;
if (context == null) {
debugPrint('❌ No context available for showing ad');
onAdFailed?.call();
return false;
}
final isPremium = context.read<SubscriptionProvider>().isSubscribed;
if (isPremium) {
debugPrint('ℹ️ Premium user, skipping ad');
onAdDismissed?.call();
return true;
}
if (_preloadedInterstitialAd == null ||
!_isAdStillValid(_interstitialLoadTime)) {
debugPrint('❌ No valid preloaded interstitial ad available');
onAdFailed?.call();
return false;
}
if (showLoadingOverlay) {
context.loaderOverlay.show();
}
// SỬA LỖI RACE CONDITION:
// 1. Sao chép ad vào biến cục bộ.
final adToShow = _preloadedInterstitialAd!;
// 2. Xóa trạng thái chung NGAY LẬP TỨC.
// Điều này "tiêu thụ" quảng cáo và cho phép lần tải trước tiếp theo (nếu được gọi) hoạt động.
_preloadedInterstitialAd = null;
_interstitialLoadTime = null;
try {
adToShow.fullScreenContentCallback =
FullScreenContentCallback(
onAdShowedFullScreenContent: (ad) {
LoggerService.log(
eventName: LoggerConstant.adShown,
parameters: {
'adId': ad.adUnitId,
'type': 'preloaded_interstitial',
'responseInfo': ad.responseInfo.toString(),
},
);
debugPrint('✅ Preloaded Interstitial Ad shown');
onAdShown?.call();
},
onAdDismissedFullScreenContent: (ad) {
LoggerService.log(
eventName: LoggerConstant.adDismissClose,
parameters: {
'adId': ad.adUnitId,
'type': 'preloaded_interstitial',
'responseInfo': ad.responseInfo.toString(),
},
);
// SỬA LỖI (BUG 2): Chỉ dispose ad. KHÔNG chạm vào trạng thái chung (_preloadedInterstitialAd)
// vì trạng thái đó hiện thuộc về lần tải tiếp theo.
ad.dispose();
if (showLoadingOverlay) {
context.loaderOverlay.hide();
}
debugPrint('✅ Preloaded Interstitial Ad dismissed');
onAdDismissed?.call();
},
onAdFailedToShowFullScreenContent: (ad, error) {
LoggerService.log(
eventName: LoggerConstant.adFailedToShow,
parameters: {
'code': error.code,
'domain': error.domain,
'message': error.message,
'type': 'preloaded_interstitial',
},
);
// SỬA LỖI (BUG 2): Chỉ dispose ad.
ad.dispose();
if (showLoadingOverlay) {
context.loaderOverlay.hide();
}
debugPrint(
'❌ Failed to show preloaded Interstitial Ad: ${error.message}');
onAdFailed?.call();
},
);
adToShow.show();
return true;
} catch (e) {
if (showLoadingOverlay) {
context.loaderOverlay.hide();
}
debugPrint('❌ Exception while showing preloaded Interstitial Ad: $e');
onAdFailed?.call();
return false;
}
}
/// ĐÃ XÓA PHƯƠNG THỨC FALLBACK DƯ THỪA (preloadAndShowInterstitialAd)
/// Phương thức này (showAndPreloadInterstitialAd) bây giờ là phương thức chính duy nhất
/// và đã xử lý cả hai trường hợp (có và không có preload).
bool _isAdStillValid(DateTime? loadTime) {
if (loadTime == null) return false;
return DateTime.now().difference(loadTime) < _maxCacheTime;
}
Future<bool> _canLoadAds() async {
final context = NavigationService.rootNavigatorKey.currentContext;
if (context == null) return false;
final isPremium = context.read<SubscriptionProvider>().isSubscribed;
if (isPremium) return false;
final connectivityResult = await Connectivity().checkConnectivity();
return connectivityResult == ConnectivityResult.wifi ||
connectivityResult == ConnectivityResult.mobile;
}
/// Show ads and preload next ads (PHƯƠNG THỨC CHÍNH)
Future<bool> showAndPreloadInterstitialAd({
required String adUnitId,
Function? onAdShown,
Function? onAdDismissed,
Function? onAdFailed,
bool showLoadingOverlay = false,
bool preloadNext = true,
}) async {
if (firstLaunch) {
firstLaunch = false;
onAdDismissed?.call(); // Gọi dismiss để luồng code tiếp tục
return true; // Trả về true (thành công) vì việc bỏ qua là dự định
}
final context = NavigationService.rootNavigatorKey.currentContext;
if (context == null) {
debugPrint('ANNE: ❌ No context available for showing ad');
onAdFailed?.call();
return false;
}
final isPremium = context.read<SubscriptionProvider>().isSubscribed;
if (isPremium) {
debugPrint('ANNE: ℹ️ Premium user, skipping ad');
onAdDismissed?.call();
return true;
}
// LUỒNG A (HAPPY PATH): Đã có quảng cáo tải trước
if (_preloadedInterstitialAd != null &&
_isAdStillValid(_interstitialLoadTime)) {
debugPrint('ANNE: 🚀 Showing preloaded ad and preloading next one');
// SỬA LỖI (BUG 1 & 2):
// 1. Lấy quảng cáo ra để hiển thị
final adToShow = _preloadedInterstitialAd!;
// 2. Tiêu thụ quảng cáo (xóa trạng thái chung) NGAY LẬP TỨC.
_preloadedInterstitialAd = null;
_interstitialLoadTime = null;
// 3. Bây giờ bắt đầu tải trước quảng cáo tiếp theo.
// Vì trạng thái đã bị xóa, hàm preloadInterstitialAd sẽ vượt qua kiểm tra và chạy đúng.
if (preloadNext) {
_preloadNextInterstitialAd(adUnitId);
}
if (showLoadingOverlay) {
context.loaderOverlay.show();
}
try {
adToShow.fullScreenContentCallback =
FullScreenContentCallback(
onAdShowedFullScreenContent: (ad) {
LoggerService.log(
eventName: LoggerConstant.adShown,
parameters: {
'adId': ad.adUnitId,
'type': 'show_and_preload_interstitial',
'responseInfo': ad.responseInfo.toString(),
},
);
debugPrint('ANNE: ✅ Interstitial Ad shown (with preload)');
onAdShown?.call();
},
onAdDismissedFullScreenContent: (ad) {
LoggerService.log(
eventName: LoggerConstant.adDismissClose,
parameters: {
'adId': ad.adUnitId,
'type': 'show_and_preload_interstitial',
'responseInfo': ad.responseInfo.toString(),
},
);
// SỬA LỖI (BUG 2): Chỉ dispose ad. KHÔNG CHẠM VÀO TRẠNG THÁI CHUNG.
ad.dispose();
if (showLoadingOverlay) {
context.loaderOverlay.hide();
}
debugPrint(
'ANNE: ✅ Interstitial Ad dismissed, next ad is being preloaded');
onAdDismissed?.call();
},
onAdFailedToShowFullScreenContent: (ad, error) {
LoggerService.log(
eventName: LoggerConstant.adFailedToShow,
parameters: {
'code': error.code,
'domain': error.domain,
'message': error.message,
'type': 'show_and_preload_interstitial',
},
);
// SỬA LỖI (BUG 2): Chỉ dispose ad.
ad.dispose();
if (showLoadingOverlay) {
context.loaderOverlay.hide();
}
debugPrint(
'ANNE: ❌ Failed to show interstitial ad: ${error.message}');
onAdFailed?.call();
},
);
adToShow.show();
return true;
} catch (e) {
if (showLoadingOverlay) {
context.loaderOverlay.hide();
}
debugPrint('❌ Exception while showing interstitial ad: $e');
onAdFailed?.call();
return false;
}
}
// LUỒNG B (FALLBACK PATH): Không có quảng cáo tải trước, tải và hiển thị ngay
debugPrint(
'ANNE: 🔄 No preloaded ad, loading and showing immediately + preloading next');
if (showLoadingOverlay) {
context.loaderOverlay.show();
}
try {
await InterstitialAd.load(
adUnitId: adUnitId,
request: const AdRequest(),
adLoadCallback: InterstitialAdLoadCallback(
onAdLoaded: (ad) {
// Quảng cáo A vừa tải xong (để hiển thị ngay)
// Bắt đầu tải Quảng cáo B trong nền
if (preloadNext) {
_preloadNextInterstitialAd(adUnitId);
}
ad.fullScreenContentCallback = FullScreenContentCallback(
onAdShowedFullScreenContent: (ad) {
LoggerService.log(
eventName: LoggerConstant.adShown,
parameters: {
'adId': ad.adUnitId,
'type': 'show_and_preload_interstitial_immediate',
'responseInfo': ad.responseInfo.toString(),
},
);
debugPrint(
'ANNE: ✅ Interstitial Ad shown (immediate load + preload)');
onAdShown?.call();
},
onAdDismissedFullScreenContent: (ad) {
LoggerService.log(
eventName: LoggerConstant.adDismissClose,
parameters: {
'adId': ad.adUnitId,
'type': 'show_and_preload_interstitial_immediate',
'responseInfo': ad.responseInfo.toString(),
},
);
// SỬA LỖI (BUG 2): Chỉ dispose ad (Quảng cáo A).
// Không chạm vào trạng thái chung (vì Quảng cáo B đang tải vào đó).
ad.dispose();
if (showLoadingOverlay) {
context.loaderOverlay.hide();
}
debugPrint(
'ANNE: ✅ Interstitial Ad dismissed, next ad is being preloaded');
onAdDismissed?.call();
},
onAdFailedToShowFullScreenContent: (ad, error) {
LoggerService.log(
eventName: LoggerConstant.adFailedToShow,
parameters: {
'code': error.code,
'domain': error.domain,
'message': error.message,
'type': 'show_and_preload_interstitial_immediate',
},
);
// SỬA LỖI (BUG 2): Chỉ dispose ad.
ad.dispose();
if (showLoadingOverlay) {
context.loaderOverlay.hide();
}
debugPrint(
'ANNE: ❌ Failed to show interstitial ad: ${error.message}');
onAdFailed?.call();
},
);
ad.show(); // Hiển thị Quảng cáo A
},
onAdFailedToLoad: (LoadAdError error) {
LoggerService.log(
eventName: LoggerConstant.adFailedToLoad,
parameters: {
'code': error.code,
'domain': error.domain,
'message': error.message,
'type': 'show_and_preload_interstitial_immediate',
'responseInfo': error.responseInfo.toString(),
},
);
if (showLoadingOverlay) {
context.loaderOverlay.hide();
}
debugPrint(
'ANNE: ❌ Failed to load interstitial ad: ${error.message}');
onAdFailed?.call();
},
),
);
return true;
} catch (e) {
if (showLoadingOverlay) {
context.loaderOverlay.hide();
}
debugPrint('ANNE: ❌ Exception while loading interstitial ad: $e');
onAdFailed?.call();
return false;
}
}
void _preloadNextInterstitialAd(String adUnitId) {
// Hàm này chỉ là một trình bao bọc (wrapper) để gọi hàm preload chính
// Nó ngăn chặn nhiều yêu cầu tải cùng lúc nếu một yêu cầu đã chạy.
if (_isInterstitialLoading) {
debugPrint('⏳ Already preloading interstitial ad, skipping...');
return;
}
debugPrint('🔄 Preloading next interstitial ad in background...');
// Cứ gọi và không cần chờ (fire-and-forget)
preloadInterstitialAd(
adUnitId: adUnitId,
onPreloadSuccess: () {
debugPrint('✅ Next interstitial ad preloaded successfully');
},
onPreloadFailed: () {
debugPrint('❌ Failed to preload next interstitial ad');
},
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment