未添加autoDispose的UserProvider重复执行build函数问题排查
问题分析:UserProvider的build重复执行导致用户信息丢失
在监听聊天室流时,使用UserProvider加载用户信息,未配置autoDispose但每次新流进入时UserProvider的build都会重复执行,已加载的用户信息丢失,且onDispose回调未触发(Provider未销毁)。
相关代码
user.dart
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:sumee/domain/model/user.dart' as model; import 'package:sumee/domain/model/user_id.dart'; import 'package:sumee/infrastructure/user_repository.dart'; class User extends FamilyNotifier<model.User, UserId> { Future<void> load() async { state = await ref.read(userRepositoryProvider).getUser(userId: state.id); } @override model.User build(UserId arg) { ref.onDispose(() { // not printed print('dispose'); }); return model.User( id: arg, phoneNumber: '', profileImages: [], planedDescription: '', ); } } final userProvider = NotifierProvider.family<User, model.User, UserId>( User.new, );
talk_room.dart
import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:sumee/domain/model/talk_room.dart'; import 'package:sumee/feature/me/me.dart'; import 'package:sumee/feature/user/user.dart'; import 'package:sumee/infrastructure/talk_room_repository.dart'; class ListTalkRoom extends AutoDisposeNotifier<List<TalkRoom>> { ListTalkRoom(); Future<void> subscribe() async { final user = ref.read(meProvider); final stream = await ref .read(talkRoomRepositoryProvider) .listByUserId(userId: user!.id); final subscription = stream.listen((talkRooms) { for (final talkRoom in talkRooms) { final me = ref.read(meProvider); final userId = talkRoom.participants .firstWhere((element) => !element.equal(me!.id)); final isExistTalkRoom = state .firstWhereOrNull((element) => element.id.equal(talkRoom.id)) != null; if (!isExistTalkRoom) { print('load'); ref.read(userProvider(userId).notifier).load(); } } state = talkRooms; }); ref.onDispose(subscription.cancel); } @override List<TalkRoom> build() { subscribe(); return []; } } final listTalkRoomProvider = NotifierProvider.autoDispose<ListTalkRoom, List<TalkRoom>>( ListTalkRoom.new, );
talk_room_list_page.dart
import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:sumee/domain/model/talk_room.dart'; import 'package:sumee/domain/model/user_id.dart'; import 'package:sumee/feature/me/me.dart'; import 'package:sumee/feature/user/user.dart'; import 'package:sumee/presentation/page/talk/feature/list_talk_room/list_talk_room.dart'; import 'package:sumee/presentation/router/router.dart'; import 'package:sumee/presentation/theme/color_theme.dart'; class TalkRoomListPage extends HookConsumerWidget { const TalkRoomListPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final state = ref.watch(listTalkRoomProvider); final currentUser = ref.watch(meProvider); final tabController = useTabController(initialLength: TalkRoomListTab.values.length); return Scaffold( appBar: AppBar( title: const Text('トーク'), bottom: TabBar( controller: tabController, tabs: TalkRoomListTab.values.map((e) => Tab(text: e.jp())).toList(), labelColor: Colors.black, ), ), body: TabBarView( controller: tabController, children: TalkRoomListTab.values.map((tab) { return ListView( children: List.generate(state.length, (index) { final userId = state[index] .participants .firstWhere((element) => !element.equal(currentUser!.id)); return TalkRoomCell(talkRoom: state[index], userId: userId); }), ); }).toList(), ), ); } } class TalkRoomCell extends HookConsumerWidget { const TalkRoomCell({required this.talkRoom, required this.userId, super.key}); final TalkRoom talkRoom; final UserId userId; @override Widget build(BuildContext context, WidgetRef ref) { final user = ref.watch(userProvider(userId)); return InkWell( onTap: () => Navigator.of(context, rootNavigator: true) .pushNamed(RouteName.talkRoom.path), child: Container( height: 70, padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 8, ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ ClipRRect( borderRadius: BorderRadius.circular(9999), child: Image.network( user.profileImages.firstOrNull ?? '', width: 56, height: 56, fit: BoxFit.cover, errorBuilder: (_, __, ___) => const SizedBox.shrink(), ), ), const SizedBox(width: 8), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( user.name ?? '', style: Theme.of(context).textTheme.displaySmall, ), Text( talkRoom.lastMessage, style: Theme.of(context).textTheme.bodyLarge, ), ], ), ), const Text( '3分前', style: TextStyle( fontSize: 8, color: ColorTheme.black, fontWeight: FontWeight.bold, ), ), ], ), ), ); } } extension TalkRoomListTabExtension on TalkRoomListTab { String jp() { switch (this) { case TalkRoomListTab.all: return 'すべて'; case TalkRoomListTab.friend: return '友達'; } } } enum TalkRoomListTab { all, friend, }
原因分析
Widget无稳定Key导致重复重建
TalkRoomListPage中生成TalkRoomCell时未设置基于talkRoom.id的稳定Key,每次listTalkRoomProvider更新state(新流数据到来),Flutter无法复用旧的TalkRoomCell,只能重建所有子Widget。每个TalkRoomCell重建时会重新调用ref.watch(userProvider(userId)),触发UserProvider的相关逻辑。User Notifier的build逻辑缺陷
UserNotifier的build方法直接返回初始空用户实例,当build被触发时,state会被重置为初始值,覆盖之前通过load()加载的用户数据。而Widget重复重建导致的重复watch,会间接触发build执行(即使Provider未销毁)。State全量更新加剧问题
ListTalkRoom中每次流数据到来时,直接用state = talkRooms全量替换列表,导致listTalkRoomProvider的state完全更新,进一步触发TalkRoomListPage的全量重建,放大了Widget重复创建的问题。
解决方案
- 给TalkRoomCell添加稳定Key
在生成TalkRoomCell时,使用talkRoom.id作为ValueKey,避免不必要的Widget重建:
return TalkRoomCell( key: ValueKey(talkRoom.id), talkRoom: state[index], userId: userId, );
- 优化User Notifier的加载逻辑
在load()方法中添加判断,避免重复加载和状态重置:
Future<void> load() async { // 已加载数据则直接返回 if (state.name != null && state.profileImages.isNotEmpty) return; final userData = await ref.read(userRepositoryProvider).getUser(userId: state.id); state = userData; }
- 增量更新聊天室列表
避免全量替换state,改为增量更新已有列表,减少Widget重建次数:
final subscription = stream.listen((talkRooms) { final updatedState = List.of(state); for (final talkRoom in talkRooms) { final me = ref.read(meProvider); final userId = talkRoom.participants.firstWhere((element) => !element.equal(me!.id)); final existingIndex = updatedState.indexWhere((e) => e.id.equal(talkRoom.id)); if (existingIndex == -1) { print('load'); ref.read(userProvider(userId).notifier).load(); updatedState.add(talkRoom); } else { // 更新已有聊天室内容 updatedState[existingIndex] = talkRoom; } } state = updatedState; });
内容的提问来源于stack exchange,提问作者smilax630
相关产品推荐
相关产品推荐

