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