颤振| Riverpod 和 Dart 未处理异常:在构建过程中调用了 setState() 或 markNeedsBuild()

问题描述

背景

我有这个 AppUser 课:

@immutable
class AppUser {
  const AppUser({
    this.displayName,this.email,required this.emailVerified,this.phoneNumber,this.photoURL,required this.uid,});

  AppUser.fromFirebaseUser(User user)
      : displayName = user.displayName,email = user.email,emailVerified = user.emailVerified,phoneNumber = user.phoneNumber,photoURL = user.photoURL,uid = user.uid;

  final String? displayName;
  final String? email;
  final bool emailVerified;
  final String? phoneNumber;
  final String? photoURL;
  final String uid;
}

为了管理和使用当前登录用户我有这个 AppUserController 类:

class AppUserController extends StateNotifier<AppUser> {
  AppUserController()
      : super(
          const AppUser(
            emailVerified: false,uid: '',),);

  Stream<User?> get onAuthStateChanges =>
      FirebaseAuth.instance.authStateChanges();

  set setAppUser(AppUser appUser) {
    state = appUser;
  }

  Future<void> signOut() async {
    await FirebaseAuth.instance.signOut();
  }
}

然后,我创建了 2 个提供者:

final appUserProvider =
    StateNotifierProvider<AppUserController,AppUser>((ref) {
  return AppUserController();
});

final appUserStreamProvider = StreamProvider<AppUser?>((ref) {
  return ref
      .read(appUserProvider.notifier)
      .onAuthStateChanges
      .map<AppUser?>((user) {
    return user != null ? AppUser.fromFirebaseUser(user) : null;
  });
});

我需要管理用户的预算列表。此外,我必须将此列表与 Cloud Firestore 数据库同步,因此我创建了 BudgetsService 类:

class BudgetsService {
  BudgetsService({
    required this.uid,}) : budgetsRef = FirebaseFirestore.instance
            .collection(FirestorePath.budgetsCollection(uid))
            .withConverter<Budget>(
              fromFirestore: (snapshot,_) => Budget.fromMap(snapshot.data()!),toFirestore: (budget,_) => budget.toMap(),);

  String uid;
  final CollectionReference<Budget> budgetsRef;

  Future<void> addUpdate(Budget budget) async {
    await budgetsRef.doc(documentPath(budget)).set(budget);
  }

  Future<void> delete(Budget budget) async {
    await budgetsRef.doc(documentPath(budget)).delete();
  }

  String documentPath(Budget budget) => FirestorePath.budgetDoc(uid,budget);

  Future<List<Budget>> getBudgets() async {
    final list = await budgetsRef.get();

    return list.docs.map((e) => e.data()).toList();
  }
}

我通过 budgetsServiceProvider 提供程序使用这个类:

final budgetsServiceProvider = Provider<BudgetsService>((ref) {
  final AppUser appUser = ref.watch(appUserProvider);
  final String uid = appUser.uid;

  return BudgetsService(uid: uid);
});

我只使用 BudgetsService 类与在线数据库交互。其余的,我使用 BudgetsController 类管理用户的预算列表:

class BudgetsController extends StateNotifier<List<Budget>> {
  BudgetsController() : super(<Budget>[]);

  List<String> get names => state.map((b) => b.name).toList();

  Future<void> addUpdate(Budget budget,BudgetsService budgetsService) async {
    await budgetsService.addUpdate(budget);

    if (budgetAlreadyExists(budget)) {
      final int index = indexOf(budget);
      final List<Budget> newState = [...state];
      newState[index] = budget;
      state = newState..sort();
    } else {
      state = [...state,budget]..sort();
    }
  }

  bool budgetAlreadyExists(Budget budget) => names.contains(budget.name);

  Future<void> delete(Budget budget,BudgetsService budgetsService) async {
    await budgetsService.delete(budget);

    final int index = indexOf(budget);
    if (index != -1) {
      final List<Budget> newState = [...state]
        ..removeAt(index)
        ..sort();
      state = newState;
    }
  }

  Future<void> retrieveBudgets(BudgetsService budgetsService) async {
    state = await budgetsService.getBudgets();
  }

  int indexOf(Budget budget) => state.indexWhere((b) => b.name == budget.name);
}

我通过 budgetsProvider 提供程序使用这个类:

final budgetsProvider =
    StateNotifierProvider<BudgetsController,List<Budget>>((ref) {
  return BudgetsController();
});

用户登录后,我的 SwitchScreen 小部件导航到 ConsoleScreen

class SwitchScreen extends HookWidget {
  const SwitchScreen({
    Key? key,}) : super(key: key);

  static const route = '/switch';

