Problem with app reconnect

Hi,

I have problem with my flutter app. When I am developing it or when I test it here in my country, I have no problems, because we have great internet.
When I send app to my customer in Syria, he has many problems with connection, app disconnects from server and is unable to connect back.

I am sending my two modules, connection_service.dart and socket_service.dart. Please, can you see something that can cause this problem? Also, there was huge problem to reconnect on Samsung A06, other devices are kinda better.

connection_service.dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:internet_connection_checker_plus/internet_connection_checker_plus.dart';
import 'package:logger/logger.dart';
import 'package:hopin_taxi_customer_app/app.dart';
import 'package:hopin_taxi_customer_app/services/toast_message_service.dart';

class ConnectionService {
  // Singleton
  static final ConnectionService _instance = ConnectionService._internal();
  factory ConnectionService() => _instance;
  ConnectionService._internal();

  final _internetChecker = InternetConnection();
  final _logger = Logger();

  // Stream controller for connection status
  final _connectionStatusController =
      StreamController<ConnectionStatus>.broadcast();

  // Stream that other parts of the app can listen to
  Stream<ConnectionStatus> get connectionStatus =>
      _connectionStatusController.stream;

  // Current connection status
  ConnectionStatus? _currentStatus;
  ConnectionStatus? get currentStatus => _currentStatus;

  bool isInitialized = false;
  StreamSubscription<InternetStatus>? _internetStatusSubscription;

  Future<void> init() async {
    try {
      // Get initial connection status
      await _updateConnectionStatus();

      // Set up internet connectivity monitoring
      _setupInternetStatusMonitoring();
    } catch (e) {
      _logger.e('Error initializing ConnectionService: $e');
    }
  }

  void _setupInternetStatusMonitoring() {
    // Listen to real-time internet connectivity changes
    _internetStatusSubscription = _internetChecker.onStatusChange.listen(
      (InternetStatus status) async {
        await _updateConnectionStatus();
      },
      onError: (error) {
        _logger.e('Error in internet status monitoring: $error');
      },
    );
  }

  Future<void> _updateConnectionStatus() async {
    try {
      // Check internet access
      final hasInternet = await _internetChecker.hasInternetAccess;

      final newStatus = ConnectionStatus(
        isConnected: hasInternet,
        type: hasInternet ? 'connected' : 'disconnected',
        details: {
          'hasInternet': hasInternet,
          'timestamp': DateTime.now().toIso8601String(),
        },
      );

      _updateStatusIfChanged(newStatus);
    } catch (e) {
      _logger.e('Error updating connection status: $e');
    }
  }

  void _updateStatusIfChanged(ConnectionStatus newStatus) {
    if (_currentStatus != newStatus) {
      _currentStatus = newStatus;
      _connectionStatusController.add(newStatus);
      // Show toast message for connection status change
      // _showConnectionToast(newStatus);
    }
  }

  void _showConnectionToast(ConnectionStatus status) {
    if (navigatorKey.currentContext == null) return;

    if (status.isConnected) {
      if (!isInitialized) return;
      ToastMessageService.showSuccess(
        context: navigatorKey.currentContext!,
        translationKey: "internetConnectionRestored",
        descriptionKey: "internetConnectionRestoredDescription",
        duration: const Duration(seconds: 5),
        // forceShow: true,
        alignment: Alignment.topCenter,
      );
    } else {
      ToastMessageService.showWarning(
        context: navigatorKey.currentContext!,
        translationKey: "internetConnectionLost",
        descriptionKey: "internetConnectionLostDescription",
        duration: const Duration(seconds: 8),
        // forceShow: true,
        alignment: Alignment.topCenter,
      );
    }

    isInitialized = true;
  }

  // Method to manually check internet connectivity
  Future<bool> checkInternetConnectivity() async {
    try {
      return await _internetChecker.hasInternetAccess;
    } catch (e) {
      _logger.e('Error checking internet connectivity: $e');
      return false;
    }
  }

  // Method to get detailed connection info
  Future<Map<String, dynamic>> getDetailedConnectionInfo() async {
    try {
      final hasInternet = await _internetChecker.hasInternetAccess;

      return {
        'hasInternet': hasInternet,
        'timestamp': DateTime.now().toIso8601String(),
      };
    } catch (e) {
      _logger.e('Error getting detailed connection info: $e');
      return {
        'hasInternet': false,
        'timestamp': DateTime.now().toIso8601String(),
      };
    }
  }

