refactor: riverpod 3

This commit is contained in:
cogwheel0
2025-09-28 23:18:24 +05:30
parent d2aeafc773
commit 0ba48030c8
12 changed files with 633 additions and 360 deletions

View File

@@ -1,6 +1,9 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../services/storage_service.dart';
// (removed duplicate) import '../services/optimized_storage_service.dart';
@@ -22,6 +25,8 @@ import '../services/optimized_storage_service.dart';
import '../services/socket_service.dart';
import '../utils/debug_logger.dart';
part 'app_providers.g.dart';
// Storage providers
final sharedPreferencesProvider = Provider<SharedPreferences>((ref) {
throw UnimplementedError();
@@ -189,45 +194,171 @@ final apiServiceProvider = Provider<ApiService?>((ref) {
});
// Socket.IO service provider
final socketServiceProvider = Provider<SocketService?>((ref) {
final reviewerMode = ref.watch(reviewerModeProvider);
if (reviewerMode) return null;
@Riverpod(keepAlive: true)
class SocketServiceManager extends _$SocketServiceManager {
SocketService? _service;
ProviderSubscription<String?>? _tokenSubscription;
final activeServer = ref.watch(activeServerProvider);
final token = ref.watch(authTokenProvider3.select((t) => t));
final transportMode = ref.watch(
appSettingsProvider.select((s) => s.socketTransportMode),
);
@override
FutureOr<SocketService?> build() async {
final reviewerMode = ref.watch(reviewerModeProvider);
if (reviewerMode) {
_disposeService();
return null;
}
return activeServer.maybeWhen(
data: (server) {
if (server == null) return null;
final s = SocketService(
final server = await ref.watch(activeServerProvider.future);
if (server == null) {
_disposeService();
return null;
}
final transportMode = ref.watch(
appSettingsProvider.select((settings) => settings.socketTransportMode),
);
final websocketOnly = transportMode == 'ws';
final token = ref.watch(authTokenProvider3);
final requiresNewService =
_service == null ||
_service!.serverConfig.id != server.id ||
_service!.websocketOnly != websocketOnly;
if (requiresNewService) {
_disposeService();
_service = SocketService(
serverConfig: server,
authToken: token,
websocketOnly: transportMode == 'ws',
websocketOnly: websocketOnly,
);
// best-effort connect, but defer to post-frame with a small delay
WidgetsBinding.instance.addPostFrameCallback((_) async {
await Future.delayed(const Duration(milliseconds: 150));
// ignore: discarded_futures
s.connect();
});
// Keep socket token up-to-date without reconstructing the service
ref.listen<String?>(authTokenProvider3, (prev, next) {
s.updateAuthToken(next);
});
ref.onDispose(() {
try {
s.dispose();
} catch (_) {}
});
return s;
},
orElse: () => null,
);
_scheduleConnect(_service!);
} else {
_service!.updateAuthToken(token);
}
_tokenSubscription ??= ref.listen<String?>(authTokenProvider3, (
previous,
next,
) {
_service?.updateAuthToken(next);
});
ref.onDispose(() {
_tokenSubscription?.close();
_tokenSubscription = null;
_disposeService();
});
return _service;
}
void _scheduleConnect(SocketService service) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
await Future.delayed(const Duration(milliseconds: 150));
if (!ref.mounted) return;
try {
unawaited(service.connect());
} catch (_) {}
});
}
void _disposeService() {
if (_service == null) return;
try {
_service!.dispose();
} catch (_) {}
_service = null;
}
}
final socketServiceProvider = Provider<SocketService?>((ref) {
final asyncService = ref.watch(socketServiceManagerProvider);
return asyncService.maybeWhen(data: (service) => service, orElse: () => null);
});
enum SocketConnectionState { disconnected, connecting, connected }
@Riverpod(keepAlive: true)
class SocketConnectionStream extends _$SocketConnectionStream {
StreamController<SocketConnectionState>? _controller;
ProviderSubscription<AsyncValue<SocketService?>>? _serviceSubscription;
void Function()? _cancelConnectListener;
void Function()? _cancelDisconnectListener;
@override
Stream<SocketConnectionState> build() {
final controller = StreamController<SocketConnectionState>.broadcast();
_controller = controller;
void emitState(SocketService? service) {
if (service == null) {
controller.add(SocketConnectionState.disconnected);
_unbindSocket();
return;
}
controller.add(
service.isConnected
? SocketConnectionState.connected
: SocketConnectionState.connecting,
);
_bindSocket(service);
}
emitState(
ref
.watch(socketServiceManagerProvider)
.maybeWhen(data: (service) => service, orElse: () => null),
);
_serviceSubscription = ref.listen<AsyncValue<SocketService?>>(
socketServiceManagerProvider,
(previous, next) {
emitState(
next.maybeWhen(data: (service) => service, orElse: () => null),
);
},
);
ref.onDispose(() {
_serviceSubscription?.close();
_serviceSubscription = null;
_unbindSocket();
_controller?.close();
_controller = null;
});
return controller.stream;
}
void _bindSocket(SocketService service) {
_unbindSocket();
void handleConnect(dynamic _) {
_controller?.add(SocketConnectionState.connected);
}
void handleDisconnect(dynamic _) {
_controller?.add(SocketConnectionState.disconnected);
}
service.socket?.on('connect', handleConnect);
service.socket?.on('disconnect', handleDisconnect);
_cancelConnectListener = () {
service.socket?.off('connect', handleConnect);
};
_cancelDisconnectListener = () {
service.socket?.off('disconnect', handleDisconnect);
};
}
void _unbindSocket() {
_cancelConnectListener?.call();
_cancelDisconnectListener?.call();
_cancelConnectListener = null;
_cancelDisconnectListener = null;
}
}
// Attachment upload queue provider
final attachmentUploadQueueProvider = Provider<AttachmentUploadQueue?>((ref) {
final api = ref.watch(apiServiceProvider);

View File

@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../providers/app_providers.dart';
import '../../features/auth/providers/unified_auth_providers.dart';
@@ -16,6 +17,8 @@ import '../services/connectivity_service.dart';
import '../utils/debug_logger.dart';
import '../models/server_config.dart';
part 'app_startup_providers.g.dart';
enum _ConversationWarmupStatus { idle, warming, complete }
final _conversationWarmupStatusProvider =
@@ -116,149 +119,174 @@ void _scheduleConversationWarmup(Ref ref, {bool force = false}) {
/// App-level startup/background task flow orchestrator.
///
/// Moves background initialization out of widgets and into a Riverpod provider,
/// keeping UI lean and business logic centralized.
final appStartupFlowProvider = Provider<void>((ref) {
// Ensure token integration listeners are active
ref.watch(authApiIntegrationProvider);
ref.watch(apiTokenUpdaterProvider);
ref.watch(silentLoginCoordinatorProvider);
/// Moves background initialization out of widgets and into a Riverpod controller,
/// keeping UI lean and business logic centralized while avoiding side effects
/// during provider build.
@Riverpod(keepAlive: true)
class AppStartupFlow extends _$AppStartupFlow {
bool _started = false;
// Kick background model loading flow (non-blocking)
ref.watch(backgroundModelLoadProvider);
@override
FutureOr<void> build() {}
// If authenticated, keep socket service alive and connected
final navState = ref.watch(authNavigationStateProvider);
if (navState == AuthNavigationState.authenticated) {
ref.watch(socketServiceProvider);
void start() {
if (_started) return;
_started = true;
state = const AsyncValue<void>.data(null);
_activate();
}
// Ensure resume-triggered foreground refresh is active
ref.watch(foregroundRefreshProvider);
void _activate() {
final ref = this.ref;
// Keep Socket.IO connection alive in background within platform limits
ref.watch(socketPersistenceProvider);
// Ensure token integration listeners are active
ref.watch(authApiIntegrationProvider);
ref.watch(apiTokenUpdaterProvider);
ref.watch(silentLoginCoordinatorProvider);
// Ensure persistent streaming uses the shared connectivity service
final connectivityService = ref.watch(connectivityServiceProvider);
PersistentStreamingService().attachConnectivityService(connectivityService);
// Kick background model loading flow (non-blocking)
ref.watch(backgroundModelLoadProvider);
// Warm the conversations list in the background as soon as possible,
// but avoid doing so on poor connectivity to reduce startup load.
// Apply a small randomized delay to smooth load spikes across app wakes.
Future.microtask(() async {
final online = ref.read(isOnlineProvider);
if (!online) return;
// Slightly increase jitter to reduce contention on startup
final jitter = Duration(
milliseconds: 150 + (DateTime.now().millisecond % 200),
);
// Defer until after first frame to keep first paint smooth
WidgetsBinding.instance.addPostFrameCallback((_) async {
await Future.delayed(jitter);
_scheduleConversationWarmup(ref);
});
});
// If authenticated, keep socket service alive and connected
final navState = ref.watch(authNavigationStateProvider);
if (navState == AuthNavigationState.authenticated) {
ref.watch(socketServiceProvider);
}
// One-time, post-frame system UI polish: set status bar icon brightness to
// match theme after the first frame. Avoids flicker at startup.
WidgetsBinding.instance.addPostFrameCallback((_) {
try {
final context = NavigationService.context;
final view = context != null ? View.maybeOf(context) : null;
final dispatcher = WidgetsBinding.instance.platformDispatcher;
final platformBrightness =
view?.platformDispatcher.platformBrightness ??
dispatcher.platformBrightness;
final isDark = platformBrightness == Brightness.dark;
SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle(
statusBarIconBrightness: isDark ? Brightness.light : Brightness.dark,
systemNavigationBarIconBrightness: isDark
? Brightness.light
: Brightness.dark,
),
// Ensure resume-triggered foreground refresh is active
ref.watch(foregroundRefreshProvider);
// Keep Socket.IO connection alive in background within platform limits
ref.watch(socketPersistenceProvider);
ref.watch(socketConnectionStreamProvider);
// Ensure persistent streaming uses the shared connectivity service
final connectivityService = ref.watch(connectivityServiceProvider);
PersistentStreamingService().attachConnectivityService(connectivityService);
// Warm the conversations list in the background as soon as possible,
// but avoid doing so on poor connectivity to reduce startup load.
// Apply a small randomized delay to smooth load spikes across app wakes.
Future.microtask(() async {
final online = ref.read(isOnlineProvider);
if (!online) return;
// Slightly increase jitter to reduce contention on startup
final jitter = Duration(
milliseconds: 150 + (DateTime.now().millisecond % 200),
);
} catch (_) {}
});
// Watch for auth transitions to trigger warmup and other background work
ref.listen<AuthNavigationState>(authNavigationStateProvider, (prev, next) {
if (next == AuthNavigationState.authenticated) {
// Schedule microtask so we don't perform side-effects inside build
Future.microtask(() async {
try {
final api = ref.read(apiServiceProvider);
if (api == null) {
DebugLogger.warning('API service not available for startup flow');
return;
}
// Ensure API has the latest token immediately
final authToken = ref.read(authTokenProvider3);
if (authToken != null && authToken.isNotEmpty) {
api.updateAuthToken(authToken);
DebugLogger.auth('StartupFlow: Applied auth token to API');
}
// Preload default model in background (best-effort) with an adaptive
// delay based on network latency to avoid hammering poor networks.
final latency = ref.read(connectivityServiceProvider).lastLatencyMs;
final delayMs = latency < 0
? 300
: latency > 800
? 600
: 200 + (latency ~/ 2);
Future.delayed(Duration(milliseconds: delayMs), () async {
try {
await ref.read(defaultModelProvider.future);
} catch (e) {
DebugLogger.warning(
'model-preload-failed',
scope: 'startup',
data: {'error': e},
);
}
});
// Kick background chat warmup now that we're authenticated
_scheduleConversationWarmup(ref, force: true);
// Show onboarding once when user reaches chat and hasn't seen it yet
await _maybeShowOnboarding(ref);
} catch (e) {
DebugLogger.error('startup-flow-failed', scope: 'startup', error: e);
}
// Defer until after first frame to keep first paint smooth
WidgetsBinding.instance.addPostFrameCallback((_) async {
await Future.delayed(jitter);
_scheduleConversationWarmup(ref);
});
} else {
// Reset warmup state when leaving authenticated flow
ref
.read(_conversationWarmupStatusProvider.notifier)
.set(_ConversationWarmupStatus.idle);
}
});
});
// Retry warmup when connectivity is restored
ref.listen<bool>(isOnlineProvider, (prev, next) {
if (next == true) {
_scheduleConversationWarmup(ref);
}
});
// One-time, post-frame system UI polish: set status bar icon brightness to
// match theme after the first frame. Avoids flicker at startup.
WidgetsBinding.instance.addPostFrameCallback((_) {
try {
final context = NavigationService.context;
final view = context != null ? View.maybeOf(context) : null;
final dispatcher = WidgetsBinding.instance.platformDispatcher;
final platformBrightness =
view?.platformDispatcher.platformBrightness ??
dispatcher.platformBrightness;
final isDark = platformBrightness == Brightness.dark;
SystemChrome.setSystemUIOverlayStyle(
SystemUiOverlayStyle(
statusBarIconBrightness: isDark
? Brightness.light
: Brightness.dark,
systemNavigationBarIconBrightness: isDark
? Brightness.light
: Brightness.dark,
),
);
} catch (_) {}
});
// When conversations reload (e.g., manual refresh), ensure warmup runs again
ref.listen<AsyncValue<List<Conversation>>>(conversationsProvider, (
previous,
next,
) {
final wasReady = previous?.hasValue == true || previous?.hasError == true;
if (wasReady && next.isLoading) {
ref
.read(_conversationWarmupStatusProvider.notifier)
.set(_ConversationWarmupStatus.idle);
Future.microtask(() => _scheduleConversationWarmup(ref, force: true));
}
});
});
// Watch for auth transitions to trigger warmup and other background work
ref.listen<AuthNavigationState>(authNavigationStateProvider, (prev, next) {
if (next == AuthNavigationState.authenticated) {
// Schedule microtask so we don't perform side-effects inside build
Future.microtask(() async {
try {
final api = ref.read(apiServiceProvider);
if (api == null) {
DebugLogger.warning('API service not available for startup flow');
return;
}
// Ensure API has the latest token immediately
final authToken = ref.read(authTokenProvider3);
if (authToken != null && authToken.isNotEmpty) {
api.updateAuthToken(authToken);
DebugLogger.auth('StartupFlow: Applied auth token to API');
}
// Preload default model in background (best-effort) with an adaptive
// delay based on network latency to avoid hammering poor networks.
final latency = ref.read(connectivityServiceProvider).lastLatencyMs;
final delayMs = latency < 0
? 300
: latency > 800
? 600
: 200 + (latency ~/ 2);
Future.delayed(Duration(milliseconds: delayMs), () async {
try {
await ref.read(defaultModelProvider.future);
} catch (e) {
DebugLogger.warning(
'model-preload-failed',
scope: 'startup',
data: {'error': e},
);
}
});
// Kick background chat warmup now that we're authenticated
_scheduleConversationWarmup(ref, force: true);
// Show onboarding once when user reaches chat and hasn't seen it yet
await _maybeShowOnboarding(ref);
} catch (e) {
DebugLogger.error(
'startup-flow-failed',
scope: 'startup',
error: e,
);
}
});
} else {
// Reset warmup state when leaving authenticated flow
ref
.read(_conversationWarmupStatusProvider.notifier)
.set(_ConversationWarmupStatus.idle);
}
});
// Retry warmup when connectivity is restored
ref.listen<bool>(isOnlineProvider, (prev, next) {
if (next == true) {
_scheduleConversationWarmup(ref);
}
});
// When conversations reload (e.g., manual refresh), ensure warmup runs again
ref.listen<AsyncValue<List<Conversation>>>(conversationsProvider, (
previous,
next,
) {
final wasReady = previous?.hasValue == true || previous?.hasError == true;
if (wasReady && next.isLoading) {
ref
.read(_conversationWarmupStatusProvider.notifier)
.set(_ConversationWarmupStatus.idle);
Future.microtask(() => _scheduleConversationWarmup(ref, force: true));
}
});
}
}
// Tracks whether we've already attempted a silent login for the current app session.
final _silentLoginAttemptedProvider =