diff --git a/CHANGELOG.md b/CHANGELOG.md index be944b3..334452a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.1.14] - 2026-01-04 + +### Fixed +- Theme toggle causing auth issues due to ThemeProvider auto-dispose + - Added `@persistentRiverpod` annotation for providers that need keepAlive + - ThemeProvider now persists for app lifetime + +### Added +- `@persistentRiverpod` annotation in `core/providers/annotations.dart` + - Reusable annotation for providers that should not auto-dispose + - Documented in ARCHITECTURE.md + ## [1.1.13] - 2026-01-04 ### Added diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index c9a742d..58bae8a 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -420,6 +420,41 @@ ContainerRepository containerRepository(Ref ref) { } ``` +### Persistent Providers + +By default, `@riverpod` generates providers with `isAutoDispose: true`, meaning they dispose when no longer watched. This causes issues for: + +- **API clients** with interceptors that store a `Ref` +- **App-level state** like theme, auth, config +- **Providers with listeners** to other providers + +Use `@persistentRiverpod` from `core/providers/annotations.dart` for these cases: + +```dart +import 'package:tatlock_ui/core/providers/annotations.dart'; + +// ✅ Correct - persists for app lifetime +@persistentRiverpod +Dio coreApiClient(Ref ref) { ... } + +@persistentRiverpod +class ThemeNotifier extends _$ThemeNotifier { ... } + +// ❌ Wrong - auto-dispose can invalidate stored Ref +@riverpod +Dio coreApiClient(Ref ref) { ... } +``` + +**When to use `@persistentRiverpod`:** + +| Use Case | Annotation | +|----------|------------| +| API clients with interceptors | `@persistentRiverpod` | +| Theme/config providers | `@persistentRiverpod` | +| Auth state provider | `@persistentRiverpod` | +| Feature data providers | `@riverpod` (default) | +| UI state providers | `@riverpod` (default) | + ## File Naming Conventions | Type | Convention | Example | diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index 1c76837..aacef54 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -2,6 +2,7 @@ import 'package:dio/dio.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:tatlock_ui/core/api/api_interceptors.dart'; import 'package:tatlock_ui/core/config/app_config.dart'; +import 'package:tatlock_ui/core/providers/annotations.dart'; import 'api_client_native.dart' if (dart.library.html) 'api_client_web.dart' as platform; @@ -9,10 +10,7 @@ import 'api_client_native.dart' if (dart.library.html) 'api_client_web.dart' part 'api_client.g.dart'; /// Provides the Dio instance for Core API. -/// -/// Uses keepAlive to prevent auto-dispose - the AuthInterceptor stores -/// a Ref that must remain valid for the lifetime of API requests. -@Riverpod(keepAlive: true) +@persistentRiverpod Dio coreApiClient(Ref ref) { final options = BaseOptions( baseUrl: AppConfig.coreApiUrl, @@ -36,10 +34,7 @@ Dio coreApiClient(Ref ref) { } /// Provides the Dio instance for Tatlock API. -/// -/// Uses keepAlive to prevent auto-dispose - the AuthInterceptor stores -/// a Ref that must remain valid for the lifetime of API requests. -@Riverpod(keepAlive: true) +@persistentRiverpod Dio tatlockApiClient(Ref ref) { final options = BaseOptions( baseUrl: AppConfig.tatlockApiUrl, diff --git a/lib/core/providers/annotations.dart b/lib/core/providers/annotations.dart new file mode 100644 index 0000000..9a77e84 --- /dev/null +++ b/lib/core/providers/annotations.dart @@ -0,0 +1,15 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +/// Riverpod annotation for providers that should persist for the app lifetime. +/// +/// Use this instead of `@riverpod` when: +/// - The provider holds app-level state (theme, auth, config) +/// - The provider stores a Ref that must remain valid (API clients with interceptors) +/// - Disposing would cause flickering or re-initialization issues +/// +/// Example: +/// ```dart +/// @persistentRiverpod +/// class ThemeNotifier extends _$ThemeNotifier { ... } +/// ``` +const persistentRiverpod = Riverpod(keepAlive: true); diff --git a/lib/core/theme/theme_provider.dart b/lib/core/theme/theme_provider.dart index 2e7707c..e57859d 100644 --- a/lib/core/theme/theme_provider.dart +++ b/lib/core/theme/theme_provider.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:tatlock_ui/core/auth/auth_provider.dart'; +import 'package:tatlock_ui/core/providers/annotations.dart'; part 'theme_provider.g.dart'; @@ -23,7 +24,7 @@ enum ThemeSetting { /// /// Syncs with API preferences when user is authenticated. On login, the theme /// from API preferences takes precedence over local storage. -@riverpod +@persistentRiverpod class ThemeNotifier extends _$ThemeNotifier { static const _prefsKey = 'theme_setting'; diff --git a/pubspec.yaml b/pubspec.yaml index 7bd28a5..f815b08 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.1.13+1 +version: 1.1.14+1 environment: sdk: ^3.10.4