  // Method to check if user has connectivity but no internet (useful for showing specific messages)
  bool get hasConnectivityButNoInternet {
    if (_currentStatus == null) return false;
    final hasInternet = _currentStatus!.details['hasInternet'] ?? false;
    return !hasInternet;
  }

  void dispose() {
    _connectionStatusController.close();
    _internetStatusSubscription?.cancel();
  }
}

class ConnectionStatus {
  final bool isConnected;
  final String type;
  final Map<String, dynamic> details;

  ConnectionStatus({
    required this.isConnected,
    required this.type,
    required this.details,
  });

  @override
  String toString() {
    return 'ConnectionStatus(isConnected: $isConnected, type: $type, details: $details)';
  }
}

socket_service.dart

import 'package:flutter/material.dart';
import 'dart:io';

import 'package:hopin_taxi_customer_app/app.dart';
import 'package:hopin_taxi_customer_app/extensions/translation_extension.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:async';
import 'dart:convert';
import 'package:hopin_taxi_customer_app/services/connection_service.dart';
import 'package:hopin_taxi_customer_app/utils/general_utils.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:hopin_taxi_customer_app/services/socket/socket_api_functions.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:hopin_taxi_customer_app/services/api/api_service.dart';

class SocketService extends ChangeNotifier {
  static final SocketService _instance = SocketService._internal();
  SocketService._internal();

  static SocketService get instance => _instance;

  String? _socketUrl;
  WebSocketChannel? _channel;
  StreamSubscription? _messageSubscription;
  int? _userId;
  String? _userKey;
  bool _isSocketConnected = false;
  bool _isConnectionEstablished = false;

  // Reconnection logic
  int _reconnectAttempts = 0;
  Timer? _reconnectTimer;
  final int _reconnectDelay = 2;

  // bool simulateConnectionError = false;
  int? lastMessageTimestamp;
  int? lastConnectionAttemptTimestamp;

  // Cleanup timer for old pending messages
  Timer? _cleanupTimer;

  bool get isSocketConnected => _isSocketConnected;
  bool get isConnectionEstablished => _isConnectionEstablished;

  bool setOrderStatusLoading = false;
  bool get isSetOrderStatusLoading => setOrderStatusLoading;

  void setIsConnectionEstablished(bool value) {
    _isConnectionEstablished = value;
    notifyListeners();
  }

  void Function(String? errorCode)? onConnectionErrorCallback;
  void Function()? onConnectionRestoredCallback;

  // Map to store pending completers for request-response pattern
  final Map<String, Completer<Map<String, dynamic>>> _pendingMessages = {};
  final Map<String, Timer> _pendingMessagesTimeout = {};
  final Map<String, Function(Map<String, dynamic>)> _boundMessages = {};

  void Function()? onNoAuth;

  StreamSubscription<ConnectionStatus>? _connectionSubscription;

  Future<Map<String, dynamic>> init() async {
    Completer<Map<String, dynamic>> completer =
        Completer<Map<String, dynamic>>();
    initSocketURL();

    try {
      final prefs = await SharedPreferences.getInstance();
      _userId = prefs.getInt('userId') ?? 0;
      _userKey = prefs.getString('userKey') ?? '';

      // Start listening to connection status
      _startListeningToConnectionStatus();

      Map<String, dynamic> mainConnection = await initConnection();
      completer.complete(mainConnection);
    } catch (e) {
      completer.complete({'status': 0, 'reasonText': 'init error: $e'});
    }

    return completer.future;
  }

  void _startListeningToConnectionStatus() {
    try {
      _connectionSubscription?.cancel();
      final connectionService = ConnectionService();
      // If already connected to internet and socket not connected, try connect
      final isInternet = connectionService.currentStatus?.isConnected ?? false;
      if (isInternet && !_isSocketConnected) {
        _tryReconnect();
      }
      _connectionSubscription = connectionService.connectionStatus.listen(
        (status) {
          // When internet is restored, try to reconnect
          if (status.isConnected && !_isSocketConnected) {
            _tryReconnect();
          }
        },
        onError: (e) {
          debugPrint('Sockets connection status stream error: $e');
        },
      );
    } catch (e) {
      debugPrint('Sockets _startListeningToConnectionStatus error: $e');
    }
  }

