riverpod icon indicating copy to clipboard operation
riverpod copied to clipboard

"Concurrent modification during iteration: Instance of _HashMap"- Error in provider with many dependencies

Open Fasust opened this issue 3 weeks ago • 0 comments

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

Fasust avatar Dec 03 '25 11:12 Fasust