refactor: optimize providers with keepAlive for improved state management

- Updated multiple providers to use `@Riverpod(keepAlive: true)` for better state retention throughout the app lifecycle.
- Enhanced `SocketConnectionStream` and `ConversationDeltaStream` with comments clarifying the purpose of public getters.
- Improved error handling in the `_ChatPageState` by ensuring proper checks for mounted state before using context.
- Added comments to clarify the rationale behind keepAlive usage in various providers, ensuring better maintainability and understanding of the codebase.
This commit is contained in:
cogwheel0
2025-09-30 23:18:06 +05:30
parent 46bd057089
commit 37ebe46e15
4 changed files with 30 additions and 19 deletions

View File

@@ -113,13 +113,13 @@ class AppLocale extends _$AppLocale {
} }
// Server connection providers - optimized with caching // Server connection providers - optimized with caching
@riverpod @Riverpod(keepAlive: true)
Future<List<ServerConfig>> serverConfigs(Ref ref) async { Future<List<ServerConfig>> serverConfigs(Ref ref) async {
final storage = ref.watch(optimizedStorageServiceProvider); final storage = ref.watch(optimizedStorageServiceProvider);
return storage.getServerConfigs(); return storage.getServerConfigs();
} }
@riverpod @Riverpod(keepAlive: true)
Future<ServerConfig?> activeServer(Ref ref) async { Future<ServerConfig?> activeServer(Ref ref) async {
final storage = ref.watch(optimizedStorageServiceProvider); final storage = ref.watch(optimizedStorageServiceProvider);
final configs = await ref.watch(serverConfigsProvider.future); final configs = await ref.watch(serverConfigsProvider.future);
@@ -385,6 +385,8 @@ class SocketConnectionStream extends _$SocketConnectionStream {
} }
/// Forces a best-effort reconnect of the underlying socket service. /// Forces a best-effort reconnect of the underlying socket service.
/// This is an action method, not state exposure.
// ignore: avoid_public_notifier_properties
Future<void> reconnect({ Future<void> reconnect({
Duration timeout = const Duration(seconds: 2), Duration timeout = const Duration(seconds: 2),
}) async { }) async {
@@ -399,9 +401,6 @@ class SocketConnectionStream extends _$SocketConnectionStream {
: SocketConnectionState.connecting, : SocketConnectionState.connecting,
); );
} }
/// Exposes the latest cached state for imperative reads.
SocketConnectionState get latest => _latestState;
} }
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
@@ -495,6 +494,11 @@ class ConversationDeltaStream extends _$ConversationDeltaStream {
_socketSubscription = null; _socketSubscription = null;
} }
/// Provides direct access to the underlying stream.
/// Note: This getter is necessary for compatibility with StreamProvider.
/// While Riverpod 3 discourages public getters on Notifiers, this is a
/// pragmatic exception for stream delegation patterns.
// ignore: avoid_public_notifier_properties
Stream<ConversationDelta> get stream => Stream<ConversationDelta> get stream =>
_controller?.stream ?? const Stream<ConversationDelta>.empty(); _controller?.stream ?? const Stream<ConversationDelta>.empty();
} }
@@ -564,7 +568,7 @@ final refreshAuthStateProvider = Provider<void>((ref) {
}); });
// Model providers // Model providers
@riverpod @Riverpod(keepAlive: true)
Future<List<Model>> models(Ref ref) async { Future<List<Model>> models(Ref ref) async {
// Reviewer mode returns mock models // Reviewer mode returns mock models
final reviewerMode = ref.watch(reviewerModeProvider); final reviewerMode = ref.watch(reviewerModeProvider);
@@ -613,7 +617,7 @@ Future<List<Model>> models(Ref ref) async {
} }
} }
@riverpod @Riverpod(keepAlive: true)
class SelectedModel extends _$SelectedModel { class SelectedModel extends _$SelectedModel {
@override @override
Model? build() => null; Model? build() => null;
@@ -624,7 +628,7 @@ class SelectedModel extends _$SelectedModel {
} }
// Track if the current model selection is manual (user-selected) or automatic (default) // Track if the current model selection is manual (user-selected) or automatic (default)
@riverpod @Riverpod(keepAlive: true)
class IsManualModelSelection extends _$IsManualModelSelection { class IsManualModelSelection extends _$IsManualModelSelection {
@override @override
bool build() => false; bool build() => false;
@@ -633,6 +637,7 @@ class IsManualModelSelection extends _$IsManualModelSelection {
} }
// Listen for settings changes and reset manual selection when default model changes // Listen for settings changes and reset manual selection when default model changes
// keepAlive to maintain listener throughout app lifecycle
final _settingsWatcherProvider = Provider<void>((ref) { final _settingsWatcherProvider = Provider<void>((ref) {
ref.listen<AppSettings>(appSettingsProvider, (previous, next) { ref.listen<AppSettings>(appSettingsProvider, (previous, next) {
if (previous?.defaultModel != next.defaultModel) { if (previous?.defaultModel != next.defaultModel) {
@@ -643,6 +648,7 @@ final _settingsWatcherProvider = Provider<void>((ref) {
}); });
// Auto-apply default model from settings when it changes (and not manually overridden) // Auto-apply default model from settings when it changes (and not manually overridden)
// keepAlive to maintain listener throughout app lifecycle
final defaultModelAutoSelectionProvider = Provider<void>((ref) { final defaultModelAutoSelectionProvider = Provider<void>((ref) {
ref.listen<AppSettings>(appSettingsProvider, (previous, next) { ref.listen<AppSettings>(appSettingsProvider, (previous, next) {
// Only react when default model value changes // Only react when default model value changes
@@ -697,7 +703,7 @@ final defaultModelAutoSelectionProvider = Provider<void>((ref) {
}); });
// Cache timestamp for conversations to prevent rapid re-fetches // Cache timestamp for conversations to prevent rapid re-fetches
@riverpod @Riverpod(keepAlive: true)
class _ConversationsCacheTimestamp extends _$ConversationsCacheTimestamp { class _ConversationsCacheTimestamp extends _$ConversationsCacheTimestamp {
@override @override
DateTime? build() => null; DateTime? build() => null;
@@ -706,7 +712,8 @@ class _ConversationsCacheTimestamp extends _$ConversationsCacheTimestamp {
} }
// Conversation providers - Now using correct OpenWebUI API with caching // Conversation providers - Now using correct OpenWebUI API with caching
@riverpod // keepAlive to maintain cache during authenticated session
@Riverpod(keepAlive: true)
Future<List<Conversation>> conversations(Ref ref) async { Future<List<Conversation>> conversations(Ref ref) async {
// Do not fetch protected data until authenticated. Use watch so we refetch // Do not fetch protected data until authenticated. Use watch so we refetch
// when the auth state transitions in either direction. // when the auth state transitions in either direction.
@@ -1459,7 +1466,7 @@ final archivedConversationsProvider = Provider<List<Conversation>>((ref) {
}); });
// Reviewer mode provider (persisted) // Reviewer mode provider (persisted)
@riverpod @Riverpod(keepAlive: true)
class ReviewerMode extends _$ReviewerMode { class ReviewerMode extends _$ReviewerMode {
late final OptimizedStorageService _storage; late final OptimizedStorageService _storage;
bool _initialized = false; bool _initialized = false;

View File

@@ -407,7 +407,7 @@ bool _listEquals(List<String> a, List<String> b) {
} }
/// Provider for app settings /// Provider for app settings
@riverpod @Riverpod(keepAlive: true)
class AppSettingsNotifier extends _$AppSettingsNotifier { class AppSettingsNotifier extends _$AppSettingsNotifier {
bool _initialized = false; bool _initialized = false;

View File

@@ -60,6 +60,8 @@ final hasSavedCredentialsProvider2 = FutureProvider<bool>((ref) async {
/// Computed providers for UI consumption /// Computed providers for UI consumption
/// These automatically update when auth state changes /// These automatically update when auth state changes
/// These are keepAlive since they derive from keepAlive authStateManagerProvider
/// and are used throughout the app lifecycle
final isAuthenticatedProvider2 = Provider<bool>((ref) { final isAuthenticatedProvider2 = Provider<bool>((ref) {
final authState = ref.watch(authStateManagerProvider); final authState = ref.watch(authStateManagerProvider);

View File

@@ -1167,9 +1167,10 @@ class _ChatPageState extends ConsumerState<ChatPage> {
// If still loading, wait for it to complete // If still loading, wait for it to complete
try { try {
final models = await ref.read(modelsProvider.future); final models = await ref.read(modelsProvider.future);
if (mounted) { // Check mounted and use context immediately together
_showModelDropdown(context, ref, models); if (!mounted) return;
} // ignore: use_build_context_synchronously
_showModelDropdown(context, ref, models);
} catch (e) { } catch (e) {
DebugLogger.error( DebugLogger.error(
'model-load-failed', 'model-load-failed',
@@ -1178,16 +1179,17 @@ class _ChatPageState extends ConsumerState<ChatPage> {
); );
} }
} else if (modelsAsync.hasValue) { } else if (modelsAsync.hasValue) {
// If we have data, show immediately // If we have data, show immediately (no async gap)
_showModelDropdown(context, ref, modelsAsync.value!); _showModelDropdown(context, ref, modelsAsync.value!);
} else if (modelsAsync.hasError) { } else if (modelsAsync.hasError) {
// If there's an error, try to refresh and load // If there's an error, try to refresh and load
try { try {
ref.invalidate(modelsProvider); ref.invalidate(modelsProvider);
final models = await ref.read(modelsProvider.future); final models = await ref.read(modelsProvider.future);
if (mounted) { // Check mounted and use context immediately together
_showModelDropdown(context, ref, models); if (!mounted) return;
} // ignore: use_build_context_synchronously
_showModelDropdown(context, ref, models);
} catch (e) { } catch (e) {
DebugLogger.error( DebugLogger.error(
'model-refresh-failed', 'model-refresh-failed',