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

@@ -10,28 +10,31 @@ import '../../../pages/home/home_feed_channel.dart';
/// 首页内容类型频道状态:每频道独立缓存、刷新和游标分页。
class HomeFeedController extends ChangeNotifier {
HomeFeedController(this._repository)
: _states = {
for (final channel in HomeFeedChannel.values)
channel: _ChannelFeedState(),
};
HomeFeedController(
this._repository, {
this.requestTimeout = defaultRequestTimeout,
}) : _states = {
for (final channel in HomeFeedChannel.values)
channel: _ChannelFeedState(),
};
static const int pageSize = 10;
static const Duration defaultRequestTimeout = Duration(seconds: 10);
final CatalogRepository _repository;
final Duration requestTimeout;
final Map<HomeFeedChannel, _ChannelFeedState> _states;
HomeFeedChannel _channel = HomeFeedChannel.recommend;
String? _lastError;
bool _disposed = false;
HomeFeedChannel get channel => _channel;
String? get lastError => _lastError;
String? get lastError => _state(_channel).lastError;
List<Drama> get dramas => _state(_channel).dramas;
String? get cursor => _state(_channel).cursor;
bool get hasMore => _state(_channel).hasMore;
bool get loaded => _state(_channel).loaded;
bool get loadingMore => _state(_channel).loadingMore;
bool get loadingFirstPage => !loaded;
bool get loadingFirstPage => !loaded && _state(_channel).loading;
/// 用首页聚合接口打平的数据填充推荐首屏,不发起网络请求。
void seedRecommend(List<Drama> dramas) {
@@ -42,27 +45,41 @@ class HomeFeedController extends ChangeNotifier {
..cursor = null
..hasMore = false
..loaded = true
..loadingMore = false;
_lastError = null;
if (_channel == HomeFeedChannel.recommend) _notify();
..loading = false
..loadingMore = false
..lastError = null
..inFlight = null;
_notifyIfCurrent(HomeFeedChannel.recommend);
}
/// 同步切到目标频道;有缓存先渲染,再在后台静默刷新。
/// 同步切到目标频道;有缓存先渲染,无缓存时复用或等待在飞请求。
Future<void> selectChannel(HomeFeedChannel next) async {
if (next == _channel) return;
_channel = next;
_lastError = null;
_notify();
final state = _state(next);
if (state.dramas.isNotEmpty) {
unawaited(_refresh(next, keepData: true));
} else {
await _loadFirst(next);
if (!state.loading) {
unawaited(_refresh(next, keepData: true));
}
return;
}
if (state.loading) {
final inFlight = state.inFlight;
if (inFlight != null) await inFlight;
return;
}
await _refresh(next, keepData: false);
}
/// 刷新当前频道,失败时保留旧数据。
Future<bool> refreshCurrent() => _refresh(_channel, keepData: true);
Future<bool> refreshCurrent() async {
final state = _state(_channel);
if (state.loading) {
return await state.inFlight ?? false;
}
return _refresh(_channel, keepData: true);
}
/// 加载当前频道下一页并去重追加。
Future<void> loadMore() async {
@@ -71,14 +88,16 @@ class HomeFeedController extends ChangeNotifier {
if (!state.hasMore || state.loadingMore || state.cursor == null) return;
final seq = ++state.requestSeq;
state.loadingMore = true;
_notify();
_notifyIfCurrent(channel);
try {
final page = await _repository.fetchFeed(
types: channel.apiTypes,
cursor: state.cursor,
size: pageSize,
);
if (!_accept(channel, seq)) return;
final page = await _repository
.fetchFeed(
types: channel.apiTypes,
cursor: state.cursor,
size: pageSize,
)
.timeout(requestTimeout);
if (!_acceptSequence(channel, seq)) return;
final seen = state.dramas.map((drama) => drama.id).toSet();
final appended = page.dramas
.where((drama) => seen.add(drama.id))
@@ -87,62 +106,78 @@ class HomeFeedController extends ChangeNotifier {
..dramas = List.unmodifiable([...state.dramas, ...appended])
..cursor = page.nextCursor
..hasMore = page.hasMore
..loaded = true;
_lastError = null;
..loaded = true
..lastError = null;
} catch (error) {
if (_accept(channel, seq)) _lastError = error.toString();
if (_acceptSequence(channel, seq)) {
state.lastError = error.toString();
}
} finally {
state.loadingMore = false;
if (_acceptChannel(channel)) _notify();
_notifyIfCurrent(channel);
}
}
Future<void> _loadFirst(HomeFeedChannel channel) async {
Future<bool> _refresh(HomeFeedChannel channel, {required bool keepData}) {
final state = _state(channel);
state.loaded = false;
await _refresh(channel, keepData: state.dramas.isNotEmpty);
final operation = _performRefresh(channel, keepData: keepData);
state.inFlight = operation;
unawaited(_clearInFlight(state, operation));
return operation;
}
Future<bool> _refresh(
Future<bool> _performRefresh(
HomeFeedChannel channel, {
required bool keepData,
}) async {
final state = _state(channel);
final seq = ++state.requestSeq;
_lastError = null;
if (_acceptChannel(channel)) _notify();
state
..loading = true
..lastError = null;
_notifyIfCurrent(channel);
try {
final page = await _repository.fetchFeed(
types: channel.apiTypes,
size: pageSize,
);
if (!_accept(channel, seq)) return false;
final page = await _repository
.fetchFeed(types: channel.apiTypes, size: pageSize)
.timeout(requestTimeout);
if (!_acceptSequence(channel, seq)) return false;
state
..dramas = List.unmodifiable(page.dramas)
..cursor = page.nextCursor
..hasMore = page.hasMore
..loaded = true
..loadingMore = false;
_lastError = null;
_notify();
..loading = false
..lastError = null;
_notifyIfCurrent(channel);
return true;
} catch (error) {
if (!_accept(channel, seq)) return false;
if (!_acceptSequence(channel, seq)) return false;
if (!keepData) state.dramas = const [];
state.loaded = true;
_lastError = error.toString();
_notify();
state
..loaded = true
..loading = false
..lastError = error.toString();
_notifyIfCurrent(channel);
return false;
}
}
Future<void> _clearInFlight(
_ChannelFeedState state,
Future<bool> operation,
) async {
await operation;
if (identical(state.inFlight, operation)) state.inFlight = null;
}
_ChannelFeedState _state(HomeFeedChannel channel) => _states[channel]!;
bool _accept(HomeFeedChannel channel, int seq) =>
!_disposed && channel == _channel && _state(channel).requestSeq == seq;
bool _acceptSequence(HomeFeedChannel channel, int seq) =>
!_disposed && _state(channel).requestSeq == seq;
bool _acceptChannel(HomeFeedChannel channel) =>
!_disposed && channel == _channel;
void _notifyIfCurrent(HomeFeedChannel channel) {
if (!_disposed && channel == _channel) _notify();
}
void _notify() {
if (!_disposed) notifyListeners();
@@ -160,6 +195,9 @@ class _ChannelFeedState {
String? cursor;
bool hasMore = true;
bool loaded = false;
bool loading = false;
bool loadingMore = false;
String? lastError;
int requestSeq = 0;
Future<bool>? inFlight;
}