realtime controller

This commit is contained in:
Marc Rejohn Castillano 2026-02-28 19:45:21 +08:00
parent 9bad41a5ee
commit c5e859ad88
2 changed files with 123 additions and 1 deletions

View File

@ -0,0 +1,107 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'supabase_provider.dart';
final realtimeControllerProvider = ChangeNotifierProvider<RealtimeController>((
ref,
) {
final client = ref.watch(supabaseClientProvider);
final controller = RealtimeController(client);
ref.onDispose(controller.dispose);
return controller;
});
/// A lightweight controller that attempts to recover the Supabase Realtime
/// connection when the app returns to the foreground or when auth tokens
/// are refreshed.
class RealtimeController extends ChangeNotifier {
RealtimeController(this._client) {
_init();
}
final SupabaseClient _client;
bool isConnecting = false;
bool _disposed = false;
void _init() {
try {
// Listen for auth changes and try to recover the realtime connection
_client.auth.onAuthStateChange.listen((data) {
final event = data.event;
if (event == AuthChangeEvent.tokenRefreshed ||
event == AuthChangeEvent.signedIn) {
recoverConnection();
}
});
} catch (_) {}
}
/// Try to reconnect the realtime client using a small exponential backoff.
Future<void> recoverConnection() async {
if (_disposed) return;
if (isConnecting) return;
isConnecting = true;
notifyListeners();
try {
int attempt = 0;
int maxAttempts = 4;
int delaySeconds = 1;
while (attempt < maxAttempts && !_disposed) {
attempt++;
try {
// Best-effort disconnect then connect so the realtime client picks
// up any refreshed tokens.
try {
// Try to refresh session/token if the SDK supports it. Use dynamic
// to avoid depending on a specific SDK version symbol.
try {
await (_client.auth as dynamic).refreshSession?.call();
} catch (_) {}
// Best-effort disconnect then connect so the realtime client picks
// up any refreshed tokens. The realtime connect/disconnect are
// marked internal by the SDK; suppress the lint here since this
// is a deliberate best-effort recovery.
// ignore: invalid_use_of_internal_member
_client.realtime.disconnect();
} catch (_) {}
await Future.delayed(const Duration(milliseconds: 300));
try {
// ignore: invalid_use_of_internal_member
_client.realtime.connect();
} catch (_) {}
// Give the socket a moment to stabilise.
await Future.delayed(const Duration(seconds: 1));
// Exit early; we don't have a reliable sync API for connection
// state across all platforms, so treat this as a best-effort
// resurrection.
break;
} catch (_) {
await Future.delayed(Duration(seconds: delaySeconds));
delaySeconds = delaySeconds * 2;
}
}
} finally {
if (!_disposed) {
isConnecting = false;
notifyListeners();
}
}
}
@override
void dispose() {
_disposed = true;
super.dispose();
}
}

View File

@ -4,6 +4,7 @@ import 'package:firebase_messaging/firebase_messaging.dart';
import '../models/notification_item.dart';
import '../providers/notifications_provider.dart';
import '../providers/realtime_controller.dart';
/// Wraps the app and installs both a Supabase realtime listener and the
/// FCM handlers described in the frontend design.
@ -24,21 +25,35 @@ class NotificationBridge extends ConsumerStatefulWidget {
ConsumerState<NotificationBridge> createState() => _NotificationBridgeState();
}
class _NotificationBridgeState extends ConsumerState<NotificationBridge> {
class _NotificationBridgeState extends ConsumerState<NotificationBridge>
with WidgetsBindingObserver {
// store previous notifications to diff
List<NotificationItem> _prevList = [];
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_setupFcmHandlers();
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
try {
// Trigger a best-effort realtime reconnection when the app resumes.
ref.read(realtimeControllerProvider).recoverConnection();
} catch (_) {}
}
}
void _showBanner(String type, NotificationItem item) {
final ctx = widget.navigatorKey.currentState?.overlay?.context;
if (ctx == null) return;