feat: add connectivity check for multiplayer training and retry logic for sync

This commit is contained in:
Patryk Hegenberg 2026-01-21 09:38:02 +01:00
parent 1e573904f2
commit 2e6052e69d
3 changed files with 267 additions and 200 deletions

View file

@ -0,0 +1,12 @@
import 'dart:io';
class ConnectivityHelper {
static Future<bool> hasConnection() async {
try {
final result = await InternetAddress.lookup('8.8.8.8');
return result.isNotEmpty && result[0].rawAddress.isNotEmpty;
} on SocketException catch (_) {
return false;
}
}
}

View file

@ -25,6 +25,7 @@ import '../widgets/level_display.dart';
import '../widgets/start_raid_button.dart';
import '../../../gamification/application/quest_service.dart';
import '../../../workout_runner/application/workout_generator_service.dart';
import '../../../../core/utils/connectivity_helper.dart';
class HubScreen extends ConsumerStatefulWidget {
const HubScreen({super.key});
@ -51,6 +52,26 @@ class _HubScreenState extends ConsumerState<HubScreen> {
});
}
Future<void> _handleMultiplayerPress() async {
final hasInternet = await ConnectivityHelper.hasConnection();
if (!hasInternet) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Für Multiplayer wird eine aktive Internetverbindung benötigt.'), //TODO: make this in l10n
backgroundColor: AppTheme.errorColor,
behavior: SnackBarBehavior.floating,
),
);
}
return;
}
if (mounted) _showMultiplayerDialog();
}
Future<void> _startNextWorkout(
CycleCollection cycle, UserCollection user) async {
try {
@ -422,7 +443,7 @@ class _HubScreenState extends ConsumerState<HubScreen> {
icon: const Icon(Icons.groups,
color: AppTheme.secondaryColor),
tooltip: 'Multiplayer Lobby',
onPressed: _showMultiplayerDialog,
onPressed: _handleMultiplayerPress,
),
],
),

View file

@ -1,10 +1,14 @@
import 'dart:async';
import 'dart:developer';
import 'dart:io';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:drift/drift.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import '../../../../main.dart';
import '../../../core/constants/app_constants.dart';
import '../../../core/utils/connectivity_helper.dart';
import '../local/app_database.dart';
import '../repositories/user_repository.dart';
import 'api_client.dart';
@ -25,9 +29,25 @@ class SyncService {
Future<void> sync() async {
if (_isSyncing) return;
if (!await ConnectivityHelper.hasConnection()) {
log('Sync skipped: No internet connection');
return;
}
_isSyncing = true;
try {
await _retry(_performSync, maxAttempts: 3);
log('✅ Sync completed successfully');
} catch (e) {
log('❌ Sync failed finally: $e');
} finally {
_isSyncing = false;
}
}
Future<void> _performSync() async {
log('🔄 Starting Sync...');
final dirtyCycles = await (db.select(db.cycles)
@ -35,7 +55,6 @@ class SyncService {
.get();
for (var cycle in dirtyCycles) {
try {
if (cycle.serverId == null) {
log('📤 Pushing new cycle ${cycle.cycleNumber}...');
final tmsMap = cycle.trainingMaxes
@ -67,9 +86,6 @@ class SyncService {
await (db.update(db.cycles)..where((c) => c.id.equals(cycle.id)))
.write(const CyclesCompanion(isDirty: Value(false)));
}
} catch (e) {
log('❌ Failed to sync cycle ${cycle.id}: $e');
}
}
final dirtyUser = await (db.select(db.users)
@ -108,9 +124,9 @@ class SyncService {
final lastSync = await _storage.read(key: AppConstants.keyLastSync);
if ((pushData['workouts'] as List).isNotEmpty ||
pushData['user_stats'] != null) {
log('📤 Pushing data...');
// if ((pushData['workouts'] as List).isNotEmpty || pushData['user_stats'] != null) { ... }
log('Checking for updates...');
final response = await apiClient.sync(
lastSyncTimestamp: lastSync ?? '',
pushData: pushData,
@ -220,16 +236,34 @@ class SyncService {
if (response['server_timestamp'] != null) {
await _storage.write(
key: AppConstants.keyLastSync,
value: response['server_timestamp']);
key: AppConstants.keyLastSync, value: response['server_timestamp']);
}
}
log('✅ Sync completed successfully');
} catch (e, stack) {
log('❌ Sync failed: $e');
log(stack.toString());
} finally {
_isSyncing = false;
Future<T> _retry<T>(
Future<T> Function() function, {
int maxAttempts = 3,
Duration initialDelay = const Duration(seconds: 2),
}) async {
int attempts = 0;
while (true) {
try {
attempts++;
return await function();
} catch (e) {
final shouldRetry = (e is SocketException) ||
(e.toString().contains('500')) ||
(e.toString().contains('502')) ||
(e.toString().contains('timeout'));
if (attempts >= maxAttempts || !shouldRetry) {
rethrow;
}
final delay = initialDelay * (1 << (attempts - 1)); // 2s, 4s, 8s
log('⚠️ Sync attempt $attempts failed. Retrying in ${delay.inSeconds}s... Error: $e');
await Future.delayed(delay);
}
}
}
}