  void initSocketURL() {
    try {
      final isIOS = Platform.isIOS;
      final bool isTesting =
          !const bool.fromEnvironment('DART_VM_PRODUCT', defaultValue: false);
      _socketUrl =
          !isTesting
              ? isIOS
                  ? dotenv.env['WS_PROD_IOS_URL']
                  : dotenv.env['WS_PROD_URL'] ??
                      'wss://a.ws.taxi.yallago.net:443'
              : dotenv.env['WS_TEST_URL'] ??
                  'wss://test.a.ws.taxi.hopintaxi.com:13006';
    } catch (e) {
      debugPrint('Sockets initSocketURL error: $e');
      // Fallback URLs
      _socketUrl = 'wss://a.ws.taxi.yallago.net:443';
    }
  }

  Future<Map<String, dynamic>> initConnection() async {
    Completer<Map<String, dynamic>> completer =
        Completer<Map<String, dynamic>>();
    try {
      await disconnect();

      if (_userId == 0 || _userKey == null) {
        debugPrint('Sockets error: userId or userKey is null');
        return {'status': 0, 'reasonText': 'userId or userKey is null'};
      }

      if (_socketUrl == null) {
        return {'status': 0, 'reasonText': 'socketUrl is null'};
      }

      // Construct URL manually to avoid encoding the token
      String tokenValue = '$_userId|||$_userKey';
      String fullUrl = '$_socketUrl?token=$tokenValue';

      try {
        setIsConnectionEstablished(false);
        lastConnectionAttemptTimestamp = DateTime.now().millisecondsSinceEpoch;
        _channel = WebSocketChannel.connect(Uri.parse(fullUrl));

        _messageSubscription = _channel!.stream.listen(
          (data) {
            _handleMessage(data);
          },
          onError: (error) {
            debugPrint('Sockets onError WebSocket error: $error');

            _isSocketConnected = false;
            _cleanupTimer?.cancel();
            notifyListeners();
            _tryReconnect();
          },
          onDone: () async {
            debugPrint('Sockets onDone WebSocket connection closed');
            _isSocketConnected = false;
            _cleanupTimer?.cancel();
            notifyListeners();
            _tryReconnect();
          },
        );

        // Wait for connection to be established
        await Future.delayed(const Duration(milliseconds: 500));

        _reconnectAttempts = 0;
        _reconnectTimer?.cancel();
        _isSocketConnected = true;
        notifyListeners();

        // Start cleanup timer for old pending messages
        _cleanupTimer?.cancel();
        _cleanupTimer = Timer.periodic(const Duration(seconds: 30), (timer) {
          _cleanupOldPendingMessages();
        });

        final pingResponse = await _checkConnectionPing();

        if (pingResponse) {
          final result = await _sendInitialDriverBeat();
          setIsConnectionEstablished(true);
          completer.complete(result);
        } else {
          completer.complete({
            'pData': {'status': 0, 'reasonText': 'initConnection error'},
          });
        }

        return completer.future;
      } catch (e) {
        debugPrint('Sockets WebSocketChannel connection failed: $e');
        _isSocketConnected = false;
        return {
          'status': 0,
          'reasonText': 'WebSocketChannel connection error: $e',
        };
      }
    } catch (e) {
      debugPrint('Sockets connection failed: $e');
      _isSocketConnected = false;
      return {'status': 0, 'reasonText': 'initConnection error: $e'};
    }
  }

  Future<bool> _checkConnectionPing() async {
    await sendMessage(pName: 'Ping', pData: {}, timeout: 4);
    return true;
  }

  Future<Map<String, dynamic>> _sendInitialDriverBeat() async {
    // Send initial DRIVER BEAT
    final beatResponse = await sendDriverBeat();

    if (beatResponse['pData']['status'] == 1) {
      return beatResponse['pData'];
    } else {
      return {
        'pData': {'status': 0, 'reasonText': 'initConnection error'},
      };
    }
  }

