ads_state.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. import 'dart:async';
  2. import 'package:flutter/material.dart';
  3. import 'package:fluttertoast/fluttertoast.dart';
  4. import 'package:logging/logging.dart';
  5. import 'package:provider/provider.dart';
  6. import 'package:puzzleweave/ads/applovin_ads_controller.dart';
  7. import 'package:puzzleweave/l10n/app_localizations.dart';
  8. import 'package:puzzleweave/models/data.dart';
  9. import 'package:puzzleweave/persistence/persistence.dart';
  10. import 'package:puzzleweave/skin/skin.dart';
  11. import '../remote_config/remote_config.dart';
  12. import 'ad_helper.dart';
  13. final Logger _log = Logger('AdsState');
  14. /// Add by guoziyi
  15. /// 自带广告的 StatefulWidget State
  16. /// 从该类继承的 State 自动带有广告功能
  17. abstract class AdsState<T extends StatefulWidget> extends State<T> {
  18. ///这个值用于数据统计,子类应该在initState初始化的时候正确设置skuId
  19. String skuId = "";
  20. VoidCallback? onEnterBackground; // app 进入后台回调, 外部如关心, 可设置此回调
  21. Function(Duration duration)? onEnterForeground; // app 回到前台回调, 外部如关心, 可设置此回调
  22. Function(AdState state)? onRewardAdState; // reward广告状态回调,外部如关心,可设置此回调
  23. Function(AdState state)? onInterstitialAdState; // reward广告状态回调,外部如关心,可设置此回调
  24. late Data data;
  25. late ApplovinAdsController _applovinAdsController;
  26. late ValueNotifier<AppLifecycleState> lifecycleNotifier;
  27. Completer<bool>? _intersCompleter;
  28. Completer<bool>? _rewardCompleter;
  29. bool earnedReward = false; //是否确定获得激励
  30. //插屏广告是否准备好
  31. late ValueNotifier<bool> intersReadyNotifier;
  32. bool get isIntersReady => intersReadyNotifier.value;
  33. //激励广告是否准备好
  34. late ValueNotifier<bool> rewardReadyNotifier;
  35. bool get isRewardReady => rewardReadyNotifier.value;
  36. //插屏广告当前状态
  37. AdState get intersState => _applovinAdsController.interstitialAdState.value;
  38. //激励广告当前状态
  39. AdState get rewardState => _applovinAdsController.rewardedAdState.value;
  40. // 新增:用于控制 Banner 实际显示的变量
  41. bool _isBannerVisible = false;
  42. bool get isBannerVisible => _isBannerVisible;
  43. @override
  44. void initState() {
  45. super.initState();
  46. data = context.read<Data>();
  47. // 1. 监听 ValueNotifier 的变化
  48. // 当 completedWorks.value 被重新赋值时(见 data.dart 的 workDone 方法),触发刷新
  49. data.completedWorks.addListener(_onLevelChanged);
  50. _applovinAdsController = context.read<ApplovinAdsController>();
  51. _applovinAdsController.interstitialAdState.removeListener(_onInterstitialAdState);
  52. _applovinAdsController.interstitialAdState.addListener(_onInterstitialAdState);
  53. _applovinAdsController.rewardedAdState.removeListener(_onRewardAdState);
  54. _applovinAdsController.rewardedAdState.addListener(_onRewardAdState);
  55. intersReadyNotifier = ValueNotifier(intersState == AdState.ready);
  56. rewardReadyNotifier = ValueNotifier(rewardState == AdState.ready);
  57. lifecycleNotifier = context.read<ValueNotifier<AppLifecycleState>>();
  58. lifecycleNotifier.removeListener(_handleAppLifecycle);
  59. lifecycleNotifier.addListener(_handleAppLifecycle);
  60. // 优化:不在 initState 立即加载,通过延迟分散启动压力
  61. Future.microtask(() {
  62. _applovinAdsController.loadInterstitialAd();
  63. _applovinAdsController.loadRewardedAd();
  64. });
  65. // 初始检查一次 Banner
  66. _updateBannerVisibility();
  67. }
  68. // 关卡变化的回调
  69. void _onLevelChanged() {
  70. _log.info("Level changed detected via ValueNotifier, updating banner...");
  71. _updateBannerVisibility();
  72. }
  73. Future<void> _updateBannerVisibility() async {
  74. // 1. 立即检查:如果已经不在此页面,直接退出
  75. if (!mounted) return;
  76. try {
  77. // 2. 检查 SDK 初始化(增加超时保护,防止 completer 永远不返回导致的内存泄漏)
  78. bool ready = await _applovinAdsController.completer.future.timeout(const Duration(seconds: 5), onTimeout: () => false);
  79. // 3. 再次检查:在 await 异步回来后,必须再次检查 mounted
  80. // 这是解决 Null check 报错的关键,因为此时 state 可能已被 dispose
  81. if (!mounted) return;
  82. // 4. 获取当前关卡数和状态
  83. int doneLevels = data.currentLevel;
  84. // 确保生命周期对象还在
  85. final currentState = lifecycleNotifier.value;
  86. // 5. 综合判断
  87. bool shouldShow = ready && shouldShowBannerAd(doneLevels) && currentState == AppLifecycleState.resumed;
  88. if (_isBannerVisible != shouldShow) {
  89. // 6. 最终加固:在 setState 前最后一刻检查
  90. setState(() {
  91. _isBannerVisible = shouldShow;
  92. });
  93. _log.info("Banner visibility updated: $_isBannerVisible");
  94. }
  95. } catch (e) {
  96. _log.warning("Update banner visibility failed: $e");
  97. }
  98. }
  99. /// 🔥 关键优化:重写此方法。子类在调用 Navigator.push 离开页面前,应手动调用
  100. void cleanBanner() {
  101. _log.info("🔥 Cleaning up ads before navigation...");
  102. // 物理销毁
  103. _applovinAdsController.destroyBanner();
  104. }
  105. @override
  106. void dispose() {
  107. data.completedWorks.removeListener(_onLevelChanged);
  108. lifecycleNotifier.removeListener(_handleAppLifecycle);
  109. _applovinAdsController.interstitialAdState.removeListener(_onInterstitialAdState);
  110. _applovinAdsController.rewardedAdState.removeListener(_onRewardAdState);
  111. // 2. 强制物理销毁原生 Banner 纹理(解决三星 J2 GPU Leak 问题)
  112. _applovinAdsController.destroyBanner();
  113. intersReadyNotifier.dispose();
  114. rewardReadyNotifier.dispose();
  115. if (_intersCompleter != null && !_intersCompleter!.isCompleted) {
  116. _intersCompleter!.complete(false);
  117. }
  118. _intersCompleter = null;
  119. if (_rewardCompleter != null && !_rewardCompleter!.isCompleted) {
  120. _rewardCompleter!.complete(false);
  121. }
  122. _rewardCompleter = null;
  123. super.dispose();
  124. }
  125. /// 广告 sdk 是否初始化完成
  126. Future<bool> adSDKReady() async {
  127. return _applovinAdsController.completer.future;
  128. }
  129. /// 检查是否应该展示banner广告
  130. bool shouldShowBannerAd(int doneLevels) {
  131. // 增加:如果是低内存设备,坚决不展示 Banner
  132. if (_applovinAdsController.isLowRamDevice) return false;
  133. if (doneLevels == 0) {
  134. _log.info("首关不展示banner广告");
  135. return false; // 首关是引导关卡,一定不要显示广告
  136. }
  137. bool levelShould = doneLevels >= RemoteConfig().intersFreeLevels;
  138. DateTime now = DateTime.now();
  139. int bannerFreeDuration = RemoteConfig().bannerFreeDuration;
  140. DateTime firstRunTime = Persistence().firstRunTime;
  141. bool timeShould = now.difference(firstRunTime).inMinutes > bannerFreeDuration;
  142. return levelShould || timeShould;
  143. }
  144. /// banner 广告
  145. Widget getBanner(String positionKey) {
  146. return _applovinAdsController.getBannerWidget(positionKey);
  147. }
  148. ///显示激励广告
  149. ///返回奖励获取结果。true:成功获取到奖励;false;未获得奖励
  150. Future<bool> showRewardAd(String src, String skuId) async {
  151. if (!(await _applovinAdsController.isRewardedAdReady())) {
  152. _applovinAdsController.loadRewardedAd();
  153. if (mounted) {
  154. Fluttertoast.showToast(
  155. msg: AppLocalizations.of(context)!.adNotReady,
  156. toastLength: Toast.LENGTH_SHORT,
  157. gravity: ToastGravity.CENTER,
  158. timeInSecForIosWeb: 1,
  159. backgroundColor: SkinHelper.slotBorderColor,
  160. textColor: Colors.white,
  161. fontSize: 16.0,
  162. );
  163. }
  164. return false;
  165. }
  166. if (_rewardCompleter != null && !_rewardCompleter!.isCompleted) {
  167. _rewardCompleter!.complete(earnedReward);
  168. }
  169. _rewardCompleter = Completer<bool>();
  170. this.skuId = skuId;
  171. earnedReward = false;
  172. _applovinAdsController.showRewardedlAd(
  173. adSrc: src,
  174. skuId: skuId,
  175. onUserEarnedReward: (ad, reward) {
  176. _log.info("................earn reward..........");
  177. earnedReward = true;
  178. },
  179. );
  180. return _rewardCompleter!.future;
  181. }
  182. bool shouldShowInterstitialAd(String src, int doneLevels) {
  183. bool shouldShowAd = false;
  184. if (src == 'level_enter') {
  185. shouldShowAd = _shouldShowEnterIntersAd(doneLevels);
  186. } else {
  187. shouldShowAd = _shouldShowExitIntersAd(doneLevels);
  188. }
  189. return shouldShowAd;
  190. }
  191. ///显示插屏广告
  192. ///返回广告播放结果。true:播放成功;false;播放失败
  193. Future<bool> showInterstitialAd(String src, String skuId, int doneLevels) async {
  194. bool shouldShowAd = shouldShowInterstitialAd(src, doneLevels);
  195. if (!shouldShowAd) return false;
  196. // 插屏广告没有预备好或者当前不符合播放插屏广告条件,直接返回false
  197. if (!(await _applovinAdsController.isInterstitialAdReady())) {
  198. _applovinAdsController.loadInterstitialAd();
  199. return false;
  200. }
  201. if (_intersCompleter != null && !_intersCompleter!.isCompleted) {
  202. _intersCompleter!.complete(false);
  203. }
  204. _intersCompleter = Completer<bool>();
  205. this.skuId = skuId;
  206. _applovinAdsController.showInterstitialAd(adSrc: src, skuId: skuId);
  207. return _intersCompleter!.future;
  208. }
  209. // 检查是否应该展示进入插屏广告
  210. bool _shouldShowEnterIntersAd(int doneLevels) {
  211. if (doneLevels == 0) return false; // 首关是引导关卡,一定不要显示广告
  212. bool levelShould = doneLevels >= RemoteConfig().intersFreeLevels;
  213. DateTime now = DateTime.now();
  214. int enterIntersDuration = RemoteConfig().enterIntersFreeDuration; //新手保护时长,单位:分钟
  215. DateTime firstRunTime = Persistence().firstRunTime;
  216. bool timeShould = now.difference(firstRunTime).inMinutes > enterIntersDuration; //判断当前时间是否已超过新手保护时长
  217. return levelShould || timeShould; // 时间限制和关卡限制两者只要满足其一都可以播放插屏
  218. }
  219. // 检查是否应该展示退出插屏广告
  220. bool _shouldShowExitIntersAd(int doneLevels) {
  221. bool levelShould = doneLevels >= RemoteConfig().intersFreeLevels;
  222. DateTime now = DateTime.now();
  223. int quitIntersDuration = RemoteConfig().quitIntersFreeDuration; //新手保护时长,单位:分钟
  224. DateTime firstRunTime = Persistence().firstRunTime;
  225. bool timeShould = now.difference(firstRunTime).inMinutes > quitIntersDuration;
  226. return levelShould || timeShould; // 时间限制和关卡限制两者只要满足其一都可以播放插屏
  227. }
  228. // 插屏广告回调处理
  229. _onInterstitialAdState() {
  230. if (intersState == AdState.dismissed) {
  231. _log.info("onInterstitialAdState AdState.dismissed");
  232. if (_intersCompleter != null && !_intersCompleter!.isCompleted) {
  233. _intersCompleter!.complete(true);
  234. }
  235. _intersCompleter = null;
  236. }
  237. intersReadyNotifier.value = (intersState == AdState.ready);
  238. onInterstitialAdState?.call(intersState);
  239. }
  240. // 激励广告回调处理
  241. _onRewardAdState() {
  242. if (_applovinAdsController.rewardedAdState.value == AdState.dismissed) {
  243. _log.info("onRewardAdState AdState.dismissed");
  244. if (_rewardCompleter != null && !_rewardCompleter!.isCompleted) {
  245. _rewardCompleter!.complete(earnedReward);
  246. }
  247. _rewardCompleter = null;
  248. }
  249. rewardReadyNotifier.value = (rewardState == AdState.ready);
  250. onRewardAdState?.call(rewardState);
  251. }
  252. ///-----------[Lifecycle]-------------
  253. ///需要处理可重写状态处理函数
  254. AppLifecycleState lifeState = AppLifecycleState.resumed;
  255. bool get isActive => lifeState == AppLifecycleState.resumed;
  256. void _handleAppLifecycle() {
  257. _log.info('AppLifecycleState changed: ${lifecycleNotifier.value}');
  258. lifeState = lifecycleNotifier.value;
  259. // 优化:回到后台时主动清理 Banner,回到前台再重建
  260. // 这能极大缓解 FD 句柄数堆积的问题
  261. if (lifeState == AppLifecycleState.paused) {
  262. _applovinAdsController.destroyBanner();
  263. if (mounted) setState(() => _isBannerVisible = false);
  264. } else if (lifeState == AppLifecycleState.resumed) {
  265. _updateBannerVisibility();
  266. }
  267. if (lifeState == AppLifecycleState.inactive) {
  268. //前台可见,但是无法交互
  269. onInactive();
  270. } else if (lifeState == AppLifecycleState.paused) {
  271. //前台不可见
  272. onPause();
  273. } else if (lifeState == AppLifecycleState.resumed) {
  274. //前台可见,可交互
  275. onResume();
  276. }
  277. }
  278. void onInactive() {}
  279. DateTime? _leaveTime;
  280. void onPause() {
  281. if (intersState == AdState.showing || rewardState == AdState.showing) {
  282. return;
  283. }
  284. _leaveTime = DateTime.now(); // 记录切换到后台的时间
  285. onEnterBackground?.call();
  286. _log.info("App enter background, leaveTime: $_leaveTime");
  287. }
  288. void onResume() {
  289. if (_leaveTime != null) {
  290. var duration = DateTime.now().difference(_leaveTime!);
  291. _log.info("App enter foreground, leaveTime: $_leaveTime, duration=$duration");
  292. onEnterForeground?.call(duration);
  293. }
  294. _leaveTime = null;
  295. }
  296. void showAppLovinDebugger() {
  297. _applovinAdsController.showAppLovinDebugger();
  298. }
  299. }