Files
ReelShort/lib/pages/home/home_view.dart
T
Vicky 5649e4c3d4 perf(home): 四个根 tab 改为首次可见才构建
原先 IndexedStack 冷启动就把推荐/发现/追剧/我的四棵子树全建出来,各自还会发请求;
现在只建当前 tab,切过去再建并常驻,冷启动少三页构建和一次观看历史请求。
2026-10-29 11:25:47 +08:00

284 lines
8.1 KiB
Dart

// 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/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 '../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<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> 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<CatalogController>();
final categories = context.read<CategoryController>();
if (catalog.error == null && categories.error == null) return;
unawaited(_retryAfterResume(catalog, categories));
}
Future<void> _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<CatalogController>();
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<void> _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<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle.light,
child: content,
);
}
Widget _body(BuildContext context) {
final builders = <WidgetBuilder>[
_recommend,
_recommendDiscover,
(context) => FollowingPage(
onGoHome: () => logic.select(0),
favoriteController: context.read<FavoriteController>(),
historyRequest: logic.followingHistoryRequest,
visible: logic.tab == 2,
),
_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<CatalogApi>();
} on ProviderNotFoundException {
return null;
}
}
void _openSearch(BuildContext context) {
Navigator.of(
context,
).push<void>(MaterialPageRoute(builder: (_) => const SearchPage()));
}
Future<void> _openWallet(BuildContext context) async {
if (!await ensureSignedIn(context) || !context.mounted) return;
await Navigator.of(
context,
).push<void>(MaterialPageRoute(builder: (_) => const WalletPage()));
}
Widget _my(BuildContext context) {
return MyPage(
active: logic.tab == 3,
callbacks: MyPageCallbacks(
onAction: logic.myAction,
onOpenHistoryTab: logic.openFollowingHistory,
),
);
}
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<Haptics>().selectionClick());
}
logic.select(tab);
}
}
/// 四个根 tab 的容器:只有被访问过的 tab 才真正构建并常驻。
///
/// 冷启动时只建推荐页,其余 tab 等第一次切过去再建,省掉三棵子树和它们各自的请求。
class _LazyTabStack extends StatefulWidget {
const _LazyTabStack({required this.index, required this.builders});
final int index;
final List<WidgetBuilder> builders;
@override
State<_LazyTabStack> createState() => _LazyTabStackState();
}
class _LazyTabStackState extends State<_LazyTabStack> {
late final Set<int> _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(),
),
],
);
}
}