merge: 修正 ChangeNotifier 监听生命周期

This commit is contained in:
Zhou Weidong committed 2026-10-22 19:14:41 +08:00
commit 2cfa655750
7 files changed
+75 -58

No files matched your search

+7
View File
@@ -1131,3 +1131,10 @@ $ flutter test
- 首页移除父级 feed 监听,网格、剧单和全部短剧改为局部监听,刷新时保留旧数据及原滚动位置。 - 首页移除父级 feed 监听,网格、剧单和全部短剧改为局部监听,刷新时保留旧数据及原滚动位置。
- 剧单、全部短剧、追剧沿用相同刷新策略;首帧加载与刷新指示器分离。 - 剧单、全部短剧、追剧沿用相同刷新策略;首帧加载与刷新指示器分离。
- 按当前流程未执行 analyze/test。 - 按当前流程未执行 analyze/test。
## 2026-10-22 ChangeNotifier 监听生命周期排查
- 全仓核对手动 add/remove;首页、推荐和追剧改为内容区单实例 ListenableBuilder,避免重复订阅。
- 播放器进度监听改为具名回调,在切换、重建会话和 dispose 时解绑;评论输入弹窗补 controller 更换迁移。
- `_onFeedChanged` 已无源码调用链,报错来源为热重载前注册的旧 tear-off;需完全关闭 App 后冷启动。
- 按当前流程未执行 analyze/test。
+7 -13
View File
@@ -105,7 +105,9 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
walletBadge: walletBadge, walletBadge: walletBadge,
), ),
Expanded( Expanded(
child: PageView.builder( child: ListenableBuilder(
listenable: widget.feedController,
builder: (context, _) => PageView.builder(
key: const Key('home-primary-page-view'), key: const Key('home-primary-page-view'),
controller: _pageController, controller: _pageController,
itemCount: HomePrimaryTab.values.length, itemCount: HomePrimaryTab.values.length,
@@ -125,6 +127,7 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
), ),
), ),
), ),
),
], ],
), ),
), ),
@@ -152,9 +155,6 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
required HomePrimaryTab tab, required HomePrimaryTab tab,
required List<HomeGridCategoryShortcut> shortcuts, required List<HomeGridCategoryShortcut> shortcuts,
}) { }) {
return ListenableBuilder(
listenable: widget.feedController,
builder: (context, _) {
if (widget.feedController.loadingFirstPage) { if (widget.feedController.loadingFirstPage) {
return const Center( return const Center(
child: SizedBox.square( child: SizedBox.square(
@@ -197,8 +197,7 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
), ),
sliver: SliverGrid( sliver: SliverGrid(
key: const Key('home-grid-cards'), key: const Key('home-grid-cards'),
gridDelegate: gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, crossAxisCount: 3,
crossAxisSpacing: AppDimens.homeGridGap, crossAxisSpacing: AppDimens.homeGridGap,
mainAxisSpacing: AppDimens.homeGridRowGap, mainAxisSpacing: AppDimens.homeGridRowGap,
@@ -216,15 +215,12 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
if (dramas.isNotEmpty) if (dramas.isNotEmpty)
SliverToBoxAdapter( SliverToBoxAdapter(
child: Padding( child: Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(bottom: AppDimens.spaceXl),
bottom: AppDimens.spaceXl,
),
child: PagedListFooter( child: PagedListFooter(
loading: widget.feedController.loadingMore, loading: widget.feedController.loadingMore,
hasMore: widget.feedController.hasMore, hasMore: widget.feedController.hasMore,
hasItems: dramas.isNotEmpty, hasItems: dramas.isNotEmpty,
errorMessage: errorMessage: widget.feedController.lastError == null
widget.feedController.lastError == null
? null ? null
: l10n.catalogError, : l10n.catalogError,
onRetry: widget.feedController.loadMore, onRetry: widget.feedController.loadMore,
@@ -237,8 +233,6 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
); );
}, },
); );
},
);
} }
Future<void> _refresh() async { Future<void> _refresh() async {
+34 -9
View File
@@ -69,6 +69,9 @@ class PlayerLogic extends ChangeNotifier {
VideoPlayerPort? trackedPlayer; VideoPlayerPort? trackedPlayer;
Episode? trackedEpisode; Episode? trackedEpisode;
Drama? trackedDrama; Drama? trackedDrama;
int? _trackedPlayerIndex;
bool _completionHandled = false;
bool _completionProgressSaved = false;
DateTime lastProgressWrite = DateTime.fromMillisecondsSinceEpoch(0); DateTime lastProgressWrite = DateTime.fromMillisecondsSinceEpoch(0);
/// 进度上报节流:契约建议每 10 秒一次 /// 进度上报节流:契约建议每 10 秒一次
@@ -317,6 +320,7 @@ class PlayerLogic extends ChangeNotifier {
unawaited(saveCurrentProgress()); unawaited(saveCurrentProgress());
final previous = session; final previous = session;
sessionEpisodes = List<Episode>.of(nextEpisodes); sessionEpisodes = List<Episode>.of(nextEpisodes);
_detachTrackedPlayerListener();
trackedPlayer = null; trackedPlayer = null;
trackedEpisode = null; trackedEpisode = null;
trackedDrama = null; trackedDrama = null;
@@ -601,6 +605,7 @@ class PlayerLogic extends ChangeNotifier {
_disposed = true; _disposed = true;
pictureInPicture.setStateListener(null); pictureInPicture.setStateListener(null);
unawaited(saveCurrentProgress()); unawaited(saveCurrentProgress());
_detachTrackedPlayerListener();
if (_started) { if (_started) {
session.setRouteVisible(false); session.setRouteVisible(false);
cancelScrub(); cancelScrub();
@@ -696,19 +701,40 @@ class PlayerLogic extends ChangeNotifier {
await player.controller.setVolume(muted ? 0 : 1); await player.controller.setVolume(muted ? 0 : 1);
await player.controller.setPlaybackSpeed(playbackSpeed); await player.controller.setPlaybackSpeed(playbackSpeed);
if (session.active != player) return; if (session.active != player) return;
_detachTrackedPlayerListener();
trackedPlayer = player; trackedPlayer = player;
trackedEpisode = episode; trackedEpisode = episode;
trackedDrama = drama; trackedDrama = drama;
_trackedPlayerIndex = index;
_completionHandled = false;
_completionProgressSaved = false;
lastProgressWrite = DateTime.now(); lastProgressWrite = DateTime.now();
var completionHandled = false; player.controller.addListener(_handleTrackedPlayerChanged);
var completionProgressSaved = false; }
player.controller.addListener(() {
if (session.active != player) return; void _detachTrackedPlayerListener() {
trackedPlayer?.controller.removeListener(_handleTrackedPlayerChanged);
_trackedPlayerIndex = null;
}
void _handleTrackedPlayerChanged() {
if (_disposed) return;
final player = trackedPlayer;
final episode = trackedEpisode;
final drama = trackedDrama;
final index = _trackedPlayerIndex;
if (player == null ||
episode == null ||
drama == null ||
index == null ||
session.active != player) {
return;
}
final snapshot = _readPlaybackSnapshot(player); final snapshot = _readPlaybackSnapshot(player);
if (snapshot == null) return; if (snapshot == null) return;
_cachePlaybackPosition(drama, episode, snapshot); _cachePlaybackPosition(drama, episode, snapshot);
if (snapshot.completed && !completionProgressSaved) { if (snapshot.completed && !_completionProgressSaved) {
completionProgressSaved = true; _completionProgressSaved = true;
unawaited( unawaited(
queueProgress( queueProgress(
drama, drama,
@@ -719,11 +745,11 @@ class PlayerLogic extends ChangeNotifier {
), ),
); );
} }
if (!completionHandled && if (!_completionHandled &&
snapshot.completed && snapshot.completed &&
session.canAutoAdvance && session.canAutoAdvance &&
session.currentIndex == index) { session.currentIndex == index) {
completionHandled = true; _completionHandled = true;
unawaited(advanceAfterEnd(player, index)); unawaited(advanceAfterEnd(player, index));
} }
if (!shouldReportProgress(DateTime.now())) return; if (!shouldReportProgress(DateTime.now())) return;
@@ -735,7 +761,6 @@ class PlayerLogic extends ChangeNotifier {
duration: snapshot.duration, duration: snapshot.duration,
), ),
); );
});
} }
Future<void> advanceAfterEnd(VideoPlayerPort player, int index) async { Future<void> advanceAfterEnd(VideoPlayerPort player, int index) async {
@@ -79,6 +79,15 @@ class _CommentComposerSheetState extends State<CommentComposerSheet> {
_trackKeyboardInset(_keyboardHeight(context)); _trackKeyboardInset(_keyboardHeight(context));
} }
@override
void didUpdateWidget(covariant CommentComposerSheet oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.controller != widget.controller) {
oldWidget.controller.removeListener(_onControllerChanged);
widget.controller.addListener(_onControllerChanged);
}
}
Future<void> _prepareInput() async { Future<void> _prepareInput() async {
if (_isAndroid) { if (_isAndroid) {
await _keyboardInset.start(); await _keyboardInset.start();
+8 -5
View File
@@ -56,13 +56,15 @@ class _RecommendPageState extends State<RecommendPage> {
children: [ children: [
_header(l10n), _header(l10n),
Expanded( Expanded(
child: IndexedStack( child: ListenableBuilder(
listenable: Listenable.merge([
widget.feedController,
categories,
]),
builder: (context, _) => IndexedStack(
index: _tab.index, index: _tab.index,
children: [ children: [
ListenableBuilder( _recommendFeed(context),
listenable: widget.feedController,
builder: (context, _) => _recommendFeed(context),
),
RecommendSectionsView( RecommendSectionsView(
feedController: widget.feedController, feedController: widget.feedController,
categories: categories, categories: categories,
@@ -77,6 +79,7 @@ class _RecommendPageState extends State<RecommendPage> {
], ],
), ),
), ),
),
], ],
), ),
), ),
@@ -44,6 +44,7 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
final dramas = _filtered();
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final itemWidth = final itemWidth =
@@ -65,18 +66,9 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
key: const Key('all-dramas-scroll'), key: const Key('all-dramas-scroll'),
physics: AppPullToRefresh.physics, physics: AppPullToRefresh.physics,
slivers: [ slivers: [
SliverToBoxAdapter( SliverToBoxAdapter(child: _filters(l10n)),
child: ListenableBuilder( if (dramas.isEmpty)
listenable: widget.feedController, SliverFillRemaining(
builder: (context, _) => _filters(l10n),
),
),
ListenableBuilder(
listenable: widget.feedController,
builder: (context, _) {
final dramas = _filtered();
if (dramas.isEmpty) {
return SliverFillRemaining(
hasScrollBody: false, hasScrollBody: false,
child: EmptyState( child: EmptyState(
key: const Key('all-dramas-empty'), key: const Key('all-dramas-empty'),
@@ -89,9 +81,9 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
_genre = null; _genre = null;
}), }),
), ),
); )
} else
return SliverPadding( SliverPadding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: AppDimens.feedPageHorizontal, horizontal: AppDimens.feedPageHorizontal,
), ),
@@ -111,18 +103,14 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
childCount: dramas.length, childCount: dramas.length,
), ),
), ),
);
},
), ),
ListenableBuilder( SliverToBoxAdapter(
listenable: widget.feedController,
builder: (context, _) => SliverToBoxAdapter(
child: Padding( child: Padding(
padding: const EdgeInsets.only(bottom: AppDimens.spaceXl), padding: const EdgeInsets.only(bottom: AppDimens.spaceXl),
child: PagedListFooter( child: PagedListFooter(
loading: widget.feedController.loadingMore, loading: widget.feedController.loadingMore,
hasMore: widget.feedController.hasMore, hasMore: widget.feedController.hasMore,
hasItems: _filtered().isNotEmpty, hasItems: dramas.isNotEmpty,
errorMessage: widget.feedController.lastError == null errorMessage: widget.feedController.lastError == null
? null ? null
: l10n.catalogError, : l10n.catalogError,
@@ -130,7 +118,6 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
), ),
), ),
), ),
),
], ],
), ),
), ),
@@ -33,9 +33,6 @@ class RecommendSectionsView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListenableBuilder(
listenable: Listenable.merge([feedController, categories]),
builder: (context, _) {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
final source = feedController.dramas; final source = feedController.dramas;
final hits = [...source] final hits = [...source]
@@ -52,10 +49,7 @@ class RecommendSectionsView extends StatelessWidget {
return AppPullToRefresh( return AppPullToRefresh(
key: const Key('recommend-sections-refresh'), key: const Key('recommend-sections-refresh'),
onRefresh: () async { onRefresh: () async {
await Future.wait([ await Future.wait([feedController.refreshCurrent(), categories.load()]);
feedController.refreshCurrent(),
categories.load(),
]);
}, },
child: ListView( child: ListView(
key: const Key('recommend-sections-scroll'), key: const Key('recommend-sections-scroll'),
@@ -94,8 +88,6 @@ class RecommendSectionsView extends StatelessWidget {
], ],
), ),
); );
},
);
} }
List<Drama> _costumeDramas(List<Drama> source) { List<Drama> _costumeDramas(List<Drama> source) {