Refactor State methods to use idiomatic mounted/context#36
Refactor State methods to use idiomatic mounted/context#36dougborg wants to merge 6 commits intoFoggedLens:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR refactors State class methods to follow idiomatic Flutter patterns by using mounted and this.context instead of passing BuildContext as parameters. Additionally, it updates deprecated APIs and removes unused code.
Changes:
- Refactored State subclass methods in add_node_sheet.dart, edit_node_sheet.dart, and other widgets to access context directly instead of passing it as a parameter
- Updated deprecated
withOpacity()API calls towithValues(alpha:)throughout the codebase - Introduced
RadioGroupwidget wrapper in language_section.dart and refine_tags_sheet.dart - Added proper
mountedchecks after async operations - Removed unused imports, variables, and dead code
- Updated test configurations and fixed minor code style issues
Reviewed changes
Copilot reviewed 80 out of 81 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/widgets/add_node_sheet.dart | Refactored async methods to use this.context and added mounted checks |
| lib/widgets/edit_node_sheet.dart | Refactored async methods to use this.context and added mounted checks |
| lib/widgets/download_area_dialog.dart | Extracted method for download logic and added mounted checks |
| lib/widgets/refine_tags_sheet.dart | Introduced RadioGroup wrapper for radio button management |
| lib/screens/settings/sections/language_section.dart | Introduced RadioGroup wrapper for language selection |
| lib/widgets/*.dart (multiple files) | Updated withOpacity to withValues API |
| lib/services/*.dart (multiple files) | Replaced print with debugPrint, removed unused code |
| lib/screens/*.dart (multiple files) | Updated withOpacity to withValues, removed unused imports |
| test/services/deflock_tile_provider_test.dart | Added mocktail for mocking, updated tests |
| test/models/tile_provider_test.dart | Updated test expectations for API key requirements |
| scripts/validate_localizations.dart | Replaced print with debugPrint |
| pubspec.yaml | Added path_provider, collection, and flutter_lints dependencies |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
6610454 to
2945147
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 77 out of 79 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 80 out of 82 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
bb784ef to
00ebda0
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 79 out of 81 changed files in this pull request and generated 5 comments.
Comments suppressed due to low confidence (3)
lib/screens/settings/sections/language_section.dart:51
_setLanguageawaitsLocalizationService.instance.setLanguage(...)and then callssetStateunconditionally. Addif (!mounted) return;after the await to avoidsetState() called after dispose()if the settings sheet is closed mid-operation.
Future<void> _setLanguage(String? languageCode) async {
await LocalizationService.instance.setLanguage(languageCode);
setState(() {
_selectedLanguage = languageCode;
});
}
lib/widgets/add_node_sheet.dart:155
_commitWithoutCheckpops the sheet and then immediately usesScaffoldMessenger.of(context). Once the sheet is popped, this context can be deactivated. Capture the messenger before popping (e.g., storefinal messenger = ScaffoldMessenger.of(context)), or show the SnackBar from an ancestor context, to avoid stale-context exceptions.
lib/widgets/edit_node_sheet.dart:153_commitWithoutCheckpops the sheet and then callsScaffoldMessenger.of(context). AfterNavigator.pop, the sheet context may be deactivated, which can throw at runtime. Capture theScaffoldMessengerState(or an ancestor context) before popping, then show the SnackBar via that captured messenger.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
3e6b065 to
d266a1b
Compare
The RawAutocomplete tests proved the pattern and fix but never instantiated NSITagValueField itself — no actual coverage gained. PR FoggedLens#36 replaces the widget entirely, so investing in testability here isn't worthwhile. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (2)
lib/widgets/add_node_sheet.dart:155
_commitWithoutCheckcallsNavigator.pop(context)and then immediately looks upScaffoldMessenger.of(context)to show a SnackBar. After popping the bottom sheet, thiscontextcan be deactivated/disposed, which can trigger “Looking up a deactivated widget's ancestor” / stale-context errors. Capture theScaffoldMessengerState(or a parent context) before popping, or show the SnackBar before closing the sheet.
void _commitWithoutCheck() {
final appState = context.read<AppState>();
final locService = LocalizationService.instance;
appState.commitSession();
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(locService.t('node.queuedForUpload'))),
);
lib/widgets/edit_node_sheet.dart:153
_commitWithoutCheckpops the sheet (Navigator.pop(context)) and then usesScaffoldMessenger.of(context)to show a SnackBar. Once the sheet is popped, thiscontextmay be deactivated, causing runtime errors. Capture the messenger (or use a stable parent/root context) before popping, or show the SnackBar before closing the sheet.
void _commitWithoutCheck() {
final appState = context.read<AppState>();
final locService = LocalizationService.instance;
appState.commitEditSession();
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(locService.t('node.editQueuedForUpload'))),
);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| when(() => mockCache.addOrUpdate(any())).thenReturn(null); | ||
| when(() => mockProvider.notifyListeners()).thenReturn(null); |
There was a problem hiding this comment.
The helper _createQueue states “Void methods are auto-stubbed by mocktail — no explicit stubs needed.”, but multiple tests still add explicit when(...).thenReturn(null) stubs for void methods (e.g., addOrUpdate, notifyListeners). This is contradictory and makes it harder to tell when stubbing is actually required; either remove the stubs or update the comment (or centralize any required stubbing in _createQueue).
| when(() => mockCache.addOrUpdate(any())).thenReturn(null); | |
| when(() => mockProvider.notifyListeners()).thenReturn(null); |
| test('detected operator profile behavior on profile change', () { | ||
| final s = SessionState(); | ||
| s.startEditSession(_nodeWithDirections(), _enabledProfiles(), _operatorProfiles()); | ||
|
|
||
| // The detected operator profile should be set | ||
| final detectedOp = s.editSession!.operatorProfile; | ||
| expect(detectedOp, isNotNull); | ||
|
|
||
| // When profile changes without explicit operatorProfile, the restoration | ||
| // inside the profile block is overridden by the unconditional operator | ||
| // comparison (null != current). This is the actual behavior: | ||
| s.updateEditSession(profile: _motorolaProfile()); | ||
| expect(s.editSession!.operatorProfile, isNull); | ||
|
|
||
| // But when operator profile is explicitly passed alongside profile change, | ||
| // it takes effect: | ||
| s.updateEditSession(profile: _flockProfile(), operatorProfile: detectedOp); | ||
| expect(s.editSession!.operatorProfile, equals(detectedOp)); | ||
| }); |
There was a problem hiding this comment.
This test asserts and documents behavior that appears unintended: updateEditSession(profile: ...) clears a previously detected operatorProfile unless it’s passed explicitly. Since SessionState.updateEditSession has logic/commentary about restoring the detected operator profile on profile changes, codifying the current “operator gets nulled” behavior will make a future bugfix look like a regression. Consider either (a) asserting the intended behavior and fixing SessionState.updateEditSession, or (b) renaming the test / adding a TODO to clearly mark it as documenting a known issue.
Private State methods were accepting BuildContext as a parameter, shadowing this.context and forcing the less-idiomatic context.mounted guard. Per dart.dev linter guidance, State.mounted is the correct guard when using the State's own context property. - add_node_sheet/edit_node_sheet: Drop context/appState/locService params from _checkProximityAndCommit, _checkSubmissionGuideAndProceed, _checkProximityOnly, _commitWithoutCheck - download_area_dialog: Extract inline async lambda to _startDownload() State method with mounted guards Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Move context.read<AppState>() before the await on getOfflineAreaDir() to avoid reading stale state if the dialog is disposed during the I/O operation. The mounted checks were already added in an earlier commit; this fixes the remaining state-capture ordering issue. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
57 tests covering the ChangeNotifier state layer that widgets depend on: - Session lifecycle: start/clear add vs edit, operator profile detection, direction initialization from nodes with and without directions - Dirty checking: updateSession only notifies on actual changes, profile change regenerates changeset comment, defensive copy of refinedTags - Edit session recalculation: profile change recalculates additionalExistingTags/refinedTags/changesetComment, extractFromWay snap-back, explicit tags override auto-calculation - Direction management: add/remove/cycle with correct min enforcement (min=1 for add, min=0 for edit when original had no directions) - Commit guards: returns null unless target+profile set (add) or profile set (edit), double commit returns null safely - Cancel: clears session and detected operator profile - Changeset comment generation for all operation types Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Small constructor change: MapDataProvider and NodeProviderWithCache are now injectable via optional constructor parameters with defaults to the existing singletons. Production code unchanged. 27 tests covering: - addFromSession: creates PendingUpload with correct operation, adds temp node with negative ID and _pending_upload tag to cache - addFromEditSession: modify marks original with _pending_edit + creates temp node; extract creates only temp node; constrained modify uses original coordinates - addFromNodeDeletion: marks node with _pending_deletion - clearQueue/removeFromQueue: correct cache cleanup dispatch (create removes temp, edit removes temp + pending_edit marker, delete removes pending_deletion marker, extract removes temp only) - Direction formatting: single as double, multiple as semicolon-separated, FOV range notation, 360 FOV, wrapping ranges - Queue persistence: save/load round-trip via SharedPreferences Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
13 tests verifying the coordination between SessionState and UploadQueueState that AppState.commitSession() performs: - Full add flow: startAddSession -> set target + profile -> commitSession -> addFromSession -> queue has 1 item, session null - Full edit flow: both modify and extract paths - Commit guards: incomplete session doesn't add to queue, double commit is safe (second returns null) - Profile deletion callback: deleting profile used in active add/edit session cancels that session; unrelated profile deletion doesn't affect session - Notification propagation: sub-module notifyListeners fires on all state-changing operations Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5ae010a to
ee21b8a
Compare
Summary
Statesubclass methods to usemountedandthis.contextinstead of passingBuildContextas parametersmountedchecks were missing afterawaitsWhy
mountedchecks matterAfter any
await, the widget may be disposed. UsingcontextorNavigatoron a disposed widget throws aFlutterError. The refactor to usethis.context(fromState) instead of capturedBuildContextparameters is the Flutter-idiomatic pattern, and combined withmountedchecks, prevents using stale context.Mounted check fixes
Three methods had async gaps without guards — if the sheet/dialog is dismissed during the async operation, these would crash on stale context:
AddNodeSheet._checkSubmissionGuideAndProceed: guards afterhasSeenSubmissionGuide()andshowDialog()awaitsEditNodeSheet._checkSubmissionGuideAndProceed: identical patternDownloadAreaDialogdownload button: capturesAppStatebefore async gap, guards aftergetOfflineAreaDir(), and guards in catch blockState management tests
The tests verify the ChangeNotifier layer invariants that prevent "stuck state" bugs:
session_state_test.dartupload_queue_state_test.dartapp_state_integration_test.dartSmall refactor:
UploadQueueStatenow accepts injectableMapDataProviderandNodeProviderWithCachevia optional constructor parameters (defaults to existing singletons). Production code unchanged.Test plan
flutter test test/state/— all 97 new tests passflutter test— all 127 tests pass (no regressions)🤖 Generated with Claude Code