  void setSetOrderStatusLoading(bool value) {
    setOrderStatusLoading = value;
    notifyListeners();
  }

  void _handleMessage(dynamic data) {
    lastMessageTimestamp = DateTime.now().millisecondsSinceEpoch;
    setIsConnectionEstablished(true);
    try {
      // Validate input data
      if (data == null) {
        debugPrint('Sockets handleMessage: Received null data');
        return;
      }

      String dataString = data.toString().trim();
      if (dataString.isEmpty) {
        debugPrint('Sockets handleMessage: Received empty data');
        return;
      }

      // Try to decode JSON with better error handling
      Map<String, dynamic> message;
      try {
        message = jsonDecode(dataString);
      } catch (jsonError) {
        debugPrint('Sockets handleMessage: Invalid JSON data: $dataString');
        debugPrint('Sockets handleMessage: JSON decode error: $jsonError');
        return;
      }

      // Validate message structure
      if (message.isEmpty) {
        debugPrint('Sockets handleMessage: Empty message object');
        return;
      }

      // Handle server-initiated messages (pType == 1)
      if (message.containsKey('pType') && message['pType'] == 1) {
        String? pName = message['pName']?.toString();
        String? pId = message['pId']?.toString();

        if (pName != null && pName.isNotEmpty) {
          // Call bound message function if exists
          if (_boundMessages[pName] != null) {
            try {
              _boundMessages[pName]!.call(message);
            } catch (e, stackTrace) {
              debugPrint(
                'Sockets bound message function error for $pName $e $stackTrace',
              );
            }
          }

          // Send acknowledgment message
          try {
            _sendAcknowledgment(pName, pId);
          } catch (e, stackTrace) {
            debugPrint(
              'Sockets acknowledgment error for $pName: $e $stackTrace',
            );
          }
        } else {
          debugPrint(
            'Sockets handleMessage: Missing or empty pName for server message',
          );
        }
        return;
      }

      // Check if this is a response to a pending message
      String? pName = message['pName']?.toString();
      dynamic pId = message['pId'];

      if (pName != null && pName.isNotEmpty && pId != null) {
        // Ensure pId is converted to string safely
        String pIdString = pId.toString();
        String responseKey = '$pName-$pIdString';

        _cancelPendingMessageTimeout(responseKey);

        if (_pendingMessages.containsKey(responseKey)) {
          _completeAndRemovePendingMessage(responseKey, message);
          return;
        }

        // If we have a pType: 2 message but no matching pending message,
        // try to find a pending message by name only (in case pId doesn't match)
        if (message.containsKey('pType') && message['pType'] == 2) {
          // Look for any pending message with the same name
          String? matchingKey;
          for (String key in _pendingMessages.keys) {
            if (key.startsWith('$pName-')) {
              matchingKey = key;
              break;
            }
          }

          if (matchingKey != null) {
            if (message['pData']['status'] == 0) {
              final reason =
                  message['pData']['reason'] != null &&
                          message['pData']['reason'].isNotEmpty
                      ? message['pData']['reason']
                      : null;
              final reasonText =
                  message['pData']['reasonText'] != null &&
                          message['pData']['reasonText'].isNotEmpty
                      ? message['pData']['reasonText']
                      : null;

              if (reason == null) {
                message['pData']['reason'] =
                    reasonText ?? 'Something went wrong. Try again please.';
              }
              if (reasonText == null) {
                message['pData']['reasonText'] =
                    reason ?? 'Something went wrong. Try again please.';
              }
            }

            _completeAndRemovePendingMessage(matchingKey, message);
            return;
          }
        }
      }

      // Check if this message has a bound handler (even if it's a response)
      if (pName != null &&
          pName.isNotEmpty &&
          _boundMessages.containsKey(pName)) {
        try {
          _boundMessages[pName]!.call(message);
        } catch (e, stackTrace) {
          debugPrint(
            'Sockets bound message function error for $pName: $e $stackTrace',
          );
        }
      }
    } catch (e, stackTrace) {
      debugPrint('Sockets handleMessage error: $e');
      debugPrint('Sockets handleMessage stack trace: $stackTrace');
      debugPrint('Sockets handleMessage raw data: $data');
    }
  }

