Riverpod StateNotifierProvider丢失state问题求助
问题背景
我开发了一个影视分类页面,每个分类以轮播组件形式展示影片,轮播组件嵌套在可展开面板中,用户可单独展开/收起分类,这部分功能正常。
需求
需要添加一个按钮,实现一键展开/收起所有分类。
实现方案
使用带family参数的StateNotifierProvider实现:
- 状态包含:
genreKeysOnPage:页面所有分类ID列表(通过family参数传入)filteredGenreKeys:用户手动收起的分类ID列表
- 实现
toggleCollapseAll方法:勾选时将genreKeysOnPage内容复制到filteredGenreKeys,取消勾选则清空filteredGenreKeys
问题现象
state中的genreKeysOnPage会随机变为空列表。在toggleCollapseAll方法中添加打印,连续点击5次的结果为:
1-3次:输出[14, 878, 28, 12]
4-5次及之后:输出空列表[]
我从未修改过genreKeysOnPage属性,且Provider仅初始化一次。
求助代码
Controller与Provider代码
class ViewCarouselByGenreController extends StateNotifier<ViewCarouselByGenreState> { ViewCarouselByGenreController( this._localUserRepo, this._userService, this._genreKeys) : super(ViewCarouselByGenreState.initial()) { _init(); } final List<String> _genreKeys; final LocalUserRepo _localUserRepo; final UserService _userService; void _init() async { final user = await _localUserRepo.fetchUserLocally(); final filteredGenres = user.filteredGenreIds; state = state.copyWith( genreKeysOnPage: _genreKeys, filteredGenreKeys: filteredGenres, ); } bool checkIfFiltered(String genreKey) { if (state.filteredGenreKeys.contains(genreKey)) { return true; } else { return false; } } // ! This stops looping because state.genreKeysOnPage randomly empties! bool? detectCollapseAllStatus() { int filteredCount = 0; for (final item in state.genreKeysOnPage) { if (state.filteredGenreKeys.contains(item)) { filteredCount++; } } if (filteredCount == state.genreKeysOnPage.length) { return true; } else if (filteredCount == 0) { return false; } else { return null; } } void toggleCollapseAll() { final allGenreKeys = state.genreKeysOnPage; // ! This will be empty after from the 4th execution, Even if i pass _genreKeys directly // ! (i.e the property received via family constructor) it still empties!!! print('ALL GENRE KEYS: $allGenreKeys'); final allCollapsed = detectCollapseAllStatus(); if (allCollapsed == true) { state = state.copyWith( filteredGenreKeys: [], allGenresCollapsed: false, ); } else { state = state.copyWith( filteredGenreKeys: allGenreKeys, allGenresCollapsed: true, ); } } void toggleCollapseSingle(String filterKey) async { var filteredGenreKeys = state.filteredGenreKeys; if (filteredGenreKeys.contains(filterKey)) { filteredGenreKeys.removeWhere((element) => element == filterKey); } else { filteredGenreKeys.add(filterKey); } state = state.copyWith(filteredGenreKeys: filteredGenreKeys); } void saveFilterUpdates() { _userService.saveFilteredGenreUpdates( state.genreKeysOnPage, state.filteredGenreKeys); } } final viewCarouselByGenreControllerProvider = StateNotifierProvider.autoDispose .family<ViewCarouselByGenreController, ViewCarouselByGenreState, List<String>>( (ref, genreKeys) { return ViewCarouselByGenreController( ref.watch(localUserRepoProvider), ref.watch(userServiceProvider), genreKeys, ); }, );
State类代码
class ViewCarouselByGenreState { ViewCarouselByGenreState({ this.allGenresCollapsed, required this.filteredGenreKeys, required this.genreKeysOnPage, }); bool? allGenresCollapsed; List<String> filteredGenreKeys; List<String> genreKeysOnPage; factory ViewCarouselByGenreState.initial() { return ViewCarouselByGenreState( filteredGenreKeys: [], genreKeysOnPage: [], ); } String get filteredKeysCommaSeparated => filteredGenreKeys.map((e) => e).toList().join(', '); ViewCarouselByGenreState copyWith({ bool? allGenresCollapsed, List<String>? filteredGenreKeys, List<String>? genreKeysOnPage, }) { return ViewCarouselByGenreState( allGenresCollapsed: allGenresCollapsed ?? this.allGenresCollapsed, filteredGenreKeys: filteredGenreKeys ?? this.filteredGenreKeys, genreKeysOnPage: genreKeysOnPage ?? this.genreKeysOnPage, ); } @override bool operator ==(covariant ViewCarouselByGenreState other) { if (identical(this, other)) return true; return other.allGenresCollapsed == allGenresCollapsed && listEquals(other.filteredGenreKeys, filteredGenreKeys) && listEquals(other.genreKeysOnPage, genreKeysOnPage); } @override int get hashCode => allGenresCollapsed.hashCode ^ filteredGenreKeys.hashCode ^ genreKeysOnPage.hashCode; @override String toString() => 'ViewCarouselByGenreState(allGenresCollapsed: $allGenresCollapsed, filteredGenreKeys: $filteredGenreKeys, genreKeysOnPage: $genreKeysOnPage)'; }
问题原因与修复方案
核心问题:可变列表的引用共享导致状态污染
代码存在两个关键问题,共同导致genreKeysOnPage被意外清空:
1. toggleCollapseSingle直接修改原状态列表
该方法中直接获取state.filteredGenreKeys的引用并修改,违反了StateNotifier的不可变状态原则:
var filteredGenreKeys = state.filteredGenreKeys; filteredGenreKeys.removeWhere(...) // 直接修改原状态的列表
这种操作会破坏状态的不可变性,导致后续状态更新时出现引用混乱。
2. toggleCollapseAll传递列表引用导致共享
在切换全选时,直接将genreKeysOnPage的引用赋值给filteredGenreKeys:
filteredGenreKeys: allGenreKeys
这意味着两个列表指向同一个内存对象,后续修改filteredGenreKeys时,genreKeysOnPage会被同步修改,最终被清空。
修复步骤
步骤1:修改toggleCollapseSingle,创建列表副本
避免直接修改原状态列表,改用副本操作:
void toggleCollapseSingle(String filterKey) { final filteredGenreKeys = List<String>.from(state.filteredGenreKeys); if (filteredGenreKeys.contains(filterKey)) { filteredGenreKeys.removeWhere((element) => element == filterKey); } else { filteredGenreKeys.add(filterKey); } state = state.copyWith(filteredGenreKeys: filteredGenreKeys); }
步骤2:修改toggleCollapseAll,传递列表副本
确保filteredGenreKeys使用独立的列表对象:
void toggleCollapseAll() { final allGenreKeys = state.genreKeysOnPage; print('ALL GENRE KEYS: $allGenreKeys'); final allCollapsed = detectCollapseAllStatus(); if (allCollapsed == true) { state = state.copyWith( filteredGenreKeys: [], allGenresCollapsed: false, ); } else { state = state.copyWith( filteredGenreKeys: List<String>.from(allGenreKeys), allGenresCollapsed: true, ); } }
步骤3:初始化时传入列表副本(可选)
为避免外部修改传入的_genreKeys影响状态,在_init中也创建副本:
void _init() async { final user = await _localUserRepo.fetchUserLocally(); final filteredGenres = user.filteredGenreIds; state = state.copyWith( genreKeysOnPage: List<String>.from(_genreKeys), filteredGenreKeys: List<String>.from(filteredGenres), ); }
额外优化
简化checkIfFiltered方法:
bool checkIfFiltered(String genreKey) => state.filteredGenreKeys.contains(genreKey);
内容的提问来源于stack exchange,提问作者Alex Cretney
相关产品推荐
相关产品推荐

