refactor: optimize startup
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
@@ -207,9 +208,12 @@ final socketServiceProvider = Provider<SocketService?>((ref) {
|
|||||||
authToken: token,
|
authToken: token,
|
||||||
websocketOnly: transportMode == 'ws',
|
websocketOnly: transportMode == 'ws',
|
||||||
);
|
);
|
||||||
// best-effort connect; errors handled internally
|
// best-effort connect, but defer to post-frame with a small delay
|
||||||
// ignore unawaited_futures
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
s.connect();
|
await Future.delayed(const Duration(milliseconds: 150));
|
||||||
|
// ignore: discarded_futures
|
||||||
|
s.connect();
|
||||||
|
});
|
||||||
// Keep socket token up-to-date without reconstructing the service
|
// Keep socket token up-to-date without reconstructing the service
|
||||||
ref.listen<String?>(authTokenProvider3, (prev, next) {
|
ref.listen<String?>(authTokenProvider3, (prev, next) {
|
||||||
s.updateAuthToken(next);
|
s.updateAuthToken(next);
|
||||||
@@ -881,24 +885,28 @@ final backgroundModelLoadProvider = Provider<void>((ref) {
|
|||||||
// Ensure API token updater is initialized
|
// Ensure API token updater is initialized
|
||||||
ref.watch(apiTokenUpdaterProvider);
|
ref.watch(apiTokenUpdaterProvider);
|
||||||
|
|
||||||
// Schedule background loading without blocking
|
// Only run when authenticated, and defer until after first frame
|
||||||
Future.microtask(() async {
|
final navState = ref.read(authNavigationStateProvider);
|
||||||
// Wait a bit to ensure auth is complete
|
if (navState != AuthNavigationState.authenticated) {
|
||||||
await Future.delayed(const Duration(milliseconds: 200));
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
DebugLogger.log('bg-start', scope: 'models/background');
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
// Schedule background loading without blocking startup frame
|
||||||
|
Future.microtask(() async {
|
||||||
|
// Small delay to allow initial build/layout to settle
|
||||||
|
await Future.delayed(const Duration(milliseconds: 250));
|
||||||
|
|
||||||
// Load default model in background
|
DebugLogger.log('bg-start', scope: 'models/background');
|
||||||
try {
|
try {
|
||||||
await ref.read(defaultModelProvider.future);
|
await ref.read(defaultModelProvider.future);
|
||||||
DebugLogger.log('bg-complete', scope: 'models/background');
|
DebugLogger.log('bg-complete', scope: 'models/background');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Ignore errors in background loading
|
DebugLogger.error('bg-failed', scope: 'models/background', error: e);
|
||||||
DebugLogger.error('bg-failed', scope: 'models/background', error: e);
|
}
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Return immediately, don't block the UI
|
|
||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -149,11 +149,15 @@ final appStartupFlowProvider = Provider<void>((ref) {
|
|||||||
Future.microtask(() async {
|
Future.microtask(() async {
|
||||||
final online = ref.read(isOnlineProvider);
|
final online = ref.read(isOnlineProvider);
|
||||||
if (!online) return;
|
if (!online) return;
|
||||||
|
// Slightly increase jitter to reduce contention on startup
|
||||||
final jitter = Duration(
|
final jitter = Duration(
|
||||||
milliseconds: 50 + (DateTime.now().millisecond % 100),
|
milliseconds: 150 + (DateTime.now().millisecond % 200),
|
||||||
);
|
);
|
||||||
await Future.delayed(jitter);
|
// Defer until after first frame to keep first paint smooth
|
||||||
_scheduleConversationWarmup(ref);
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
|
await Future.delayed(jitter);
|
||||||
|
_scheduleConversationWarmup(ref);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// One-time, post-frame system UI polish: set status bar icon brightness to
|
// One-time, post-frame system UI polish: set status bar icon brightness to
|
||||||
|
|||||||
@@ -33,13 +33,14 @@ class ConnectivityService {
|
|||||||
bool get isCurrentlyConnected => _lastStatus == ConnectivityStatus.online;
|
bool get isCurrentlyConnected => _lastStatus == ConnectivityStatus.online;
|
||||||
|
|
||||||
void _startConnectivityMonitoring() {
|
void _startConnectivityMonitoring() {
|
||||||
// Initial check after a slightly longer delay to avoid competing with
|
// Initial check after a brief delay to avoid showing offline during startup
|
||||||
// first-frame work. Start periodic checks only after the first probe.
|
Timer(const Duration(milliseconds: 800), () {
|
||||||
Timer(const Duration(milliseconds: 1200), () async {
|
_checkConnectivity();
|
||||||
await _checkConnectivity();
|
});
|
||||||
_connectivityTimer = Timer.periodic(_interval, (_) {
|
|
||||||
_checkConnectivity();
|
// Check periodically; interval adapts to recent failures
|
||||||
});
|
_connectivityTimer = Timer.periodic(_interval, (_) {
|
||||||
|
_checkConnectivity();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:share_handler/share_handler.dart' as sh;
|
import 'package:share_handler/share_handler.dart' as sh;
|
||||||
|
|
||||||
@@ -72,45 +73,49 @@ final shareReceiverInitializerProvider = Provider<void>((ref) {
|
|||||||
() => maybeProcessPending(),
|
() => maybeProcessPending(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Hook into share_handler
|
// Hook into share_handler after a short defer to avoid startup contention
|
||||||
final handler = sh.ShareHandler.instance;
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 300));
|
||||||
|
|
||||||
// Handle initial share when app is cold-started via Share
|
final handler = sh.ShareHandler.instance;
|
||||||
Future.microtask(() async {
|
|
||||||
try {
|
// Handle initial share when app is cold-started via Share
|
||||||
final dynamic media = await handler.getInitialSharedMedia();
|
Future.microtask(() async {
|
||||||
final payload = _toPayload(media);
|
try {
|
||||||
if (payload.hasAnything) {
|
final dynamic media = await handler.getInitialSharedMedia();
|
||||||
ref.read(pendingSharedPayloadProvider.notifier).set(payload);
|
final payload = _toPayload(media);
|
||||||
maybeProcessPending();
|
if (payload.hasAnything) {
|
||||||
|
ref.read(pendingSharedPayloadProvider.notifier).set(payload);
|
||||||
|
maybeProcessPending();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
DebugLogger.log(
|
||||||
|
'ShareReceiver: failed to get initial shared media: $e',
|
||||||
|
scope: 'share',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
});
|
||||||
DebugLogger.log(
|
|
||||||
'ShareReceiver: failed to get initial shared media: $e',
|
|
||||||
scope: 'share',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle subsequent shares while app is alive
|
// Handle subsequent shares while app is alive
|
||||||
final streamSub = handler.sharedMediaStream.listen((dynamic media) {
|
final streamSub = handler.sharedMediaStream.listen((dynamic media) {
|
||||||
try {
|
try {
|
||||||
final payload = _toPayload(media);
|
final payload = _toPayload(media);
|
||||||
if (payload.hasAnything) {
|
if (payload.hasAnything) {
|
||||||
ref.read(pendingSharedPayloadProvider.notifier).set(payload);
|
ref.read(pendingSharedPayloadProvider.notifier).set(payload);
|
||||||
maybeProcessPending();
|
maybeProcessPending();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
DebugLogger.log(
|
||||||
|
'ShareReceiver: failed to parse shared media: $e',
|
||||||
|
scope: 'share',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
});
|
||||||
DebugLogger.log(
|
|
||||||
'ShareReceiver: failed to parse shared media: $e',
|
|
||||||
scope: 'share',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ensure cleanup
|
// Ensure cleanup
|
||||||
ref.onDispose(() async {
|
ref.onDispose(() async {
|
||||||
await streamSub.cancel();
|
await streamSub.cancel();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -55,12 +55,12 @@ void main() {
|
|||||||
_startupTimeline!.start('app_startup');
|
_startupTimeline!.start('app_startup');
|
||||||
_startupTimeline!.instant('bindings_initialized');
|
_startupTimeline!.instant('bindings_initialized');
|
||||||
|
|
||||||
// Enable edge-to-edge globally (back-compat on pre-Android 15)
|
// Defer edge-to-edge mode to post-frame to avoid impacting first paint
|
||||||
// Pairs with Activity's EdgeToEdge.enable and our SafeArea usage.
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
// Do not block first frame on system UI mode; apply shortly after startup
|
// ignore: discarded_futures
|
||||||
// ignore: discarded_futures
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
_startupTimeline?.instant('edge_to_edge_enabled');
|
||||||
_startupTimeline!.instant('edge_to_edge_enabled');
|
});
|
||||||
|
|
||||||
final sharedPrefs = await SharedPreferences.getInstance();
|
final sharedPrefs = await SharedPreferences.getInstance();
|
||||||
_startupTimeline!.instant('shared_prefs_ready');
|
_startupTimeline!.instant('shared_prefs_ready');
|
||||||
|
|||||||
Reference in New Issue
Block a user