  Future<Map<String, dynamic>> sendMessage({
    Map<String, dynamic> pData = const {},
    String pName = '',
    int? pType,
    String? pId,
    int timeout = 6,
  }) {
    Completer<Map<String, dynamic>> completer =
        Completer<Map<String, dynamic>>();

    // Validate input parameters
    if (pName.isEmpty) {
      completer.complete({'error': 'Message name cannot be empty'});
      return completer.future;
    }

    if (_channel != null && _isSocketConnected) {
      Map<String, dynamic> payload = buildDefaultPayloadData(
        pName,
        pData,
        pType: pType,
        pId: pId,
      );
      pId ??= createPid();
      payload['pId'] = pId;
      String messageKey = '$pName-$pId';

      // Track socket message sent
      // final trackingService = NetworkTrackingService();
      // trackingService.trackSocketMessageSent(
      //   messageName: pName,
      //   messageData: payload,
      // );

      // Store the completer for this message
      _pendingMessages[messageKey] = completer;
      _setPendingMessageTimeout(messageKey, timeout);

      String jsonMessage;
      try {
        jsonMessage = jsonEncode(payload);
      } catch (e) {
        debugPrint('Sockets sendMessage jsonEncode error: $e');

        // Track socket error
        // trackingService.trackSocketError(
        //   messageName: pName,
        //   error: 'JSON encode error: $e',
        // );

        _completeAndRemovePendingMessage(messageKey, {
          'pData': {'status': 0, 'reasonText': 'Failed to encode message: $e'},
        });
        return completer.future;
      }

      try {
        _channel!.sink.add(jsonMessage);
      } catch (e) {
        debugPrint('Sockets sendMessage error: $e');

        // Track socket error
        // trackingService.trackSocketError(
        //   messageName: pName,
        //   error: 'Send error: $e',
        // );

        _isSocketConnected = false;
        _completeAndRemovePendingMessage(messageKey, {
          'pData': {'status': 0, 'reasonText': 'Failed to send message: $e'},
        });
      }
    } else {
      completer.complete({'error': 'Not connected to server'});
    }

    return completer.future;
  }

  void bindMessageToFunction(
    String pName,
    Function(Map<String, dynamic>) function,
  ) {
    try {
      _boundMessages[pName] = function;
    } catch (e) {
      debugPrint('Sockets bindMessageToFunction error: $e');
    }
  }

  void _setPendingMessageTimeout(String messageKey, int timeout) {
    try {
      _pendingMessagesTimeout[messageKey] = Timer(
        Duration(seconds: timeout),
        () {
          // Track socket timeout error
          // final trackingService = NetworkTrackingService();
          // final parts = messageKey.split('-');
          // if (parts.isNotEmpty) {
          //   trackingService.trackSocketError(
          //     messageName: parts[0],
          //     error: 'Message timeout',
          //   );
          // }

          final connectionService = ConnectionService();
          final isConnected =
              connectionService.currentStatus?.isConnected ?? false;

          final context = navigatorKey.currentContext;
          final noInternetText = context?.translate('noInternetError');
          final noConnectionText = context?.translate('noConnectionError');

          _completeAndRemovePendingMessage(messageKey, {
            'pData': {
              'status': 0,
              'reasonText':
                  isConnected == false ? noInternetText : noConnectionText,
            },
          });
        },
      );
    } catch (e) {
      debugPrint('Sockets _setPendingMessageTimeout error: $e');
    }
  }

  void _cancelPendingMessageTimeout(String messageKey) {
    try {
      if (_pendingMessagesTimeout.containsKey(messageKey)) {
        _pendingMessagesTimeout[messageKey]!.cancel();
        _pendingMessagesTimeout.remove(messageKey);
      }
    } catch (e) {
      debugPrint('Sockets _cancelPendingMessageTimeout error: $e');
    }
  }

  void _completeAndRemovePendingMessage(
    String messageKey,
    Map<String, dynamic> response,
  ) {
    try {
      Completer<Map<String, dynamic>>? completer = _pendingMessages[messageKey];
      if (completer != null && !completer.isCompleted) {
        completer.complete(response);
        _pendingMessages.remove(messageKey);
        _cancelPendingMessageTimeout(messageKey);
      }
    } catch (e) {
      debugPrint('Sockets _completeAndRemovePendingMessage error: $e');
    }
  }

  Map<String, dynamic> buildDefaultPayloadData(
    String pName,
    Map<String, dynamic> pData, {
    int? pType,
    String? pId,
  }) {
    try {
      pType ??= 1;
      pId ??= createPid();

      return {'pId': pId, 'pName': pName, 'pType': pType, 'pData': pData};
    } catch (e) {
      debugPrint('Sockets buildDefaultPayloadData error: $e');
      // Fallback to a simple timestamp-based ID if createPid fails
      final fallbackId = DateTime.now().millisecondsSinceEpoch.toString();
      return {
        'pId': fallbackId,
        'pName': pName,
        'pType': pType ?? 1,
        'pData': pData,
      };
    }
  }

  Future<void> disconnect() async {
    Completer<void> completer = Completer<void>();
    // _manuallyDisconnected = true;
    _reconnectTimer?.cancel();
    _cleanupTimer?.cancel();
    _messageSubscription?.cancel();
    setIsConnectionEstablished(false);

    if (_channel == null) {
      Future.delayed(const Duration(seconds: 1), () async {
        completer.complete();
      });
    } else {
      _isSocketConnected = false;
      try {
        await _channel!.sink.close();
        _channel = null;
      } catch (e) {
        debugPrint('Sockets disconnect error: $e');
      }

      // Complete any remaining pending messages with empty response
      _clearPendingMessages();

      Future.delayed(const Duration(seconds: 1), () async {
        completer.complete();
        notifyListeners();
      });
    }

    return completer.future;
  }

  void _clearPendingMessages() {
    try {
      List<String> keysToRemove = _pendingMessages.keys.toList();
      for (final messageKey in keysToRemove) {
        // Track socket error for connection closed
        // final trackingService = NetworkTrackingService();
        // final parts = messageKey.split('-');
        // if (parts.isNotEmpty) {
        //   trackingService.trackSocketError(
        //     messageName: parts[0],
        //     error: 'Connection closed',
        //   );
        // }

        final connectionService = ConnectionService();
        final isConnected =
            connectionService.currentStatus?.isConnected ?? false;
        final context = navigatorKey.currentContext;
        final noInternetText = context?.translate('noInternetError');
        final noConnectionText = context?.translate('noConnectionError');
        final reasonText =
            isConnected == false ? noInternetText : noConnectionText;

        _completeAndRemovePendingMessage(messageKey, {
          'pData': {'status': 0, 'reasonText': reasonText},
        });
      }
    } catch (e) {
      debugPrint('Sockets _clearPendingMessages error: $e');
    }
  }

  void _cleanupOldPendingMessages() {
    try {
      // Clean up pending messages that are older than 30 seconds
      final now = DateTime.now().millisecondsSinceEpoch;
      List<String> keysToRemove = [];

      for (final messageKey in _pendingMessages.keys) {
        try {
          // Extract timestamp from message key (format: pName-timestamp-counter)
          final parts = messageKey.split('-');
          if (parts.length >= 2) {
            final timestamp = int.tryParse(parts[1]);
            if (timestamp != null && (now - timestamp) > 30000) {
              // 30 seconds
              keysToRemove.add(messageKey);
            }
          }
        } catch (e) {
          debugPrint(
            'Sockets _cleanupOldPendingMessages parse error for key $messageKey: $e',
          );
        }
      }

      for (final messageKey in keysToRemove) {
        // Track socket error for old message cleanup
        // final trackingService = NetworkTrackingService();
        // final parts = messageKey.split('-');
        // if (parts.isNotEmpty) {
        //   trackingService.trackSocketError(
        //     messageName: parts[0],
        //     error: 'Message timeout - cleaned up',
        //   );
        // }

        _completeAndRemovePendingMessage(messageKey, {
          'error': 'Message timeout - cleaned up',
        });
      }
    } catch (e) {
      debugPrint('Sockets _cleanupOldPendingMessages error: $e');
    }
  }

  Future<bool> _canReconnect() async {
    try {
      final apiService = ApiService();
      final result = await apiService.request(
        'PingWithAuth',
        data: {},
        showError: false,
      );

      if (result.data != null &&
          result.data['status'] == 0 &&
          result.data['reason'] == 'noAuth') {
        return false;
      }

      return true;
    } catch (e) {
      return false;
    }
  }

