"Concurrent modification during iteration: Instance of _HashMap"- Error in provider with many dependencies
Describe the bug
We are getting reports of the following error via Crashlytics:
Non-fatal Exception: FlutterError
Concurrent modification during iteration: Instance of '_HashMap<ProviderElementBase<Object?>, Object>'.. Error thrown
0 ??? 0x0 _HashMapKeyIterable.forEach (dart:collection)
1 ??? 0x0 ProviderElementBase.visitAncestors + 834 (element.dart:834)
2 ??? 0x0 ProviderElementBase._maybeRebuildDependencies + 337 (element.dart:337)
3 ??? 0x0 ProviderElementBase.flush + 321 (element.dart:321)
4 ??? 0x0 ProviderBase.addListener + 81 (provider_base.dart:81)
5 ??? 0x0 ProviderElementBase.listen + 763 (element.dart:763)
6 ??? 0x0 _ProviderSelector.addListener + 109 (selector.dart:109)
7 ??? 0x0 ProviderElementBase.listen + 763 (element.dart:763)
8 ??? 0x0 ProviderElementBase.watch + 704 (element.dart:704)
9 ??? 0x0 (null).analyticsProfile + 39 (analytics_provider.gen.dart:39)
10 ??? 0x0 Provider._create + 59 (base.dart:59)
11 ??? 0x0 ProviderElement.create + 345 (base.dart:345)
12 ??? 0x0 ProviderElementBase.buildState + 426 (element.dart:426)
13 ??? 0x0 ProviderElementBase._performBuild + 363 (element.dart:363)
14 ??? 0x0 ProviderElementBase.flush + 324 (element.dart:324)
15 ??? 0x0 ProviderScheduler._performRefresh + 100 (scheduler.dart:100)
16 ??? 0x0 ProviderScheduler._task + 88 (scheduler.dart:88)
17 ??? 0x0 ProviderScheduler.vsync.<fn>.invoke + 41 (scheduler.dart:41)
18 ??? 0x0 _UncontrolledProviderScopeElement.build + 396 (framework.dart:396)
19 ??? 0x0 ComponentElement.performRebuild + 5723 (framework.dart:5723)
20 ??? 0x0 Element.rebuild + 5435 (framework.dart:5435)
21 ??? 0x0 BuildScope._tryRebuild + 2695 (framework.dart:2695)
22 ??? 0x0 BuildScope._flushDirtyElements + 2752 (framework.dart:2752)
23 ??? 0x0 BuildOwner.buildScope + 3056 (framework.dart:3056)
24 ??? 0x0 WidgetsBinding.drawFrame + 1259 (binding.dart:1259)
25 ??? 0x0 RendererBinding._handlePersistentFrameCallback + 495 (binding.dart:495)
26 ??? 0x0 SchedulerBinding._invokeFrameCallback + 1434 (binding.dart:1434)
27 ??? 0x0 SchedulerBinding.handleDrawFrame + 1347 (binding.dart:1347)
28 ??? 0x0 SchedulerBinding._handleDrawFrame + 1200 (binding.dart:1200)
We are on Riverpod version 2.6.1.
The analyticsProfileProvider throwing the error looks like this:
@Riverpod(keepAlive: true)
AnalyticsProfile analyticsProfile(Ref ref) {
final isInOnboarding = ref.watch(isInOnboardingProvider);
final userData = ref.watch(userDataProvider.select((value) => value.value));
final userInfo = ref.watch(userInfoProvider.select((value) => value.value));
final userProfile = ref.watch(ownUserProfileProvider.select((value) => value.value));
final abState = ref.watch(aBStateProvider.select((value) => value.value));
final currentSessionId = ref.watch(
currentSessionIdProvider.select((value) => value),
);
final numberOfFriends = ref.watch( // Line 39 -> This threw the error
acceptedFriendRequestsProvider.select((value) => value.length),
);
final connections = ref.watch(
activeSubscriptionProvider.select(
(value) => value?.connection.data.connections,
),
);
final pushAuthorizationStatus = ref.watch(
pushAuthorizationStatusProvider.select((value) => value.value),
);
final associatedGym =
isInOnboarding == true
? userInfo?.homeGym?.value
: userData?.associatedGym?.value;
return AnalyticsProfile(
age:
userInfo?.dateOfBirth != null
? Dates.calculateAge(userInfo!.dateOfBirth)
: null,
activeAbTests: _getActiveAbTests(abState?.values),
paymentMethod: _paymentMethodToWallet(userData?.paymentMethod),
hasProfilePic: userInfo?.avatar != null,
isPrivate: userInfo?.isPrivate,
homeGym: userInfo?.homeGym?.value ?? 'none',
associatedGym: associatedGym ?? 'none',
city: userInfo?.address.city,
postalCode: userInfo?.address.postalCode,
subTier: userData?.subscription.tier.name,
subStatus: userData?.subscription.status?.name,
numOfConnections: connections?.length,
sessionCount: userProfile?.sessionStats.sessionsCount,
numberOfFriends: numberOfFriends,
currentSessionId: currentSessionId,
pushAuthorization: _getPushAuthorizationStatusString(
pushAuthorizationStatus,
),
locale: userInfo?.locale.name,
);
}
List<String>? _getActiveAbTests(Map<String, dynamic>? state) {
if (state == null) return null;
final activeIds = <String>[];
for (final flag in ABRegistry.configValues.whereType<FeatureFlag>()) {
final value = state[flag.name];
if (value == true) activeIds.add(flag.id);
}
return activeIds;
}
String? _getPushAuthorizationStatusString(AuthorizationStatus? status) {
if (status == null) return null;
switch (status) {
case AuthorizationStatus.notDetermined:
return 'undetermined';
case AuthorizationStatus.provisional:
return 'provisional';
case AuthorizationStatus.authorized:
return 'authorized';
case AuthorizationStatus.denied:
return 'denied';
}
}
The analyticsProfileProvider is being listened to exactly once by the following provider:
@Riverpod(keepAlive: true)
class AnalyticsProfileSync extends _$AnalyticsProfileSync {
@override
void build() {
ref.listen(analyticsProfileProvider, _syncProfile, fireImmediately: true);
}
Future<void> _syncProfile(
AnalyticsProfile? previous,
AnalyticsProfile profile,
) async {
final json = profile.toJson();
final previousJson = previous?.toJson();
await Future.wait(
json.entries
.where((i) => _valueChanged(i, previousJson))
.map(
(pair) => ref
.read(analyticsRepoProvider)
.setUserProperty(pair.key, pair.value),
),
);
}
bool _valueChanged(
MapEntry<String, dynamic> insertion,
Map<String, dynamic>? previousJson,
) {
if (previousJson == null) return true;
final lastValue = previousJson[insertion.key];
final newValue = insertion.value;
if (newValue is List && lastValue is List) {
return !const ListEquality<dynamic>().equals(newValue, lastValue);
} else if (newValue is Map && lastValue is Map) {
return !const MapEquality<dynamic, dynamic>().equals(newValue, lastValue);
} else if (newValue is Set && lastValue is Set) {
return !const SetEquality<dynamic>().equals(newValue, lastValue);
}
return lastValue != newValue;
}
}
String? _paymentMethodToWallet(PaymentMethod? paymentMethod) {
if (paymentMethod == null) return null;
if (paymentMethod.payPalEmailAddress != null) {
return 'paypal';
} else if (paymentMethod.wallet == Wallet.googlePay) {
return 'google_pay';
} else if (paymentMethod.wallet == Wallet.applePay) {
return 'apple_pay';
}
return 'card';
}
The analyticsProfileSyncProvider is watched by the ui to setup the sync.
To Reproduce
I have not been able to narrow down exactly what is causing this issue. What set's the analyticsProfileProvider apart from other providers in out code base, is that it's watching a lot of values and thus state changes are frequent. Could watching so many providers that all change frequently cause a race condition of some kind?
Expected behavior No exception is thrown