mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-13 09:20:29 +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:
@@ -25,6 +25,8 @@ class AppProvider with ChangeNotifier {
|
|||||||
bool _isInitialized = false;
|
bool _isInitialized = false;
|
||||||
bool get isInitialized => _isInitialized;
|
bool get isInitialized => _isInitialized;
|
||||||
|
|
||||||
|
bool _isDisposed = false;
|
||||||
|
|
||||||
bool _isSimpleMode = true;
|
bool _isSimpleMode = true;
|
||||||
bool get isSimpleMode => _isSimpleMode;
|
bool get isSimpleMode => _isSimpleMode;
|
||||||
|
|
||||||
@@ -683,4 +685,17 @@ class AppProvider with ChangeNotifier {
|
|||||||
'sarMarkers': messagesProvider.sarMarkerStats,
|
'sarMarkers': messagesProvider.sarMarkerStats,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_isDisposed = true;
|
||||||
|
// Remove connection state listener
|
||||||
|
connectionProvider.removeListener(_handleConnectionStateChange);
|
||||||
|
// Clear location service callbacks
|
||||||
|
locationTrackingService.onPositionUpdate = null;
|
||||||
|
locationTrackingService.onBroadcastSent = null;
|
||||||
|
locationTrackingService.onError = null;
|
||||||
|
locationTrackingService.onTrackingStateChanged = null;
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,9 +51,11 @@ class _ContactsTabState extends State<ContactsTab> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleRefresh() async {
|
Future<void> _handleRefresh() async {
|
||||||
|
if (!mounted) return;
|
||||||
final appProvider = context.read<AppProvider>();
|
final appProvider = context.read<AppProvider>();
|
||||||
await appProvider.refresh();
|
await appProvider.refresh();
|
||||||
// Also refresh location
|
// Also refresh location
|
||||||
|
if (!mounted) return;
|
||||||
await _getCurrentLocation();
|
await _getCurrentLocation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -347,6 +347,8 @@ class _DeviceConfigScreenState extends State<DeviceConfigScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_latController.text = position.latitude.toStringAsFixed(6);
|
_latController.text = position.latitude.toStringAsFixed(6);
|
||||||
_lonController.text = position.longitude.toStringAsFixed(6);
|
_lonController.text = position.longitude.toStringAsFixed(6);
|
||||||
|
|||||||
@@ -279,6 +279,7 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
final appProvider = context.watch<AppProvider>();
|
final appProvider = context.watch<AppProvider>();
|
||||||
if (_isMapEnabled != appProvider.isMapEnabled) {
|
if (_isMapEnabled != appProvider.isMapEnabled) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted) return;
|
||||||
_updateTabController(appProvider.isMapEnabled);
|
_updateTabController(appProvider.isMapEnabled);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -409,6 +409,7 @@ class _MapManagementScreenState extends State<MapManagementScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
setState(() => _isLoading = false);
|
setState(() => _isLoading = false);
|
||||||
_showError(AppLocalizations.of(context)!.clearCacheFailed(e.toString()));
|
_showError(AppLocalizations.of(context)!.clearCacheFailed(e.toString()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,9 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
|||||||
// MBTiles layers
|
// MBTiles layers
|
||||||
List<MapLayer> _mbtilesLayers = [];
|
List<MapLayer> _mbtilesLayers = [];
|
||||||
|
|
||||||
|
// Store original location callback to restore in dispose
|
||||||
|
void Function(Position)? _originalLocationCallback;
|
||||||
|
|
||||||
// WMS layers (Slovenian)
|
// WMS layers (Slovenian)
|
||||||
late final MapLayer _slovenianAerialLayer;
|
late final MapLayer _slovenianAerialLayer;
|
||||||
late final MapLayer _dtk25Layer;
|
late final MapLayer _dtk25Layer;
|
||||||
@@ -128,6 +131,7 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
|||||||
|
|
||||||
// Listen to map provider for navigation requests
|
// Listen to map provider for navigation requests
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted) return; // Check if widget is still mounted
|
||||||
final mapProvider = context.read<MapProvider>();
|
final mapProvider = context.read<MapProvider>();
|
||||||
mapProvider.addListener(_handleMapNavigation);
|
mapProvider.addListener(_handleMapNavigation);
|
||||||
// Load WMS overlay state
|
// Load WMS overlay state
|
||||||
@@ -146,13 +150,13 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
|||||||
/// Note: LocationTrackingService is initialized and started by AppProvider
|
/// Note: LocationTrackingService is initialized and started by AppProvider
|
||||||
/// This method only adds map-specific callbacks for rotation and UI updates
|
/// This method only adds map-specific callbacks for rotation and UI updates
|
||||||
void _setupLocationCallbacks() {
|
void _setupLocationCallbacks() {
|
||||||
// Store the original callback from AppProvider
|
// Store the original callback from AppProvider to restore in dispose
|
||||||
final originalCallback = _locationService.onPositionUpdate;
|
_originalLocationCallback = _locationService.onPositionUpdate;
|
||||||
|
|
||||||
// Add map-specific callback that chains with the original
|
// Add map-specific callback that chains with the original
|
||||||
_locationService.onPositionUpdate = (position) {
|
_locationService.onPositionUpdate = (position) {
|
||||||
// Call original callback first (AppProvider's logging)
|
// Call original callback first (AppProvider's logging)
|
||||||
originalCallback?.call(position);
|
_originalLocationCallback?.call(position);
|
||||||
|
|
||||||
// Then handle map-specific logic - early exit if not mounted or disposing
|
// Then handle map-specific logic - early exit if not mounted or disposing
|
||||||
if (!mounted || _isDisposing) {
|
if (!mounted || _isDisposing) {
|
||||||
@@ -437,8 +441,8 @@ class _MapTabState extends State<MapTab> with AutomaticKeepAliveClientMixin {
|
|||||||
mapProvider.removeListener(_handleMapNavigation);
|
mapProvider.removeListener(_handleMapNavigation);
|
||||||
|
|
||||||
// DO NOT stop location tracking - it's managed by AppProvider
|
// DO NOT stop location tracking - it's managed by AppProvider
|
||||||
// Just clear the map-specific callback
|
// Restore the original callback instead of setting to null
|
||||||
_locationService.onPositionUpdate = null;
|
_locationService.onPositionUpdate = _originalLocationCallback;
|
||||||
_mapController.dispose();
|
_mapController.dispose();
|
||||||
_tileCache.dispose();
|
_tileCache.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@@ -33,6 +34,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
int _characterCount = 0;
|
int _characterCount = 0;
|
||||||
static const int _maxCharacters = 160;
|
static const int _maxCharacters = 160;
|
||||||
String? _highlightedMessageId;
|
String? _highlightedMessageId;
|
||||||
|
Timer? _highlightTimer; // Timer for clearing message highlight
|
||||||
|
|
||||||
// Message destination state
|
// Message destination state
|
||||||
String _destinationType =
|
String _destinationType =
|
||||||
@@ -73,6 +75,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_highlightTimer?.cancel();
|
||||||
_textController.dispose();
|
_textController.dispose();
|
||||||
_focusNode.dispose();
|
_focusNode.dispose();
|
||||||
_scrollController.dispose();
|
_scrollController.dispose();
|
||||||
@@ -112,8 +115,9 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
_highlightedMessageId = messageId;
|
_highlightedMessageId = messageId;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear highlight after 2 seconds
|
// Clear highlight after 2 seconds using a properly managed Timer
|
||||||
Future.delayed(const Duration(seconds: 2), () {
|
_highlightTimer?.cancel();
|
||||||
|
_highlightTimer = Timer(const Duration(seconds: 2), () {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_highlightedMessageId = null;
|
_highlightedMessageId = null;
|
||||||
|
|||||||
@@ -73,10 +73,12 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
|||||||
|
|
||||||
// Save to temporary file
|
// Save to temporary file
|
||||||
final tempDir = await getTemporaryDirectory();
|
final tempDir = await getTemporaryDirectory();
|
||||||
|
if (!context.mounted) return;
|
||||||
final file = File('${tempDir.path}/ble_packets_${DateTime.now().millisecondsSinceEpoch}.csv');
|
final file = File('${tempDir.path}/ble_packets_${DateTime.now().millisecondsSinceEpoch}.csv');
|
||||||
await file.writeAsString(buffer.toString());
|
await file.writeAsString(buffer.toString());
|
||||||
|
|
||||||
// Share the file
|
// Share the file
|
||||||
|
if (!context.mounted) return;
|
||||||
await Share.shareXFiles(
|
await Share.shareXFiles(
|
||||||
[XFile(file.path)],
|
[XFile(file.path)],
|
||||||
subject: 'MeshCore BLE Packet Logs',
|
subject: 'MeshCore BLE Packet Logs',
|
||||||
@@ -118,10 +120,12 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
|||||||
|
|
||||||
// Save to temporary file
|
// Save to temporary file
|
||||||
final tempDir = await getTemporaryDirectory();
|
final tempDir = await getTemporaryDirectory();
|
||||||
|
if (!context.mounted) return;
|
||||||
final file = File('${tempDir.path}/ble_packets_${DateTime.now().millisecondsSinceEpoch}.txt');
|
final file = File('${tempDir.path}/ble_packets_${DateTime.now().millisecondsSinceEpoch}.txt');
|
||||||
await file.writeAsString(buffer.toString());
|
await file.writeAsString(buffer.toString());
|
||||||
|
|
||||||
// Share the file
|
// Share the file
|
||||||
|
if (!context.mounted) return;
|
||||||
await Share.shareXFiles(
|
await Share.shareXFiles(
|
||||||
[XFile(file.path)],
|
[XFile(file.path)],
|
||||||
subject: 'MeshCore BLE Packet Logs',
|
subject: 'MeshCore BLE Packet Logs',
|
||||||
@@ -147,27 +151,31 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _clearLogs(BuildContext context) {
|
void _clearLogs(BuildContext context) {
|
||||||
|
final parentContext = context; // Store parent context for setState
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (dialogContext) => AlertDialog(
|
||||||
title: Text(AppLocalizations.of(context)!.clearAllData),
|
title: Text(AppLocalizations.of(dialogContext)!.clearAllData),
|
||||||
content: const Text('Are you sure you want to clear all packet logs? This cannot be undone.'),
|
content: const Text('Are you sure you want to clear all packet logs? This cannot be undone.'),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(dialogContext),
|
||||||
child: Text(AppLocalizations.of(context)!.cancel),
|
child: Text(AppLocalizations.of(dialogContext)!.cancel),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
widget.bleService.clearPacketLogs();
|
widget.bleService.clearPacketLogs();
|
||||||
Navigator.pop(context);
|
Navigator.pop(dialogContext);
|
||||||
|
if (!mounted) return;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
if (parentContext.mounted) {
|
||||||
const SnackBar(content: Text('Packet logs cleared')),
|
ScaffoldMessenger.of(parentContext).showSnackBar(
|
||||||
);
|
const SnackBar(content: Text('Packet logs cleared')),
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||||
child: Text(AppLocalizations.of(context)!.clear),
|
child: Text(AppLocalizations.of(dialogContext)!.clear),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -381,6 +389,7 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
|||||||
// Auto-scroll to bottom
|
// Auto-scroll to bottom
|
||||||
if (_autoScroll && index == logs.length - 1) {
|
if (_autoScroll && index == logs.length - 1) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted) return;
|
||||||
if (_scrollController.hasClients) {
|
if (_scrollController.hasClients) {
|
||||||
_scrollController.animateTo(
|
_scrollController.animateTo(
|
||||||
_scrollController.position.maxScrollExtent,
|
_scrollController.position.maxScrollExtent,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class _SarTemplateManagementScreenState extends State<SarTemplateManagementScree
|
|||||||
|
|
||||||
Future<void> _initializeService() async {
|
Future<void> _initializeService() async {
|
||||||
if (!_templateService.isInitialized) {
|
if (!_templateService.isInitialized) {
|
||||||
|
if (!mounted) return;
|
||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
await _templateService.initialize();
|
await _templateService.initialize();
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@@ -111,6 +112,7 @@ class _SarTemplateManagementScreenState extends State<SarTemplateManagementScree
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _importFromClipboard() async {
|
Future<void> _importFromClipboard() async {
|
||||||
|
if (!mounted) return;
|
||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -57,6 +57,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
_loadRxTxPreference();
|
_loadRxTxPreference();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
// Clear location service callbacks to prevent memory leaks
|
||||||
|
_locationService.onError = null;
|
||||||
|
_locationService.onBroadcastSent = null;
|
||||||
|
_locationService.onTrackingStateChanged = null;
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _loadPackageInfo() async {
|
Future<void> _loadPackageInfo() async {
|
||||||
final info = await PackageInfo.fromPlatform();
|
final info = await PackageInfo.fromPlatform();
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
|||||||
@@ -22,6 +22,26 @@ class _ConnectionDialogState extends State<ConnectionDialog>
|
|||||||
int _totalToScan = 0;
|
int _totalToScan = 0;
|
||||||
String? _connectingToServerUrl; // Track which server is being connected to
|
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
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -55,25 +75,8 @@ class _ConnectionDialogState extends State<ConnectionDialog>
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Listen to tab changes
|
// Listen to tab changes using named method for proper cleanup
|
||||||
_tabController.addListener(() {
|
_tabController.addListener(_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
|
@override
|
||||||
@@ -84,6 +87,8 @@ class _ConnectionDialogState extends State<ConnectionDialog>
|
|||||||
);
|
);
|
||||||
connectionProvider.stopScan();
|
connectionProvider.stopScan();
|
||||||
_networkScanner.stopScan();
|
_networkScanner.stopScan();
|
||||||
|
// Remove listener before disposing to prevent memory leaks
|
||||||
|
_tabController.removeListener(_onTabChanged);
|
||||||
_tabController.dispose();
|
_tabController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ class _DirectMessageSheetState extends State<DirectMessageSheet> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _updateCharacterCount() {
|
void _updateCharacterCount() {
|
||||||
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_characterCount = _textController.text.length;
|
_characterCount = _textController.text.length;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
|
|||||||
final FocusNode _focusNode = FocusNode();
|
final FocusNode _focusNode = FocusNode();
|
||||||
bool _isLoggingIn = false;
|
bool _isLoggingIn = false;
|
||||||
bool _obscurePassword = true;
|
bool _obscurePassword = true;
|
||||||
|
bool _isDisposed = false; // Track disposal state for async callbacks
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -30,6 +31,7 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_isDisposed = true;
|
||||||
_passwordController.dispose();
|
_passwordController.dispose();
|
||||||
_focusNode.dispose();
|
_focusNode.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
@@ -245,15 +247,15 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
|
|||||||
' Messages will be fetched when onMessageWaiting callback is triggered',
|
' Messages will be fetched when onMessageWaiting callback is triggered',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (mounted) {
|
// Check both _isDisposed flag and mounted to handle race conditions
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
if (_isDisposed || !mounted) return;
|
||||||
SnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
content: Text(AppLocalizations.of(context)!.loggedInSuccessfully),
|
SnackBar(
|
||||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
content: Text(AppLocalizations.of(context)!.loggedInSuccessfully),
|
||||||
duration: const Duration(seconds: 3),
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
),
|
duration: const Duration(seconds: 3),
|
||||||
);
|
),
|
||||||
}
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
connectionProvider.onLoginFail = (publicKeyPrefix) {
|
connectionProvider.onLoginFail = (publicKeyPrefix) {
|
||||||
@@ -263,15 +265,15 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
|
|||||||
|
|
||||||
debugPrint('❌ [RoomLogin] Login failed - incorrect password');
|
debugPrint('❌ [RoomLogin] Login failed - incorrect password');
|
||||||
|
|
||||||
if (mounted) {
|
// Check both _isDisposed flag and mounted to handle race conditions
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
if (_isDisposed || !mounted) return;
|
||||||
SnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
content: Text(AppLocalizations.of(context)!.loginFailed),
|
SnackBar(
|
||||||
backgroundColor: Theme.of(context).colorScheme.error,
|
content: Text(AppLocalizations.of(context)!.loginFailed),
|
||||||
duration: const Duration(seconds: 3),
|
backgroundColor: Theme.of(context).colorScheme.error,
|
||||||
),
|
duration: const Duration(seconds: 3),
|
||||||
);
|
),
|
||||||
}
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -231,6 +231,7 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
|||||||
try {
|
try {
|
||||||
// Check if location services are enabled
|
// Check if location services are enabled
|
||||||
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||||
|
if (!mounted) return;
|
||||||
if (!serviceEnabled) {
|
if (!serviceEnabled) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_locationError = 'Location services are disabled';
|
_locationError = 'Location services are disabled';
|
||||||
@@ -241,8 +242,10 @@ class _SarUpdateSheetState extends State<SarUpdateSheet> {
|
|||||||
|
|
||||||
// Check permissions
|
// Check permissions
|
||||||
LocationPermission permission = await Geolocator.checkPermission();
|
LocationPermission permission = await Geolocator.checkPermission();
|
||||||
|
if (!mounted) return;
|
||||||
if (permission == LocationPermission.denied) {
|
if (permission == LocationPermission.denied) {
|
||||||
permission = await Geolocator.requestPermission();
|
permission = await Geolocator.requestPermission();
|
||||||
|
if (!mounted) return;
|
||||||
if (permission == LocationPermission.denied) {
|
if (permission == LocationPermission.denied) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_locationError = 'Location permission denied';
|
_locationError = 'Location permission denied';
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ class _PermissionRequestDialogState extends State<PermissionRequestDialog> {
|
|||||||
try {
|
try {
|
||||||
// Check if location service is enabled
|
// Check if location service is enabled
|
||||||
final serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
final serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||||
|
if (!mounted) return;
|
||||||
if (!serviceEnabled) {
|
if (!serviceEnabled) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_errorMessage = 'Location services are disabled. Please enable location services in your device settings.';
|
_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
|
// Check current permission
|
||||||
LocationPermission permission = await Geolocator.checkPermission();
|
LocationPermission permission = await Geolocator.checkPermission();
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
if (permission == LocationPermission.denied) {
|
if (permission == LocationPermission.denied) {
|
||||||
// Request permission
|
// Request permission
|
||||||
permission = await Geolocator.requestPermission();
|
permission = await Geolocator.requestPermission();
|
||||||
|
if (!mounted) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (permission == LocationPermission.denied) {
|
if (permission == LocationPermission.denied) {
|
||||||
@@ -78,11 +81,10 @@ class _PermissionRequestDialogState extends State<PermissionRequestDialog> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Close dialog and notify parent
|
// Close dialog and notify parent
|
||||||
if (mounted) {
|
Navigator.of(context).pop();
|
||||||
Navigator.of(context).pop();
|
widget.onPermissionsGranted();
|
||||||
widget.onPermissionsGranted();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_errorMessage = 'Error requesting permissions: $e';
|
_errorMessage = 'Error requesting permissions: $e';
|
||||||
_isRequesting = false;
|
_isRequesting = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user