  void _tryReconnect() async {
    if (_isSocketConnected) return;

    final reconnectAllowed = await _canReconnect();
    if (!reconnectAllowed) {
      onNoAuth?.call();
      return;
    }

    _reconnectTimer?.cancel();
    // _reconnectAttempts++;
    _reconnectTimer = Timer(Duration(seconds: _reconnectDelay), () async {
      try {
        final connResult = await initConnection();
        if (connResult['status'] == 1) {
          _reconnectAttempts = 0;
          onConnectionRestoredCallback?.call();
        } else {
          // if (_reconnectAttempts == 2) {
          final connectionService = ConnectionService();
          final isConnected =
              connectionService.currentStatus?.isConnected ?? false;
          onConnectionErrorCallback?.call(isConnected == true ? '20' : '10');
          // }
        }
      } catch (e) {
        debugPrint('Sockets _tryReconnect error: $e');
        if (_reconnectAttempts == 2) {
          final connectionService = ConnectionService();
          final isConnected =
              connectionService.currentStatus?.isConnected ?? false;
          onConnectionErrorCallback?.call(isConnected == true ? '20' : '10');
        }
      }
    });
  }

  // Public method to allow external services to trigger reconnect attempts
  void triggerReconnect() {
    _tryReconnect();
  }

  // Helper method to send acknowledgment without creating pending messages
  void _sendAcknowledgment(String pName, String? pId) {
    if (_channel != null && _isSocketConnected) {
      Map<String, dynamic> payload = {
        'pId': pId ?? createPid(),
        'pName': pName,
        'pType': 2,
        'pData': {'status': 1, 'reason': ''},
      };

      String jsonMessage;
      try {
        jsonMessage = jsonEncode(payload);
      } catch (e) {
        debugPrint('Sockets _sendAcknowledgment jsonEncode error: $e');
        return;
      }

      try {
        _channel!.sink.add(jsonMessage);
      } catch (e) {
        debugPrint('Sockets acknowledgment send error: $e');
        _isSocketConnected = false;
      }
    } else {
      debugPrint('Sockets sendAcknowledgment DEBUG: Cannot send ACK');
    }
  }
}

Thanks

Sockets are really not good for this kind of scenario. MQTT would be a better choice.

For me, mobile apps should ALWAYS work offline (if not offline R/W, at least R/O), so interruptions are not really that important (i.e.: basically, the network stack only updates a local stack, that interfaces with the UI, so, a lack of signal only means stale data… just like checking a Uber location that is going into a tunnel).


MQTT’s Advantages for Bad Networks

  • Persistent Sessions: MQTT supports persistent sessions, which allow clients to reconnect to a broker and resume their session without resending all connection information. This is crucial for maintaining state and message delivery in environments where connections are frequently interrupted. 1
  • Keep Alive Mechanism: MQTT includes a “Keep Alive” feature that helps detect and mitigate issues with half-open connections. This mechanism ensures that the connection remains stable even with intermittent network issues. 2
  • Quality of Service (QoS) Levels: MQTT offers different Quality of Service levels (0, 1, and 2) for message delivery. QoS 1 and 2 provide guaranteed delivery, which is essential when dealing with unreliable networks, ensuring that messages are not lost. 3
  • Designed for Constrained Environments: MQTT was developed with the needs of low-power devices and unreliable networks in mind, making it efficient and resilient. 4
  • Connection Persistence: Both MQTT and WebSockets can support connection persistence, reducing the overhead of establishing new connections for each request. 5

Socket Programming in Bad Networks

While sockets (like TCP sockets) are the foundation for many network communications, they require more manual handling to achieve robustness in bad networks.

  • Buffer Management: In scenarios with bad networks, socket send buffers can fill up, leading to data loss or stalled connections if not managed properly. 6
  • Reconnection Logic: Implementing reliable reconnection logic, error handling, and state management for sockets in a poor network environment can be complex and requires significant custom development.
  • No Built-in Features: Unlike MQTT, raw sockets do not inherently provide features like persistent sessions, keep-alive mechanisms, or defined QoS levels for message delivery. These must be built on top of the socket layer.