Skip to content

BuildContext-across-async-gaps audit (~20 sites; wallet/billing/face-mgmt highest risk) #8

Description

@ehsan6sha

Audit finding: ~20 BuildContext-across-async-gaps warnings

flutter analyze reports use_build_context_synchronously at the sites listed below. The warning comes in two flavours; the second is more dangerous:

  • "Don't use BuildContext's across async gaps" (no guard at all — direct use of context after await).
  • "…guarded by an unrelated mounted check" (there IS a mounted check, but the analyzer believes it's for a different State than the context belongs to — common when context was passed in as a parameter from a parent widget / dialog / bottom sheet).

Both can crash at runtime if the underlying element was disposed during the async gap. The "unrelated mounted" variant is often a false-positive when the parameter context is in fact the host State's own context, but the analyzer can't prove that — and the cases where it ISN'T the same context (dialog context, BottomSheet context) do crash in practice during fast navigate-away.

Per the "100% sure" rule, this issue is inventory only. No code changes applied. Each site needs individual investigation before fix.


Full inventory (~20 sites)

Privacy/payment-adjacent — needs careful review

File Line Variant Why higher risk
lib/core/services/wallet_service.dart 203 no guard Wallet/payments — WalletConnect reinit + retry path
lib/features/billing/providers/billing_provider.dart 165, 277 no guard Billing — money flow
lib/features/settings/screens/face_management_screen.dart 124 no guard Face recognition data — privacy

Auth / onboarding

File Line Variant
lib/features/home/screens/home_screen.dart 150, 625, 636 unrelated mounted
lib/features/home/screens/home_screen.dart 366, 446 no guard

File/sharing UI (medium risk)

File Line Variant
lib/features/browser/screens/file_browser_screen.dart 2069 unrelated mounted
lib/features/browser/screens/file_browser_screen.dart 3109, 3169 no guard
lib/features/websites/screens/website_detail_screen.dart 520, 543 unrelated mounted
lib/features/nft/screens/nfts_browser_screen.dart 413, 419 unrelated mounted
lib/features/nft/screens/nft_detail_screen.dart 418 no guard

Pure UI (lower risk)

File Line Variant
lib/features/viewer/screens/audio_player_screen.dart 702, 703, 767, 768 unrelated mounted
lib/features/audio/screens/playlists_screen.dart 213, 252 unrelated mounted
lib/features/viewer/screens/image_editor_screen.dart 720 no guard
lib/core/services/battery_optimization_service.dart 59 no guard

Sample sites (showing the risk gradient)

Sample 1 — home_screen.dart:148-156 (unrelated mounted, but likely safe)

Future<void> _getApiKey(BuildContext context) async {
  setState(() => _isGettingApiKey = true);
  final success = await DeepLinkService.instance.openGetApiKeyPage();
  if (!success && mounted) {                     // `mounted` is _HomeScreenState's
    setState(() => _isGettingApiKey = false);
    ScaffoldMessenger.of(context).showSnackBar(  // `context` was a parameter
      const SnackBar(content: Text('Could not open browser...')),
    );
    return;
  }
}

Analyzer can't prove context belongs to _HomeScreenState. Likely safe in practice (caller passes _HomeScreenState's own context), but to PROVE it safe you have to audit every caller of _getApiKey. Fix per-site: use this.context (the State's context) instead of the parameter, or drop the parameter entirely.

Sample 2 — wallet_service.dart:203 (no guard, payment path)

} on StateError catch (e) {
  debugPrint('WalletService: StateError when opening modal: $e');
  await reinitialize(context);                    // context across await, NO mounted check
  // ... retry block uses context again
}

Real risk: if user navigates away during the StateError path (likely during a WalletConnect modal crash → recovery), reinitialize(context) runs against a stale context. Could throw or worse — re-initialise wallet state against the wrong widget tree.

Sample 3 — audio_player_screen.dart:701-706 (unrelated mounted, pure UI)

onTap: () async {
  await PlaylistService.instance.addTrackToPlaylist(playlist.id, currentTrack);
  if (mounted) {
    Navigator.pop(context);                      // context from bottom-sheet builder?
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Added to ${playlist.name}')));
  }
},

Lower-stakes: if the context is from a showModalBottomSheet builder, it can be disposed independently of the parent State. mounted is for the parent State, not the bottom-sheet context. Likely real false-positive flag, but worth confirming.


Proposed approach (NOT APPLIED)

  1. Triage tier by tier. Privacy/payment tier first (3 sites: wallet_service.dart:203, billing_provider.dart:{165,277}, face_management_screen.dart:124) — these warrant individual reads + fixes with widget-test coverage simulating unmount-during-async.
  2. For "unrelated mounted" sites: most likely fix is if (!context.mounted) return; after the await (Flutter's BuildContext.mounted getter — separate from State's mounted — is the correct guard).
  3. For "no guard" sites: same — add if (!context.mounted) return; after the await.
  4. Service-level sites (wallet_service.dart, battery_optimization_service.dart): consider whether passing BuildContext to a service is the right pattern at all; often a callback or stream is cleaner.

Test approach (NOT APPLIED)

A representative widget test would:

  • Pump a widget that triggers an async operation
  • Await a microtask, then tester.pumpWidget(emptyWidget) to unmount
  • Resolve the async operation
  • Assert no exception is thrown

Worth writing once per non-trivial fix.

Recommendation

Address the privacy/payment tier first (3 sites). Open a follow-up issue scoping that batch. Defer the rest to a janitorial PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions