diff --git a/lib/core/providers/app_startup_providers.dart b/lib/core/providers/app_startup_providers.dart index a64f852..f12a39c 100644 --- a/lib/core/providers/app_startup_providers.dart +++ b/lib/core/providers/app_startup_providers.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -11,6 +12,7 @@ import '../services/navigation_service.dart'; import '../models/conversation.dart'; import '../services/background_streaming_handler.dart'; import '../services/persistent_streaming_service.dart'; +import '../services/socket_service.dart'; import '../../features/onboarding/views/onboarding_sheet.dart'; import '../../shared/theme/theme_extensions.dart'; import '../services/connectivity_service.dart'; @@ -125,6 +127,7 @@ void _scheduleConversationWarmup(Ref ref, {bool force = false}) { @Riverpod(keepAlive: true) class AppStartupFlow extends _$AppStartupFlow { bool _started = false; + ProviderSubscription? _socketSubscription; @override FutureOr build() {} @@ -133,36 +136,61 @@ class AppStartupFlow extends _$AppStartupFlow { if (_started) return; _started = true; state = const AsyncValue.data(null); - _activate(); + SchedulerBinding.instance.addPostFrameCallback((_) { + if (!ref.mounted) return; + _activate(); + }); } void _activate() { final ref = this.ref; + ref.onDispose(() { + _socketSubscription?.close(); + _socketSubscription = null; + }); + + void keepAlive(ProviderListenable provider) { + ref.listen(provider, (previous, value) {}); + } + // Ensure token integration listeners are active - ref.watch(authApiIntegrationProvider); - ref.watch(apiTokenUpdaterProvider); - ref.watch(silentLoginCoordinatorProvider); + keepAlive(authApiIntegrationProvider); + keepAlive(apiTokenUpdaterProvider); + keepAlive(silentLoginCoordinatorProvider); // Kick background model loading flow (non-blocking) - ref.watch(backgroundModelLoadProvider); + Future.delayed(const Duration(milliseconds: 120), () { + if (!ref.mounted) return; + ref.read(backgroundModelLoadProvider); + }); // If authenticated, keep socket service alive and connected - final navState = ref.watch(authNavigationStateProvider); + final navState = ref.read(authNavigationStateProvider); if (navState == AuthNavigationState.authenticated) { - ref.watch(socketServiceProvider); + _ensureSocketAttached(); } // Ensure resume-triggered foreground refresh is active - ref.watch(foregroundRefreshProvider); + Future.delayed(const Duration(milliseconds: 48), () { + if (!ref.mounted) return; + keepAlive(foregroundRefreshProvider); + }); // Keep Socket.IO connection alive in background within platform limits - ref.watch(socketPersistenceProvider); - ref.watch(socketConnectionStreamProvider); + Future.delayed(const Duration(milliseconds: 96), () { + if (!ref.mounted) return; + keepAlive(socketPersistenceProvider); + }); // Ensure persistent streaming uses the shared connectivity service - final connectivityService = ref.watch(connectivityServiceProvider); - PersistentStreamingService().attachConnectivityService(connectivityService); + final connectivityService = ref.read(connectivityServiceProvider); + Future.delayed(const Duration(milliseconds: 160), () { + if (!ref.mounted) return; + 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. @@ -217,6 +245,8 @@ class AppStartupFlow extends _$AppStartupFlow { return; } + _ensureSocketAttached(); + // Ensure API has the latest token immediately final authToken = ref.read(authTokenProvider3); if (authToken != null && authToken.isNotEmpty) { @@ -286,6 +316,13 @@ class AppStartupFlow extends _$AppStartupFlow { } }); } + + void _ensureSocketAttached() { + _socketSubscription ??= ref.listen( + socketServiceProvider, + (previous, value) {}, + ); + } } // Tracks whether we've already attempted a silent login for the current app session. diff --git a/lib/features/chat/views/chat_page.dart b/lib/features/chat/views/chat_page.dart index eb3d26a..a6ce323 100644 --- a/lib/features/chat/views/chat_page.dart +++ b/lib/features/chat/views/chat_page.dart @@ -1012,20 +1012,31 @@ class _ChatPageState extends ConsumerState { ), ) .animate() - .scale(duration: const Duration(milliseconds: 300)) + .fadeIn( + duration: const Duration(milliseconds: 220), + curve: Curves.easeOutCubic, + ) .then() .shimmer(duration: const Duration(milliseconds: 1200)), const SizedBox(height: Spacing.xl), - Text( - l10n.onboardStartTitle(greetingName), - style: theme.textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.w600, - color: context.conduitTheme.textPrimary, + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + switchInCurve: Curves.easeOutCubic, + switchOutCurve: Curves.easeInCubic, + transitionBuilder: (child, animation) => + FadeTransition(opacity: animation, child: child), + child: Text( + l10n.onboardStartTitle(greetingName), + key: ValueKey(greetingName), + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w600, + color: context.conduitTheme.textPrimary, + ), + textAlign: TextAlign.center, ), - textAlign: TextAlign.center, - ).animate().fadeIn(delay: const Duration(milliseconds: 150)), + ), ], ), ), diff --git a/lib/main.dart b/lib/main.dart index a5c43c1..b249b74 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:developer' as developer; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'core/widgets/error_boundary.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; @@ -126,18 +127,39 @@ class _ConduitAppState extends ConsumerState { @override void initState() { super.initState(); - // Defer heavy provider initialization to after first frame to render UI sooner + // Delay heavy provider initialization until after the first frame so the + // initial paint stays responsive. WidgetsBinding.instance.addPostFrameCallback((_) => _initializeAppState()); } void _initializeAppState() { DebugLogger.auth('init', scope: 'app'); - ref.read(authStateManagerProvider); - ref.read(authApiIntegrationProvider); - ref.read(defaultModelAutoSelectionProvider); - ref.read(shareReceiverInitializerProvider); - ref.read(appStartupFlowProvider.notifier).start(); + void queueInit(void Function() action, {Duration delay = Duration.zero}) { + Future.delayed(delay, () { + if (!mounted) return; + action(); + }); + } + + queueInit(() => ref.read(authStateManagerProvider)); + queueInit( + () => ref.read(authApiIntegrationProvider), + delay: const Duration(milliseconds: 16), + ); + queueInit( + () => ref.read(defaultModelAutoSelectionProvider), + delay: const Duration(milliseconds: 24), + ); + queueInit( + () => ref.read(shareReceiverInitializerProvider), + delay: const Duration(milliseconds: 32), + ); + + SchedulerBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + ref.read(appStartupFlowProvider.notifier).start(); + }); } @override