fix(home): 修复频道快速切换卡 loading

This commit is contained in:
Zhou Weidong committed 2026-10-21 17:46:42 +08:00
1 parent c8392b2f2a
commit 3c4d65a9b2
5 files changed
+408 -110

No files matched your search

+117 -38
View File
@@ -102,6 +102,13 @@ class PlayerLogic extends ChangeNotifier {
bool _disposed = false;
PlayerFactory? _sessionFactory;
PlaybackCoordinator? _sessionCoordinator;
int feedRevision = 0;
List<Drama> _latestFeed = const [];
String? _latestKeepDramaId;
bool _latestResetToFirst = false;
bool _feedUpdateRunning = false;
bool _hasPendingFeed = false;
Future<void> _feedUpdateFuture = Future<void>.value();
/// [start] 是否已经跑过:没跑过时 session/pages 还是 late 未初始化,
/// dispose 不能直接碰它们(例如页面构建失败或纯逻辑测试)。
@@ -111,6 +118,9 @@ class PlayerLogic extends ChangeNotifier {
bool get chromeHidden => immersive;
/// 最近一次 feed 更新流程完成;测试和页面退出可等待它结束。
Future<void> get feedUpdateSettled => _feedUpdateFuture;
/// 当前 PageView 对应用户正在观看的短剧 id,尚未起播时为 null。
String? get currentDramaId {
if (!_started || visibleIndices.isEmpty) return null;
@@ -173,38 +183,26 @@ class PlayerLogic extends ChangeNotifier {
unawaited(selectEpisode(initialIndex));
}
/// 原地替换首页 feed 队列;频道变化时重置到第 1 个,刷新时尽量保留当前剧。
void updateFeed(List<Drama> dramas, {String? keepDramaId}) {
final currentId = currentDramaId;
final nextQueue = PlaybackQueue.home(List<Drama>.unmodifiable(dramas));
final nextVisible = List<int>.generate(nextQueue.length, (index) => index);
final nextEpisodes = [for (final entry in nextQueue.entries) entry.episode];
feedDramas = List<Drama>.unmodifiable(dramas);
queue = nextQueue;
visibleIndices = nextVisible;
if (dramas.isNotEmpty) pageDrama = dramas.first;
if (!_started) return;
if (nextQueue.length == 0) {
session.setRouteVisible(false);
notify();
/// 串行替换首页 feed 队列;频道变化时重置到第 1 个,刷新时尽量保留当前剧。
void updateFeed(
List<Drama> dramas, {
String? keepDramaId,
bool resetToFirst = false,
}) {
feedRevision++;
pageRequest++;
_latestFeed = List<Drama>.unmodifiable(dramas);
_latestKeepDramaId = keepDramaId;
_latestResetToFirst = resetToFirst;
_hasPendingFeed = true;
if (!_started) {
feedDramas = _latestFeed;
return;
}
final keepIndex = keepDramaId != null && keepDramaId == currentId
? nextQueue.entries.indexWhere((entry) => entry.drama.id == keepDramaId)
: -1;
if (keepIndex >= 0 && session.active != null) {
sessionEpisodes
..clear()
..addAll(nextEpisodes);
session.remapCurrentIndex(
keepIndex,
preloadIndex: nextVisibleIndex(keepIndex),
);
_jumpToFeedPage(keepIndex);
notify();
return;
if (!_feedUpdateRunning) {
_feedUpdateFuture = _drainFeedUpdates();
unawaited(_feedUpdateFuture);
}
_replaceSession(nextEpisodes, keepIndex >= 0 ? keepIndex : 0);
}
PlaybackSession _createSession(
@@ -222,7 +220,77 @@ class PlayerLogic extends ChangeNotifier {
);
}
void _replaceSession(List<Episode> nextEpisodes, int targetIndex) {
Future<void> _drainFeedUpdates() async {
_feedUpdateRunning = true;
while (true) {
if (!_hasPendingFeed) {
_feedUpdateRunning = false;
return;
}
_hasPendingFeed = false;
final revision = feedRevision;
final dramas = _latestFeed;
final keepDramaId = _latestKeepDramaId;
final resetToFirst = _latestResetToFirst;
if (revision != feedRevision) continue;
await _applyFeedUpdate(
dramas,
keepDramaId: keepDramaId,
resetToFirst: resetToFirst,
revision: revision,
);
}
}
Future<void> _applyFeedUpdate(
List<Drama> dramas, {
required String? keepDramaId,
required bool resetToFirst,
required int revision,
}) async {
if (!_started || revision != feedRevision) return;
final currentId = currentDramaId;
final nextQueue = PlaybackQueue.home(dramas);
final nextVisible = List<int>.generate(nextQueue.length, (index) => index);
final nextEpisodes = [for (final entry in nextQueue.entries) entry.episode];
feedDramas = dramas;
queue = nextQueue;
visibleIndices = nextVisible;
if (dramas.isNotEmpty) pageDrama = dramas.first;
if (nextQueue.length == 0) {
session.setRouteVisible(false);
notify();
return;
}
final keepIndex =
!resetToFirst && keepDramaId != null && keepDramaId == currentId
? nextQueue.entries.indexWhere((entry) => entry.drama.id == keepDramaId)
: -1;
if (keepIndex >= 0 && session.active != null) {
sessionEpisodes
..clear()
..addAll(nextEpisodes);
session.remapCurrentIndex(
keepIndex,
preloadIndex: nextVisibleIndex(keepIndex),
);
_jumpToFeedPage(keepIndex);
notify();
return;
}
await _replaceSession(
nextEpisodes,
keepIndex >= 0 ? keepIndex : 0,
revision: revision,
);
}
Future<void> _replaceSession(
List<Episode> nextEpisodes,
int targetIndex, {
required int revision,
}) async {
if (revision != feedRevision) return;
final factory = _sessionFactory;
final coordinator = _sessionCoordinator;
if (factory == null || coordinator == null) return;
@@ -232,12 +300,14 @@ class PlayerLogic extends ChangeNotifier {
trackedPlayer = null;
trackedEpisode = null;
trackedDrama = null;
session = _createSession(factory, coordinator);
session.setRouteVisible(active && visibleIndices.isNotEmpty);
unawaited(previous.close());
_jumpToFeedPage(targetIndex);
unawaited(selectEpisode(targetIndex));
final nextSession = _createSession(factory, coordinator);
session = nextSession;
nextSession.setRouteVisible(active && visibleIndices.isNotEmpty);
notify();
await previous.close();
if (revision != feedRevision || !identical(session, nextSession)) return;
_jumpToFeedPage(targetIndex);
await selectEpisode(targetIndex, revision: revision);
}
void _jumpToFeedPage(int index) {
@@ -625,15 +695,24 @@ class PlayerLogic extends ChangeNotifier {
);
}
Future<void> selectEpisode(int index) async {
Future<void> selectEpisode(int index, {int? revision}) async {
final feedRevisionAtStart = revision ?? feedRevision;
final request = ++pageRequest;
cancelScrub();
// Capture the old position synchronously, persist without blocking a swipe.
unawaited(saveCurrentProgress());
if (request != pageRequest || !isMounted()) return;
if (request != pageRequest ||
feedRevisionAtStart != feedRevision ||
!isMounted()) {
return;
}
// 起播前先要票据:拿不到就不起播,交给三态 UI 提示(失败可重试)
final ready = await ensureTicket(index);
if (request != pageRequest || !isMounted()) return;
if (request != pageRequest ||
feedRevisionAtStart != feedRevision ||
!isMounted()) {
return;
}
if (!ready && ticketError != null) return;
await session.select(index, preloadIndex: nextVisibleIndex(index));
}
+10 -6
View File
@@ -2,7 +2,6 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
@@ -131,20 +130,25 @@ class _PlayerPageState extends State<PlayerPage>
void didUpdateWidget(covariant PlayerPage oldWidget) {
super.didUpdateWidget(oldWidget);
logic.attach(widget, isMounted: () => mounted);
final channelChanged = oldWidget.feedChannel != widget.feedChannel;
final feedChanged =
_feedSignature(oldWidget.feedDramas) !=
_feedSignature(widget.feedDramas);
if (widget.homeMode &&
widget.feedDramas != null &&
(!listEquals(oldWidget.feedDramas, widget.feedDramas) ||
oldWidget.feedChannel != widget.feedChannel)) {
(feedChanged || channelChanged)) {
logic.updateFeed(
widget.feedDramas!,
keepDramaId: oldWidget.feedChannel == widget.feedChannel
? logic.currentDramaId
: null,
keepDramaId: channelChanged ? null : logic.currentDramaId,
resetToFirst: channelChanged,
);
}
logic.onPageUpdated(oldWidget);
}
String _feedSignature(List<Drama>? dramas) =>
dramas == null ? '' : dramas.map((drama) => drama.id).join('|');
@override
void didChangeDependencies() {
super.didChangeDependencies();