原先 IndexedStack 冷启动就把推荐/发现/追剧/我的四棵子树全建出来,各自还会发请求; 现在只建当前 tab,切过去再建并常驻,冷启动少三页构建和一次观看历史请求。
284 lines
8.1 KiB
Dart
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(),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|