You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

未添加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,
}

原因分析

  1. Widget无稳定Key导致重复重建
    TalkRoomListPage中生成TalkRoomCell时未设置基于talkRoom.id的稳定Key,每次listTalkRoomProvider更新state(新流数据到来),Flutter无法复用旧的TalkRoomCell,只能重建所有子Widget。每个TalkRoomCell重建时会重新调用ref.watch(userProvider(userId)),触发UserProvider的相关逻辑。

  2. User Notifier的build逻辑缺陷
    User Notifier的build方法直接返回初始空用户实例,当build被触发时,state会被重置为初始值,覆盖之前通过load()加载的用户数据。而Widget重复重建导致的重复watch,会间接触发build执行(即使Provider未销毁)。

  3. State全量更新加剧问题
    ListTalkRoom中每次流数据到来时,直接用state = talkRooms全量替换列表,导致listTalkRoomProvider的state完全更新,进一步触发TalkRoomListPage的全量重建,放大了Widget重复创建的问题。

解决方案

  1. 给TalkRoomCell添加稳定Key
    在生成TalkRoomCell时,使用talkRoom.id作为ValueKey,避免不必要的Widget重建:
return TalkRoomCell(
  key: ValueKey(talkRoom.id),
  talkRoom: state[index],
  userId: userId,
);
  1. 优化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;
}
  1. 增量更新聊天室列表
    避免全量替换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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 19:19:56