Reputation: 593
I have this AppUser
class:
@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;
}
In order to manage and use the current user signed in, I have this AppUserController
class:
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();
}
}
Then, I created 2 providers:
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;
});
});
I need to manage a user’s budgets list. Also, I have to synchronize this list with a Cloud Firestore database, so I created the BudgetsService
class:
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();
}
}
I use this class through budgetsServiceProvider
provider:
final budgetsServiceProvider = Provider<BudgetsService>((ref) {
final AppUser appUser = ref.watch(appUserProvider);
final String uid = appUser.uid;
return BudgetsService(uid: uid);
});
I use BudgetsService
class only to interact with the online database. For the rest, I manage the user’s budget list with BudgetsController
class:
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);
}
I use this class through budgetsProvider
provider:
final budgetsProvider =
StateNotifierProvider<BudgetsController, List<Budget>>((ref) {
return BudgetsController();
});
After the user is signed in, my SwitchScreen
widget navigates to 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'),
),
);
},
);
}
}
The first time I build the app, I have no problem. But when I perform the hot reload, I get the following error message:
══════ 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
How can I solve the problem?
Thank you very much!
In my main.dart
file I have:
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'),
],
darkTheme: darkTheme,
theme: theme,
initialRoute: SwitchScreen.route,
routes: {
SwitchScreen.route: (context) => const SwitchScreen(),
},
);
}
return FlutterFireInitWaitingScreen(
appTitle: 'FM App',
darkTheme: darkTheme,
theme: theme,
);
},
);
}
}
For now I solved it by replacing, in switch_screen.dart
file, this code:
final budgetsService = useProvider(budgetsServiceProvider);
final budgetsController = context.read<BudgetsController>(budgetsProvider.notifier);
budgetsController.retrieveBudgets(budgetsService);
with the following:
final budgetsService = BudgetsService(uid: data.uid);
context
.read(budgetsControllerProvider)
.retrieveBudgets(budgetsService);
What do you think? Is this a good solution? Is there a better one? Thank you!
Upvotes: 0
Views: 1529
Reputation: 416
The interpretation of the error is that two widgets are updating at the same time, probably because they watch the same provider.
When a Child Widget tries to rebuild while its Parent Widget also tries to rebuild, it generates this error. To solve this error, only the Parent Widget needs to rebuild, because the Child Widget will automatically rebuild.
Unfortunately, in the code you provide, I cannot see from where your SwitchScreen
is displayed so I cannot tell you where the exact problem could be.
Upvotes: 2