// Author: fruit.com.cn import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../../core/data/remote/catalog_api.dart'; import '../../core/data/services/home_feed_controller.dart'; import '../../core/data/models/reward.dart'; import '../../core/data/services/catalog_controller.dart'; import '../../core/data/services/category_controller.dart'; import '../../core/data/services/favorite_controller.dart'; import '../../core/platform/haptics.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_theme.dart'; import '../../core/widgets/auth_gate.dart'; import '../../core/widgets/app_root_tab.dart'; import '../discover/search_page.dart'; import '../following/following_page.dart'; import '../mine/mine_types.dart'; import '../recommend/recommend_page.dart'; import '../recommend/recommend_discover_page.dart'; import '../reward/reward_page.dart'; import '../mine/mine_view.dart'; import '../wallet/wallet_page.dart'; import 'home_binding.dart'; import 'home_logic.dart'; class HomePage extends StatefulWidget { const HomePage({super.key}); @override State createState() => _HomePageState(); } class _HomePageState extends State with WidgetsBindingObserver { bool _resumeRetryInFlight = false; HomeFeedController? _homeFeed; bool _recommendSeeded = false; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); } @override void dispose() { _homeFeed?.dispose(); WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state != AppLifecycleState.resumed || _resumeRetryInFlight) return; final catalog = context.read(); final categories = context.read(); if (catalog.error == null && categories.error == null) return; unawaited(_retryAfterResume(catalog, categories)); } Future _retryAfterResume( CatalogController catalog, CategoryController categories, ) async { _resumeRetryInFlight = true; try { // 地址是构建期常量,回前台只需要重试失败的数据请求。 await Future.wait([ if (catalog.error != null) catalog.load(), if (categories.error != null) categories.load(), ]); } finally { _resumeRetryInFlight = false; } } @override Widget build(BuildContext context) { final catalog = context.watch(); return Theme(data: AppTheme.dark(), child: _buildHome(context, catalog)); } Widget _buildHome(BuildContext context, CatalogController catalog) { final homeFeed = _ensureHomeFeed(catalog); if (!_recommendSeeded && catalog.dramas.isNotEmpty) { _recommendSeeded = true; homeFeed.seedRecommend(catalog.dramas); WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) unawaited(_refreshRecommendAndPrefetch(homeFeed)); }); } return _HomeTabs(key: const Key('home-tabs'), feedController: homeFeed); } HomeFeedController _ensureHomeFeed(CatalogController catalog) { return _homeFeed ??= HomeFeedController(catalog.repository); } Future _refreshRecommendAndPrefetch(HomeFeedController homeFeed) async { if (await homeFeed.refreshCurrent()) { await homeFeed.prefetchChannels(); } } } /// 五个根页面共享同一路由并保持各自状态。 class _HomeTabs extends StatefulWidget { const _HomeTabs({super.key, required this.feedController}); final HomeFeedController feedController; @override State<_HomeTabs> createState() => _HomeTabsState(); } class _HomeTabsState extends State<_HomeTabs> { late final HomeLogic logic; @override void initState() { super.initState(); logic = HomeBinding().create(widget.feedController.dramas); } @override void dispose() { logic.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return ListenableBuilder( listenable: logic, builder: (context, _) => _scaffold(context), ); } Widget _scaffold(BuildContext context) { // 各 tab 均自绘背景(媒体 tab 纯黑,追剧/我的带顶部红光晕),无需外层渐变。 final content = PopScope( canPop: false, onPopInvokedWithResult: (didPop, _) { if (didPop) return; logic.onSystemBack(); }, child: Scaffold( key: logic.scaffoldKey, backgroundColor: AppColors.transparent, resizeToAvoidBottomInset: false, body: _body(context), bottomNavigationBar: logic.videoImmersive ? null : _nav(context), ), ); return AnnotatedRegion( value: SystemUiOverlayStyle.light, child: content, ); } Widget _body(BuildContext context) { final builders = [ _recommend, _recommendDiscover, _reward, (context) => FollowingPage( onGoHome: () => logic.select(0), favoriteController: context.read(), historyRequest: logic.followingHistoryRequest, visible: logic.tab == 3, ), _my, ]; return _LazyTabStack(index: logic.tab, builders: builders); } Widget _recommend(BuildContext context) { return RecommendPage( feedController: widget.feedController, catalogApi: _catalogApi(context), active: logic.tab == 0, videoOnly: logic.videoImmersive, onImmersiveChanged: logic.onImmersiveChanged, onSearch: () => _openSearch(context), onOpenWallet: () => unawaited(_openWallet(context)), ); } Widget _recommendDiscover(BuildContext context) { return RecommendDiscoverPage( active: logic.tab == 1, onSearch: () => _openSearch(context), onOpenWallet: () => unawaited(_openWallet(context)), ); } CatalogApi? _catalogApi(BuildContext context) { try { return context.read(); } on ProviderNotFoundException { return null; } } void _openSearch(BuildContext context) { Navigator.of( context, ).push(MaterialPageRoute(builder: (_) => const SearchPage())); } Future _openWallet(BuildContext context) async { if (!await ensureSignedIn(context) || !context.mounted) return; await Navigator.of( context, ).push(MaterialPageRoute(builder: (_) => const WalletPage())); } Widget _my(BuildContext context) { return MyPage( active: logic.tab == 4, callbacks: MyPageCallbacks( onAction: logic.myAction, onOpenHistoryTab: logic.openFollowingHistory, ), ); } Widget _reward(BuildContext context) { return RewardPage( active: logic.tab == 2, onBack: () => logic.select(0), onOpenWallet: () => unawaited(_openWallet(context)), onTaskAction: (task) => _handleRewardAction(context, task), ); } void _handleRewardAction(BuildContext context, RewardTask task) { switch (task.actionType) { case 'OPEN_PROFILE': logic.select(4); case 'OPEN_WALLET': unawaited(_openWallet(context)); default: logic.select(0); } } Widget _nav(BuildContext context) { return AppRootBottomNav( barKey: const Key('home-tabs-bar'), selected: AppRootTab.values[logic.tab], onSelected: (tab) => _selectTab(context, tab.index), ); } void _selectTab(BuildContext context, int tab) { final same = logic.tab == tab; if (!same) { unawaited(context.read().selectionClick()); } logic.select(tab); } } /// 五个根 tab 的容器:只有被访问过的 tab 才真正构建并常驻。 /// /// 冷启动时只建推荐页,其余 tab 等第一次切过去再建,减少首屏子树和请求。 class _LazyTabStack extends StatefulWidget { const _LazyTabStack({required this.index, required this.builders}); final int index; final List builders; @override State<_LazyTabStack> createState() => _LazyTabStackState(); } class _LazyTabStackState extends State<_LazyTabStack> { late final Set _visited = {widget.index}; @override void didUpdateWidget(covariant _LazyTabStack oldWidget) { super.didUpdateWidget(oldWidget); _visited.add(widget.index); } @override Widget build(BuildContext context) { return IndexedStack( index: widget.index, children: [ for (var index = 0; index < widget.builders.length; index++) TickerMode( enabled: widget.index == index, child: _visited.contains(index) ? widget.builders[index](context) : const SizedBox.shrink(), ), ], ); } }