refactor: app startup improvements

This commit is contained in:
cogwheel0
2025-09-23 13:43:01 +05:30
parent 8da8a78001
commit f6a1b6123b
10 changed files with 1153 additions and 214 deletions

804
AGENTS.md
View File

@@ -1,26 +1,788 @@
# Repository Guidelines # AI rules for Flutter
## Project Structure & Module Organization You are an expert in Flutter and Dart development. Your goal is to build
- `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. beautiful, performant, and maintainable applications following modern best
- `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/`. 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 ## Interaction Guidelines
- `flutter pub get` installs pub dependencies after manifest edits. * **User Persona:** Assume the user is familiar with programming concepts but
- `flutter pub run build_runner build --delete-conflicting-outputs` regenerates serializers and other codegen output. may be new to Dart.
- `flutter run -d <device>` launches a debug build against an emulator or physical device (`-d ios`, `-d android`). * **Explanations:** When generating code, provide explanations for Dart-specific
- `flutter analyze` executes static analysis checks; fix warnings before committing. features like null safety, futures, and streams.
- `flutter build apk --release`, `flutter build appbundle --release`, and `flutter build ios --release` assemble store packages. * **Clarification:** If a request is ambiguous, ask for clarification on the
- `./scripts/release.sh` orchestrates the tagged release workflow once CI succeeds. 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 ## Project Structure
- Use Flutter defaults: two-space indentation, `lowerCamelCase` for members, `UpperCamelCase` for types, and snake_case filenames across `lib/` and `test/`. * **Standard Structure:** Assumes a standard Flutter project structure with
- 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. `lib/main.dart` as the primary application entry point.
## Commit & Pull Request Guidelines ## Flutter style guide
- 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. * **SOLID Principles:** Apply SOLID principles throughout the codebase.
- Pull requests should outline the change, link issues, and list manual validation steps. Attach screenshots or recordings for UI updates. * **Concise and Declarative:** Write concise, modern, technical Dart code.
- 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. 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 ## Package Management
- Update generated delegates in `lib/l10n/` via Flutters localization toolchain (`flutter gen-l10n` from IDE or pub). Commit regenerated files with the feature change. * **Pub Tool:** To manage packages, use the `pub` tool, if available.
- Keep environment secrets outside source control; configuration surfaces and self-hosted setup notes live in `docs/`. * **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 <package_name>`.
* **Adding Dev Dependencies:** To add a development dependency, use the `pub`
tool, if it is available, with `dev:<package name>`. Otherwise, run `flutter
pub add dev:<package_name>`.
* **Dependency Overrides:** To add a dependency override, use the `pub` tool, if
it is available, with `override:<package name>:1.0.0`. Otherwise, run `flutter
pub add override:<package_name>:1.0.0`.
* **Removing Dependencies:** To remove a dependency, use the `pub` tool, if it
is available. Otherwise, run `dart pub remove <package_name>`.
## 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<int> _counter = ValueNotifier<int>(0);
// Use ValueListenableBuilder to listen and rebuild.
ValueListenableBuilder<int>(
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: <RouteBase>[
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
routes: <RouteBase>[
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<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> 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 users 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<T>` 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<MyColors>()!`
to access your custom tokens.
```dart
// 1. Define the extension
@immutable
class MyColors extends ThemeExtension<MyColors> {
const MyColors({required this.success, required this.danger});
final Color? success;
final Color? danger;
@override
ThemeExtension<MyColors> copyWith({Color? success, Color? danger}) {
return MyColors(success: success ?? this.success, danger: danger ?? this.danger);
}
@override
ThemeExtension<MyColors> lerp(ThemeExtension<MyColors>? 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 <ThemeExtension<dynamic>>[
MyColors(success: Colors.green, danger: Colors.red),
],
),
// 3. Use it in a widget
Container(
color: Theme.of(context).extension<MyColors>()!.success,
)
```
### Styling with `WidgetStateProperty`
* **`WidgetStateProperty.resolveWith`:** Provide a function that receives a
`Set<WidgetState>` 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<Color>(
(Set<WidgetState> 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<MyDropdown> createState() => _MyDropdownState();
}
class _MyDropdownState extends State<MyDropdown> {
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).

View File

@@ -396,7 +396,8 @@ class AuthStateManager extends Notifier<AuthState> {
state = state.copyWith( state = state.copyWith(
status: AuthStatus.error, 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, isLoading: false,
); );
return false; return false;

View File

@@ -197,10 +197,10 @@ final socketServiceProvider = Provider<SocketService?>((ref) {
if (reviewerMode) return null; if (reviewerMode) return null;
final activeServer = ref.watch(activeServerProvider); final activeServer = ref.watch(activeServerProvider);
final token = ref.watch(authTokenProvider3); final token = ref.watch(authTokenProvider3.select((t) => t));
final transportMode = ref final transportMode = ref.watch(
.watch(appSettingsProvider) appSettingsProvider.select((s) => s.socketTransportMode),
.socketTransportMode; // 'auto' or 'ws' );
return activeServer.maybeWhen( return activeServer.maybeWhen(
data: (server) { data: (server) {
@@ -213,6 +213,10 @@ final socketServiceProvider = Provider<SocketService?>((ref) {
// best-effort connect; errors handled internally // best-effort connect; errors handled internally
// ignore unawaited_futures // ignore unawaited_futures
s.connect(); s.connect();
// Keep socket token up-to-date without reconstructing the service
ref.listen<String?>(authTokenProvider3, (prev, next) {
s.updateAuthToken(next);
});
ref.onDispose(() { ref.onDispose(() {
try { try {
s.dispose(); s.dispose();
@@ -242,12 +246,12 @@ final attachmentUploadQueueProvider = Provider<AttachmentUploadQueue?>((ref) {
// Auth token integration with API service - using unified auth system // Auth token integration with API service - using unified auth system
final apiTokenUpdaterProvider = Provider<void>((ref) { final apiTokenUpdaterProvider = Provider<void>((ref) {
// Listen to unified auth token changes and update API service // Listen to unified auth token changes and update API service
ref.listen(authTokenProvider3, (previous, next) { ref.listen<String?>(authTokenProvider3, (previous, next) {
final api = ref.read(apiServiceProvider); final api = ref.read(apiServiceProvider);
if (api != null && next != null && next.isNotEmpty) { if (api != null) {
api.updateAuthToken(next); api.updateAuthToken(next ?? '');
foundation.debugPrint( foundation.debugPrint(
'DEBUG: Updated API service with unified auth token', 'DEBUG: Applied auth token to API (len=${next?.length ?? 0})',
); );
} }
}); });

View File

@@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers/app_providers.dart'; import '../providers/app_providers.dart';
@@ -13,6 +14,7 @@ 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';
import '../utils/debug_logger.dart'; import '../utils/debug_logger.dart';
import '../models/server_config.dart';
enum _ConversationWarmupStatus { idle, warming, complete } enum _ConversationWarmupStatus { idle, warming, complete }
@@ -56,6 +58,14 @@ void _scheduleConversationWarmup(Ref ref, {bool force = false}) {
return; 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 statusController = ref.read(_conversationWarmupStatusProvider.notifier);
final status = ref.read(_conversationWarmupStatusProvider); final status = ref.read(_conversationWarmupStatusProvider);
@@ -80,6 +90,9 @@ void _scheduleConversationWarmup(Ref ref, {bool force = false}) {
statusController.set(_ConversationWarmupStatus.warming); statusController.set(_ConversationWarmupStatus.warming);
Future.microtask(() async { Future.microtask(() async {
if (extraDelay > 0) {
await Future.delayed(Duration(milliseconds: extraDelay));
}
try { try {
final existing = ref.read(conversationsProvider); final existing = ref.read(conversationsProvider);
if (existing.hasValue) { if (existing.hasValue) {
@@ -109,6 +122,7 @@ final appStartupFlowProvider = Provider<void>((ref) {
// Ensure token integration listeners are active // Ensure token integration listeners are active
ref.watch(authApiIntegrationProvider); ref.watch(authApiIntegrationProvider);
ref.watch(apiTokenUpdaterProvider); ref.watch(apiTokenUpdaterProvider);
ref.watch(silentLoginCoordinatorProvider);
// Kick background model loading flow (non-blocking) // Kick background model loading flow (non-blocking)
ref.watch(backgroundModelLoadProvider); ref.watch(backgroundModelLoadProvider);
@@ -129,8 +143,35 @@ final appStartupFlowProvider = Provider<void>((ref) {
final connectivityService = ref.watch(connectivityServiceProvider); final connectivityService = ref.watch(connectivityServiceProvider);
PersistentStreamingService().attachConnectivityService(connectivityService); 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,
Future.microtask(() => _scheduleConversationWarmup(ref)); // 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 // Watch for auth transitions to trigger warmup and other background work
ref.listen<AuthNavigationState>(authNavigationStateProvider, (prev, next) { ref.listen<AuthNavigationState>(authNavigationStateProvider, (prev, next) {
@@ -151,7 +192,15 @@ final appStartupFlowProvider = Provider<void>((ref) {
DebugLogger.auth('StartupFlow: Applied auth token to API'); DebugLogger.auth('StartupFlow: Applied auth token to API');
} }
// Preload default model in background (best-effort) // 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 { try {
await ref.read(defaultModelProvider.future); await ref.read(defaultModelProvider.future);
} catch (e) { } catch (e) {
@@ -159,6 +208,7 @@ final appStartupFlowProvider = Provider<void>((ref) {
'StartupFlow: default model preload failed: $e', 'StartupFlow: default model preload failed: $e',
); );
} }
});
// Kick background chat warmup now that we're authenticated // Kick background chat warmup now that we're authenticated
_scheduleConversationWarmup(ref, force: true); _scheduleConversationWarmup(ref, force: true);
@@ -199,6 +249,66 @@ final appStartupFlowProvider = Provider<void>((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<bool> {
@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<void>((ref) {
Future<void> 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<AuthNavigationState>(authNavigationStateProvider, (prev, next) {
check();
});
ref.listen<AsyncValue<ServerConfig?>>(activeServerProvider, (prev, next) {
check();
});
});
/// Listens to app lifecycle and refreshes server state when app returns to foreground. /// 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, /// Rationale: Socket.IO does not replay historical events. If the app was suspended,

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@@ -35,7 +36,16 @@ class RouterNotifier extends ChangeNotifier {
late final List<ProviderSubscription<dynamic>> _subscriptions; late final List<ProviderSubscription<dynamic>> _subscriptions;
void _onStateChanged(dynamic previous, dynamic next) { void _onStateChanged(dynamic previous, dynamic next) {
// Debounce router refreshes to avoid thrashing on rapid state changes
_scheduleRefresh();
}
Timer? _refreshDebounce;
void _scheduleRefresh() {
_refreshDebounce?.cancel();
_refreshDebounce = Timer(const Duration(milliseconds: 50), () {
notifyListeners(); notifyListeners();
});
} }
String? redirect(BuildContext context, GoRouterState state) { String? redirect(BuildContext context, GoRouterState state) {
@@ -44,11 +54,13 @@ class RouterNotifier extends ChangeNotifier {
final activeServerAsync = ref.read(activeServerProvider); final activeServerAsync = ref.read(activeServerProvider);
if (reviewerMode) { if (reviewerMode) {
// Stay on whatever route if already in chat; otherwise go to chat
if (location == Routes.chat) return null; if (location == Routes.chat) return null;
return Routes.chat; return Routes.chat;
} }
if (activeServerAsync.isLoading) { if (activeServerAsync.isLoading) {
// Avoid redirect loops by keeping splash during server loading
return location == Routes.splash ? null : Routes.splash; return location == Routes.splash ? null : Routes.splash;
} }
@@ -60,6 +72,7 @@ class RouterNotifier extends ChangeNotifier {
final activeServer = activeServerAsync.asData?.value; final activeServer = activeServerAsync.asData?.value;
if (activeServer == null) { if (activeServer == null) {
// Allow auth-related routes while no server configured
if (_isAuthLocation(location)) return null; if (_isAuthLocation(location)) return null;
return Routes.serverConnection; return Routes.serverConnection;
} }
@@ -67,12 +80,14 @@ class RouterNotifier extends ChangeNotifier {
final authState = ref.read(authNavigationStateProvider); final authState = ref.read(authNavigationStateProvider);
switch (authState) { switch (authState) {
case AuthNavigationState.loading: case AuthNavigationState.loading:
// Keep splash while establishing session
return location == Routes.splash ? null : Routes.splash; return location == Routes.splash ? null : Routes.splash;
case AuthNavigationState.needsLogin: case AuthNavigationState.needsLogin:
case AuthNavigationState.error: case AuthNavigationState.error:
if (_isAuthLocation(location)) return null; if (_isAuthLocation(location)) return null;
return Routes.serverConnection; return Routes.serverConnection;
case AuthNavigationState.authenticated: case AuthNavigationState.authenticated:
// Avoid unnecessary redirects if already on a non-auth route
if (_isAuthLocation(location) || location == Routes.splash) { if (_isAuthLocation(location) || location == Routes.splash) {
return Routes.chat; return Routes.chat;
} }
@@ -88,6 +103,7 @@ class RouterNotifier extends ChangeNotifier {
@override @override
void dispose() { void dispose() {
_refreshDebounce?.cancel();
for (final sub in _subscriptions) { for (final sub in _subscriptions) {
sub.close(); sub.close();
} }

View File

@@ -12,6 +12,9 @@ class ConnectivityService {
final _connectivityController = final _connectivityController =
StreamController<ConnectivityStatus>.broadcast(); StreamController<ConnectivityStatus>.broadcast();
ConnectivityStatus _lastStatus = ConnectivityStatus.checking; ConnectivityStatus _lastStatus = ConnectivityStatus.checking;
int _recentFailures = 0;
Duration _interval = const Duration(seconds: 10);
int _lastLatencyMs = -1;
ConnectivityService(this._dio) { ConnectivityService(this._dio) {
_startConnectivityMonitoring(); _startConnectivityMonitoring();
@@ -20,6 +23,7 @@ class ConnectivityService {
Stream<ConnectivityStatus> get connectivityStream => Stream<ConnectivityStatus> get connectivityStream =>
_connectivityController.stream; _connectivityController.stream;
ConnectivityStatus get currentStatus => _lastStatus; ConnectivityStatus get currentStatus => _lastStatus;
int get lastLatencyMs => _lastLatencyMs;
/// Stream that emits true when connected, false when offline /// Stream that emits true when connected, false when offline
Stream<bool> get isConnected => Stream<bool> get isConnected =>
@@ -30,12 +34,12 @@ class ConnectivityService {
void _startConnectivityMonitoring() { void _startConnectivityMonitoring() {
// Initial check after a brief delay to avoid showing offline during startup // Initial check after a brief delay to avoid showing offline during startup
Timer(const Duration(milliseconds: 1000), () { Timer(const Duration(milliseconds: 800), () {
_checkConnectivity(); _checkConnectivity();
}); });
// Check periodically; balance responsiveness with battery/network usage // Check periodically; interval adapts to recent failures
_connectivityTimer = Timer.periodic(const Duration(seconds: 10), (_) { _connectivityTimer = Timer.periodic(_interval, (_) {
_checkConnectivity(); _checkConnectivity();
}); });
} }
@@ -45,7 +49,7 @@ class ConnectivityService {
// DNS lookup is a lightweight, permission-free reachability check // DNS lookup is a lightweight, permission-free reachability check
final result = await InternetAddress.lookup( final result = await InternetAddress.lookup(
'google.com', 'google.com',
).timeout(const Duration(seconds: 3)); ).timeout(const Duration(seconds: 2));
if (result.isNotEmpty && result[0].rawAddress.isNotEmpty) { if (result.isNotEmpty && result[0].rawAddress.isNotEmpty) {
_updateStatus(ConnectivityStatus.online); _updateStatus(ConnectivityStatus.online);
@@ -57,20 +61,23 @@ class ConnectivityService {
// As a secondary check, hit a public 204 endpoint that returns quickly // As a secondary check, hit a public 204 endpoint that returns quickly
try { try {
final start = DateTime.now();
await _dio await _dio
.get( .get(
'https://www.google.com/generate_204', 'https://www.google.com/generate_204',
options: Options( options: Options(
method: 'GET', method: 'GET',
sendTimeout: const Duration(seconds: 3), sendTimeout: const Duration(seconds: 2),
receiveTimeout: const Duration(seconds: 3), receiveTimeout: const Duration(seconds: 2),
followRedirects: false, followRedirects: false,
validateStatus: (status) => status != null && status < 400, 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); _updateStatus(ConnectivityStatus.online);
} catch (_) { } catch (_) {
_lastLatencyMs = -1;
_updateStatus(ConnectivityStatus.offline); _updateStatus(ConnectivityStatus.offline);
} }
} }
@@ -80,6 +87,28 @@ class ConnectivityService {
_lastStatus = status; _lastStatus = status;
_connectivityController.add(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<bool> checkConnectivity() async { Future<bool> checkConnectivity() async {

View File

@@ -4,18 +4,19 @@ import '../models/server_config.dart';
class SocketService { class SocketService {
final ServerConfig serverConfig; final ServerConfig serverConfig;
final String? authToken;
final bool websocketOnly; final bool websocketOnly;
io.Socket? _socket; io.Socket? _socket;
String? _authToken;
SocketService({ SocketService({
required this.serverConfig, required this.serverConfig,
required this.authToken, String? authToken,
this.websocketOnly = false, this.websocketOnly = false,
}); }) : _authToken = authToken;
String? get sessionId => _socket?.id; String? get sessionId => _socket?.id;
io.Socket? get socket => _socket; io.Socket? get socket => _socket;
String? get authToken => _authToken;
bool get isConnected => _socket?.connected == true; bool get isConnected => _socket?.connected == true;
@@ -39,9 +40,7 @@ class SocketService {
final builder = io.OptionBuilder() final builder = io.OptionBuilder()
// Transport selection // Transport selection
.setTransports( .setTransports(websocketOnly ? ['websocket'] : ['polling', 'websocket'])
websocketOnly ? ['websocket'] : ['polling', 'websocket'],
)
.setRememberUpgrade(!websocketOnly) .setRememberUpgrade(!websocketOnly)
.setUpgrade(!websocketOnly) .setUpgrade(!websocketOnly)
// Tune reconnect/backoff and timeouts // Tune reconnect/backoff and timeouts
@@ -55,9 +54,9 @@ class SocketService {
// Merge Authorization (if any) with user-defined custom headers for the // Merge Authorization (if any) with user-defined custom headers for the
// Socket.IO handshake. Avoid overriding reserved headers. // Socket.IO handshake. Avoid overriding reserved headers.
final Map<String, String> extraHeaders = {}; final Map<String, String> extraHeaders = {};
if (authToken != null && authToken!.isNotEmpty) { if (_authToken != null && _authToken!.isNotEmpty) {
extraHeaders['Authorization'] = 'Bearer $authToken'; extraHeaders['Authorization'] = 'Bearer $_authToken';
builder.setAuth({'token': authToken}); builder.setAuth({'token': _authToken});
} }
if (serverConfig.customHeaders.isNotEmpty) { if (serverConfig.customHeaders.isNotEmpty) {
final reserved = { final reserved = {
@@ -78,7 +77,8 @@ class SocketService {
final lower = key.toLowerCase(); final lower = key.toLowerCase();
if (!reserved.contains(lower) && value.isNotEmpty) { if (!reserved.contains(lower) && value.isNotEmpty) {
// Do not overwrite Authorization we already set from authToken // Do not overwrite Authorization we already set from authToken
if (lower == 'authorization' && extraHeaders.containsKey('Authorization')) { if (lower == 'authorization' &&
extraHeaders.containsKey('Authorization')) {
return; return;
} }
extraHeaders[key] = value; extraHeaders[key] = value;
@@ -93,9 +93,9 @@ class SocketService {
_socket!.on('connect', (_) { _socket!.on('connect', (_) {
debugPrint('Socket connected: ${_socket!.id}'); debugPrint('Socket connected: ${_socket!.id}');
if (authToken != null && authToken!.isNotEmpty) { if (_authToken != null && _authToken!.isNotEmpty) {
_socket!.emit('user-join', { _socket!.emit('user-join', {
'auth': {'token': authToken} 'auth': {'token': _authToken},
}); });
} }
}); });
@@ -110,10 +110,10 @@ class SocketService {
_socket!.on('reconnect', (attempt) { _socket!.on('reconnect', (attempt) {
debugPrint('Socket reconnected after $attempt attempts'); debugPrint('Socket reconnected after $attempt attempts');
if (authToken != null && authToken!.isNotEmpty) { if (_authToken != null && _authToken!.isNotEmpty) {
// Best-effort rejoin // Best-effort rejoin
_socket!.emit('user-join', { _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<String, dynamic> event) handler) { void onChatEvents(void Function(Map<String, dynamic> event) handler) {
_socket?.on('chat-events', (data) { _socket?.on('chat-events', (data) {
try { try {
@@ -168,6 +183,7 @@ class SocketService {
void offEvent(String eventName) { void offEvent(String eventName) {
_socket?.off(eventName); _socket?.off(eventName);
} }
void dispose() { void dispose() {
try { try {
_socket?.dispose(); _socket?.dispose();
@@ -177,7 +193,9 @@ class SocketService {
// Best-effort: ensure there is an active connection and wait briefly. // Best-effort: ensure there is an active connection and wait briefly.
// Returns true if connected by the end of the timeout. // Returns true if connected by the end of the timeout.
Future<bool> ensureConnected({Duration timeout = const Duration(seconds: 2)}) async { Future<bool> ensureConnected({
Duration timeout = const Duration(seconds: 2),
}) async {
if (isConnected) return true; if (isConnected) return true;
try { try {
await connect(); await connect();

View File

@@ -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,
),
);
}

View File

@@ -10,8 +10,7 @@ class AuthActions {
final Ref _ref; final Ref _ref;
AuthActions(this._ref); AuthActions(this._ref);
AuthStateManager get _auth => AuthStateManager get _auth => _ref.read(authStateManagerProvider.notifier);
_ref.read(authStateManagerProvider.notifier);
Future<bool> login( Future<bool> login(
String username, String username,
@@ -19,21 +18,25 @@ class AuthActions {
bool rememberCredentials = false, bool rememberCredentials = false,
}) { }) {
// Defer mutation to a microtask to avoid provider-build side-effects // Defer mutation to a microtask to avoid provider-build side-effects
return Future(() => _auth.login( return Future(
() => _auth.login(
username, username,
password, password,
rememberCredentials: rememberCredentials, rememberCredentials: rememberCredentials,
)); ),
);
} }
Future<bool> loginWithApiKey( Future<bool> loginWithApiKey(
String apiKey, { String apiKey, {
bool rememberCredentials = false, bool rememberCredentials = false,
}) { }) {
return Future(() => _auth.loginWithApiKey( return Future(
() => _auth.loginWithApiKey(
apiKey, apiKey,
rememberCredentials: rememberCredentials, rememberCredentials: rememberCredentials,
)); ),
);
} }
Future<bool> silentLogin() { Future<bool> silentLogin() {

View File

@@ -1,3 +1,5 @@
import 'dart:async';
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_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -8,27 +10,51 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'core/providers/app_providers.dart'; import 'core/providers/app_providers.dart';
import 'core/router/app_router.dart'; import 'core/router/app_router.dart';
import 'shared/theme/app_theme.dart'; import 'shared/theme/app_theme.dart';
import 'shared/theme/theme_extensions.dart';
import 'shared/widgets/offline_indicator.dart'; import 'shared/widgets/offline_indicator.dart';
import 'features/auth/providers/unified_auth_providers.dart'; import 'features/auth/providers/unified_auth_providers.dart';
import 'core/auth/auth_state_manager.dart'; import 'core/auth/auth_state_manager.dart';
import 'core/utils/debug_logger.dart'; import 'core/utils/debug_logger.dart';
import 'core/utils/system_ui_style.dart';
import 'package:conduit/l10n/app_localizations.dart'; import 'package:conduit/l10n/app_localizations.dart';
import 'core/services/share_receiver_service.dart'; import 'core/services/share_receiver_service.dart';
import 'core/providers/app_startup_providers.dart'; import 'core/providers/app_startup_providers.dart';
import 'core/models/server_config.dart';
void main() async { developer.TimelineTask? _startupTimeline;
void main() {
runZonedGuarded(
() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
// 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;
};
// 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) // Enable edge-to-edge globally (back-compat on pre-Android 15)
// Pairs with Activity's EdgeToEdge.enable and our SafeArea usage. // Pairs with Activity's EdgeToEdge.enable and our SafeArea usage.
// Do not block first frame on system UI mode; apply shortly after startup // 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');
final sharedPrefs = await SharedPreferences.getInstance(); final sharedPrefs = await SharedPreferences.getInstance();
_startupTimeline!.instant('shared_prefs_ready');
const secureStorage = FlutterSecureStorage( const secureStorage = FlutterSecureStorage(
aOptions: AndroidOptions( aOptions: AndroidOptions(
encryptedSharedPreferences: true, encryptedSharedPreferences: true,
@@ -41,6 +67,14 @@ void main() async {
synchronizable: false, 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( runApp(
ProviderScope( ProviderScope(
@@ -51,6 +85,13 @@ void main() async {
child: const ConduitApp(), child: const ConduitApp(),
), ),
); );
developer.Timeline.instantSync('runApp_called');
},
(error, stack) {
DebugLogger.error('Uncaught zone error', error);
debugPrint(stack.toString());
},
);
} }
class ConduitApp extends ConsumerStatefulWidget { class ConduitApp extends ConsumerStatefulWidget {
@@ -61,42 +102,19 @@ class ConduitApp extends ConsumerStatefulWidget {
} }
class _ConduitAppState extends ConsumerState<ConduitApp> { class _ConduitAppState extends ConsumerState<ConduitApp> {
bool _attemptedSilentAutoLogin = false; ProviderSubscription<void>? _startupFlowSubscription;
ProviderSubscription<AuthNavigationState>? _authNavSubscription; Brightness? _lastAppliedOverlayBrightness;
ProviderSubscription<AsyncValue<ServerConfig?>>? _activeServerSubscription;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Defer heavy provider initialization to after first frame to render UI sooner // Defer heavy provider initialization to after first frame to render UI sooner
WidgetsBinding.instance.addPostFrameCallback((_) => _initializeAppState()); WidgetsBinding.instance.addPostFrameCallback((_) => _initializeAppState());
_authNavSubscription = ref.listenManual<AuthNavigationState>( // Activate app startup flow without tying it to root widget rebuilds
authNavigationStateProvider, _startupFlowSubscription = ref.listenManual<void>(
(previous, next) { appStartupFlowProvider,
if (next == AuthNavigationState.needsLogin) { (previous, next) {},
_maybeAttemptSilentLogin();
} else {
_attemptedSilentAutoLogin = false;
}
},
); );
_activeServerSubscription = ref.listenManual<AsyncValue<ServerConfig?>>(
activeServerProvider,
(previous, next) {
next.when(
data: (server) {
if (server != null) {
_maybeAttemptSilentLogin();
}
},
loading: () {},
error: (error, stackTrace) {},
);
},
);
Future.microtask(_maybeAttemptSilentLogin);
} }
void _initializeAppState() { void _initializeAppState() {
@@ -110,63 +128,17 @@ class _ConduitAppState extends ConsumerState<ConduitApp> {
@override @override
void dispose() { void dispose() {
_authNavSubscription?.close(); _startupFlowSubscription?.close();
_activeServerSubscription?.close();
super.dispose(); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final themeMode = ref.watch(themeModeProvider.select((mode) => mode)); final themeMode = ref.watch(themeModeProvider.select((mode) => mode));
final router = ref.watch(goRouterProvider); 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); final locale = ref.watch(localeProvider);
return AnimatedThemeWrapper( return ErrorBoundary(
theme: currentTheme,
duration: AnimationDuration.medium,
child: ErrorBoundary(
child: MaterialApp.router( child: MaterialApp.router(
routerConfig: router, routerConfig: router,
onGenerateTitle: (context) => AppLocalizations.of(context)!.appTitle, onGenerateTitle: (context) => AppLocalizations.of(context)!.appTitle,
@@ -190,6 +162,14 @@ class _ConduitAppState extends ConsumerState<ConduitApp> {
return supported.first; return supported.first;
}, },
builder: (context, child) { 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); final mediaQuery = MediaQuery.of(context);
return MediaQuery( return MediaQuery(
data: mediaQuery.copyWith( data: mediaQuery.copyWith(
@@ -202,7 +182,6 @@ class _ConduitAppState extends ConsumerState<ConduitApp> {
); );
}, },
), ),
),
); );
} }
} }