  @override
  Widget build(BuildContext context) {
    final appUserStream =
        useProvider<AsyncValue<AppUser?>>(appUserStreamProvider);
    final googleSignIn =
        useProvider<GoogleSignInService>(googleSignInServiceProvider);
    final appUserController =
        useProvider<AppUserController>(appUserProvider.notifier);

    return appUserStream.when(
      data: (data) {
        if (data != null) {
          appUserController.setAppUser = data;

          final budgetsService = useProvider(budgetsServiceProvider);

          return const ConsoleScreen();
        } else {
          return SignInScreen(
            onGooglepressed: googleSignIn.signInWithGoogle,);
        }
      },loading: () {
        return const Scaffold(
          body: Center(
            child: LinearProgressIndicator(),);
      },error: (error,stack) {
        return Scaffold(
          body: Center(
            child: Text('Error: $error'),);
  }
}

问题

我第一次构建应用程序时没有问题。但是当我执行热重载时,我收到以下错误消息:

══════ Exception caught by widgets library ═══════════════════════════════════
The following Error was thrown building SwitchScreen(dirty,dependencies: [UncontrolledProviderScope],AsyncValue<AppUser?>.data(value: Instance of 'AppUser'),Instance of 'GoogleSignInService',Instance of 'AppUserController'):
Instance of 'Error'

The relevant error-causing widget was
SwitchScreen
lib\main.dart:67
When the exception was thrown,this was the stack
#0      StateNotifier.state=
package:state_notifier/state_notifier.dart:173
#1      AppUserController.setAppUser=
package:financesmanager/controllers/app_user_controller.dart:42
#2      SwitchScreen.build.<anonymous closure>
package:financesmanager/screens/switch_screen.dart:33
#3      _$AsyncData.when
package:riverpod/src/common.freezed.dart:148
#4      SwitchScreen.build
package:financesmanager/screens/switch_screen.dart:28
...
════════════════════════════════════════════════════════════════════════════════
E/Flutter (13932): [ERROR:Flutter/shell/common/shell.cc(103)] Dart Unhandled Exception: setState() or markNeedsBuild() called during build.
E/Flutter (13932): This UncontrolledProviderScope widget cannot be marked as needing to build because the framework is already in the process of building widgets.  A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children,which means a dirty descendant will always be built. Otherwise,the framework might not visit this widget during this build phase.
E/Flutter (13932): The widget on which setState() or markNeedsBuild() was called was:
E/Flutter (13932):   UncontrolledProviderScope
E/Flutter (13932): The widget which was currently being built when the offending call was made was:
E/Flutter (13932):   SwitchScreen,stack trace: #0      Element.markNeedsBuild.<anonymous closure>
package:Flutter/…/widgets/framework.dart:4217
E/Flutter (13932): #1      Element.markNeedsBuild
package:Flutter/…/widgets/framework.dart:4232
E/Flutter (13932): #2      ProviderElement._debugMarkWillChange.<anonymous closure>
package:riverpod/…/framework/base_provider.dart:660
E/Flutter (13932): #3      ProviderElement._debugMarkWillChange
package:riverpod/…/framework/base_provider.dart:664
E/Flutter (13932): #4      ProviderStateBase.exposedValue=.<anonymous closure>
package:riverpod/…/framework/base_provider.dart:900
E/Flutter (13932): #5      ProviderStateBase.exposedValue=
package:riverpod/…/framework/base_provider.dart:902
E/Flutter (13932): #6      _StateNotifierProviderState._listener
package:riverpod/src/state_notifier_provider.dart:92
E/Flutter (13932): #7      StateNotifier.state=
package:state_notifier/state_notifier.dart:162
E/Flutter (13932): #8      AppUserController.setAppUser=
package:financesmanager/controllers/app_user_controller.dart:42
E/Flutter (13932): #9      SwitchScreen.build.<anonymous closure>
package:financesmanager/screens/switch_screen.dart:33

问题

我该如何解决问题?

非常感谢!

更新 (2021-06-08)

在我的 main.dart 文件中,我有

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Hive.initFlutter();
  runApp(ProviderScope(child: FMApp()));
}

class FMApp extends HookWidget {
  FMApp({
    Key? key,}) : super(key: key);

  final Future<FirebaseApp> _initialization = Firebase.initializeApp();

  @override
  Widget build(BuildContext context) {
    final darkTheme = AppTheme.theme(Brightness.dark);
    final lightTheme = AppTheme.theme(Brightness.light);
    final isLightTheme = useProvider<bool>(themePreferenceProvider);
    final theme = isLightTheme ? lightTheme : darkTheme;

    return FutureBuilder(
      future: _initialization,builder: (context,snapshot) {
        if (snapshot.hasError) {
          return FlutterFireInitErrorScreen(
            appTitle: 'FM App',darkTheme: darkTheme,error: snapshot.error,theme: theme,);
        }

        if (snapshot.connectionState == ConnectionState.done) {
          return MaterialApp(
            debugShowCheckedModeBanner: false,title: 'FM App',localizationsDelegates: const [
              AppLocalizations.delegate,GlobalMaterialLocalizations.delegate,GlobalWidgetsLocalizations.delegate,GlobalCupertinoLocalizations.delegate,],supportedLocales: const [
              Locale.fromSubtags(languageCode: 'en'),Locale.fromSubtags(languageCode: 'es'),Locale.fromSubtags(languageCode: 'it'),initialRoute: SwitchScreen.route,routes: {
              SwitchScreen.route: (context) => const SwitchScreen(),},);
        }

        return FlutterFireInitWaitingScreen(
          appTitle: 'FM App',);
  }
}

可能的解决方

现在我通过替换 switch_screen.dart 文件中的代码解决了这个问题:

final budgetsService = useProvider(budgetsServiceProvider);

final budgetsController = context.read<BudgetsController>(budgetsProvider.notifier);
budgetsController.retrieveBudgets(budgetsService);

具有以下内容

final budgetsService = BudgetsService(uid: data.uid);
context
    .read(budgetsControllerProvider)
    .retrieveBudgets(budgetsService);

你怎么看?这是一个很好的解决方案吗?有没有更好的?谢谢!

解决方法

错误的解释是两个小部件同时更新,可能是因为它们监视的是同一个提供程序。

当子 Widget 尝试重建而其父 Widget 也尝试重建时,会生成此错误。为了解决这个错误,只需要重建Parent Widget,因为Child Widget会自动重建。

很遗憾,在您提供的代码中,我看不到您的 SwitchScreen 的显示位置,因此我无法告诉您确切的问题可能出在哪里。