From f6a1b6123b7549dfc204a8e5aa096536a815f7d0 Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Tue, 23 Sep 2025 13:43:01 +0530 Subject: [PATCH] refactor: app startup improvements --- AGENTS.md | 804 +++++++++++++++++- lib/core/auth/auth_state_manager.dart | 3 +- lib/core/providers/app_providers.dart | 20 +- lib/core/providers/app_startup_providers.dart | 130 ++- lib/core/router/app_router.dart | 18 +- lib/core/services/connectivity_service.dart | 43 +- lib/core/services/socket_service.dart | 48 +- lib/core/utils/system_ui_style.dart | 17 + .../providers/unified_auth_providers.dart | 25 +- lib/main.dart | 259 +++--- 10 files changed, 1153 insertions(+), 214 deletions(-) create mode 100644 lib/core/utils/system_ui_style.dart diff --git a/AGENTS.md b/AGENTS.md index d5fb083..eb758c0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,26 +1,788 @@ -# Repository Guidelines +# AI rules for Flutter -## Project Structure & Module Organization -- `lib/` hosts Flutter code: `core/` for services, `features/` for screens and flows, `shared/` for reusable UI, `l10n/` for generated localization, and `main.dart` as the bootstrap entry. -- `assets/` contains bundled media referenced in `pubspec.yaml`; platform bits live inside `android/` and `ios/`. Release collateral is under `fastlane/`, while helper scripts sit in `scripts/`. +You are an expert in Flutter and Dart development. Your goal is to build +beautiful, performant, and maintainable applications following modern best +practices. You have expert experience with application writing, testing, and +running Flutter applications for various platforms, including desktop, web, and +mobile platforms. -## Build, Test, and Development Commands -- `flutter pub get` installs pub dependencies after manifest edits. -- `flutter pub run build_runner build --delete-conflicting-outputs` regenerates serializers and other codegen output. -- `flutter run -d ` launches a debug build against an emulator or physical device (`-d ios`, `-d android`). -- `flutter analyze` executes static analysis checks; fix warnings before committing. -- `flutter build apk --release`, `flutter build appbundle --release`, and `flutter build ios --release` assemble store packages. -- `./scripts/release.sh` orchestrates the tagged release workflow once CI succeeds. +## Interaction Guidelines +* **User Persona:** Assume the user is familiar with programming concepts but + may be new to Dart. +* **Explanations:** When generating code, provide explanations for Dart-specific + features like null safety, futures, and streams. +* **Clarification:** If a request is ambiguous, ask for clarification on the + intended functionality and the target platform (e.g., command-line, web, + server). +* **Dependencies:** When suggesting new dependencies from `pub.dev`, explain + their benefits. +* **Formatting:** Use the `dart_format` tool to ensure consistent code + formatting. +* **Fixes:** Use the `dart_fix` tool to automatically fix many common errors, + and to help code conform to configured analysis options. +* **Linting:** Use the Dart linter with a recommended set of rules to catch + common issues. Use the `analyze_files` tool to run the linter. -## Coding Style & Naming Conventions -- Use Flutter defaults: two-space indentation, `lowerCamelCase` for members, `UpperCamelCase` for types, and snake_case filenames across `lib/` and `test/`. -- Format code with `dart format .` and rely on `flutter analyze` to enforce `package:flutter_lints` (see `analysis_options.yaml`). Avoid `print`; prefer injected loggers or platform channels. +## Project Structure +* **Standard Structure:** Assumes a standard Flutter project structure with + `lib/main.dart` as the primary application entry point. -## Commit & Pull Request Guidelines -- Follow Conventional Commits (`feat:`, `fix:`, `chore:`, `refactor:`) as in existing history. Keep subject lines ≤72 characters and add context in the body when behavior changes. -- Pull requests should outline the change, link issues, and list manual validation steps. Attach screenshots or recordings for UI updates. -- Rebase onto `main`, rerun codegen, and ensure CI is green before requesting review. Delete obsolete assets and localization strings in the same patch when touched. +## Flutter style guide +* **SOLID Principles:** Apply SOLID principles throughout the codebase. +* **Concise and Declarative:** Write concise, modern, technical Dart code. + Prefer functional and declarative patterns. +* **Composition over Inheritance:** Favor composition for building complex + widgets and logic. +* **Immutability:** Prefer immutable data structures. Widgets (especially + `StatelessWidget`) should be immutable. +* **State Management:** Separate ephemeral state and app state. Use a state + management solution for app state to handle the separation of concerns. +* **Widgets are for UI:** Everything in Flutter's UI is a widget. Compose + complex UIs from smaller, reusable widgets. +* **Navigation:** Use a modern routing package like `auto_route` or `go_router`. + See the [navigation guide](./navigation.md) for a detailed example using + `go_router`. -## Localization & Configuration Notes -- Update generated delegates in `lib/l10n/` via Flutter’s localization toolchain (`flutter gen-l10n` from IDE or pub). Commit regenerated files with the feature change. -- Keep environment secrets outside source control; configuration surfaces and self-hosted setup notes live in `docs/`. +## Package Management +* **Pub Tool:** To manage packages, use the `pub` tool, if available. +* **External Packages:** If a new feature requires an external package, use the + `pub_dev_search` tool, if it is available. Otherwise, identify the most + suitable and stable package from pub.dev. +* **Adding Dependencies:** To add a regular dependency, use the `pub` tool, if + it is available. Otherwise, run `flutter pub add `. +* **Adding Dev Dependencies:** To add a development dependency, use the `pub` + tool, if it is available, with `dev:`. Otherwise, run `flutter + pub add dev:`. +* **Dependency Overrides:** To add a dependency override, use the `pub` tool, if + it is available, with `override::1.0.0`. Otherwise, run `flutter + pub add override::1.0.0`. +* **Removing Dependencies:** To remove a dependency, use the `pub` tool, if it + is available. Otherwise, run `dart pub remove `. + +## Code Quality +* **Code structure:** Adhere to maintainable code structure and separation of + concerns (e.g., UI logic separate from business logic). +* **Naming conventions:** Avoid abbreviations and use meaningful, consistent, + descriptive names for variables, functions, and classes. +* **Conciseness:** Write code that is as short as it can be while remaining + clear. +* **Simplicity:** Write straightforward code. Code that is clever or + obscure is difficult to maintain. +* **Error Handling:** Anticipate and handle potential errors. Don't let your + code fail silently. +* **Styling:** + * Line length: Lines should be 80 characters or fewer. + * Use `PascalCase` for classes, `camelCase` for + members/variables/functions/enums, and `snake_case` for files. +* **Functions:** + * Functions short and with a single purpose (strive for less than 20 lines). +* **Testing:** Write code with testing in mind. Use the `file`, `process`, and + `platform` packages, if appropriate, so you can inject in-memory and fake + versions of the objects. +* **Logging:** Use the `logging` package instead of `print`. + +## Dart Best Practices +* **Effective Dart:** Follow the official Effective Dart guidelines + (https://dart.dev/effective-dart) +* **Class Organization:** Define related classes within the same library file. + For large libraries, export smaller, private libraries from a single top-level + library. +* **Library Organization:** Group related libraries in the same folder. +* **API Documentation:** Add documentation comments to all public APIs, + including classes, constructors, methods, and top-level functions. +* **Comments:** Write clear comments for complex or non-obvious code. Avoid + over-commenting. +* **Trailing Comments:** Don't add trailing comments. +* **Async/Await:** Ensure proper use of `async`/`await` for asynchronous + operations with robust error handling. + * Use `Future`s, `async`, and `await` for asynchronous operations. + * Use `Stream`s for sequences of asynchronous events. +* **Null Safety:** Write code that is soundly null-safe. Leverage Dart's null + safety features. Avoid `!` unless the value is guaranteed to be non-null. +* **Pattern Matching:** Use pattern matching features where they simplify the + code. +* **Records:** Use records to return multiple types in situations where defining + an entire class is cumbersome. +* **Switch Statements:** Prefer using exhaustive `switch` statements or + expressions, which don't require `break` statements. +* **Exception Handling:** Use `try-catch` blocks for handling exceptions, and + use exceptions appropriate for the type of exception. Use custom exceptions + for situations specific to your code. +* **Arrow Functions:** Use arrow syntax for simple one-line functions. + +## Flutter Best Practices +* **Immutability:** Widgets (especially `StatelessWidget`) are immutable; when + the UI needs to change, Flutter rebuilds the widget tree. +* **Composition:** Prefer composing smaller widgets over extending existing + ones. Use this to avoid deep widget nesting. +* **Private Widgets:** Use small, private `Widget` classes instead of private + helper methods that return a `Widget`. +* **Build Methods:** Break down large `build()` methods into smaller, reusable + private Widget classes. +* **List Performance:** Use `ListView.builder` or `SliverList` for long lists to + create lazy-loaded lists for performance. +* **Isolates:** Use `compute()` to run expensive calculations in a separate + isolate to avoid blocking the UI thread, such as JSON parsing. +* **Const Constructors:** Use `const` constructors for widgets and in `build()` + methods whenever possible to reduce rebuilds. +* **Build Method Performance:** Avoid performing expensive operations, like + network calls or complex computations, directly within `build()` methods. + +## API Design Principles +When building reusable APIs, such as a library, follow these principles. + +* **Consider the User:** Design APIs from the perspective of the person who will + be using them. The API should be intuitive and easy to use correctly. +* **Documentation is Essential:** Good documentation is a part of good API + design. It should be clear, concise, and provide examples. + +## Application Architecture +* **Separation of Concerns:** Aim for separation of concerns similar to MVC/MVVM, with defined Model, + View, and ViewModel/Controller roles. +* **Logical Layers:** Organize the project into logical layers: + * Presentation (widgets, screens) + * Domain (business logic classes) + * Data (model classes, API clients) + * Core (shared classes, utilities, and extension types) +* **Feature-based Organization:** For larger projects, organize code by feature, + where each feature has its own presentation, domain, and data subfolders. This + improves navigability and scalability. + +## Lint Rules + +Include the package in the `analysis_options.yaml` file. Use the following +analysis_options.yaml file as a starting point: + +```yaml +include: package:flutter_lints/flutter.yaml + +linter: + rules: + # Add additional lint rules here: + # avoid_print: false + # prefer_single_quotes: true +``` + +### State Management +* **Built-in Solutions:** Prefer Flutter's built-in state management solutions. + Do not use a third-party package unless explicitly requested. +* **Streams:** Use `Streams` and `StreamBuilder` for handling a sequence of + asynchronous events. +* **Futures:** Use `Futures` and `FutureBuilder` for handling a single + asynchronous operation that will complete in the future. +* **ValueNotifier:** Use `ValueNotifier` with `ValueListenableBuilder` for + simple, local state that involves a single value. + + ```dart + // Define a ValueNotifier to hold the state. + final ValueNotifier _counter = ValueNotifier(0); + + // Use ValueListenableBuilder to listen and rebuild. + ValueListenableBuilder( + valueListenable: _counter, + builder: (context, value, child) { + return Text('Count: $value'); + }, + ); + ``` + +* **ChangeNotifier:** For state that is more complex or shared across multiple + widgets, use `ChangeNotifier`. +* **ListenableBuilder:** Use `ListenableBuilder` to listen to changes from a + `ChangeNotifier` or other `Listenable`. +* **MVVM:** When a more robust solution is needed, structure the app using the + Model-View-ViewModel (MVVM) pattern. +* **Dependency Injection:** Use simple manual constructor dependency injection + to make a class's dependencies explicit in its API, and to manage dependencies + between different layers of the application. +* **Provider:** If a dependency injection solution beyond manual constructor + injection is explicitly requested, `provider` can be used to make services, + repositories, or complex state objects available to the UI layer without tight + coupling (note: this document generally defaults against third-party packages + for state management unless explicitly requested). + +### Data Flow +* **Data Structures:** Define data structures (classes) to represent the data + used in the application. +* **Data Abstraction:** Abstract data sources (e.g., API calls, database + operations) using Repositories/Services to promote testability. + +### Routing +* **GoRouter:** Use the `go_router` package for declarative navigation, deep + linking, and web support. +* **GoRouter Setup:** To use `go_router`, first add it to your `pubspec.yaml` + using the `pub` tool's `add` command. + + ```dart + // 1. Add the dependency + // flutter pub add go_router + + // 2. Configure the router + final GoRouter _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (context, state) => const HomeScreen(), + routes: [ + GoRoute( + path: 'details/:id', // Route with a path parameter + builder: (context, state) { + final String id = state.pathParameters['id']!; + return DetailScreen(id: id); + }, + ), + ], + ), + ], + ); + + // 3. Use it in your MaterialApp + MaterialApp.router( + routerConfig: _router, + ); + ``` +* **Authentication Redirects:** Configure `go_router`'s `redirect` property to + handle authentication flows, ensuring users are redirected to the login screen + when unauthorized, and back to their intended destination after successful + login. + +* **Navigator:** Use the built-in `Navigator` for short-lived screens that do + not need to be deep-linkable, such as dialogs or temporary views. + + ```dart + // Push a new screen onto the stack + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const DetailsScreen()), + ); + + // Pop the current screen to go back + Navigator.pop(context); + ``` + +### Data Handling & Serialization +* **JSON Serialization:** Use `json_serializable` and `json_annotation` for + parsing and encoding JSON data. +* **Field Renaming:** When encoding data, use `fieldRename: FieldRename.snake` + to convert Dart's camelCase fields to snake_case JSON keys. + + ```dart + // In your model file + import 'package:json_annotation/json_annotation.dart'; + + part 'user.g.dart'; + + @JsonSerializable(fieldRename: FieldRename.snake) + class User { + final String firstName; + final String lastName; + + User({required this.firstName, required this.lastName}); + + factory User.fromJson(Map json) => _$UserFromJson(json); + Map toJson() => _$UserToJson(this); + } + ``` + + +### Logging +* **Structured Logging:** Use the `log` function from `dart:developer` for + structured logging that integrates with Dart DevTools. + + ```dart + import 'dart:developer' as developer; + + // For simple messages + developer.log('User logged in successfully.'); + + // For structured error logging + try { + // ... code that might fail + } catch (e, s) { + developer.log( + 'Failed to fetch data', + name: 'myapp.network', + level: 1000, // SEVERE + error: e, + stackTrace: s, + ); + } + ``` + +## Code Generation +* **Build Runner:** If the project uses code generation, ensure that + `build_runner` is listed as a dev dependency in `pubspec.yaml`. +* **Code Generation Tasks:** Use `build_runner` for all code generation tasks, + such as for `json_serializable`. +* **Running Build Runner:** After modifying files that require code generation, + run the build command: + + ```shell + dart run build_runner build --delete-conflicting-outputs + ``` + +## Testing +* **Running Tests:** To run tests, use the `run_tests` tool if it is available, + otherwise use `flutter test`. +* **Unit Tests:** Use `package:test` for unit tests. +* **Widget Tests:** Use `package:flutter_test` for widget tests. +* **Integration Tests:** Use `package:integration_test` for integration tests. +* **Assertions:** Prefer using `package:checks` for more expressive and readable + assertions over the default `matchers`. + +### Testing Best practices +* **Convention:** Follow the Arrange-Act-Assert (or Given-When-Then) pattern. +* **Unit Tests:** Write unit tests for domain logic, data layer, and state + management. +* **Widget Tests:** Write widget tests for UI components. +* **Integration Tests:** For broader application validation, use integration + tests to verify end-to-end user flows. +* **integration_test package:** Use the `integration_test` package from the + Flutter SDK for integration tests. Add it as a `dev_dependency` in + `pubspec.yaml` by specifying `sdk: flutter`. +* **Mocks:** Prefer fakes or stubs over mocks. If mocks are absolutely + necessary, use `mockito` or `mocktail` to create mocks for dependencies. While + code generation is common for state management (e.g., with `freezed`), try to + avoid it for mocks. +* **Coverage:** Aim for high test coverage. + +## Visual Design & Theming +* **UI Design:** Build beautiful and intuitive user interfaces that follow + modern design guidelines. +* **Responsiveness:** Ensure the app is mobile responsive and adapts to + different screen sizes, working perfectly on mobile and web. +* **Navigation:** If there are multiple pages for the user to interact with, + provide an intuitive and easy navigation bar or controls. +* **Typography:** Stress and emphasize font sizes to ease understanding, e.g., + hero text, section headlines, list headlines, keywords in paragraphs. +* **Background:** Apply subtle noise texture to the main background to add a + premium, tactile feel. +* **Shadows:** Multi-layered drop shadows create a strong sense of depth; cards + have a soft, deep shadow to look "lifted." +* **Icons:** Incorporate icons to enhance the user’s understanding and the + logical navigation of the app. +* **Interactive Elements:** Buttons, checkboxes, sliders, lists, charts, graphs, + and other interactive elements have a shadow with elegant use of color to + create a "glow" effect. + +### Theming +* **Centralized Theme:** Define a centralized `ThemeData` object to ensure a + consistent application-wide style. +* **Light and Dark Themes:** Implement support for both light and dark themes, + ideal for a user-facing theme toggle (`ThemeMode.light`, `ThemeMode.dark`, + `ThemeMode.system`). +* **Color Scheme Generation:** Generate harmonious color palettes from a single + color using `ColorScheme.fromSeed`. + + ```dart + final ThemeData lightTheme = ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: Colors.deepPurple, + brightness: Brightness.light, + ), + // ... other theme properties + ); + ``` +* **Color Palette:** Include a wide range of color concentrations and hues in + the palette to create a vibrant and energetic look and feel. +* **Component Themes:** Use specific theme properties (e.g., `appBarTheme`, + `elevatedButtonTheme`) to customize the appearance of individual Material + components. +* **Custom Fonts:** For custom fonts, use the `google_fonts` package. Define a + `TextTheme` to apply fonts consistently. + + ```dart + // 1. Add the dependency + // flutter pub add google_fonts + + // 2. Define a TextTheme with a custom font + final TextTheme appTextTheme = TextTheme( + displayLarge: GoogleFonts.oswald(fontSize: 57, fontWeight: FontWeight.bold), + titleLarge: GoogleFonts.roboto(fontSize: 22, fontWeight: FontWeight.w500), + bodyMedium: GoogleFonts.openSans(fontSize: 14), + ); + ``` + +### Assets and Images +* **Image Guidelines:** If images are needed, make them relevant and meaningful, + with appropriate size, layout, and licensing (e.g., freely available). Provide + placeholder images if real ones are not available. +* **Asset Declaration:** Declare all asset paths in your `pubspec.yaml` file. + + ```yaml + flutter: + uses-material-design: true + assets: + - assets/images/ + ``` + +* **Local Images:** Use `Image.asset` for local images from your asset + bundle. + + ```dart + Image.asset('assets/images/placeholder.png') + ``` +* **Network images:** Use NetworkImage for images loaded from the network. +* **Cached images:** For cached images, use NetworkImage a package like + `cached_network_image`. +* **Custom Icons:** Use `ImageIcon` to display an icon from an `ImageProvider`, + useful for custom icons not in the `Icons` class. +* **Network Images:** Use `Image.network` to display images from a URL, and + always include `loadingBuilder` and `errorBuilder` for a better user + experience. + + ```dart + Image.network( + 'https://picsum.photos/200/300', + loadingBuilder: (context, child, progress) { + if (progress == null) return child; + return const Center(child: CircularProgressIndicator()); + }, + errorBuilder: (context, error, stackTrace) { + return const Icon(Icons.error); + }, + ) + ``` +## UI Theming and Styling Code + +* **Responsiveness:** Use `LayoutBuilder` or `MediaQuery` to create responsive + UIs. +* **Text:** Use `Theme.of(context).textTheme` for text styles. +* **Text Fields:** Configure `textCapitalization`, `keyboardType`, and +* **Responsiveness:** Use `LayoutBuilder` or `MediaQuery` to create responsive + UIs. +* **Text:** Use `Theme.of(context).textTheme` for text styles. + remote images. + +```dart +// When using network images, always provide an errorBuilder. +Image.network( + 'https://example.com/image.png', + errorBuilder: (context, error, stackTrace) { + return const Icon(Icons.error); // Show an error icon + }, +); +``` + +## Material Theming Best Practices + +### Embrace `ThemeData` and Material 3 + +* **Use `ColorScheme.fromSeed()`:** Use this to generate a complete, harmonious + color palette for both light and dark modes from a single seed color. +* **Define Light and Dark Themes:** Provide both `theme` and `darkTheme` to your + `MaterialApp` to support system brightness settings seamlessly. +* **Centralize Component Styles:** Customize specific component themes (e.g., + `elevatedButtonTheme`, `cardTheme`, `appBarTheme`) within `ThemeData` to + ensure consistency. +* **Dark/Light Mode and Theme Toggle:** Implement support for both light and + dark themes using `theme` and `darkTheme` properties of `MaterialApp`. The + `themeMode` property can be dynamically controlled (e.g., via a + `ChangeNotifierProvider`) to allow for toggling between `ThemeMode.light`, + `ThemeMode.dark`, or `ThemeMode.system`. + +```dart +// main.dart +MaterialApp( + theme: ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: Colors.deepPurple, + brightness: Brightness.light, + ), + textTheme: const TextTheme( + displayLarge: TextStyle(fontSize: 57.0, fontWeight: FontWeight.bold), + bodyMedium: TextStyle(fontSize: 14.0, height: 1.4), + ), + ), + darkTheme: ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: Colors.deepPurple, + brightness: Brightness.dark, + ), + ), + home: const MyHomePage(), +); +``` + +### Implement Design Tokens with `ThemeExtension` + +For custom styles that aren't part of the standard `ThemeData`, use +`ThemeExtension` to define reusable design tokens. + +* **Create a Custom Theme Extension:** Define a class that extends + `ThemeExtension` and include your custom properties. +* **Implement `copyWith` and `lerp`:** These methods are required for the + extension to work correctly with theme transitions. +* **Register in `ThemeData`:** Add your custom extension to the `extensions` + list in your `ThemeData`. +* **Access Tokens in Widgets:** Use `Theme.of(context).extension()!` + to access your custom tokens. + +```dart +// 1. Define the extension +@immutable +class MyColors extends ThemeExtension { + const MyColors({required this.success, required this.danger}); + + final Color? success; + final Color? danger; + + @override + ThemeExtension copyWith({Color? success, Color? danger}) { + return MyColors(success: success ?? this.success, danger: danger ?? this.danger); + } + + @override + ThemeExtension lerp(ThemeExtension? other, double t) { + if (other is! MyColors) return this; + return MyColors( + success: Color.lerp(success, other.success, t), + danger: Color.lerp(danger, other.danger, t), + ); + } +} + +// 2. Register it in ThemeData +theme: ThemeData( + extensions: const >[ + MyColors(success: Colors.green, danger: Colors.red), + ], +), + +// 3. Use it in a widget +Container( + color: Theme.of(context).extension()!.success, +) +``` + +### Styling with `WidgetStateProperty` + +* **`WidgetStateProperty.resolveWith`:** Provide a function that receives a + `Set` and returns the appropriate value for the current state. +* **`WidgetStateProperty.all`:** A shorthand for when the value is the same for + all states. + +```dart +// Example: Creating a button style that changes color when pressed. +final ButtonStyle myButtonStyle = ButtonStyle( + backgroundColor: WidgetStateProperty.resolveWith( + (Set states) { + if (states.contains(WidgetState.pressed)) { + return Colors.green; // Color when pressed + } + return Colors.red; // Default color + }, + ), +); +``` + +## Layout Best Practices + +### Building Flexible and Overflow-Safe Layouts + +#### For Rows and Columns + +* **`Expanded`:** Use to make a child widget fill the remaining available space + along the main axis. +* **`Flexible`:** Use when you want a widget to shrink to fit, but not + necessarily grow. Don't combine `Flexible` and `Expanded` in the same `Row` or + `Column`. +* **`Wrap`:** Use when you have a series of widgets that would overflow a `Row` + or `Column`, and you want them to move to the next line. + +#### For General Content + +* **`SingleChildScrollView`:** Use when your content is intrinsically larger + than the viewport, but is a fixed size. +* **`ListView` / `GridView`:** For long lists or grids of content, always use a + builder constructor (`.builder`). +* **`FittedBox`:** Use to scale or fit a single child widget within its parent. +* **`LayoutBuilder`:** Use for complex, responsive layouts to make decisions + based on the available space. + +### Layering Widgets with Stack + +* **`Positioned`:** Use to precisely place a child within a `Stack` by anchoring it to the edges. +* **`Align`:** Use to position a child within a `Stack` using alignments like `Alignment.center`. + +### Advanced Layout with Overlays + +* **`OverlayPortal`:** Use this widget to show UI elements (like custom + dropdowns or tooltips) "on top" of everything else. It manages the + `OverlayEntry` for you. + + ```dart + class MyDropdown extends StatefulWidget { + const MyDropdown({super.key}); + + @override + State createState() => _MyDropdownState(); + } + + class _MyDropdownState extends State { + final _controller = OverlayPortalController(); + + @override + Widget build(BuildContext context) { + return OverlayPortal( + controller: _controller, + overlayChildBuilder: (BuildContext context) { + return const Positioned( + top: 50, + left: 10, + child: Card( + child: Padding( + padding: EdgeInsets.all(8.0), + child: Text('I am an overlay!'), + ), + ), + ); + }, + child: ElevatedButton( + onPressed: _controller.toggle, + child: const Text('Toggle Overlay'), + ), + ); + } + } + ``` + +## Color Scheme Best Practices + +### Contrast Ratios + +* **WCAG Guidelines:** Aim to meet the Web Content Accessibility Guidelines + (WCAG) 2.1 standards. +* **Minimum Contrast:** + * **Normal Text:** A contrast ratio of at least **4.5:1**. + * **Large Text:** (18pt or 14pt bold) A contrast ratio of at least **3:1**. + +### Palette Selection + +* **Primary, Secondary, and Accent:** Define a clear color hierarchy. +* **The 60-30-10 Rule:** A classic design rule for creating a balanced color scheme. + * **60%** Primary/Neutral Color (Dominant) + * **30%** Secondary Color + * **10%** Accent Color + +### Complementary Colors + +* **Use with Caution:** They can be visually jarring if overused. +* **Best Use Cases:** They are excellent for accent colors to make specific + elements pop, but generally poor for text and background pairings as they can + cause eye strain. + +### Example Palette + +* **Primary:** #0D47A1 (Dark Blue) +* **Secondary:** #1976D2 (Medium Blue) +* **Accent:** #FFC107 (Amber) +* **Neutral/Text:** #212121 (Almost Black) +* **Background:** #FEFEFE (Almost White) + +## Font Best Practices + +### Font Selection + +* **Limit Font Families:** Stick to one or two font families for the entire + application. +* **Prioritize Legibility:** Choose fonts that are easy to read on screens of + all sizes. Sans-serif fonts are generally preferred for UI body text. +* **System Fonts:** Consider using platform-native system fonts. +* **Google Fonts:** For a wide selection of open-source fonts, use the + `google_fonts` package. + +### Hierarchy and Scale + +* **Establish a Scale:** Define a set of font sizes for different text elements + (e.g., headlines, titles, body text, captions). +* **Use Font Weight:** Differentiate text effectively using font weights. +* **Color and Opacity:** Use color and opacity to de-emphasize less important + text. + +### Readability + +* **Line Height (Leading):** Set an appropriate line height, typically **1.4x to + 1.6x** the font size. +* **Line Length:** For body text, aim for a line length of **45-75 characters**. +* **Avoid All Caps:** Do not use all caps for long-form text. + +### Example Typographic Scale + +```dart +// In your ThemeData +textTheme: const TextTheme( + displayLarge: TextStyle(fontSize: 57.0, fontWeight: FontWeight.bold), + titleLarge: TextStyle(fontSize: 22.0, fontWeight: FontWeight.bold), + bodyLarge: TextStyle(fontSize: 16.0, height: 1.5), + bodyMedium: TextStyle(fontSize: 14.0, height: 1.4), + labelSmall: TextStyle(fontSize: 11.0, color: Colors.grey), +), +``` + +## Documentation + +* **`dartdoc`:** Write `dartdoc`-style comments for all public APIs. + + +### Documentation Philosophy + +* **Comment wisely:** Use comments to explain why the code is written a certain + way, not what the code does. The code itself should be self-explanatory. +* **Document for the user:** Write documentation with the reader in mind. If you + had a question and found the answer, add it to the documentation where you + first looked. This ensures the documentation answers real-world questions. +* **No useless documentation:** If the documentation only restates the obvious + from the code's name, it's not helpful. Good documentation provides context + and explains what isn't immediately apparent. +* **Consistency is key:** Use consistent terminology throughout your + documentation. + +### Commenting Style + +* **Use `///` for doc comments:** This allows documentation generation tools to + pick them up. +* **Start with a single-sentence summary:** The first sentence should be a + concise, user-centric summary ending with a period. +* **Separate the summary:** Add a blank line after the first sentence to create + a separate paragraph. This helps tools create better summaries. +* **Avoid redundancy:** Don't repeat information that's obvious from the code's + context, like the class name or signature. +* **Don't document both getter and setter:** For properties with both, only + document one. The documentation tool will treat them as a single field. + +### Writing Style + +* **Be brief:** Write concisely. +* **Avoid jargon and acronyms:** Don't use abbreviations unless they are widely + understood. +* **Use Markdown sparingly:** Avoid excessive markdown and never use HTML for + formatting. +* **Use backticks for code:** Enclose code blocks in backtick fences, and + specify the language. + +### What to Document + +* **Public APIs are a priority:** Always document public APIs. +* **Consider private APIs:** It's a good idea to document private APIs as well. +* **Library-level comments are helpful:** Consider adding a doc comment at the + library level to provide a general overview. +* **Include code samples:** Where appropriate, add code samples to illustrate usage. +* **Explain parameters, return values, and exceptions:** Use prose to describe + what a function expects, what it returns, and what errors it might throw. +* **Place doc comments before annotations:** Documentation should come before + any metadata annotations. + +## Accessibility (A11Y) +Implement accessibility features to empower all users, assuming a wide variety +of users with different physical abilities, mental abilities, age groups, +education levels, and learning styles. + +* **Color Contrast:** Ensure text has a contrast ratio of at least **4.5:1** + against its background. +* **Dynamic Text Scaling:** Test your UI to ensure it remains usable when users + increase the system font size. +* **Semantic Labels:** Use the `Semantics` widget to provide clear, descriptive + labels for UI elements. +* **Screen Reader Testing:** Regularly test your app with TalkBack (Android) and + VoiceOver (iOS). \ No newline at end of file diff --git a/lib/core/auth/auth_state_manager.dart b/lib/core/auth/auth_state_manager.dart index a40a0c5..33b00ad 100644 --- a/lib/core/auth/auth_state_manager.dart +++ b/lib/core/auth/auth_state_manager.dart @@ -396,7 +396,8 @@ class AuthStateManager extends Notifier { state = state.copyWith( status: AuthStatus.error, - error: 'Saved server configuration is no longer available. Please reconnect.', + error: + 'Saved server configuration is no longer available. Please reconnect.', isLoading: false, ); return false; diff --git a/lib/core/providers/app_providers.dart b/lib/core/providers/app_providers.dart index c250e5d..4de7131 100644 --- a/lib/core/providers/app_providers.dart +++ b/lib/core/providers/app_providers.dart @@ -197,10 +197,10 @@ final socketServiceProvider = Provider((ref) { if (reviewerMode) return null; final activeServer = ref.watch(activeServerProvider); - final token = ref.watch(authTokenProvider3); - final transportMode = ref - .watch(appSettingsProvider) - .socketTransportMode; // 'auto' or 'ws' + final token = ref.watch(authTokenProvider3.select((t) => t)); + final transportMode = ref.watch( + appSettingsProvider.select((s) => s.socketTransportMode), + ); return activeServer.maybeWhen( data: (server) { @@ -213,6 +213,10 @@ final socketServiceProvider = Provider((ref) { // best-effort connect; errors handled internally // ignore unawaited_futures s.connect(); + // Keep socket token up-to-date without reconstructing the service + ref.listen(authTokenProvider3, (prev, next) { + s.updateAuthToken(next); + }); ref.onDispose(() { try { s.dispose(); @@ -242,12 +246,12 @@ final attachmentUploadQueueProvider = Provider((ref) { // Auth token integration with API service - using unified auth system final apiTokenUpdaterProvider = Provider((ref) { // Listen to unified auth token changes and update API service - ref.listen(authTokenProvider3, (previous, next) { + ref.listen(authTokenProvider3, (previous, next) { final api = ref.read(apiServiceProvider); - if (api != null && next != null && next.isNotEmpty) { - api.updateAuthToken(next); + if (api != null) { + api.updateAuthToken(next ?? ''); foundation.debugPrint( - 'DEBUG: Updated API service with unified auth token', + 'DEBUG: Applied auth token to API (len=${next?.length ?? 0})', ); } }); diff --git a/lib/core/providers/app_startup_providers.dart b/lib/core/providers/app_startup_providers.dart index 5089e61..658a044 100644 --- a/lib/core/providers/app_startup_providers.dart +++ b/lib/core/providers/app_startup_providers.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../providers/app_providers.dart'; @@ -13,6 +14,7 @@ import '../../features/onboarding/views/onboarding_sheet.dart'; import '../../shared/theme/theme_extensions.dart'; import '../services/connectivity_service.dart'; import '../utils/debug_logger.dart'; +import '../models/server_config.dart'; enum _ConversationWarmupStatus { idle, warming, complete } @@ -56,6 +58,14 @@ void _scheduleConversationWarmup(Ref ref, {bool force = false}) { return; } + // If network latency is high, delay warmup further to reduce contention + final latency = ref.read(connectivityServiceProvider).lastLatencyMs; + final extraDelay = latency > 800 + ? 400 + : latency > 400 + ? 200 + : 0; + final statusController = ref.read(_conversationWarmupStatusProvider.notifier); final status = ref.read(_conversationWarmupStatusProvider); @@ -80,6 +90,9 @@ void _scheduleConversationWarmup(Ref ref, {bool force = false}) { statusController.set(_ConversationWarmupStatus.warming); Future.microtask(() async { + if (extraDelay > 0) { + await Future.delayed(Duration(milliseconds: extraDelay)); + } try { final existing = ref.read(conversationsProvider); if (existing.hasValue) { @@ -109,6 +122,7 @@ final appStartupFlowProvider = Provider((ref) { // Ensure token integration listeners are active ref.watch(authApiIntegrationProvider); ref.watch(apiTokenUpdaterProvider); + ref.watch(silentLoginCoordinatorProvider); // Kick background model loading flow (non-blocking) ref.watch(backgroundModelLoadProvider); @@ -129,8 +143,35 @@ final appStartupFlowProvider = Provider((ref) { final connectivityService = ref.watch(connectivityServiceProvider); PersistentStreamingService().attachConnectivityService(connectivityService); - // Warm the conversations list in the background as soon as possible - Future.microtask(() => _scheduleConversationWarmup(ref)); + // Warm the conversations list in the background as soon as possible, + // but avoid doing so on poor connectivity to reduce startup load. + // Apply a small randomized delay to smooth load spikes across app wakes. + Future.microtask(() async { + final online = ref.read(isOnlineProvider); + if (!online) return; + final jitter = Duration( + milliseconds: 50 + (DateTime.now().millisecond % 100), + ); + await Future.delayed(jitter); + _scheduleConversationWarmup(ref); + }); + + // One-time, post-frame system UI polish: set status bar icon brightness to + // match theme after the first frame. Avoids flicker at startup. + WidgetsBinding.instance.addPostFrameCallback((_) { + try { + final isDark = + WidgetsBinding.instance.window.platformBrightness == Brightness.dark; + SystemChrome.setSystemUIOverlayStyle( + SystemUiOverlayStyle( + statusBarIconBrightness: isDark ? Brightness.light : Brightness.dark, + systemNavigationBarIconBrightness: isDark + ? Brightness.light + : Brightness.dark, + ), + ); + } catch (_) {} + }); // Watch for auth transitions to trigger warmup and other background work ref.listen(authNavigationStateProvider, (prev, next) { @@ -151,14 +192,23 @@ final appStartupFlowProvider = Provider((ref) { DebugLogger.auth('StartupFlow: Applied auth token to API'); } - // Preload default model in background (best-effort) - try { - await ref.read(defaultModelProvider.future); - } catch (e) { - DebugLogger.warning( - 'StartupFlow: default model preload failed: $e', - ); - } + // Preload default model in background (best-effort) with an adaptive + // delay based on network latency to avoid hammering poor networks. + final latency = ref.read(connectivityServiceProvider).lastLatencyMs; + final delayMs = latency < 0 + ? 300 + : latency > 800 + ? 600 + : 200 + (latency ~/ 2); + Future.delayed(Duration(milliseconds: delayMs), () async { + try { + await ref.read(defaultModelProvider.future); + } catch (e) { + DebugLogger.warning( + 'StartupFlow: default model preload failed: $e', + ); + } + }); // Kick background chat warmup now that we're authenticated _scheduleConversationWarmup(ref, force: true); @@ -199,6 +249,66 @@ final appStartupFlowProvider = Provider((ref) { }); }); +// Tracks whether we've already attempted a silent login for the current app session. +final _silentLoginAttemptedProvider = + NotifierProvider<_SilentLoginAttemptedNotifier, bool>( + _SilentLoginAttemptedNotifier.new, + ); + +class _SilentLoginAttemptedNotifier extends Notifier { + @override + bool build() => false; + + void markAttempted() => state = true; +} + +/// Coordinates a one-time silent login attempt when: +/// - There is an active server +/// - The auth navigation state requires login +/// - Saved credentials are present +final silentLoginCoordinatorProvider = Provider((ref) { + Future attempt() async { + final attempted = ref.read(_silentLoginAttemptedProvider); + if (attempted) return; + + final authState = ref.read(authNavigationStateProvider); + if (authState != AuthNavigationState.needsLogin) return; + + final activeServerAsync = ref.read(activeServerProvider); + final hasActiveServer = activeServerAsync.maybeWhen( + data: (server) => server != null, + orElse: () => false, + ); + if (!hasActiveServer) return; + + // Perform the attempt in a microtask to avoid side-effects in build + Future.microtask(() async { + try { + final hasCreds = await ref.read(hasSavedCredentialsProvider2.future); + if (hasCreds) { + ref.read(_silentLoginAttemptedProvider.notifier).markAttempted(); + await ref.read(authActionsProvider).silentLogin(); + } + } catch (_) { + // Ignore silent login errors; app will proceed to manual login + } + }); + } + + void check() => attempt(); + + // Initial check + check(); + + // React to changes in server or auth state + ref.listen(authNavigationStateProvider, (prev, next) { + check(); + }); + ref.listen>(activeServerProvider, (prev, next) { + check(); + }); +}); + /// Listens to app lifecycle and refreshes server state when app returns to foreground. /// /// Rationale: Socket.IO does not replay historical events. If the app was suspended, diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index 233fd2f..ea68fda 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; @@ -35,7 +36,16 @@ class RouterNotifier extends ChangeNotifier { late final List> _subscriptions; void _onStateChanged(dynamic previous, dynamic next) { - notifyListeners(); + // Debounce router refreshes to avoid thrashing on rapid state changes + _scheduleRefresh(); + } + + Timer? _refreshDebounce; + void _scheduleRefresh() { + _refreshDebounce?.cancel(); + _refreshDebounce = Timer(const Duration(milliseconds: 50), () { + notifyListeners(); + }); } String? redirect(BuildContext context, GoRouterState state) { @@ -44,11 +54,13 @@ class RouterNotifier extends ChangeNotifier { final activeServerAsync = ref.read(activeServerProvider); if (reviewerMode) { + // Stay on whatever route if already in chat; otherwise go to chat if (location == Routes.chat) return null; return Routes.chat; } if (activeServerAsync.isLoading) { + // Avoid redirect loops by keeping splash during server loading return location == Routes.splash ? null : Routes.splash; } @@ -60,6 +72,7 @@ class RouterNotifier extends ChangeNotifier { final activeServer = activeServerAsync.asData?.value; if (activeServer == null) { + // Allow auth-related routes while no server configured if (_isAuthLocation(location)) return null; return Routes.serverConnection; } @@ -67,12 +80,14 @@ class RouterNotifier extends ChangeNotifier { final authState = ref.read(authNavigationStateProvider); switch (authState) { case AuthNavigationState.loading: + // Keep splash while establishing session return location == Routes.splash ? null : Routes.splash; case AuthNavigationState.needsLogin: case AuthNavigationState.error: if (_isAuthLocation(location)) return null; return Routes.serverConnection; case AuthNavigationState.authenticated: + // Avoid unnecessary redirects if already on a non-auth route if (_isAuthLocation(location) || location == Routes.splash) { return Routes.chat; } @@ -88,6 +103,7 @@ class RouterNotifier extends ChangeNotifier { @override void dispose() { + _refreshDebounce?.cancel(); for (final sub in _subscriptions) { sub.close(); } diff --git a/lib/core/services/connectivity_service.dart b/lib/core/services/connectivity_service.dart index c2a68d4..d23590c 100644 --- a/lib/core/services/connectivity_service.dart +++ b/lib/core/services/connectivity_service.dart @@ -12,6 +12,9 @@ class ConnectivityService { final _connectivityController = StreamController.broadcast(); ConnectivityStatus _lastStatus = ConnectivityStatus.checking; + int _recentFailures = 0; + Duration _interval = const Duration(seconds: 10); + int _lastLatencyMs = -1; ConnectivityService(this._dio) { _startConnectivityMonitoring(); @@ -20,6 +23,7 @@ class ConnectivityService { Stream get connectivityStream => _connectivityController.stream; ConnectivityStatus get currentStatus => _lastStatus; + int get lastLatencyMs => _lastLatencyMs; /// Stream that emits true when connected, false when offline Stream get isConnected => @@ -30,12 +34,12 @@ class ConnectivityService { void _startConnectivityMonitoring() { // Initial check after a brief delay to avoid showing offline during startup - Timer(const Duration(milliseconds: 1000), () { + Timer(const Duration(milliseconds: 800), () { _checkConnectivity(); }); - // Check periodically; balance responsiveness with battery/network usage - _connectivityTimer = Timer.periodic(const Duration(seconds: 10), (_) { + // Check periodically; interval adapts to recent failures + _connectivityTimer = Timer.periodic(_interval, (_) { _checkConnectivity(); }); } @@ -45,7 +49,7 @@ class ConnectivityService { // DNS lookup is a lightweight, permission-free reachability check final result = await InternetAddress.lookup( 'google.com', - ).timeout(const Duration(seconds: 3)); + ).timeout(const Duration(seconds: 2)); if (result.isNotEmpty && result[0].rawAddress.isNotEmpty) { _updateStatus(ConnectivityStatus.online); @@ -57,20 +61,23 @@ class ConnectivityService { // As a secondary check, hit a public 204 endpoint that returns quickly try { + final start = DateTime.now(); await _dio .get( 'https://www.google.com/generate_204', options: Options( method: 'GET', - sendTimeout: const Duration(seconds: 3), - receiveTimeout: const Duration(seconds: 3), + sendTimeout: const Duration(seconds: 2), + receiveTimeout: const Duration(seconds: 2), followRedirects: false, validateStatus: (status) => status != null && status < 400, ), ) - .timeout(const Duration(seconds: 3)); + .timeout(const Duration(seconds: 2)); + _lastLatencyMs = DateTime.now().difference(start).inMilliseconds; _updateStatus(ConnectivityStatus.online); } catch (_) { + _lastLatencyMs = -1; _updateStatus(ConnectivityStatus.offline); } } @@ -80,6 +87,28 @@ class ConnectivityService { _lastStatus = status; _connectivityController.add(status); } + + // Adapt polling interval based on recent failures to reduce battery/CPU + if (status == ConnectivityStatus.offline) { + _recentFailures = (_recentFailures + 1).clamp(0, 10); + } else if (status == ConnectivityStatus.online) { + _recentFailures = 0; + } + + final newInterval = _recentFailures >= 3 + ? const Duration(seconds: 20) + : _recentFailures == 2 + ? const Duration(seconds: 15) + : const Duration(seconds: 10); + + if (newInterval != _interval) { + _interval = newInterval; + _connectivityTimer?.cancel(); + _connectivityTimer = Timer.periodic( + _interval, + (_) => _checkConnectivity(), + ); + } } Future checkConnectivity() async { diff --git a/lib/core/services/socket_service.dart b/lib/core/services/socket_service.dart index e7c856a..6c6f422 100644 --- a/lib/core/services/socket_service.dart +++ b/lib/core/services/socket_service.dart @@ -4,18 +4,19 @@ import '../models/server_config.dart'; class SocketService { final ServerConfig serverConfig; - final String? authToken; final bool websocketOnly; io.Socket? _socket; + String? _authToken; SocketService({ required this.serverConfig, - required this.authToken, + String? authToken, this.websocketOnly = false, - }); + }) : _authToken = authToken; String? get sessionId => _socket?.id; io.Socket? get socket => _socket; + String? get authToken => _authToken; bool get isConnected => _socket?.connected == true; @@ -39,9 +40,7 @@ class SocketService { final builder = io.OptionBuilder() // Transport selection - .setTransports( - websocketOnly ? ['websocket'] : ['polling', 'websocket'], - ) + .setTransports(websocketOnly ? ['websocket'] : ['polling', 'websocket']) .setRememberUpgrade(!websocketOnly) .setUpgrade(!websocketOnly) // Tune reconnect/backoff and timeouts @@ -55,9 +54,9 @@ class SocketService { // Merge Authorization (if any) with user-defined custom headers for the // Socket.IO handshake. Avoid overriding reserved headers. final Map extraHeaders = {}; - if (authToken != null && authToken!.isNotEmpty) { - extraHeaders['Authorization'] = 'Bearer $authToken'; - builder.setAuth({'token': authToken}); + if (_authToken != null && _authToken!.isNotEmpty) { + extraHeaders['Authorization'] = 'Bearer $_authToken'; + builder.setAuth({'token': _authToken}); } if (serverConfig.customHeaders.isNotEmpty) { final reserved = { @@ -78,7 +77,8 @@ class SocketService { final lower = key.toLowerCase(); if (!reserved.contains(lower) && value.isNotEmpty) { // Do not overwrite Authorization we already set from authToken - if (lower == 'authorization' && extraHeaders.containsKey('Authorization')) { + if (lower == 'authorization' && + extraHeaders.containsKey('Authorization')) { return; } extraHeaders[key] = value; @@ -93,9 +93,9 @@ class SocketService { _socket!.on('connect', (_) { debugPrint('Socket connected: ${_socket!.id}'); - if (authToken != null && authToken!.isNotEmpty) { + if (_authToken != null && _authToken!.isNotEmpty) { _socket!.emit('user-join', { - 'auth': {'token': authToken} + 'auth': {'token': _authToken}, }); } }); @@ -110,10 +110,10 @@ class SocketService { _socket!.on('reconnect', (attempt) { debugPrint('Socket reconnected after $attempt attempts'); - if (authToken != null && authToken!.isNotEmpty) { + if (_authToken != null && _authToken!.isNotEmpty) { // Best-effort rejoin _socket!.emit('user-join', { - 'auth': {'token': authToken} + 'auth': {'token': _authToken}, }); } }); @@ -127,6 +127,21 @@ class SocketService { }); } + /// Update the auth token used by the socket service. + /// If connected, emits a best-effort rejoin with the new token. + void updateAuthToken(String? token) { + _authToken = token; + if (_socket?.connected == true && + _authToken != null && + _authToken!.isNotEmpty) { + try { + _socket!.emit('user-join', { + 'auth': {'token': _authToken}, + }); + } catch (_) {} + } + } + void onChatEvents(void Function(Map event) handler) { _socket?.on('chat-events', (data) { try { @@ -168,6 +183,7 @@ class SocketService { void offEvent(String eventName) { _socket?.off(eventName); } + void dispose() { try { _socket?.dispose(); @@ -177,7 +193,9 @@ class SocketService { // Best-effort: ensure there is an active connection and wait briefly. // Returns true if connected by the end of the timeout. - Future ensureConnected({Duration timeout = const Duration(seconds: 2)}) async { + Future ensureConnected({ + Duration timeout = const Duration(seconds: 2), + }) async { if (isConnected) return true; try { await connect(); diff --git a/lib/core/utils/system_ui_style.dart b/lib/core/utils/system_ui_style.dart new file mode 100644 index 0000000..c53b6a4 --- /dev/null +++ b/lib/core/utils/system_ui_style.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// Applies a single System UI overlay style after first frame to avoid flicker +/// at startup and to align with the active theme brightness. +void applySystemUiOverlayStyleOnce({required Brightness brightness}) { + // On Android 15+, avoid setting bar colors; only control icon brightness. + final isDark = brightness == Brightness.dark; + SystemChrome.setSystemUIOverlayStyle( + SystemUiOverlayStyle( + statusBarIconBrightness: isDark ? Brightness.light : Brightness.dark, + systemNavigationBarIconBrightness: isDark + ? Brightness.light + : Brightness.dark, + ), + ); +} diff --git a/lib/features/auth/providers/unified_auth_providers.dart b/lib/features/auth/providers/unified_auth_providers.dart index 38f39d8..a8b0a59 100644 --- a/lib/features/auth/providers/unified_auth_providers.dart +++ b/lib/features/auth/providers/unified_auth_providers.dart @@ -10,8 +10,7 @@ class AuthActions { final Ref _ref; AuthActions(this._ref); - AuthStateManager get _auth => - _ref.read(authStateManagerProvider.notifier); + AuthStateManager get _auth => _ref.read(authStateManagerProvider.notifier); Future login( String username, @@ -19,21 +18,25 @@ class AuthActions { bool rememberCredentials = false, }) { // Defer mutation to a microtask to avoid provider-build side-effects - return Future(() => _auth.login( - username, - password, - rememberCredentials: rememberCredentials, - )); + return Future( + () => _auth.login( + username, + password, + rememberCredentials: rememberCredentials, + ), + ); } Future loginWithApiKey( String apiKey, { bool rememberCredentials = false, }) { - return Future(() => _auth.loginWithApiKey( - apiKey, - rememberCredentials: rememberCredentials, - )); + return Future( + () => _auth.loginWithApiKey( + apiKey, + rememberCredentials: rememberCredentials, + ), + ); } Future silentLogin() { diff --git a/lib/main.dart b/lib/main.dart index b948bea..6ee1201 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,5 @@ +import 'dart:async'; +import 'dart:developer' as developer; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -8,48 +10,87 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'core/providers/app_providers.dart'; import 'core/router/app_router.dart'; import 'shared/theme/app_theme.dart'; -import 'shared/theme/theme_extensions.dart'; import 'shared/widgets/offline_indicator.dart'; import 'features/auth/providers/unified_auth_providers.dart'; import 'core/auth/auth_state_manager.dart'; import 'core/utils/debug_logger.dart'; +import 'core/utils/system_ui_style.dart'; import 'package:conduit/l10n/app_localizations.dart'; import 'core/services/share_receiver_service.dart'; import 'core/providers/app_startup_providers.dart'; -import 'core/models/server_config.dart'; -void main() async { - WidgetsFlutterBinding.ensureInitialized(); +developer.TimelineTask? _startupTimeline; - // 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); +void main() { + runZonedGuarded( + () async { + WidgetsFlutterBinding.ensureInitialized(); - final sharedPrefs = await SharedPreferences.getInstance(); - const secureStorage = FlutterSecureStorage( - aOptions: AndroidOptions( - encryptedSharedPreferences: true, - sharedPreferencesName: 'conduit_secure_prefs', - preferencesKeyPrefix: 'conduit_', - resetOnError: false, - ), - iOptions: IOSOptions( - accountName: 'conduit_secure_storage', - synchronizable: false, - ), - ); + // Global error handlers + FlutterError.onError = (FlutterErrorDetails details) { + DebugLogger.error('Flutter error', details.exception); + final stack = details.stack; + if (stack != null) { + debugPrint(stack.toString()); + } + }; + WidgetsBinding.instance.platformDispatcher.onError = (error, stack) { + DebugLogger.error('Uncaught platform error', error); + debugPrint(stack.toString()); + return true; + }; - runApp( - ProviderScope( - overrides: [ - sharedPreferencesProvider.overrideWithValue(sharedPrefs), - secureStorageProvider.overrideWithValue(secureStorage), - ], - child: const ConduitApp(), - ), + // Start startup timeline instrumentation + _startupTimeline = developer.TimelineTask(); + _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'); + + final sharedPrefs = await SharedPreferences.getInstance(); + _startupTimeline!.instant('shared_prefs_ready'); + const secureStorage = FlutterSecureStorage( + aOptions: AndroidOptions( + encryptedSharedPreferences: true, + sharedPreferencesName: 'conduit_secure_prefs', + preferencesKeyPrefix: 'conduit_', + resetOnError: false, + ), + iOptions: IOSOptions( + accountName: 'conduit_secure_storage', + synchronizable: false, + ), + ); + _startupTimeline!.instant('secure_storage_ready'); + + // Finish timeline after first frame paints + WidgetsBinding.instance.addPostFrameCallback((_) { + _startupTimeline?.instant('first_frame_rendered'); + _startupTimeline?.finish(); + _startupTimeline = null; + }); + + runApp( + ProviderScope( + overrides: [ + sharedPreferencesProvider.overrideWithValue(sharedPrefs), + secureStorageProvider.overrideWithValue(secureStorage), + ], + child: const ConduitApp(), + ), + ); + developer.Timeline.instantSync('runApp_called'); + }, + (error, stack) { + DebugLogger.error('Uncaught zone error', error); + debugPrint(stack.toString()); + }, ); } @@ -61,42 +102,19 @@ class ConduitApp extends ConsumerStatefulWidget { } class _ConduitAppState extends ConsumerState { - bool _attemptedSilentAutoLogin = false; - ProviderSubscription? _authNavSubscription; - ProviderSubscription>? _activeServerSubscription; + ProviderSubscription? _startupFlowSubscription; + Brightness? _lastAppliedOverlayBrightness; @override void initState() { super.initState(); // Defer heavy provider initialization to after first frame to render UI sooner WidgetsBinding.instance.addPostFrameCallback((_) => _initializeAppState()); - _authNavSubscription = ref.listenManual( - authNavigationStateProvider, - (previous, next) { - if (next == AuthNavigationState.needsLogin) { - _maybeAttemptSilentLogin(); - } else { - _attemptedSilentAutoLogin = false; - } - }, + // Activate app startup flow without tying it to root widget rebuilds + _startupFlowSubscription = ref.listenManual( + appStartupFlowProvider, + (previous, next) {}, ); - - _activeServerSubscription = ref.listenManual>( - activeServerProvider, - (previous, next) { - next.when( - data: (server) { - if (server != null) { - _maybeAttemptSilentLogin(); - } - }, - loading: () {}, - error: (error, stackTrace) {}, - ); - }, - ); - - Future.microtask(_maybeAttemptSilentLogin); } void _initializeAppState() { @@ -110,98 +128,59 @@ class _ConduitAppState extends ConsumerState { @override void dispose() { - _authNavSubscription?.close(); - _activeServerSubscription?.close(); + _startupFlowSubscription?.close(); super.dispose(); } - void _maybeAttemptSilentLogin() { - if (_attemptedSilentAutoLogin) return; - - final authState = ref.read(authNavigationStateProvider); - if (authState != AuthNavigationState.needsLogin) { - return; - } - - final activeServerAsync = ref.read(activeServerProvider); - final hasActiveServer = activeServerAsync.maybeWhen( - data: (server) => server != null, - orElse: () => false, - ); - - if (!hasActiveServer) { - return; - } - - _attemptedSilentAutoLogin = true; - - Future.microtask(() async { - try { - final hasCreds = await ref.read(hasSavedCredentialsProvider2.future); - if (hasCreds) { - await ref.read(authActionsProvider).silentLogin(); - } - } catch (_) { - // Ignore silent login errors; fall back to manual login. - } - }); - } - @override Widget build(BuildContext context) { final themeMode = ref.watch(themeModeProvider.select((mode) => mode)); final router = ref.watch(goRouterProvider); - ref.watch(appStartupFlowProvider); - - final currentTheme = themeMode == ThemeMode.dark - ? AppTheme.conduitDarkTheme - : themeMode == ThemeMode.light - ? AppTheme.conduitLightTheme - : MediaQuery.platformBrightnessOf(context) == Brightness.dark - ? AppTheme.conduitDarkTheme - : AppTheme.conduitLightTheme; - final locale = ref.watch(localeProvider); - return AnimatedThemeWrapper( - theme: currentTheme, - duration: AnimationDuration.medium, - child: ErrorBoundary( - child: MaterialApp.router( - routerConfig: router, - onGenerateTitle: (context) => AppLocalizations.of(context)!.appTitle, - theme: AppTheme.conduitLightTheme, - darkTheme: AppTheme.conduitDarkTheme, - themeMode: themeMode, - debugShowCheckedModeBanner: false, - locale: locale, - localizationsDelegates: AppLocalizations.localizationsDelegates, - supportedLocales: AppLocalizations.supportedLocales, - localeListResolutionCallback: (deviceLocales, supported) { - if (locale != null) return locale; - if (deviceLocales == null || deviceLocales.isEmpty) { - return supported.first; - } - for (final device in deviceLocales) { - for (final loc in supported) { - if (loc.languageCode == device.languageCode) return loc; - } - } + return ErrorBoundary( + child: MaterialApp.router( + routerConfig: router, + onGenerateTitle: (context) => AppLocalizations.of(context)!.appTitle, + theme: AppTheme.conduitLightTheme, + darkTheme: AppTheme.conduitDarkTheme, + themeMode: themeMode, + debugShowCheckedModeBanner: false, + locale: locale, + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + localeListResolutionCallback: (deviceLocales, supported) { + if (locale != null) return locale; + if (deviceLocales == null || deviceLocales.isEmpty) { return supported.first; - }, - builder: (context, child) { - final mediaQuery = MediaQuery.of(context); - return MediaQuery( - data: mediaQuery.copyWith( - textScaler: mediaQuery.textScaler.clamp( - minScaleFactor: 1.0, - maxScaleFactor: 3.0, - ), + } + for (final device in deviceLocales) { + for (final loc in supported) { + if (loc.languageCode == device.languageCode) return loc; + } + } + return supported.first; + }, + builder: (context, child) { + final brightness = Theme.of(context).brightness; + if (_lastAppliedOverlayBrightness != brightness) { + _lastAppliedOverlayBrightness = brightness; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + applySystemUiOverlayStyleOnce(brightness: brightness); + }); + } + final mediaQuery = MediaQuery.of(context); + return MediaQuery( + data: mediaQuery.copyWith( + textScaler: mediaQuery.textScaler.clamp( + minScaleFactor: 1.0, + maxScaleFactor: 3.0, ), - child: OfflineIndicator(child: child ?? const SizedBox.shrink()), - ); - }, - ), + ), + child: OfflineIndicator(child: child ?? const SizedBox.shrink()), + ); + }, ), ); }