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:
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
if (!mounted) return;
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
_showModelDropdown(context, ref, models);
|
_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
|
||||||
|
if (!mounted) return;
|
||||||
|
// ignore: use_build_context_synchronously
|
||||||
_showModelDropdown(context, ref, models);
|
_showModelDropdown(context, ref, models);
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
DebugLogger.error(
|
DebugLogger.error(
|
||||||
'model-refresh-failed',
|
'model-refresh-failed',
|
||||||
|
|||||||
Reference in New Issue
Block a user