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:
@@ -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.
|
||||||
|
|||||||
@@ -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)),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user