mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-13 17:30:28 +00:00
fix: Add mounted checks and fix memory leaks across UI components
Address potential memory leaks and state-after-dispose issues: - AppProvider: Add dispose method to clean up listeners and callbacks - MapTab: Store and restore original location callback instead of nullifying - MessagesTab: Use Timer instead of Future.delayed for highlight cleanup - ConnectionDialog: Use named listener method for proper tab controller cleanup - RoomLoginSheet: Add _isDisposed flag to handle async callback race conditions - SettingsScreen: Clear location service callbacks in dispose - Various screens: Add mounted checks before setState after async operations (contacts_tab, device_config, home_screen, map_management, packet_log, sar_template_management, sar_update_sheet, permission_request_dialog)
This commit is contained in:
@@ -22,6 +22,26 @@ class _ConnectionDialogState extends State<ConnectionDialog>
|
||||
int _totalToScan = 0;
|
||||
String? _connectingToServerUrl; // Track which server is being connected to
|
||||
|
||||
// Named listener method for proper cleanup
|
||||
void _onTabChanged() {
|
||||
if (_tabController.index == 1) {
|
||||
// Switched to network tab
|
||||
if (_networkScanner.hasCachedResults && _discoveredServers.isEmpty) {
|
||||
// Load cached results
|
||||
setState(() {
|
||||
_discoveredServers.addAll(_networkScanner.cachedServers);
|
||||
});
|
||||
debugPrint(
|
||||
'📦 [NetworkScanner] Loaded ${_discoveredServers.length} servers from cache',
|
||||
);
|
||||
} else if (!_networkScanner.isScanning &&
|
||||
!_networkScanner.hasCachedResults) {
|
||||
// No cache, start initial scan
|
||||
_startNetworkScan();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -55,25 +75,8 @@ class _ConnectionDialogState extends State<ConnectionDialog>
|
||||
}
|
||||
};
|
||||
|
||||
// Listen to tab changes
|
||||
_tabController.addListener(() {
|
||||
if (_tabController.index == 1) {
|
||||
// Switched to network tab
|
||||
if (_networkScanner.hasCachedResults && _discoveredServers.isEmpty) {
|
||||
// Load cached results
|
||||
setState(() {
|
||||
_discoveredServers.addAll(_networkScanner.cachedServers);
|
||||
});
|
||||
debugPrint(
|
||||
'📦 [NetworkScanner] Loaded ${_discoveredServers.length} servers from cache',
|
||||
);
|
||||
} else if (!_networkScanner.isScanning &&
|
||||
!_networkScanner.hasCachedResults) {
|
||||
// No cache, start initial scan
|
||||
_startNetworkScan();
|
||||
}
|
||||
}
|
||||
});
|
||||
// Listen to tab changes using named method for proper cleanup
|
||||
_tabController.addListener(_onTabChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -84,6 +87,8 @@ class _ConnectionDialogState extends State<ConnectionDialog>
|
||||
);
|
||||
connectionProvider.stopScan();
|
||||
_networkScanner.stopScan();
|
||||
// Remove listener before disposing to prevent memory leaks
|
||||
_tabController.removeListener(_onTabChanged);
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ class _DirectMessageSheetState extends State<DirectMessageSheet> {
|
||||
}
|
||||
|
||||
void _updateCharacterCount() {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_characterCount = _textController.text.length;
|
||||
});
|
||||
|
||||
@@ -21,6 +21,7 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
bool _isLoggingIn = false;
|
||||
bool _obscurePassword = true;
|
||||
bool _isDisposed = false; // Track disposal state for async callbacks
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -30,6 +31,7 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_isDisposed = true;
|
||||
_passwordController.dispose();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
@@ -245,15 +247,15 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
|
||||
' Messages will be fetched when onMessageWaiting callback is triggered',
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.loggedInSuccessfully),
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
// Check both _isDisposed flag and mounted to handle race conditions
|
||||
if (_isDisposed || !mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.loggedInSuccessfully),
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
connectionProvider.onLoginFail = (publicKeyPrefix) {
|
||||
@@ -263,15 +265,15 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
|
||||
|
||||
debugPrint('❌ [RoomLogin] Login failed - incorrect password');
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.loginFailed),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
// Check both _isDisposed flag and mounted to handle race conditions
|
||||
if (_isDisposed || !mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.loginFailed),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
@@ -231,6 +231,7 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
||||
try {
|
||||
// Check if location services are enabled
|
||||
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!mounted) return;
|
||||
if (!serviceEnabled) {
|
||||
setState(() {
|
||||
_locationError = 'Location services are disabled';
|
||||
@@ -241,8 +242,10 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
||||
|
||||
// Check permissions
|
||||
LocationPermission permission = await Geolocator.checkPermission();
|
||||
if (!mounted) return;
|
||||
if (permission == LocationPermission.denied) {
|
||||
permission = await Geolocator.requestPermission();
|
||||
if (!mounted) return;
|
||||
if (permission == LocationPermission.denied) {
|
||||
setState(() {
|
||||
_locationError = 'Location permission denied';
|
||||
|
||||
@@ -38,6 +38,7 @@ class _PermissionRequestDialogState extends State<PermissionRequestDialog> {
|
||||
try {
|
||||
// Check if location service is enabled
|
||||
final serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!mounted) return;
|
||||
if (!serviceEnabled) {
|
||||
setState(() {
|
||||
_errorMessage = 'Location services are disabled. Please enable location services in your device settings.';
|
||||
@@ -48,10 +49,12 @@ class _PermissionRequestDialogState extends State<PermissionRequestDialog> {
|
||||
|
||||
// Check current permission
|
||||
LocationPermission permission = await Geolocator.checkPermission();
|
||||
if (!mounted) return;
|
||||
|
||||
if (permission == LocationPermission.denied) {
|
||||
// Request permission
|
||||
permission = await Geolocator.requestPermission();
|
||||
if (!mounted) return;
|
||||
}
|
||||
|
||||
if (permission == LocationPermission.denied) {
|
||||
@@ -78,11 +81,10 @@ class _PermissionRequestDialogState extends State<PermissionRequestDialog> {
|
||||
});
|
||||
|
||||
// Close dialog and notify parent
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
widget.onPermissionsGranted();
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
widget.onPermissionsGranted();
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_errorMessage = 'Error requesting permissions: $e';
|
||||
_isRequesting = false;
|
||||
|
||||
Reference in New Issue
Block a user