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 '../widgets/start_raid_button.dart';
import '../../../gamification/application/quest_service.dart'; import '../../../gamification/application/quest_service.dart';
import '../../../workout_runner/application/workout_generator_service.dart'; import '../../../workout_runner/application/workout_generator_service.dart';
import '../../../../core/utils/connectivity_helper.dart';
class HubScreen extends ConsumerStatefulWidget { class HubScreen extends ConsumerStatefulWidget {
const HubScreen({super.key}); 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( Future<void> _startNextWorkout(
CycleCollection cycle, UserCollection user) async { CycleCollection cycle, UserCollection user) async {
try { try {
@ -422,7 +443,7 @@ class _HubScreenState extends ConsumerState<HubScreen> {
icon: const Icon(Icons.groups, icon: const Icon(Icons.groups,
color: AppTheme.secondaryColor), color: AppTheme.secondaryColor),
tooltip: 'Multiplayer Lobby', tooltip: 'Multiplayer Lobby',
onPressed: _showMultiplayerDialog, onPressed: _handleMultiplayerPress,
), ),
], ],
), ),

View file

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