refactor: optimize app initialization and startup flow

- Delayed heavy provider initialization to improve initial UI responsiveness.
- Introduced a queuing mechanism for provider initialization with staggered delays.
- Updated app startup flow to ensure proper handling of mounted state during asynchronous operations.
- Enhanced socket service management and background model loading to improve app performance and reliability.
This commit is contained in:
cogwheel0
2025-10-02 21:33:58 +05:30
parent e58a6e533f
commit 5138491cfa
3 changed files with 96 additions and 26 deletions

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -11,6 +12,7 @@ import '../services/navigation_service.dart';
import '../models/conversation.dart'; import '../models/conversation.dart';
import '../services/background_streaming_handler.dart'; import '../services/background_streaming_handler.dart';
import '../services/persistent_streaming_service.dart'; import '../services/persistent_streaming_service.dart';
import '../services/socket_service.dart';
import '../../features/onboarding/views/onboarding_sheet.dart'; import '../../features/onboarding/views/onboarding_sheet.dart';
import '../../shared/theme/theme_extensions.dart'; import '../../shared/theme/theme_extensions.dart';
import '../services/connectivity_service.dart'; import '../services/connectivity_service.dart';
@@ -125,6 +127,7 @@ void _scheduleConversationWarmup(Ref ref, {bool force = false}) {
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
class AppStartupFlow extends _$AppStartupFlow { class AppStartupFlow extends _$AppStartupFlow {
bool _started = false; bool _started = false;
ProviderSubscription<SocketService?>? _socketSubscription;
@override @override
FutureOr<void> build() {} FutureOr<void> build() {}
@@ -133,36 +136,61 @@ class AppStartupFlow extends _$AppStartupFlow {
if (_started) return; if (_started) return;
_started = true; _started = true;
state = const AsyncValue<void>.data(null); state = const AsyncValue<void>.data(null);
_activate(); SchedulerBinding.instance.addPostFrameCallback((_) {
if (!ref.mounted) return;
_activate();
});
} }
void _activate() { void _activate() {
final ref = this.ref; final ref = this.ref;
ref.onDispose(() {
_socketSubscription?.close();
_socketSubscription = null;
});
void keepAlive<T>(ProviderListenable<T> provider) {
ref.listen<T>(provider, (previous, value) {});
}
// Ensure token integration listeners are active // Ensure token integration listeners are active
ref.watch(authApiIntegrationProvider); keepAlive(authApiIntegrationProvider);
ref.watch(apiTokenUpdaterProvider); keepAlive(apiTokenUpdaterProvider);
ref.watch(silentLoginCoordinatorProvider); keepAlive(silentLoginCoordinatorProvider);
// Kick background model loading flow (non-blocking) // Kick background model loading flow (non-blocking)
ref.watch(backgroundModelLoadProvider); Future<void>.delayed(const Duration(milliseconds: 120), () {
if (!ref.mounted) return;
ref.read(backgroundModelLoadProvider);
});
// If authenticated, keep socket service alive and connected // If authenticated, keep socket service alive and connected
final navState = ref.watch(authNavigationStateProvider); final navState = ref.read(authNavigationStateProvider);
if (navState == AuthNavigationState.authenticated) { if (navState == AuthNavigationState.authenticated) {
ref.watch(socketServiceProvider); _ensureSocketAttached();
} }
// Ensure resume-triggered foreground refresh is active // Ensure resume-triggered foreground refresh is active
ref.watch(foregroundRefreshProvider); Future<void>.delayed(const Duration(milliseconds: 48), () {
if (!ref.mounted) return;
keepAlive(foregroundRefreshProvider);
});
// Keep Socket.IO connection alive in background within platform limits // Keep Socket.IO connection alive in background within platform limits
ref.watch(socketPersistenceProvider); Future<void>.delayed(const Duration(milliseconds: 96), () {
ref.watch(socketConnectionStreamProvider); if (!ref.mounted) return;
keepAlive(socketPersistenceProvider);
});
// Ensure persistent streaming uses the shared connectivity service // Ensure persistent streaming uses the shared connectivity service
final connectivityService = ref.watch(connectivityServiceProvider); final connectivityService = ref.read(connectivityServiceProvider);
PersistentStreamingService().attachConnectivityService(connectivityService); Future<void>.delayed(const Duration(milliseconds: 160), () {
if (!ref.mounted) return;
PersistentStreamingService().attachConnectivityService(
connectivityService,
);
});
// Warm the conversations list in the background as soon as possible, // Warm the conversations list in the background as soon as possible,
// but avoid doing so on poor connectivity to reduce startup load. // but avoid doing so on poor connectivity to reduce startup load.
@@ -217,6 +245,8 @@ class AppStartupFlow extends _$AppStartupFlow {
return; return;
} }
_ensureSocketAttached();
// Ensure API has the latest token immediately // Ensure API has the latest token immediately
final authToken = ref.read(authTokenProvider3); final authToken = ref.read(authTokenProvider3);
if (authToken != null && authToken.isNotEmpty) { if (authToken != null && authToken.isNotEmpty) {
@@ -286,6 +316,13 @@ class AppStartupFlow extends _$AppStartupFlow {
} }
}); });
} }
void _ensureSocketAttached() {
_socketSubscription ??= ref.listen<SocketService?>(
socketServiceProvider,
(previous, value) {},
);
}
} }
// Tracks whether we've already attempted a silent login for the current app session. // Tracks whether we've already attempted a silent login for the current app session.

View File

@@ -1012,20 +1012,31 @@ class _ChatPageState extends ConsumerState<ChatPage> {
), ),
) )
.animate() .animate()
.scale(duration: const Duration(milliseconds: 300)) .fadeIn(
duration: const Duration(milliseconds: 220),
curve: Curves.easeOutCubic,
)
.then() .then()
.shimmer(duration: const Duration(milliseconds: 1200)), .shimmer(duration: const Duration(milliseconds: 1200)),
const SizedBox(height: Spacing.xl), const SizedBox(height: Spacing.xl),
Text( AnimatedSwitcher(
l10n.onboardStartTitle(greetingName), duration: const Duration(milliseconds: 200),
style: theme.textTheme.headlineSmall?.copyWith( switchInCurve: Curves.easeOutCubic,
fontWeight: FontWeight.w600, switchOutCurve: Curves.easeInCubic,
color: context.conduitTheme.textPrimary, transitionBuilder: (child, animation) =>
FadeTransition(opacity: animation, child: child),
child: Text(
l10n.onboardStartTitle(greetingName),
key: ValueKey<String>(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)),
], ],
), ),
), ),

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:developer' as developer; import 'dart:developer' as developer;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'core/widgets/error_boundary.dart'; import 'core/widgets/error_boundary.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
@@ -126,18 +127,39 @@ class _ConduitAppState extends ConsumerState<ConduitApp> {
@override @override
void initState() { void initState() {
super.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()); WidgetsBinding.instance.addPostFrameCallback((_) => _initializeAppState());
} }
void _initializeAppState() { void _initializeAppState() {
DebugLogger.auth('init', scope: 'app'); DebugLogger.auth('init', scope: 'app');
ref.read(authStateManagerProvider); void queueInit(void Function() action, {Duration delay = Duration.zero}) {
ref.read(authApiIntegrationProvider); Future<void>.delayed(delay, () {
ref.read(defaultModelAutoSelectionProvider); if (!mounted) return;
ref.read(shareReceiverInitializerProvider); action();
ref.read(appStartupFlowProvider.notifier).start(); });
}
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 @override