From 4b97f89c0b32309a910bc6213c56e9e7b9a22abe Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Tue, 27 Oct 2026 09:19:18 +0800 Subject: [PATCH] =?UTF-8?q?fix(recommend):=20=E5=BB=B6=E5=90=8E=E6=8E=A8?= =?UTF-8?q?=E8=8D=90=E6=95=B0=E6=8D=AE=E5=88=B7=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/recommend/recommend_page.dart | 40 +++- .../recommend_refresh_scheduling_test.dart | 183 ++++++++++++++++++ 2 files changed, 218 insertions(+), 5 deletions(-) create mode 100644 test/recommend/recommend_refresh_scheduling_test.dart diff --git a/lib/pages/recommend/recommend_page.dart b/lib/pages/recommend/recommend_page.dart index d6f0967..137be25 100644 --- a/lib/pages/recommend/recommend_page.dart +++ b/lib/pages/recommend/recommend_page.dart @@ -14,6 +14,7 @@ import '../../core/data/services/playlist_controller.dart'; import '../../core/theme/app_colors.dart'; import '../../core/widgets/empty_state.dart'; import '../../l10n/app_localizations.dart'; +import '../home/home_feed_channel.dart'; import '../player/open_player.dart'; import '../player/player_view.dart'; import 'recommend_types.dart'; @@ -51,6 +52,8 @@ class _RecommendPageState extends State RecommendInnerTab _tab = RecommendInnerTab.feed; final PageController _pageController = PageController(); int _scrubCancelRequest = 0; + bool _recommendDataRefreshScheduled = false; + HomeFeedChannel? _observedFeedChannel; @override bool get wantKeepAlive => true; @@ -58,22 +61,48 @@ class _RecommendPageState extends State @override void initState() { super.initState(); + _observedFeedChannel = widget.feedController.channel; + widget.feedController.addListener(_handleFeedControllerChanged); if (widget.active) _scheduleRecommendData(); } @override void didUpdateWidget(RecommendPage oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.active && !oldWidget.active) _scheduleRecommendData(); + if (oldWidget.feedController != widget.feedController) { + oldWidget.feedController.removeListener(_handleFeedControllerChanged); + _observedFeedChannel = widget.feedController.channel; + widget.feedController.addListener(_handleFeedControllerChanged); + } + if (_requiresRecommendDataRefresh(oldWidget)) { + _scheduleRecommendData(); + } + } + + bool _requiresRecommendDataRefresh(RecommendPage oldWidget) { + if (!widget.active) return false; + return !oldWidget.active || + oldWidget.feedController != widget.feedController || + oldWidget.catalogApi != widget.catalogApi; + } + + void _handleFeedControllerChanged() { + final channel = widget.feedController.channel; + if (channel == _observedFeedChannel) return; + _observedFeedChannel = channel; + if (widget.active) _scheduleRecommendData(); } void _scheduleRecommendData() { - final feed = widget.feedController; - if (!feed.loaded && !feed.loadingFirstPage) { - unawaited(feed.refreshCurrent()); - } + if (_recommendDataRefreshScheduled) return; + _recommendDataRefreshScheduled = true; WidgetsBinding.instance.addPostFrameCallback((_) { + _recommendDataRefreshScheduled = false; if (!mounted || !widget.active) return; + final feed = widget.feedController; + if (!feed.loaded && !feed.loadingFirstPage) { + unawaited(feed.refreshCurrent()); + } final playlists = _readPlaylists(); if (playlists != null && !playlists.loaded && !playlists.loading) { unawaited(playlists.load()); @@ -83,6 +112,7 @@ class _RecommendPageState extends State @override void dispose() { + widget.feedController.removeListener(_handleFeedControllerChanged); _pageController.dispose(); super.dispose(); } diff --git a/test/recommend/recommend_refresh_scheduling_test.dart b/test/recommend/recommend_refresh_scheduling_test.dart new file mode 100644 index 0000000..523adcc --- /dev/null +++ b/test/recommend/recommend_refresh_scheduling_test.dart @@ -0,0 +1,183 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fruitshorts/core/data/services/category_controller.dart'; +import 'package:fruitshorts/core/data/services/home_feed_controller.dart'; +import 'package:fruitshorts/l10n/app_localizations.dart'; +import 'package:fruitshorts/pages/home/home_feed_channel.dart'; +import 'package:fruitshorts/pages/recommend/recommend_page.dart'; +import 'package:provider/provider.dart'; + +import '../support/fake_catalog.dart'; +import '../support/fake_category.dart'; + +void main() { + testWidgets('should_refresh_once_when_parent_rebuilds_repeatedly', ( + tester, + ) async { + final fixture = await _pumpRecommend(tester, active: true); + + expect(fixture.controller.refreshCalls, 1); + for (var index = 0; index < 20; index++) { + fixture.harnessKey.currentState!.rebuildSameInputs(); + await tester.pump(); + } + + expect(tester.takeException(), isNull); + expect(fixture.controller.refreshCalls, 1); + }); + + testWidgets('should_refresh_once_when_active_turns_on', (tester) async { + final fixture = await _pumpRecommend(tester, active: false); + expect(fixture.controller.refreshCalls, 0); + + fixture.harnessKey.currentState!.update(active: true); + await tester.pump(); + + expect(fixture.controller.refreshCalls, 1); + fixture.harnessKey.currentState!.rebuildSameInputs(); + await tester.pump(); + expect(fixture.controller.refreshCalls, 1); + }); + + testWidgets('should_refresh_again_when_data_source_changes', (tester) async { + final fixture = await _pumpRecommend(tester, active: true); + expect(fixture.controller.refreshCalls, 1); + + fixture.harnessKey.currentState!.update(active: false); + await tester.pump(); + expect(fixture.controller.refreshCalls, 1); + + fixture.harnessKey.currentState!.update(active: true); + await tester.pump(); + expect(fixture.controller.refreshCalls, 2); + }); + + testWidgets('should_not_refresh_after_dispose_when_channel_changes', ( + tester, + ) async { + final fixture = await _pumpRecommend(tester, active: true); + expect(fixture.controller.refreshCalls, 1); + + fixture.harnessKey.currentState!.update(active: true); + final selection = fixture.controller.selectChannel(HomeFeedChannel.anime); + await tester.pumpWidget(const MaterialApp(home: SizedBox.shrink())); + await selection; + await tester.pump(); + + expect(fixture.controller.refreshCalls, 1); + }); +} + +Future<_RecommendFixture> _pumpRecommend( + WidgetTester tester, { + required bool active, +}) async { + tester.view.physicalSize = const Size(390, 844); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + + final catalog = FakeCatalogRepository(); + final controller = _RecordingHomeFeedController(catalog); + final categories = CategoryController( + FakeCategoryRepository(catalog: catalog), + ); + final harnessKey = GlobalKey<_RecommendHarnessState>(); + final fixture = _RecommendFixture( + controller: controller, + categoryController: categories, + harnessKey: harnessKey, + ); + addTearDown(controller.dispose); + addTearDown(categories.dispose); + + await tester.pumpWidget(fixture.app(active: active)); + await tester.pump(); + return fixture; +} + +class _RecommendFixture { + const _RecommendFixture({ + required this.controller, + required this.categoryController, + required this.harnessKey, + }); + + final _RecordingHomeFeedController controller; + final CategoryController categoryController; + final GlobalKey<_RecommendHarnessState> harnessKey; + + Widget app({required bool active}) { + return MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: ChangeNotifierProvider.value( + value: categoryController, + child: _RecommendHarness( + key: harnessKey, + controller: controller, + active: active, + ), + ), + ); + } +} + +class _RecommendHarness extends StatefulWidget { + const _RecommendHarness({ + super.key, + required this.controller, + required this.active, + }); + + final HomeFeedController controller; + final bool active; + + @override + State<_RecommendHarness> createState() => _RecommendHarnessState(); +} + +class _RecommendHarnessState extends State<_RecommendHarness> { + late HomeFeedController _controller; + late bool _active; + + @override + void initState() { + super.initState(); + _controller = widget.controller; + _active = widget.active; + } + + void rebuildSameInputs() { + setState(() {}); + } + + void update({required bool active}) { + setState(() { + _active = active; + }); + } + + @override + Widget build(BuildContext context) { + return RecommendPage( + feedController: _controller, + active: _active, + videoOnly: true, + onSearch: () {}, + ); + } +} + +class _RecordingHomeFeedController extends HomeFeedController { + _RecordingHomeFeedController(super.repository); + + int refreshCalls = 0; + + @override + Future refreshCurrent() async { + refreshCalls++; + notifyListeners(); + return true; + } +}