import 'dart:async'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:logging/logging.dart'; import 'package:provider/provider.dart'; import 'package:puzzleweave/ads/applovin_ads_controller.dart'; import 'package:puzzleweave/l10n/app_localizations.dart'; import 'package:puzzleweave/models/data.dart'; import 'package:puzzleweave/persistence/persistence.dart'; import 'package:puzzleweave/skin/skin.dart'; import '../remote_config/remote_config.dart'; import 'ad_helper.dart'; final Logger _log = Logger('AdsState'); /// Add by guoziyi /// 自带广告的 StatefulWidget State /// 从该类继承的 State 自动带有广告功能 abstract class AdsState extends State { ///这个值用于数据统计,子类应该在initState初始化的时候正确设置skuId String skuId = ""; VoidCallback? onEnterBackground; // app 进入后台回调, 外部如关心, 可设置此回调 Function(Duration duration)? onEnterForeground; // app 回到前台回调, 外部如关心, 可设置此回调 Function(AdState state)? onRewardAdState; // reward广告状态回调,外部如关心,可设置此回调 Function(AdState state)? onInterstitialAdState; // reward广告状态回调,外部如关心,可设置此回调 late Data data; late ApplovinAdsController _applovinAdsController; late ValueNotifier lifecycleNotifier; Completer? _intersCompleter; Completer? _rewardCompleter; bool earnedReward = false; //是否确定获得激励 //插屏广告是否准备好 late ValueNotifier intersReadyNotifier; bool get isIntersReady => intersReadyNotifier.value; //激励广告是否准备好 late ValueNotifier rewardReadyNotifier; bool get isRewardReady => rewardReadyNotifier.value; //插屏广告当前状态 AdState get intersState => _applovinAdsController.interstitialAdState.value; //激励广告当前状态 AdState get rewardState => _applovinAdsController.rewardedAdState.value; // 新增:用于控制 Banner 实际显示的变量 bool _isBannerVisible = false; bool get isBannerVisible => _isBannerVisible; @override void initState() { super.initState(); data = context.read(); // 1. 监听 ValueNotifier 的变化 // 当 completedWorks.value 被重新赋值时(见 data.dart 的 workDone 方法),触发刷新 data.completedWorks.addListener(_onLevelChanged); _applovinAdsController = context.read(); _applovinAdsController.interstitialAdState.removeListener(_onInterstitialAdState); _applovinAdsController.interstitialAdState.addListener(_onInterstitialAdState); _applovinAdsController.rewardedAdState.removeListener(_onRewardAdState); _applovinAdsController.rewardedAdState.addListener(_onRewardAdState); intersReadyNotifier = ValueNotifier(intersState == AdState.ready); rewardReadyNotifier = ValueNotifier(rewardState == AdState.ready); lifecycleNotifier = context.read>(); lifecycleNotifier.removeListener(_handleAppLifecycle); lifecycleNotifier.addListener(_handleAppLifecycle); // 优化:不在 initState 立即加载,通过延迟分散启动压力 Future.microtask(() { _applovinAdsController.loadInterstitialAd(); _applovinAdsController.loadRewardedAd(); }); // 初始检查一次 Banner _updateBannerVisibility(); } // 关卡变化的回调 void _onLevelChanged() { _log.info("Level changed detected via ValueNotifier, updating banner..."); _updateBannerVisibility(); } Future _updateBannerVisibility() async { // 1. 立即检查:如果已经不在此页面,直接退出 if (!mounted) return; try { // 2. 检查 SDK 初始化(增加超时保护,防止 completer 永远不返回导致的内存泄漏) bool ready = await _applovinAdsController.completer.future.timeout(const Duration(seconds: 5), onTimeout: () => false); // 3. 再次检查:在 await 异步回来后,必须再次检查 mounted // 这是解决 Null check 报错的关键,因为此时 state 可能已被 dispose if (!mounted) return; // 4. 获取当前关卡数和状态 int doneLevels = data.currentLevel; // 确保生命周期对象还在 final currentState = lifecycleNotifier.value; // 5. 综合判断 bool shouldShow = ready && shouldShowBannerAd(doneLevels) && currentState == AppLifecycleState.resumed; if (_isBannerVisible != shouldShow) { // 6. 最终加固:在 setState 前最后一刻检查 setState(() { _isBannerVisible = shouldShow; }); _log.info("Banner visibility updated: $_isBannerVisible"); } } catch (e) { _log.warning("Update banner visibility failed: $e"); } } /// 🔥 关键优化:重写此方法。子类在调用 Navigator.push 离开页面前,应手动调用 void cleanBanner() { _log.info("🔥 Cleaning up ads before navigation..."); // 物理销毁 _applovinAdsController.destroyBanner(); } @override void dispose() { data.completedWorks.removeListener(_onLevelChanged); lifecycleNotifier.removeListener(_handleAppLifecycle); _applovinAdsController.interstitialAdState.removeListener(_onInterstitialAdState); _applovinAdsController.rewardedAdState.removeListener(_onRewardAdState); // 2. 强制物理销毁原生 Banner 纹理(解决三星 J2 GPU Leak 问题) _applovinAdsController.destroyBanner(); intersReadyNotifier.dispose(); rewardReadyNotifier.dispose(); if (_intersCompleter != null && !_intersCompleter!.isCompleted) { _intersCompleter!.complete(false); } _intersCompleter = null; if (_rewardCompleter != null && !_rewardCompleter!.isCompleted) { _rewardCompleter!.complete(false); } _rewardCompleter = null; super.dispose(); } /// 广告 sdk 是否初始化完成 Future adSDKReady() async { return _applovinAdsController.completer.future; } /// 检查是否应该展示banner广告 bool shouldShowBannerAd(int doneLevels) { // 增加:如果是低内存设备,坚决不展示 Banner if (_applovinAdsController.isLowRamDevice) return false; if (doneLevels == 0) { _log.info("首关不展示banner广告"); return false; // 首关是引导关卡,一定不要显示广告 } bool levelShould = doneLevels >= RemoteConfig().intersFreeLevels; DateTime now = DateTime.now(); int bannerFreeDuration = RemoteConfig().bannerFreeDuration; DateTime firstRunTime = Persistence().firstRunTime; bool timeShould = now.difference(firstRunTime).inMinutes > bannerFreeDuration; return levelShould || timeShould; } /// banner 广告 Widget getBanner(String positionKey) { return _applovinAdsController.getBannerWidget(positionKey); } ///显示激励广告 ///返回奖励获取结果。true:成功获取到奖励;false;未获得奖励 Future showRewardAd(String src, String skuId) async { if (!(await _applovinAdsController.isRewardedAdReady())) { _applovinAdsController.loadRewardedAd(); if (mounted) { Fluttertoast.showToast( msg: AppLocalizations.of(context)!.adNotReady, toastLength: Toast.LENGTH_SHORT, gravity: ToastGravity.CENTER, timeInSecForIosWeb: 1, backgroundColor: SkinHelper.slotBorderColor, textColor: Colors.white, fontSize: 16.0, ); } return false; } if (_rewardCompleter != null && !_rewardCompleter!.isCompleted) { _rewardCompleter!.complete(earnedReward); } _rewardCompleter = Completer(); this.skuId = skuId; earnedReward = false; _applovinAdsController.showRewardedlAd( adSrc: src, skuId: skuId, onUserEarnedReward: (ad, reward) { _log.info("................earn reward.........."); earnedReward = true; }, ); return _rewardCompleter!.future; } bool shouldShowInterstitialAd(String src, int doneLevels) { bool shouldShowAd = false; if (src == 'level_enter') { shouldShowAd = _shouldShowEnterIntersAd(doneLevels); } else { shouldShowAd = _shouldShowExitIntersAd(doneLevels); } return shouldShowAd; } ///显示插屏广告 ///返回广告播放结果。true:播放成功;false;播放失败 Future showInterstitialAd(String src, String skuId, int doneLevels) async { bool shouldShowAd = shouldShowInterstitialAd(src, doneLevels); if (!shouldShowAd) return false; // 插屏广告没有预备好或者当前不符合播放插屏广告条件,直接返回false if (!(await _applovinAdsController.isInterstitialAdReady())) { _applovinAdsController.loadInterstitialAd(); return false; } if (_intersCompleter != null && !_intersCompleter!.isCompleted) { _intersCompleter!.complete(false); } _intersCompleter = Completer(); this.skuId = skuId; _applovinAdsController.showInterstitialAd(adSrc: src, skuId: skuId); return _intersCompleter!.future; } // 检查是否应该展示进入插屏广告 bool _shouldShowEnterIntersAd(int doneLevels) { if (doneLevels == 0) return false; // 首关是引导关卡,一定不要显示广告 bool levelShould = doneLevels >= RemoteConfig().intersFreeLevels; DateTime now = DateTime.now(); int enterIntersDuration = RemoteConfig().enterIntersFreeDuration; //新手保护时长,单位:分钟 DateTime firstRunTime = Persistence().firstRunTime; bool timeShould = now.difference(firstRunTime).inMinutes > enterIntersDuration; //判断当前时间是否已超过新手保护时长 return levelShould || timeShould; // 时间限制和关卡限制两者只要满足其一都可以播放插屏 } // 检查是否应该展示退出插屏广告 bool _shouldShowExitIntersAd(int doneLevels) { bool levelShould = doneLevels >= RemoteConfig().intersFreeLevels; DateTime now = DateTime.now(); int quitIntersDuration = RemoteConfig().quitIntersFreeDuration; //新手保护时长,单位:分钟 DateTime firstRunTime = Persistence().firstRunTime; bool timeShould = now.difference(firstRunTime).inMinutes > quitIntersDuration; return levelShould || timeShould; // 时间限制和关卡限制两者只要满足其一都可以播放插屏 } // 插屏广告回调处理 _onInterstitialAdState() { if (intersState == AdState.dismissed) { _log.info("onInterstitialAdState AdState.dismissed"); if (_intersCompleter != null && !_intersCompleter!.isCompleted) { _intersCompleter!.complete(true); } _intersCompleter = null; } intersReadyNotifier.value = (intersState == AdState.ready); onInterstitialAdState?.call(intersState); } // 激励广告回调处理 _onRewardAdState() { if (_applovinAdsController.rewardedAdState.value == AdState.dismissed) { _log.info("onRewardAdState AdState.dismissed"); if (_rewardCompleter != null && !_rewardCompleter!.isCompleted) { _rewardCompleter!.complete(earnedReward); } _rewardCompleter = null; } rewardReadyNotifier.value = (rewardState == AdState.ready); onRewardAdState?.call(rewardState); } ///-----------[Lifecycle]------------- ///需要处理可重写状态处理函数 AppLifecycleState lifeState = AppLifecycleState.resumed; bool get isActive => lifeState == AppLifecycleState.resumed; void _handleAppLifecycle() { _log.info('AppLifecycleState changed: ${lifecycleNotifier.value}'); lifeState = lifecycleNotifier.value; // 优化:回到后台时主动清理 Banner,回到前台再重建 // 这能极大缓解 FD 句柄数堆积的问题 if (lifeState == AppLifecycleState.paused) { _applovinAdsController.destroyBanner(); if (mounted) setState(() => _isBannerVisible = false); } else if (lifeState == AppLifecycleState.resumed) { _updateBannerVisibility(); } if (lifeState == AppLifecycleState.inactive) { //前台可见,但是无法交互 onInactive(); } else if (lifeState == AppLifecycleState.paused) { //前台不可见 onPause(); } else if (lifeState == AppLifecycleState.resumed) { //前台可见,可交互 onResume(); } } void onInactive() {} DateTime? _leaveTime; void onPause() { if (intersState == AdState.showing || rewardState == AdState.showing) { return; } _leaveTime = DateTime.now(); // 记录切换到后台的时间 onEnterBackground?.call(); _log.info("App enter background, leaveTime: $_leaveTime"); } void onResume() { if (_leaveTime != null) { var duration = DateTime.now().difference(_leaveTime!); _log.info("App enter foreground, leaveTime: $_leaveTime, duration=$duration"); onEnterForeground?.call(duration); } _leaveTime = null; } void showAppLovinDebugger() { _applovinAdsController.showAppLovinDebugger(); } }