mirror of
https://github.com/hanskokx/arcane_framework.git
synced 2026-08-12 06:10:55 +02:00
v2.0.0: Overhaul core, services, and app integration (#6)
* [UNTESTED] Fixes notifiers and adds some additional methods. Adds tests. Changes: // ArcaneEnvironment breaking: context.read<ArcaneEnvironment>() -> ArcaneEnvironment.of(context) breaking: context.read<ArcaneEnvironment>().state -> ArcaneEnvironment.of(context).environment; // Feature flag service added: reset() // Logging service added: registerInterface() added: unregisterInterfaces() added: unregisterAllInterfaces() // ArcaneReactiveTheme fixed: currentMode, dark, light now actually emit new values when changed added: getters for lightTheme, darkTheme, and systemTheme TODO: test systemTheme Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * ArcaneReactiveTheme now optionally takes a ThemeMode parameter when calling switchTheme Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Added `of(context)` extension to `ArcaneService` * Updated changelog * Logging service: - added: reset() - added: `skipAutodetection` option for module, method, and metadata - added: unregisterInterface() - Created FileAndLineNumber mixin Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fix module/method/fileAndLineNumber calculations in logging service Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Minor fix to fileAndLineParts calculation Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Remove debug logging Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Minor change Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fixed theme - fixed: currentMode, dark, light now actually emit new values when changed - added: getters for lightTheme, darkTheme, and systemTheme - breaking: currentMode -> systemTheme - added: currentTheme - breaking: currentMode => currentTheme - change: Arcane.theme.followSystemTheme(context) required to follow system theme - change: After following the system theme, calling Arcane.theme.switchTheme() cancels following the system theme - added: ArcaneReactiveTheme is now registered in ArcaneServiceProvider when using ArcaneApp Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Removed arcane theme from service provider Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fixed late initialized value in logging service * Fixes the reactive theme service to properly follow the system brightness Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fixes tests and updates reactive theme Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fixes broken tests Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fixes bug with following/unfollowing system theme Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Re-add getters on ArcaneTheme Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Update example Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Added logStream to logger. Updated example code. Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Setting a theme style now automatically switches to that theme Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Updated theme example to set color of theme Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Added examples for feature flags Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Added environment example Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Center text Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Added environment logging Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Rename onEnvironmentChanged to switchEnvironment Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Made ArcaneServiceProvider.of(context) nullable Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Remove invalid test Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Extra log messages will not longer be added to the log stream if more than one LoggingInterface is registered Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Only send log messages to the LoggingInterfaces if the logging service has been initialized Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Check for empty interfaces before initializing them in the logger Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Moved platform brightness checking from ArcaneApp to ArcaneThemeSwitcher, where it is more appropriate Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Correctly export the theme switcher widget Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Remove unused import Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Made the switchEnvironment field in ArcaneEnvironment private (_switchEnvironment) Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Testing GH actions Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Set correct .puro.json filename in github action config Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Check different path for .puro.json Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Test different GH action workflow Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Attempt to correctly address "stable" version in .puro.json Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Another GH action attempt Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Add environment variables Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Debug Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Add puro config to git :) Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Remove debug Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * feat(framework): Overhauls framework core, services, and app integration for 2.0.0 (#5) * Made ArcaneTheme private (_ArcaneTheme) Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Updated changelog Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fix updateShouldNotify on ArcaneServiceProvider Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Update service provider Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Put service locators in an extension Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Try using a mixin instead Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Remove 'of' and 'requiredOf' locators Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Re-add service locators. They should be called as: ArcaneService.of<MyService>(context) Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Renamed getters on ArcaneService Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Renamed serviceInstances => registeredServices and added a removeService method Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Broke up arcane service into separate files Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Added requiredServiceOfType getter on ArcaneServiceProvider Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Updated services section of the readme Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Updated example to add a services example Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Removed unused variable Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Update example documentation Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Update example Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Breaking up example into smaller widgets Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Remove bloc dependency Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fixes the notifier for feature flags and updates the example Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fixed a bug in the example Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Update ServiceProvider to make service instances optional Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Remove unnecessary notifyListeners calls Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Organize theme service Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Fix authentication interface and service methods for logout and login. * Fix DebugAuthInterface logout method Added environment key to ArcaneEnvironment constructor Renamed switchEnvironment parameter in ArcaneEnvironment constructor Bumped arcane_helper_utils dependency version * Enhance theme management with StreamBuilder for dynamic updates Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Add configuration files and update authentication service error handling - Introduced .vscode/settings.json and .vscode/launch.json for IDE configuration. - Updated DebugAuthInterface and ArcaneAuthenticationService to return const Result.ok() for consistency. - Added ArcaneTheme class for theme management. - Updated pubspec.yaml to change result_monad dependency version. - Modified authentication_service_test to return const Result.ok() in mock setups. Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Update dependency versions to use 'any' for arcane_helper_utils and arcane_analysis Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Remove unnecessary 'const' keyword from Result.ok calls in DebugAuthInterface methods Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Add 'const' keyword to Result.ok calls in DebugAuthInterface methods Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Enhance logging service with new lifecycle capabilities and interceptor support - Updated `LoggingInterface` to remove singleton-style initialization. - Introduced `LoggingInitializable` and `LoggingInitializationMixin` for optional lifecycle management. - Added `LogEvent` and `LogInterceptor` classes for improved logging event handling and interception. - Updated `ArcaneLogger` to support multiple logging interfaces and global interceptors. - Modified `DebugPrint` and `DebugAuthInterface` to align with new logging structure. - Enhanced tests to cover new logging features and interceptor functionality. Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Add stream-based updates for authentication, feature flags, logging, and theming - Added `statusChanges` and `signedInChanges` streams to `ArcaneAuth` for real-time authentication updates. - Introduced `enabledFeaturesChanges` stream in `ArcaneFeatureFlags` for observing feature updates. - Improved `ArcaneLogger` to maintain `logStream` usability after listener cancellation. - Updated `ArcaneTheme` to provide streams for theme mode and theme data changes. - Enhanced documentation and examples to reflect new stream APIs and usage patterns. - Added regression tests for stream listener cancellation and re-subscription across services. Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Refactor ArcaneEnvironment and AuthenticationService for improved environment management and status handling Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Add application environment management and tests for authentication status coherence Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * feat: Enhance service management with ArcaneApp provider composition This commit significantly refactors Arcane's service architecture. ArcaneApp now acts as a StatefulWidget, composing InheritedWidget- based providers for core services (features, environment, theme, auth). Service lookups (e.g., Arcane.features, context.service()) now prioritize provider-registered instances, falling back to singletons. Key changes include: - Renamed ArcaneFeatureFlags to ArcaneFeatureFlagService and ArcaneReactiveTheme to ArcaneThemeService (with typedefs). - Introduced ArcaneEnvironmentService and ArcaneEnvironmentProvider for centralized environment management. - Added BuildContext extensions (e.g., context.featureFlags) for reactive, convenient service access. - Removed .puro.json and its references, streamlining SDK setup. - Updated CHANGELOG.md to reflect a 2.0.0 release, detailing all breaking changes and new features. * chore(example): Add ignore rules for example builds * feat(logging): Introduce log interceptors and origin control Introduces the LogInterceptor class, enabling pre-processing, modification, or suppression of log events before they reach registered interfaces. The LogInterceptorContext provides contextual information such as the originating LoggingInterface. Adds a `skipAutodetection` parameter to Arcane.log, allowing control over the automatic detection of log origin (module, method, file/line). Also makes the LogEvent class extendable to support custom event types and updates existing interceptor callback signatures for consistency. * fix(theme): Update theme mode check to use currentModeOf method Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * fix(theme): Improve theme mode detection and toggling `context.isDarkMode` now reflects the app's effective theme brightness, not the raw platform brightness. This ensures `isDarkMode` accurately reports the currently rendered theme. `switchTheme()` now intelligently toggles from the *effective* theme when in `ThemeMode.system` by considering the current app brightness. `followSystemTheme()` is updated to always read the platform brightness directly, decoupling its behavior from the app's overridden theme settings. * feat(example): Improve theme and service integration demo Updates the example application to better demonstrate service and theme integration. The `FavoriteColorService` now actively sets the app's color scheme and can initialize its state from the current theme. Environment display is now reactive, and the UI responds dynamically to service registration and removal. Initial themes are now explicitly seeded. * feat(example): Add interactive persistent metadata toggle Previously, persistent metadata was added unconditionally. This change allows users to dynamically enable or disable the demo persistent metadata via a UI switch, better showcasing the API functionality. * feat(theme): Add assignment-style theme setters Introduces convenience setters for dark and light themes, allowing `Arcane.theme.dark = ...` and `Arcane.theme.light = ...` as an alternative to `setDarkTheme` and `setLightTheme`. This improves developer ergonomics and conciseness. * docs(README): Add Application Environments section to features list Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * Adds `ArcaneApp.builder` to provide a provider-aware `BuildContext` for the application root. This deprecates the `ArcaneApp.child` parameter, improving developer experience for accessing Arcane's providers at build time. Updates the `ArcaneThemeSwitcher` to default to `followSystemTheme` when mounted under `ArcaneApp`, ensuring system theme behavior is enabled out of the box. Fixes `setDarkTheme` and `setLightTheme` to only update the rendered theme if their respective modes are currently active. This prevents unintended UI changes when updating the definition of an inactive theme. Updates README and examples to reflect the new `builder` API and clarified theme logic. Adds new tests for these changes. * docs(arcane_app): Clarify ArcaneApp API and provider access documentation Updates the documentation for the `ArcaneApp` constructor to explicitly detail the preferred `builder` API and the deprecated `child` parameter, including migration guidance. Also updates the list of provided services to include `ArcaneFeatureFlagsProvider`. * feat: Overhaul Arcane framework for 2.0.0 release notes Migrate Arcane to a static utility surface, removing the instantiable singleton constructor for simpler and more direct access to services. Introduce new reactive streams for environment, authentication, and theme services, enabling real-time observation of state changes. Enhance `ArcaneApp` integration with the new `builder` callback and `BuildContext` convenience accessors for services and feature flags. Update `ArcaneAuthInterface.logout` signature and revise `src` import paths, requiring consumers to update their implementations and imports. Deprecate `ArcaneApp.child` and `BuildContext.serviceOfType<T>()` in favor of new, more idiomatic APIs. Remove direct `flutter_bloc` dependency and upgrade `result_monad` to `^4.0.0`. * refactor(environment): Rename environment access getter to current This change unifies how the current application environment is accessed across Arcane's services and providers. The `state` and `environment` getters have been replaced with a consistent `current` getter, simplifying API usage and aligning with new patterns. Includes corresponding updates in example and internal code, and documents the migration in the CHANGELOG. * chore: Relax result_monad dependency constraint The `result_monad` dependency was recently upgraded to `^4.0.0`. This update relaxes the constraint to `any` to provide greater flexibility and mitigate potential dependency conflicts for consumers of Arcane, allowing them to use other compatible versions. * refactor(logging): Replace LoggingInitializationMixin with LoggingInitialization and add LoggingFeature annotation support Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * docs(changelog): Add migration steps for ArcaneThemeService and update themeMode configuration examples Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * refactor(workflow): Simplify analyze-and-test workflow by removing unused environment variables and steps Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * docs(changelog): Update Arcane.log metadata type and add migration steps for structured values Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * test: Add unit tests for authentication and feature flags functionality Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * feat(theme): track user-defined theme overrides in ArcaneThemeService Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * refactor(service): update service access methods in ArcaneServiceProvider Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * refactor(tests): format login and logout method signatures in MockAuth for improved readability Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * feat(tests): add unit tests for ArcaneThemeService functionality Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> --------- Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * fix(dependencies): update result_monad and arcane_helper_utils constraints Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * fix(version): update version from 2.0.0-dev to 2.0.0 Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * feat(auth): add callbacks for login/logout events in DebugAuthInterface fix(logging): ensure proper cancellation of log stream subscriber refactor(arcane): make built-in services list unmodifiable fix(app): update service notifier on widget updates fix(service): improve removeService method to return boolean fix(auth): update BuildContext parameter to unused in setDebug/setNormal fix(logging): correct log level reference in LogInterceptorContext documentation fix(theme): update documentation to reflect ThemeMode instead of ThemeData fix(theme): ensure unawaited cancellation of theme subscriptions chore(dependencies): update arcane_analysis version to ^1.0.4 test: make ArcaneAuthenticationService reset asynchronous in tests Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * feat(theme): enhance theme synchronization and documentation for ArcaneApp Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> * fix(gitignore): add example/.metadata to .gitignore Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> --------- Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com> Co-authored-by: Hans Kokx <hans.kokx@airahome.com>
This commit is contained in:
@@ -11,10 +11,10 @@
|
||||
/// - **Service Management**: Centralized access to critical services like
|
||||
/// logging, feature flags, and theming.
|
||||
/// - **Feature Flags**: Dynamically enable or disable features using
|
||||
/// `ArcaneFeatureFlags`.
|
||||
/// `ArcaneFeatureFlagService`.
|
||||
/// - **Logging**: Flexible logging with different severity levels
|
||||
/// (`debug`, `info`, `error`, etc.).
|
||||
/// - **Theming**: Easy light/dark mode switching with `ArcaneReactiveTheme`.
|
||||
/// - **Theming**: Easy light/dark mode switching with `ArcaneThemeService`.
|
||||
/// - **Authentication**: Manage user login, sign up, and token-based
|
||||
/// authentication.
|
||||
///
|
||||
@@ -39,10 +39,17 @@ library;
|
||||
|
||||
export "package:arcane_framework/src/arcane.dart";
|
||||
export "package:arcane_framework/src/arcane_app.dart";
|
||||
export "package:arcane_framework/src/providers/environment_provider.dart";
|
||||
export "package:arcane_framework/src/providers/service_provider.dart";
|
||||
export "package:arcane_framework/src/service/arcane_service.dart";
|
||||
export "package:arcane_framework/src/services/authentication/authentication_service.dart";
|
||||
export "package:arcane_framework/src/services/environment/environment_interface.dart";
|
||||
export "package:arcane_framework/src/services/environment/environment_provider.dart";
|
||||
export "package:arcane_framework/src/services/environment/environment_service.dart";
|
||||
export "package:arcane_framework/src/services/feature_flags/feature_flags_context_extensions.dart";
|
||||
export "package:arcane_framework/src/services/feature_flags/feature_flags_provider.dart";
|
||||
export "package:arcane_framework/src/services/feature_flags/feature_flags_service.dart";
|
||||
export "package:arcane_framework/src/services/logging/logging_service.dart";
|
||||
export "package:arcane_framework/src/services/reactive_theme/reactive_theme_service.dart";
|
||||
export "package:arcane_framework/src/services/theme/arcane_theme.dart";
|
||||
export "package:arcane_framework/src/services/theme/theme_extensions.dart";
|
||||
export "package:arcane_framework/src/services/theme/theme_service.dart";
|
||||
export "package:arcane_framework/src/services/theme/theme_switcher.dart";
|
||||
export "package:result_monad/result_monad.dart";
|
||||
|
||||
+61
-30
@@ -1,4 +1,11 @@
|
||||
import "package:arcane_framework/arcane_framework.dart";
|
||||
import "package:flutter/foundation.dart";
|
||||
|
||||
import "service/arcane_service.dart";
|
||||
import "services/authentication/authentication_service.dart";
|
||||
import "services/environment/environment_service.dart";
|
||||
import "services/feature_flags/feature_flags_service.dart";
|
||||
import "services/logging/logging_service.dart";
|
||||
import "services/theme/theme_service.dart";
|
||||
|
||||
/// A singleton class that acts as the central hub for various services in the
|
||||
/// Arcane framework.
|
||||
@@ -6,45 +13,63 @@ import "package:arcane_framework/arcane_framework.dart";
|
||||
/// `Arcane` provides access to important services like logging, feature flags,
|
||||
/// authentication, theming, secure storage, and ID management. It also offers a
|
||||
/// convenient method for logging messages using the integrated logger.
|
||||
class Arcane {
|
||||
Arcane._internal();
|
||||
abstract class Arcane {
|
||||
// Internal registry for service instances, set by ArcaneApp if present.
|
||||
static ValueNotifier<List<ArcaneService>>? registry;
|
||||
|
||||
/// Creates a singleton instance of `Arcane`.
|
||||
///
|
||||
/// This factory constructor always returns the same instance of `Arcane`.
|
||||
factory Arcane() => Arcane._internal();
|
||||
// Called by ArcaneApp to register the live service registry.
|
||||
/// Called by ArcaneApp to register the live service registry.
|
||||
static void setRegistry(ValueNotifier<List<ArcaneService>> r) {
|
||||
registry = r;
|
||||
}
|
||||
|
||||
// Called by ArcaneApp to clear the registry when disposed.
|
||||
/// Called by ArcaneApp to clear the registry when disposed.
|
||||
static void clearRegistry() {
|
||||
registry = null;
|
||||
}
|
||||
|
||||
// The built-in singleton services (used as fallback if no ArcaneApp is present).
|
||||
static List<ArcaneService> get builtInServices => List.unmodifiable([
|
||||
ArcaneFeatureFlagService.I,
|
||||
ArcaneAuthenticationService.I,
|
||||
ArcaneThemeService.I,
|
||||
ArcaneEnvironmentService.I,
|
||||
]);
|
||||
|
||||
/// Provides access to the singleton instance of the logger service.
|
||||
///
|
||||
/// The `ArcaneLogger` is used for logging messages throughout the app.
|
||||
/// Logger is not a service and is always the singleton.
|
||||
static ArcaneLogger get logger => ArcaneLogger.I;
|
||||
|
||||
/// Provides access to the singleton instance of the feature flags service.
|
||||
///
|
||||
/// `ArcaneFeatureFlags` manages feature toggles, allowing you to enable or
|
||||
/// disable features dynamically.
|
||||
static ArcaneFeatureFlags get features => ArcaneFeatureFlags.I;
|
||||
/// Provides access to the feature flags service instance registered in ArcaneApp, or the singleton if not present.
|
||||
static ArcaneFeatureFlagService get features =>
|
||||
services.whereType<ArcaneFeatureFlagService>().firstOrNull ??
|
||||
ArcaneFeatureFlagService.I;
|
||||
|
||||
/// Provides access to the singleton instance of the authentication service.
|
||||
///
|
||||
/// `ArcaneAuthenticationService` manages user authentication, login, and
|
||||
/// signup processes.
|
||||
static ArcaneAuthenticationService get auth => ArcaneAuthenticationService.I;
|
||||
/// Provides access to the authentication service instance registered in ArcaneApp, or the singleton if not present.
|
||||
static ArcaneAuthenticationService get auth =>
|
||||
services.whereType<ArcaneAuthenticationService>().firstOrNull ??
|
||||
ArcaneAuthenticationService.I;
|
||||
|
||||
/// Provides access to the singleton instance of the theme management service.
|
||||
///
|
||||
/// `ArcaneReactiveTheme` allows switching between light and dark themes and
|
||||
/// customizing them.
|
||||
static ArcaneReactiveTheme get theme => ArcaneReactiveTheme.I;
|
||||
/// Provides access to the theme management service instance registered in ArcaneApp, or the singleton if not present.
|
||||
/// Returns ArcaneThemeService, but is also assignable to ArcaneReactiveTheme for backward compatibility.
|
||||
static ArcaneThemeService get theme =>
|
||||
services.whereType<ArcaneThemeService>().firstOrNull ??
|
||||
ArcaneThemeService.I;
|
||||
|
||||
/// Provides access to the environment service instance registered in ArcaneApp, or the singleton if not present.
|
||||
static ArcaneEnvironmentService get environment =>
|
||||
services.whereType<ArcaneEnvironmentService>().firstOrNull ??
|
||||
ArcaneEnvironmentService.I;
|
||||
|
||||
/// Returns a list of all services available in the Arcane framework.
|
||||
///
|
||||
/// This list includes the feature flags, authentication, theme, and ID services.
|
||||
static List<ArcaneService> get services => [
|
||||
features,
|
||||
auth,
|
||||
theme,
|
||||
];
|
||||
/// This list includes the feature flags, authentication, theme, and environment services.
|
||||
/// If ArcaneApp is present, this reflects the live registry; otherwise, falls back to built-in singletons.
|
||||
static List<ArcaneService> get services =>
|
||||
List.unmodifiable(registry?.value ?? builtInServices);
|
||||
|
||||
/// Logs a message using the integrated logger.
|
||||
///
|
||||
@@ -63,16 +88,21 @@ class Arcane {
|
||||
/// - [level]: The log level (e.g., `Level.debug`, `Level.error`), defaults to
|
||||
/// `Level.debug`.
|
||||
/// - [stackTrace]: Optional stack trace information.
|
||||
/// - [metadata]: Optional additional metadata in key-value pairs.
|
||||
/// - [metadata]: Optional additional metadata in key-value pairs
|
||||
/// (`Map<String, Object?>`), which supports structured values such as
|
||||
/// numbers, booleans, nested maps, and lists.
|
||||
/// - [extra]: Optional data passed to the logger.
|
||||
/// - [skipAutodetection]: Bypass automatically determining the module, method,
|
||||
/// and file/line number of log messages.
|
||||
static void log(
|
||||
String message, {
|
||||
String? module,
|
||||
String? method,
|
||||
Level level = Level.debug,
|
||||
StackTrace? stackTrace,
|
||||
Map<String, String>? metadata,
|
||||
Map<String, Object?>? metadata,
|
||||
Object? extra,
|
||||
bool skipAutodetection = false,
|
||||
}) {
|
||||
ArcaneLogger.I.log(
|
||||
message,
|
||||
@@ -82,6 +112,7 @@ class Arcane {
|
||||
stackTrace: stackTrace,
|
||||
metadata: metadata,
|
||||
extra: extra,
|
||||
skipAutodetection: skipAutodetection,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+159
-24
@@ -1,53 +1,188 @@
|
||||
import "package:arcane_framework/arcane_framework.dart";
|
||||
import "package:arcane_framework/src/service/arcane_service.dart";
|
||||
import "package:collection/collection.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
import "arcane.dart";
|
||||
import "services/environment/environment_provider.dart";
|
||||
import "services/feature_flags/feature_flags_provider.dart";
|
||||
import "services/theme/theme_switcher.dart";
|
||||
|
||||
/// A root widget for an Arcane-powered application.
|
||||
///
|
||||
/// `ArcaneApp` serves as the entry point for an application using the Arcane
|
||||
/// framework. It provides access to the application's services and environment
|
||||
/// settings throughout the widget tree using the `ArcaneServiceProvider` and
|
||||
/// `ArcaneEnvironmentProvider`.
|
||||
/// settings throughout the widget tree using the `ArcaneServiceProvider`,
|
||||
/// `ArcaneEnvironmentProvider`, and `ArcaneFeatureFlagsProvider`.
|
||||
///
|
||||
/// This widget wraps the provided [child] widget with the necessary providers
|
||||
/// to make the Arcane services available to all descendant widgets.
|
||||
/// This widget wraps your app root with Arcane's built-in providers so
|
||||
/// descendant widgets can access services, environment, feature flags, and
|
||||
/// theme updates.
|
||||
///
|
||||
/// Preferred API: [builder]
|
||||
///
|
||||
/// Use [builder] when your app root needs a provider-aware `BuildContext`
|
||||
/// during construction. This is the recommended and future-facing API.
|
||||
///
|
||||
/// Legacy API: [child]
|
||||
///
|
||||
/// [child] is deprecated but still supported for compatibility while migrating
|
||||
/// existing apps.
|
||||
///
|
||||
/// Migration:
|
||||
/// ```dart
|
||||
/// // Before (deprecated)
|
||||
/// ArcaneApp(child: MyApp())
|
||||
///
|
||||
/// // After (preferred)
|
||||
/// ArcaneApp(builder: (context, _) => MyApp())
|
||||
/// ```
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// ArcaneApp(
|
||||
/// services: [ArcaneAuthenticationService(), ArcaneFeatureFlags()],
|
||||
/// child: MyApp(),
|
||||
/// services: [MyArcaneService()],
|
||||
/// builder: (context, _) => MyApp(),
|
||||
/// );
|
||||
/// ```
|
||||
class ArcaneApp extends StatelessWidget {
|
||||
class ArcaneApp extends StatefulWidget {
|
||||
/// A list of Arcane services that will be made available to the application.
|
||||
///
|
||||
/// These services will be provided to the widget tree using
|
||||
/// `ArcaneServiceProvider`.
|
||||
/// If no services are specified, an empty list is used by default.
|
||||
final List<ArcaneService> services;
|
||||
|
||||
/// Optional builder invoked inside Arcane's provider tree.
|
||||
///
|
||||
/// This mirrors Flutter's `TransitionBuilder` pattern and allows consumers
|
||||
/// to capture a provider-aware context without adding their own wrapper
|
||||
/// widgets around [child].
|
||||
final TransitionBuilder? builder;
|
||||
|
||||
/// The root widget of the application.
|
||||
///
|
||||
/// This widget will be wrapped by the service and environment providers.
|
||||
final Widget child;
|
||||
/// Deprecated: prefer [builder] to construct your root widget with a
|
||||
/// provider-aware BuildContext from inside `ArcaneApp`.
|
||||
@Deprecated(
|
||||
"Deprecated in 2.0.0. "
|
||||
"Prefer ArcaneApp.builder so your app root is built with Arcane-provided context.",
|
||||
)
|
||||
final Widget? child;
|
||||
|
||||
/// Creates an `ArcaneApp` with the specified [child] widget and optional
|
||||
/// [services].
|
||||
/// A root widget for an Arcane-powered application.
|
||||
///
|
||||
/// The [child] is required, while the [services] list is optional. By
|
||||
/// default, the [services] list is empty.
|
||||
/// `ArcaneApp` serves as the entry point for an application using the Arcane
|
||||
/// framework. It provides access to the application's services and environment
|
||||
/// settings throughout the widget tree using the `ArcaneServiceProvider`,
|
||||
/// `ArcaneEnvironmentProvider`, and `ArcaneFeatureFlagsProvider`.
|
||||
///
|
||||
/// This widget wraps your app root with Arcane's built-in providers so
|
||||
/// descendant widgets can access services, environment, feature flags, and
|
||||
/// theme updates.
|
||||
///
|
||||
/// Preferred API: [builder]
|
||||
///
|
||||
/// Use [builder] when your app root needs a provider-aware `BuildContext`
|
||||
/// during construction. This is the recommended and future-facing API.
|
||||
///
|
||||
/// Legacy API: [child]
|
||||
///
|
||||
/// [child] is deprecated but still supported for compatibility while migrating
|
||||
/// existing apps.
|
||||
///
|
||||
/// Migration:
|
||||
/// ```dart
|
||||
/// // Before (deprecated)
|
||||
/// ArcaneApp(child: MyApp())
|
||||
///
|
||||
/// // After (preferred)
|
||||
/// ArcaneApp(builder: (context, _) => MyApp())
|
||||
/// ```
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// ArcaneApp(
|
||||
/// services: [MyArcaneService()],
|
||||
/// builder: (context, _) => MyApp(),
|
||||
/// );
|
||||
/// ```
|
||||
const ArcaneApp({
|
||||
required this.child,
|
||||
@Deprecated(
|
||||
"Deprecated in 2.0.0. "
|
||||
"Prefer ArcaneApp.builder so your app root is built with Arcane-provided context.",
|
||||
)
|
||||
this.child,
|
||||
this.services = const [],
|
||||
this.builder,
|
||||
super.key,
|
||||
});
|
||||
}) : assert(
|
||||
child != null || builder != null,
|
||||
"ArcaneApp requires either a child or a builder.",
|
||||
);
|
||||
|
||||
@override
|
||||
State<ArcaneApp> createState() => _ArcaneAppState();
|
||||
}
|
||||
|
||||
class _ArcaneAppState extends State<ArcaneApp> {
|
||||
static const ListEquality<ArcaneService> _serviceListEquality =
|
||||
ListEquality<ArcaneService>(IdentityEquality<ArcaneService>());
|
||||
|
||||
late final ValueNotifier<List<ArcaneService>> _serviceNotifier;
|
||||
|
||||
List<ArcaneService> _computeMergedServices() {
|
||||
final List<ArcaneService> merged =
|
||||
List<ArcaneService>.from(widget.services);
|
||||
final Set<Type> existingTypes =
|
||||
merged.map((service) => service.runtimeType).toSet();
|
||||
|
||||
// Use Arcane._builtInServices directly to avoid registry recursion during init.
|
||||
for (final ArcaneService builtIn in Arcane.builtInServices) {
|
||||
if (existingTypes.contains(builtIn.runtimeType)) continue;
|
||||
merged.add(builtIn);
|
||||
existingTypes.add(builtIn.runtimeType);
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_serviceNotifier =
|
||||
ValueNotifier<List<ArcaneService>>(_computeMergedServices());
|
||||
Arcane.setRegistry(_serviceNotifier);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant ArcaneApp oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
final List<ArcaneService> mergedServices = _computeMergedServices();
|
||||
if (!_serviceListEquality.equals(_serviceNotifier.value, mergedServices)) {
|
||||
_serviceNotifier.value = mergedServices;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
Arcane.clearRegistry();
|
||||
_serviceNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ArcaneEnvironmentProvider(
|
||||
child: ArcaneServiceProvider(
|
||||
serviceInstances: services,
|
||||
child: child,
|
||||
final Widget appChild = widget.builder != null
|
||||
? Builder(
|
||||
builder: (context) => widget.builder!(context, widget.child),
|
||||
)
|
||||
: widget.child!;
|
||||
|
||||
return ArcaneServiceProvider(
|
||||
serviceNotifier: _serviceNotifier,
|
||||
child: ArcaneFeatureFlagsProvider(
|
||||
child: ArcaneEnvironmentProvider(
|
||||
child: ArcaneThemeSwitcher(
|
||||
child: appChild,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import "package:arcane_framework/arcane_framework.dart";
|
||||
import "package:flutter/widgets.dart";
|
||||
import "package:flutter_bloc/flutter_bloc.dart";
|
||||
|
||||
/// A `Cubit` that manages the application environment state.
|
||||
///
|
||||
/// The `ArcaneEnvironment` cubit holds the current environment (`debug` or `normal`)
|
||||
/// and provides a method to enable debug mode.
|
||||
class ArcaneEnvironment extends Cubit<Environment> {
|
||||
/// Initializes the cubit with the `normal` environment as the default state.
|
||||
ArcaneEnvironment() : super(Environment.normal);
|
||||
|
||||
/// Enables debug mode by setting the environment to `Environment.debug`.
|
||||
void enableDebugMode() {
|
||||
if (state == Environment.debug) return;
|
||||
|
||||
emit(Environment.debug);
|
||||
}
|
||||
|
||||
/// Disables debug mode by setting the environment to `Environment.normal`.
|
||||
void disableDebugMode() {
|
||||
if (state == Environment.normal) return;
|
||||
|
||||
emit(Environment.normal);
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget that provides `ArcaneEnvironment` to the widget tree using `BlocProvider`.
|
||||
///
|
||||
/// This widget wraps around a child widget and makes `ArcaneEnvironment` available
|
||||
/// to the rest of the widget tree. It should be used in combination with `BlocProvider`
|
||||
/// from the `flutter_bloc` package.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// ArcaneEnvironmentProvider(
|
||||
/// child: MyApp(),
|
||||
/// );
|
||||
/// ```
|
||||
class ArcaneEnvironmentProvider extends StatelessWidget {
|
||||
/// The widget that will be provided with access to the `ArcaneEnvironment`.
|
||||
final Widget child;
|
||||
|
||||
/// Constructs an `ArcaneEnvironmentProvider` with the given [child].
|
||||
const ArcaneEnvironmentProvider({required this.child, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => ArcaneEnvironment(),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
import "package:arcane_framework/arcane_framework.dart";
|
||||
import "package:collection/collection.dart";
|
||||
import "package:flutter/widgets.dart";
|
||||
|
||||
/// A provider that makes a list of `ArcaneService` instances available to the widget tree.
|
||||
///
|
||||
/// This class extends `InheritedNotifier` and allows `ArcaneService` instances to be
|
||||
/// accessed throughout the widget tree by descendant widgets. It should be used to
|
||||
/// provide service instances that are shared across the application.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// ArcaneServiceProvider(
|
||||
/// serviceInstances: [myService],
|
||||
/// child: MyApp(),
|
||||
/// );
|
||||
/// ```
|
||||
/// To access the provided services:
|
||||
/// ```dart
|
||||
/// final provider = ArcaneServiceProvider.of(context);
|
||||
/// ```
|
||||
class ArcaneServiceProvider extends InheritedNotifier {
|
||||
/// A list of `ArcaneService` instances available through the provider.
|
||||
final List<ArcaneService> serviceInstances;
|
||||
|
||||
/// Creates an `ArcaneServiceProvider` that provides [serviceInstances] to the widget tree.
|
||||
///
|
||||
/// The [child] widget will be the root of the widget subtree that has access to the services.
|
||||
@override
|
||||
const ArcaneServiceProvider({
|
||||
required this.serviceInstances,
|
||||
required super.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// Determines whether the widget should notify its dependents.
|
||||
///
|
||||
/// This always returns `true`, meaning dependents will always be notified
|
||||
/// when this widget is rebuilt.
|
||||
@override
|
||||
bool updateShouldNotify(ArcaneServiceProvider oldWidget) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Retrieves the nearest `ArcaneServiceProvider` in the widget tree.
|
||||
///
|
||||
/// This method is used to access the `ArcaneServiceProvider` and its provided services
|
||||
/// from any descendant widget. It throws an exception if no `ArcaneServiceProvider`
|
||||
/// is found in the widget tree.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final provider = ArcaneServiceProvider.of(context);
|
||||
/// ```
|
||||
static ArcaneServiceProvider of(BuildContext context) {
|
||||
final ArcaneServiceProvider? result =
|
||||
context.dependOnInheritedWidgetOfExactType<ArcaneServiceProvider>();
|
||||
|
||||
if (result == null) {
|
||||
throw Exception("ArcaneServiceProvider not found in context");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// An extension on `BuildContext` to provide easy access to `ArcaneService` instances
|
||||
/// that are registered in an `ArcaneServiceProvider`.
|
||||
///
|
||||
/// This extension provides a `serviceOfType` method, which searches for a specific
|
||||
/// service of type `T` in the current `ArcaneServiceProvider` or in the list of built-in
|
||||
/// services.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// final MyService? myService = context.serviceOfType<MyService>();
|
||||
/// ```
|
||||
extension ServiceProvider on BuildContext {
|
||||
/// Finds and returns the `ArcaneService` instance of type `T` that has been registered
|
||||
/// in the `ArcaneServiceProvider` or in the list of built-in services (`Arcane.services`).
|
||||
///
|
||||
/// If no such service is found, it returns `null`.
|
||||
///
|
||||
/// - `T`: The type of the service to be retrieved, which extends `ArcaneService`.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final MyService? myService = context.serviceOfType<MyService>();
|
||||
/// ```
|
||||
T? serviceOfType<T extends ArcaneService>() {
|
||||
final T? builtInService =
|
||||
Arcane.services.firstWhereOrNull((s) => s.runtimeType == T) as T?;
|
||||
|
||||
if (builtInService != null) return builtInService;
|
||||
|
||||
final T? foundService =
|
||||
dependOnInheritedWidgetOfExactType<ArcaneServiceProvider>()
|
||||
?.serviceInstances
|
||||
.firstWhereOrNull((s) => s.runtimeType == T) as T?;
|
||||
return foundService;
|
||||
}
|
||||
}
|
||||
|
||||
/// An abstract class representing a service in the Arcane architecture.
|
||||
///
|
||||
/// Classes that extend `ArcaneService` can use `ChangeNotifier` functionality
|
||||
/// to notify listeners of changes. Services are typically registered in
|
||||
/// `ArcaneServiceProvider` and can be accessed using the `serviceOfType`
|
||||
/// method on `BuildContext`.
|
||||
abstract class ArcaneService with ChangeNotifier {}
|
||||
@@ -0,0 +1,36 @@
|
||||
import "package:arcane_framework/arcane_framework.dart";
|
||||
import "package:collection/collection.dart";
|
||||
import "package:flutter/widgets.dart";
|
||||
|
||||
part "service_provider.dart";
|
||||
part "service_provider_extensions.dart";
|
||||
|
||||
/// An abstract class representing a service in the Arcane architecture.
|
||||
///
|
||||
/// Classes that extend `ArcaneService` can use `ChangeNotifier` functionality
|
||||
/// to notify listeners of changes. Services are typically registered in
|
||||
/// `ArcaneServiceProvider` and can be accessed using the `service`
|
||||
/// method on `BuildContext`.
|
||||
abstract class ArcaneService with ChangeNotifier {
|
||||
/// Retrieves a service of the specified type from the context.
|
||||
///
|
||||
/// Returns null if no service of type `T` is found.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final myService = ArcaneService.ofType<MyService>(context);
|
||||
/// ```
|
||||
static T? ofType<T extends ArcaneService>(BuildContext context) =>
|
||||
context.service<T>();
|
||||
|
||||
/// Retrieves a service of the specified type from the context.
|
||||
///
|
||||
/// Throws an assertion error if no service of type `T` is found.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final myService = ArcaneService.requiredOfType<MyService>(context);
|
||||
/// ```
|
||||
static T requiredOfType<T extends ArcaneService>(BuildContext context) =>
|
||||
context.requiredService<T>();
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
part of "arcane_service.dart";
|
||||
|
||||
/// A provider that makes a list of `ArcaneService` instances available to the widget tree.
|
||||
///
|
||||
/// This class extends `InheritedNotifier` and allows `ArcaneService` instances to be
|
||||
/// accessed throughout the widget tree by descendant widgets. It should be used to
|
||||
/// provide service instances that are shared across the application.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// ArcaneServiceProvider(
|
||||
/// serviceInstances: [myService],
|
||||
/// child: MyApp(),
|
||||
/// );
|
||||
/// ```
|
||||
/// To access the provided services:
|
||||
/// ```dart
|
||||
/// final provider = ArcaneServiceProvider.of(context);
|
||||
/// final myService = ArcaneServiceProvider.serviceOfType<MyService>(context);
|
||||
/// ```
|
||||
class ArcaneServiceProvider
|
||||
extends InheritedNotifier<ValueNotifier<List<ArcaneService>>> {
|
||||
/// A list of `ArcaneService` instances available through the provider.
|
||||
List<ArcaneService> get registeredServices =>
|
||||
List<ArcaneService>.from([...?notifier?.value]);
|
||||
|
||||
/// Creates an `ArcaneServiceProvider` that provides [serviceInstances] to the widget tree.
|
||||
///
|
||||
/// If [serviceNotifier] is provided, it will be used as the backing notifier for the provider.
|
||||
/// Otherwise, a new notifier will be created from [serviceInstances].
|
||||
ArcaneServiceProvider({
|
||||
required super.child,
|
||||
List<ArcaneService> serviceInstances = const [],
|
||||
ValueNotifier<List<ArcaneService>>? serviceNotifier,
|
||||
super.key,
|
||||
}) : super(
|
||||
notifier: serviceNotifier ??
|
||||
ValueNotifier<List<ArcaneService>>(serviceInstances),
|
||||
);
|
||||
|
||||
/// Retrieves the nearest `ArcaneServiceProvider` in the widget tree.
|
||||
///
|
||||
/// Returns null if no provider is found.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final provider = ArcaneServiceProvider.maybeOf(context);
|
||||
/// ```
|
||||
static ArcaneServiceProvider? maybeOf(BuildContext context) {
|
||||
return context.dependOnInheritedWidgetOfExactType<ArcaneServiceProvider>();
|
||||
}
|
||||
|
||||
/// Retrieves the nearest `ArcaneServiceProvider` in the widget tree.
|
||||
///
|
||||
/// Throws an assertion error if no provider is found.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final provider = ArcaneServiceProvider.of(context);
|
||||
/// ```
|
||||
static ArcaneServiceProvider of(BuildContext context) {
|
||||
final provider = maybeOf(context);
|
||||
assert(provider != null, "No ArcaneServiceProvider found in context");
|
||||
return provider!;
|
||||
}
|
||||
|
||||
/// Retrieves a service of type `T` from the nearest provider.
|
||||
///
|
||||
/// Returns null if no service of type `T` is found or if no provider exists.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final myService = ArcaneServiceProvider.serviceOfType<MyService>(context);
|
||||
/// ```
|
||||
static T? serviceOfType<T extends ArcaneService>(BuildContext context) {
|
||||
final provider = maybeOf(context);
|
||||
if (provider == null) return null;
|
||||
|
||||
return provider.registeredServices.whereType<T>().firstOrNull;
|
||||
}
|
||||
|
||||
/// Retrieves a service of type `T` from the nearest provider.
|
||||
///
|
||||
/// Throws an exception if no service of type `T` is found or if no provider exists.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final myService = ArcaneServiceProvider.requiredServiceOfType<MyService>(context);
|
||||
/// ```
|
||||
static T requiredServiceOfType<T extends ArcaneService>(
|
||||
BuildContext context,
|
||||
) {
|
||||
final provider = maybeOf(context);
|
||||
assert(provider != null, "No ArcaneServiceProvider found");
|
||||
|
||||
final T? service = provider!.registeredServices.whereType<T>().firstOrNull;
|
||||
|
||||
assert(service != null, "No service of type $T found");
|
||||
|
||||
return service!;
|
||||
}
|
||||
|
||||
/// Updates the service instances in this provider.
|
||||
///
|
||||
/// This will trigger a rebuild of all widgets that depend on this provider.
|
||||
void setServices(List<ArcaneService> newServices) {
|
||||
notifier?.value = newServices;
|
||||
}
|
||||
|
||||
/// Adds a new service to this provider.
|
||||
///
|
||||
/// If a service of the same type already exists, it will be replaced.
|
||||
void addService(ArcaneService service) {
|
||||
final int existingIndex = registeredServices.indexWhere(
|
||||
(s) => s.runtimeType == service.runtimeType,
|
||||
);
|
||||
|
||||
final List<ArcaneService> newList =
|
||||
List<ArcaneService>.from(registeredServices);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
newList[existingIndex] = service;
|
||||
} else {
|
||||
newList.add(service);
|
||||
}
|
||||
|
||||
notifier?.value = newList;
|
||||
}
|
||||
|
||||
/// Removes all services of the specified type from the registry.
|
||||
/// Returns true if any services were removed, false otherwise.
|
||||
bool removeService<T extends ArcaneService>() {
|
||||
final List<ArcaneService> newList =
|
||||
List<ArcaneService>.from(registeredServices);
|
||||
final int originalLength = newList.length;
|
||||
|
||||
newList.removeWhere((service) => service.runtimeType == T);
|
||||
|
||||
if (newList.length == originalLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
notifier?.value = newList;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
part of "arcane_service.dart";
|
||||
|
||||
/// An extension on `BuildContext` to provide easy access to `ArcaneService` instances
|
||||
/// that are registered in an `ArcaneServiceProvider`.
|
||||
///
|
||||
/// This extension provides methods for retrieving services in various ways.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// final myService = context.service<MyService>();
|
||||
/// ```
|
||||
extension ServiceProviderExtension on BuildContext {
|
||||
/// Finds and returns the `ArcaneService` instance of type `T` that has been registered
|
||||
/// in the `ArcaneServiceProvider` or in the list of built-in services (`Arcane.services`).
|
||||
///
|
||||
/// If no such service is found, it returns `null`.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final myService = context.service<MyService>();
|
||||
/// ```
|
||||
T? service<T extends ArcaneService>() {
|
||||
// First check provider-registered services so app-specific overrides win.
|
||||
final providerService = ArcaneServiceProvider.serviceOfType<T>(this);
|
||||
if (providerService != null) return providerService;
|
||||
|
||||
// Fall back to built-in services.
|
||||
return Arcane.services.whereType<T>().firstOrNull;
|
||||
}
|
||||
|
||||
/// Finds and returns the `ArcaneService` instance of type `T` that has been registered
|
||||
/// in the `ArcaneServiceProvider` or in the list of built-in services (`Arcane.services`).
|
||||
///
|
||||
/// Throws an assertion error if no service is found.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final myService = context.requiredService<MyService>();
|
||||
/// ```
|
||||
T requiredService<T extends ArcaneService>() {
|
||||
final service = this.service<T>();
|
||||
assert(service != null, "No service of type $T found");
|
||||
return service!;
|
||||
}
|
||||
|
||||
/// Legacy method to maintain backward compatibility.
|
||||
///
|
||||
/// Prefer using `service<T>()` instead.
|
||||
@Deprecated("Deprecated in 2.0.0. Use service<T>() instead")
|
||||
T? serviceOfType<T extends ArcaneService>() => service<T>();
|
||||
}
|
||||
@@ -24,10 +24,9 @@ enum SignUpStep {
|
||||
|
||||
/// An enum representing the authentication status of a user.
|
||||
///
|
||||
/// This enum has three possible states:
|
||||
/// This enum has two possible states:
|
||||
/// - `authenticated`: The user is authenticated.
|
||||
/// - `unauthenticated`: The user is not authenticated.
|
||||
/// - `debug`: The application is in debug mode for testing.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
@@ -41,13 +40,7 @@ enum AuthenticationStatus {
|
||||
authenticated,
|
||||
|
||||
/// The user is not authenticated.
|
||||
unauthenticated,
|
||||
|
||||
/// The application is in debug mode, typically for testing or development purposes.
|
||||
debug;
|
||||
|
||||
/// Returns `true` if the current status is `debug`.
|
||||
bool get isDebug => this == debug;
|
||||
unauthenticated;
|
||||
|
||||
/// Returns `true` if the current status is `authenticated`.
|
||||
bool get isAuthenticated => this == authenticated;
|
||||
@@ -55,16 +48,3 @@ enum AuthenticationStatus {
|
||||
/// Returns `true` if the current status is `unauthenticated`.
|
||||
bool get isUnauthenticated => this == unauthenticated;
|
||||
}
|
||||
|
||||
/// An enum representing the different application environments.
|
||||
///
|
||||
/// This enum has two possible values:
|
||||
/// - `debug`: The application is in debug mode, typically for development and testing.
|
||||
/// - `normal`: The application is running in a normal mode, for production or standard use.
|
||||
enum Environment {
|
||||
/// The debug environment for development and testing purposes.
|
||||
debug,
|
||||
|
||||
/// The normal environment for production use.
|
||||
normal,
|
||||
}
|
||||
|
||||
@@ -36,7 +36,11 @@ abstract class ArcaneAuthInterface {
|
||||
/// This method terminates the current session and removes any stored tokens.
|
||||
/// Returns a `Result` that either contains a `void` on success or an error
|
||||
/// message.
|
||||
Future<Result<void, String>> logout();
|
||||
/// Upon a successful logout, the `onLoggedOut` method will be called if it
|
||||
/// has been provided.
|
||||
Future<Result<void, String>> logout({
|
||||
Future<void> Function()? onLoggedOut,
|
||||
});
|
||||
|
||||
/// Logs the user in using an optional, generic `T` type of input.
|
||||
/// This login method is a generic method that can be used to login with any
|
||||
@@ -44,6 +48,8 @@ abstract class ArcaneAuthInterface {
|
||||
/// and password. Any type of input can be passed in, and it will be handled
|
||||
/// by the implementation of the method wihin the specific authentication
|
||||
/// service.
|
||||
/// Upon a successful login, the `onLoggedIn` method will be called if it
|
||||
/// has been provided.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
|
||||
@@ -2,7 +2,6 @@ import "dart:async";
|
||||
|
||||
import "package:arcane_framework/arcane_framework.dart";
|
||||
import "package:flutter/widgets.dart";
|
||||
import "package:flutter_bloc/flutter_bloc.dart";
|
||||
|
||||
part "authentication_enums.dart";
|
||||
part "authentication_interface.dart";
|
||||
@@ -26,12 +25,26 @@ class ArcaneAuthenticationService extends ArcaneService {
|
||||
/// A `ValueNotifier` that emits the current `AuthenticationStatus`.
|
||||
ValueNotifier<AuthenticationStatus> get notifier => _notifier;
|
||||
|
||||
/// Returns the current `AuthenticationStatus`.
|
||||
StreamController<AuthenticationStatus>? _statusStreamController;
|
||||
|
||||
StreamController<AuthenticationStatus> get _statusController {
|
||||
_statusStreamController ??=
|
||||
StreamController<AuthenticationStatus>.broadcast();
|
||||
return _statusStreamController!;
|
||||
}
|
||||
|
||||
/// Stream of authentication status updates.
|
||||
Stream<AuthenticationStatus> get statusChanges => I._statusController.stream;
|
||||
|
||||
/// Returns the current `AuthenticationStatus` as a snapshot value.
|
||||
///
|
||||
/// Reading this getter does not subscribe to changes and does not trigger
|
||||
/// widget rebuilds. Use [notifier] (for `ValueListenableBuilder`) or
|
||||
/// [statusChanges] (for streams) when you need reactive updates.
|
||||
///
|
||||
/// Available values:
|
||||
/// - `authenticated`: The user has successfully authenticated and is logged in.
|
||||
/// - `unauthenticated`: The user has not yet logged in.
|
||||
/// - `debug`: Debug mode has been enabled, enabling development features.
|
||||
AuthenticationStatus get status => _notifier.value;
|
||||
|
||||
static ArcaneAuthInterface? _authInterface;
|
||||
@@ -40,23 +53,33 @@ class ArcaneAuthenticationService extends ArcaneService {
|
||||
/// been registered.
|
||||
ArcaneAuthInterface? get authInterface => _authInterface;
|
||||
|
||||
/// A shortcut to `status != AuthenticationStatus.unauthenticated`.
|
||||
bool get isAuthenticated => status != AuthenticationStatus.unauthenticated;
|
||||
/// Returns `true` when the current status is authenticated.
|
||||
bool get isAuthenticated => status == AuthenticationStatus.authenticated;
|
||||
|
||||
final ValueNotifier<bool> _isSignedIn = ValueNotifier<bool>(false);
|
||||
|
||||
/// A `ValueNotifier` that emits `true` if the user is currently signed in.
|
||||
ValueNotifier<bool> get isSignedIn => _isSignedIn;
|
||||
|
||||
StreamController<bool>? _signedInStreamController;
|
||||
|
||||
StreamController<bool> get _signedInController {
|
||||
_signedInStreamController ??= StreamController<bool>.broadcast();
|
||||
return _signedInStreamController!;
|
||||
}
|
||||
|
||||
/// Stream of signed-in boolean updates.
|
||||
Stream<bool> get signedInChanges => I._signedInController.stream;
|
||||
|
||||
/// Returns a JWT access token if the registered `ArcaneAuthInterface`
|
||||
/// provides one. This token is often used in the headers of HTTP requests
|
||||
/// to the backend API.
|
||||
Future<String?> get accessToken =>
|
||||
Future<String?> get accessToken async =>
|
||||
authInterface?.accessToken ?? Future.value("");
|
||||
|
||||
/// Returns a JWT refresh token if the registered `ArcaneAuthInterface`
|
||||
/// provides one.
|
||||
Future<String?> get refreshToken =>
|
||||
Future<String?> get refreshToken async =>
|
||||
authInterface?.refreshToken ?? Future.value("");
|
||||
|
||||
/// Removes any registered `ArcaneAuthInterface` and resets all values to
|
||||
@@ -65,7 +88,8 @@ class ArcaneAuthenticationService extends ArcaneService {
|
||||
_authInterface = null;
|
||||
_notifier.value = AuthenticationStatus.unauthenticated;
|
||||
_isSignedIn.value = isAuthenticated;
|
||||
notifyListeners();
|
||||
_statusController.add(_notifier.value);
|
||||
_signedInController.add(_isSignedIn.value);
|
||||
}
|
||||
|
||||
/// Registers an `ArcaneAuthInterface` within the `ArcaneAuthenticationService`.
|
||||
@@ -78,64 +102,36 @@ class ArcaneAuthenticationService extends ArcaneService {
|
||||
await authInterface.init();
|
||||
}
|
||||
|
||||
/// Sets `status` to `AuthenticationStatus.debug`. If `onDebugModeSet` has
|
||||
/// been specified, the method will be triggered after the new status has been
|
||||
/// set.
|
||||
/// Enables the debug environment.
|
||||
///
|
||||
/// This method does not mutate authentication status.
|
||||
Future<void> setDebug(
|
||||
BuildContext context, {
|
||||
BuildContext _, {
|
||||
Future<void> Function()? onDebugModeSet,
|
||||
}) async {
|
||||
ArcaneEnvironment? environment;
|
||||
final Environment previousEnvironment = Arcane.environment.current;
|
||||
|
||||
try {
|
||||
environment = context.read<ArcaneEnvironment>();
|
||||
final Environment previousEnvironment = environment.state;
|
||||
if (previousEnvironment == Environment.debug) return;
|
||||
|
||||
if (previousEnvironment == Environment.debug) return;
|
||||
Arcane.environment.enableDebugMode();
|
||||
|
||||
environment.enableDebugMode();
|
||||
|
||||
final Environment currentEnvironment = environment.state;
|
||||
|
||||
if (previousEnvironment == currentEnvironment) {
|
||||
throw Exception("Unable to switch to debug mode.");
|
||||
}
|
||||
|
||||
_setStatus(AuthenticationStatus.debug);
|
||||
if (onDebugModeSet != null) await onDebugModeSet();
|
||||
} catch (_) {
|
||||
throw Exception("No ArcaneEnvironment found in BuildContext");
|
||||
}
|
||||
if (onDebugModeSet != null) await onDebugModeSet();
|
||||
}
|
||||
|
||||
/// Sets `status` to `AuthenticationStatus.normal`. If `onDebugModeUnset` has
|
||||
/// been specified, the method will be triggered after the new status has been
|
||||
/// set.
|
||||
/// Enables the normal environment.
|
||||
///
|
||||
/// This method does not mutate authentication status.
|
||||
Future<void> setNormal(
|
||||
BuildContext context, {
|
||||
BuildContext _, {
|
||||
Future<void> Function()? onDebugModeUnset,
|
||||
}) async {
|
||||
ArcaneEnvironment? environment;
|
||||
final Environment previousEnvironment = Arcane.environment.current;
|
||||
|
||||
try {
|
||||
environment = context.read<ArcaneEnvironment>();
|
||||
final Environment previousEnvironment = environment.state;
|
||||
if (previousEnvironment == Environment.normal) return;
|
||||
|
||||
if (previousEnvironment == Environment.normal) return;
|
||||
Arcane.environment.disableDebugMode();
|
||||
|
||||
environment.disableDebugMode();
|
||||
|
||||
final Environment currentEnvironment = environment.state;
|
||||
|
||||
if (previousEnvironment == currentEnvironment) {
|
||||
throw Exception("Unable to switch to normal mode.");
|
||||
}
|
||||
|
||||
_setStatus(AuthenticationStatus.debug);
|
||||
if (onDebugModeUnset != null) await onDebugModeUnset();
|
||||
} catch (_) {
|
||||
throw Exception("No ArcaneEnvironment found in BuildContext");
|
||||
}
|
||||
if (onDebugModeUnset != null) await onDebugModeUnset();
|
||||
}
|
||||
|
||||
/// Sets `status` to `AuthenticationStatus.authenticated`.
|
||||
@@ -152,8 +148,18 @@ class ArcaneAuthenticationService extends ArcaneService {
|
||||
if (_notifier.value != newStatus) {
|
||||
_notifier.value = newStatus;
|
||||
_isSignedIn.value = isAuthenticated;
|
||||
_statusController.add(_notifier.value);
|
||||
_signedInController.add(_isSignedIn.value);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
unawaited(_statusStreamController?.close());
|
||||
unawaited(_signedInStreamController?.close());
|
||||
_statusStreamController = null;
|
||||
_signedInStreamController = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Logs the current user out. Upon successful logout, `status` will be set to
|
||||
@@ -162,16 +168,19 @@ class ArcaneAuthenticationService extends ArcaneService {
|
||||
Future<void> Function()? onLoggedOut,
|
||||
}) async {
|
||||
if (_authInterface == null) {
|
||||
return Result.error("No ArcaneAuthInterface has been registered");
|
||||
return const Result.error("No ArcaneAuthInterface has been registered");
|
||||
}
|
||||
|
||||
if (!isAuthenticated) Result.error("User is not authenticated.");
|
||||
if (!isAuthenticated) {
|
||||
return const Result.error("User is not authenticated.");
|
||||
}
|
||||
|
||||
final Result<void, String> loggedOut = await authInterface!.logout();
|
||||
final Result<void, String> loggedOut = await authInterface!.logout(
|
||||
onLoggedOut: onLoggedOut,
|
||||
);
|
||||
|
||||
if (loggedOut.isSuccess) {
|
||||
setUnauthenticated();
|
||||
if (onLoggedOut != null) await onLoggedOut();
|
||||
}
|
||||
|
||||
return loggedOut;
|
||||
@@ -183,16 +192,16 @@ class ArcaneAuthenticationService extends ArcaneService {
|
||||
Future<void> Function()? onLoggedIn,
|
||||
}) async {
|
||||
if (_authInterface == null) {
|
||||
return Result.error("No ArcaneAuthInterface has been registered");
|
||||
return const Result.error("No ArcaneAuthInterface has been registered");
|
||||
}
|
||||
|
||||
final Result<void, String> result = await authInterface!.login(
|
||||
input: input,
|
||||
onLoggedIn: onLoggedIn,
|
||||
);
|
||||
|
||||
if (result.isSuccess) {
|
||||
setAuthenticated();
|
||||
if (onLoggedIn != null) await onLoggedIn();
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -205,11 +214,11 @@ class ArcaneAuthenticationService extends ArcaneService {
|
||||
T? input,
|
||||
}) async {
|
||||
if (_authInterface == null) {
|
||||
return Result.error("No ArcaneAuthInterface has been registered");
|
||||
return const Result.error("No ArcaneAuthInterface has been registered");
|
||||
}
|
||||
|
||||
if (authInterface is! ArcaneAuthAccountRegistration) {
|
||||
return Result.error(
|
||||
return const Result.error(
|
||||
"The provided ArcaneAuthInterface does not support account registration.",
|
||||
);
|
||||
}
|
||||
@@ -221,7 +230,7 @@ class ArcaneAuthenticationService extends ArcaneService {
|
||||
);
|
||||
|
||||
if (result == null) {
|
||||
return Result.error(
|
||||
return const Result.error(
|
||||
"Registered ArcaneAuthInterface returned a null value.",
|
||||
);
|
||||
}
|
||||
@@ -236,11 +245,11 @@ class ArcaneAuthenticationService extends ArcaneService {
|
||||
required String confirmationCode,
|
||||
}) async {
|
||||
if (_authInterface == null) {
|
||||
return Result.error("No ArcaneAuthInterface has been registered");
|
||||
return const Result.error("No ArcaneAuthInterface has been registered");
|
||||
}
|
||||
|
||||
if (authInterface is! ArcaneAuthAccountRegistration) {
|
||||
return Result.error(
|
||||
return const Result.error(
|
||||
"The provided ArcaneAuthInterface does not support account registration.",
|
||||
);
|
||||
}
|
||||
@@ -253,7 +262,7 @@ class ArcaneAuthenticationService extends ArcaneService {
|
||||
);
|
||||
|
||||
if (result == null) {
|
||||
return Result.error(
|
||||
return const Result.error(
|
||||
"Registered ArcaneAuthInterface returned a null value.",
|
||||
);
|
||||
}
|
||||
@@ -265,11 +274,11 @@ class ArcaneAuthenticationService extends ArcaneService {
|
||||
/// registration.
|
||||
Future<Result<String, String>> resendVerificationCode(String email) async {
|
||||
if (_authInterface == null) {
|
||||
return Result.error("No ArcaneAuthInterface has been registered");
|
||||
return const Result.error("No ArcaneAuthInterface has been registered");
|
||||
}
|
||||
|
||||
if (authInterface is! ArcaneAuthAccountRegistration) {
|
||||
return Result.error(
|
||||
return const Result.error(
|
||||
"The provided ArcaneAuthInterface does not support account registration.",
|
||||
);
|
||||
}
|
||||
@@ -280,7 +289,7 @@ class ArcaneAuthenticationService extends ArcaneService {
|
||||
auth.resendVerificationCode(input: email);
|
||||
|
||||
if (result == null) {
|
||||
return Result.error(
|
||||
return const Result.error(
|
||||
"Registered ArcaneAuthInterface returned a null value.",
|
||||
);
|
||||
}
|
||||
@@ -300,11 +309,11 @@ class ArcaneAuthenticationService extends ArcaneService {
|
||||
String? confirmationCode,
|
||||
}) async {
|
||||
if (_authInterface == null) {
|
||||
return Result.error("No ArcaneAuthInterface has been registered");
|
||||
return const Result.error("No ArcaneAuthInterface has been registered");
|
||||
}
|
||||
|
||||
if (authInterface is! ArcaneAuthPasswordManagement) {
|
||||
return Result.error(
|
||||
return const Result.error(
|
||||
"The provided ArcaneAuthInterface does not support password management.",
|
||||
);
|
||||
}
|
||||
@@ -318,7 +327,7 @@ class ArcaneAuthenticationService extends ArcaneService {
|
||||
);
|
||||
|
||||
if (result == null) {
|
||||
return Result.error(
|
||||
return const Result.error(
|
||||
"Registered ArcaneAuthInterface returned a null value.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/// A value object representing the current application environment.
|
||||
///
|
||||
/// Built-in values are available through [Environment.debug] and
|
||||
/// [Environment.normal], but custom values can be created for app-specific
|
||||
/// environments such as `staging`.
|
||||
class Environment {
|
||||
/// Creates an environment with a human-readable [name].
|
||||
const Environment(this.name);
|
||||
|
||||
/// Built-in debug environment for development and testing purposes.
|
||||
static const Environment debug = Environment("debug");
|
||||
|
||||
/// Built-in normal environment for production use.
|
||||
static const Environment normal = Environment("normal");
|
||||
|
||||
/// Human-readable environment name.
|
||||
final String name;
|
||||
|
||||
/// Returns `true` when this environment is the built-in debug environment.
|
||||
bool get isDebug => this == debug;
|
||||
|
||||
/// Returns `true` when this environment is the built-in normal environment.
|
||||
bool get isNormal => this == normal;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
return other is Environment && other.name == name;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => name.hashCode;
|
||||
|
||||
@override
|
||||
String toString() => "Environment($name)";
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import "package:arcane_framework/src/arcane.dart";
|
||||
import "package:flutter/widgets.dart";
|
||||
|
||||
import "environment_interface.dart";
|
||||
|
||||
/// An `InheritedWidget` that provides access to the application environment.
|
||||
///
|
||||
/// The `ArcaneEnvironment` widget holds the current environment and allows
|
||||
/// descendant widgets to access and mutate it.
|
||||
class ArcaneEnvironment extends InheritedWidget {
|
||||
/// Returns the current environment (alias for [environment]) for API consistency.
|
||||
Environment get current => environment;
|
||||
|
||||
/// The current application environment.
|
||||
final Environment environment;
|
||||
|
||||
final ValueChanged<Environment> _switchEnvironment;
|
||||
|
||||
/// Creates an `ArcaneEnvironment` widget.
|
||||
const ArcaneEnvironment({
|
||||
required this.environment,
|
||||
required void Function(Environment) switchEnvironment,
|
||||
required super.child,
|
||||
super.key,
|
||||
}) : _switchEnvironment = switchEnvironment;
|
||||
|
||||
/// Retrieves the `ArcaneEnvironment` instance from the nearest ancestor.
|
||||
///
|
||||
/// Returns `null` if no `ArcaneEnvironment` ancestor is found.
|
||||
static ArcaneEnvironment? maybeOf(BuildContext context) {
|
||||
return context.dependOnInheritedWidgetOfExactType<ArcaneEnvironment>();
|
||||
}
|
||||
|
||||
/// Retrieves the `ArcaneEnvironment` instance from the nearest ancestor.
|
||||
///
|
||||
/// Throws a `StateError` if no `ArcaneEnvironment` ancestor is found.
|
||||
static ArcaneEnvironment of(BuildContext context) {
|
||||
final ArcaneEnvironment? result = maybeOf(context);
|
||||
if (result == null) {
|
||||
throw StateError("No ArcaneEnvironment found in context");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(ArcaneEnvironment oldWidget) {
|
||||
return environment != oldWidget.environment;
|
||||
}
|
||||
|
||||
void setEnvironment(Environment environment) =>
|
||||
_switchEnvironment(environment);
|
||||
|
||||
void enableDebugMode() => _switchEnvironment(Environment.debug);
|
||||
void disableDebugMode() => _switchEnvironment(Environment.normal);
|
||||
}
|
||||
|
||||
/// A `StatefulWidget` that manages and provides the `ArcaneEnvironment`.
|
||||
///
|
||||
/// This widget holds the internal state of the environment and rebuilds
|
||||
/// its descendants when the environment changes.
|
||||
class ArcaneEnvironmentProvider extends StatefulWidget {
|
||||
/// The child widget that will have access to the `ArcaneEnvironment`.
|
||||
final Widget child;
|
||||
|
||||
/// The initial environment state. Defaults to `Environment.normal`.
|
||||
final Environment environment;
|
||||
|
||||
/// Creates an `ArcaneEnvironmentProvider`.
|
||||
const ArcaneEnvironmentProvider({
|
||||
required this.child,
|
||||
Key? key,
|
||||
this.environment = Environment.normal,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<ArcaneEnvironmentProvider> createState() =>
|
||||
_ArcaneEnvironmentProviderState();
|
||||
}
|
||||
|
||||
class _ArcaneEnvironmentProviderState extends State<ArcaneEnvironmentProvider> {
|
||||
late Environment _environment;
|
||||
|
||||
void _handleEnvironmentChange() {
|
||||
if (!mounted) return;
|
||||
|
||||
final nextEnvironment = Arcane.environment.current;
|
||||
if (nextEnvironment == _environment) return;
|
||||
|
||||
setState(() {
|
||||
_environment = nextEnvironment;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_environment = Arcane.environment.current;
|
||||
|
||||
if (_environment != widget.environment) {
|
||||
Arcane.environment.setEnvironment(widget.environment);
|
||||
_environment = Arcane.environment.current;
|
||||
}
|
||||
|
||||
Arcane.environment.notifier.addListener(_handleEnvironmentChange);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
Arcane.environment.notifier.removeListener(_handleEnvironmentChange);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Enables debug mode by setting the environment to `Environment.debug`.
|
||||
void enableDebugMode() {
|
||||
if (_environment == Environment.debug) return;
|
||||
setEnvironment(Environment.debug);
|
||||
}
|
||||
|
||||
/// Disables debug mode by setting the environment to `Environment.normal`.
|
||||
void disableDebugMode() {
|
||||
if (_environment == Environment.normal) return;
|
||||
setEnvironment(Environment.normal);
|
||||
}
|
||||
|
||||
void setEnvironment(Environment environment) {
|
||||
Arcane.environment.setEnvironment(environment);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ArcaneEnvironment(
|
||||
environment: _environment,
|
||||
switchEnvironment: setEnvironment,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import "dart:async";
|
||||
|
||||
import "package:arcane_framework/arcane_framework.dart";
|
||||
import "package:flutter/widgets.dart";
|
||||
|
||||
/// A singleton service that stores and broadcasts the current application
|
||||
/// environment.
|
||||
class ArcaneEnvironmentService extends ArcaneService {
|
||||
ArcaneEnvironmentService._internal();
|
||||
|
||||
static final ArcaneEnvironmentService _instance =
|
||||
ArcaneEnvironmentService._internal();
|
||||
|
||||
/// Provides access to the singleton instance.
|
||||
static ArcaneEnvironmentService get I => _instance;
|
||||
|
||||
final ValueNotifier<Environment> _notifier =
|
||||
ValueNotifier<Environment>(Environment.normal);
|
||||
|
||||
/// A notifier that emits updates when [current] changes.
|
||||
ValueNotifier<Environment> get notifier => _notifier;
|
||||
|
||||
StreamController<Environment>? _environmentStreamController;
|
||||
|
||||
StreamController<Environment> get _environmentController {
|
||||
_environmentStreamController ??= StreamController<Environment>.broadcast();
|
||||
return _environmentStreamController!;
|
||||
}
|
||||
|
||||
/// Stream of environment updates.
|
||||
Stream<Environment> get environmentChanges => I._environmentController.stream;
|
||||
|
||||
/// The current application environment as a snapshot value.
|
||||
///
|
||||
/// Reading this getter does not subscribe to changes and does not trigger
|
||||
/// widget rebuilds. Use [notifier] (for `ValueListenableBuilder`) or
|
||||
/// [environmentChanges] (for streams) when you need reactive updates.
|
||||
Environment get current => _notifier.value;
|
||||
|
||||
/// Sets the environment when the incoming value is different.
|
||||
void setEnvironment(Environment environment) {
|
||||
if (_notifier.value == environment) return;
|
||||
_notifier.value = environment;
|
||||
_environmentController.add(_notifier.value);
|
||||
}
|
||||
|
||||
/// Switches the app to [Environment.debug].
|
||||
void enableDebugMode() => setEnvironment(Environment.debug);
|
||||
|
||||
/// Switches the app to [Environment.normal].
|
||||
void disableDebugMode() => setEnvironment(Environment.normal);
|
||||
|
||||
/// Restores defaults and emits the current state.
|
||||
void reset() {
|
||||
_notifier.value = Environment.normal;
|
||||
_environmentController.add(_notifier.value);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
unawaited(_environmentStreamController?.close());
|
||||
_environmentStreamController = null;
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import "package:arcane_framework/src/arcane.dart";
|
||||
import "package:flutter/widgets.dart";
|
||||
|
||||
import "feature_flags_provider.dart";
|
||||
|
||||
/// Convenience accessors for feature flags from `BuildContext`.
|
||||
extension ArcaneFeatureFlagsContext on BuildContext {
|
||||
/// Returns the nearest [ArcaneFeatureFlagProvider].
|
||||
///
|
||||
/// This creates an inherited dependency, so widgets using this getter in
|
||||
/// `build` rebuild when enabled features change.
|
||||
ArcaneFeatureFlagProvider get featureFlags =>
|
||||
ArcaneFeatureFlagProvider.of(this);
|
||||
|
||||
/// Returns the nearest [ArcaneFeatureFlagProvider], if one exists.
|
||||
ArcaneFeatureFlagProvider? get maybeFeatureFlags =>
|
||||
ArcaneFeatureFlagProvider.maybeOf(this);
|
||||
|
||||
/// Returns `true` when [feature] is enabled.
|
||||
///
|
||||
/// If no [ArcaneFeatureFlagProvider] is available in the tree, this falls
|
||||
/// back
|
||||
/// to [Arcane.features] snapshot state.
|
||||
bool isFeatureEnabled(Enum feature) {
|
||||
return maybeFeatureFlags?.isEnabled(feature) ??
|
||||
Arcane.features.isEnabled(feature);
|
||||
}
|
||||
|
||||
/// Returns `true` when [feature] is disabled.
|
||||
///
|
||||
/// If no [ArcaneFeatureFlagProvider] is available in the tree, this falls
|
||||
/// back
|
||||
/// to [Arcane.features] snapshot state.
|
||||
bool isFeatureDisabled(Enum feature) {
|
||||
return maybeFeatureFlags?.isDisabled(feature) ??
|
||||
Arcane.features.isDisabled(feature);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ part of "feature_flags_service.dart";
|
||||
/// An extension on `Enum` to manage feature toggles.
|
||||
///
|
||||
/// This extension provides a convenient way to enable, disable, and check the status
|
||||
/// of feature flags associated with enum values. It interacts with the `ArcaneFeatureFlags`
|
||||
/// of feature flags associated with enum values. It interacts with the `ArcaneFeatureFlagService`
|
||||
/// system to manage these feature flags at runtime.
|
||||
extension FeatureToggles on Enum {
|
||||
/// Returns `true` if the feature represented by this enum is currently enabled.
|
||||
@@ -31,7 +31,7 @@ extension FeatureToggles on Enum {
|
||||
/// Enables the feature represented by this enum.
|
||||
///
|
||||
/// If the feature is already enabled, this method has no effect. It interacts with
|
||||
/// the `ArcaneFeatureFlags` system to enable the feature.
|
||||
/// the `ArcaneFeatureFlagService` system to enable the feature.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
@@ -42,7 +42,7 @@ extension FeatureToggles on Enum {
|
||||
/// Disables the feature represented by this enum.
|
||||
///
|
||||
/// If the feature is already disabled, this method has no effect. It interacts with
|
||||
/// the `ArcaneFeatureFlags` system to disable the feature.
|
||||
/// the `ArcaneFeatureFlagService` system to disable the feature.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import "package:arcane_framework/src/arcane.dart";
|
||||
import "package:flutter/foundation.dart";
|
||||
import "package:flutter/widgets.dart";
|
||||
|
||||
/// An `InheritedWidget` that provides access to enabled feature flags.
|
||||
///
|
||||
/// Descendant widgets that call [of] or [maybeOf] will rebuild when the
|
||||
/// enabled feature set changes.
|
||||
class ArcaneFeatureFlagProvider extends InheritedWidget {
|
||||
/// The currently enabled feature flags.
|
||||
final List<Enum> enabledFeatures;
|
||||
|
||||
final ValueChanged<Enum> _enableFeature;
|
||||
final ValueChanged<Enum> _disableFeature;
|
||||
|
||||
/// Creates an `ArcaneFeatureFlagProvider` widget.
|
||||
const ArcaneFeatureFlagProvider({
|
||||
required this.enabledFeatures,
|
||||
required void Function(Enum) enableFeature,
|
||||
required void Function(Enum) disableFeature,
|
||||
required super.child,
|
||||
super.key,
|
||||
}) : _enableFeature = enableFeature,
|
||||
_disableFeature = disableFeature;
|
||||
|
||||
/// Retrieves the nearest `ArcaneFeatureFlagProvider` from the widget tree.
|
||||
///
|
||||
/// Returns `null` if no `ArcaneFeatureFlagProvider` ancestor is found.
|
||||
static ArcaneFeatureFlagProvider? maybeOf(BuildContext context) {
|
||||
return context
|
||||
.dependOnInheritedWidgetOfExactType<ArcaneFeatureFlagProvider>();
|
||||
}
|
||||
|
||||
/// Retrieves the nearest `ArcaneFeatureFlagProvider` from the widget tree.
|
||||
///
|
||||
/// Throws a `StateError` if no `ArcaneFeatureFlagProvider` ancestor is found.
|
||||
static ArcaneFeatureFlagProvider of(BuildContext context) {
|
||||
final ArcaneFeatureFlagProvider? result = maybeOf(context);
|
||||
if (result == null) {
|
||||
throw StateError("No ArcaneFeatureFlagProvider found in context");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Returns whether [feature] is currently enabled.
|
||||
bool isEnabled(Enum feature) => enabledFeatures.contains(feature);
|
||||
|
||||
/// Returns whether [feature] is currently disabled.
|
||||
bool isDisabled(Enum feature) => !isEnabled(feature);
|
||||
|
||||
/// Enables [feature].
|
||||
void enableFeature(Enum feature) => _enableFeature(feature);
|
||||
|
||||
/// Disables [feature].
|
||||
void disableFeature(Enum feature) => _disableFeature(feature);
|
||||
|
||||
/// A `ValueListenable` that can be used for reactive feature flag updates.
|
||||
ValueListenable<List<Enum>> get notifier => Arcane.features.notifier;
|
||||
|
||||
/// A stream of enabled feature flag updates.
|
||||
Stream<List<Enum>> get enabledFeaturesChanges =>
|
||||
Arcane.features.enabledFeaturesChanges;
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(ArcaneFeatureFlagProvider oldWidget) {
|
||||
return !listEquals(enabledFeatures, oldWidget.enabledFeatures);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
"Deprecated in 2.0.0. "
|
||||
"ArcaneFeatureFlagsScope has been renamed to ArcaneFeatureFlagProvider. "
|
||||
"Please use ArcaneFeatureFlagProvider instead.",
|
||||
)
|
||||
typedef ArcaneFeatureFlagsScope = ArcaneFeatureFlagProvider;
|
||||
|
||||
/// A `StatefulWidget` that keeps [ArcaneFeatureFlagProvider] in sync with
|
||||
/// [ArcaneFeatureFlagService] and rebuilds descendants when flags change.
|
||||
class ArcaneFeatureFlagsProvider extends StatefulWidget {
|
||||
/// The child widget that will have access to feature flags.
|
||||
final Widget child;
|
||||
|
||||
/// Creates an `ArcaneFeatureFlagsProvider`.
|
||||
const ArcaneFeatureFlagsProvider({
|
||||
required this.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ArcaneFeatureFlagsProvider> createState() =>
|
||||
_ArcaneFeatureFlagsProviderState();
|
||||
}
|
||||
|
||||
class _ArcaneFeatureFlagsProviderState
|
||||
extends State<ArcaneFeatureFlagsProvider> {
|
||||
late List<Enum> _enabledFeatures;
|
||||
|
||||
void _handleFeatureFlagsChange() {
|
||||
if (!mounted) return;
|
||||
|
||||
final List<Enum> nextEnabled =
|
||||
List<Enum>.from(Arcane.features.notifier.value);
|
||||
if (listEquals(nextEnabled, _enabledFeatures)) return;
|
||||
|
||||
setState(() {
|
||||
_enabledFeatures = nextEnabled;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_enabledFeatures = List<Enum>.from(Arcane.features.notifier.value);
|
||||
Arcane.features.notifier.addListener(_handleFeatureFlagsChange);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
Arcane.features.notifier.removeListener(_handleFeatureFlagsChange);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void enableFeature(Enum feature) {
|
||||
Arcane.features.enableFeature(feature);
|
||||
}
|
||||
|
||||
void disableFeature(Enum feature) {
|
||||
Arcane.features.disableFeature(feature);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ArcaneFeatureFlagProvider(
|
||||
enabledFeatures: List<Enum>.unmodifiable(_enabledFeatures),
|
||||
enableFeature: enableFeature,
|
||||
disableFeature: disableFeature,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,45 @@
|
||||
import "dart:async";
|
||||
|
||||
import "package:arcane_framework/arcane_framework.dart";
|
||||
import "package:flutter/foundation.dart";
|
||||
|
||||
part "feature_flags_extensions.dart";
|
||||
|
||||
@Deprecated(
|
||||
"Deprecated in 2.0.0. "
|
||||
"ArcaneFeatureFlags has been renamed to ArcaneFeatureFlagService for clarity. "
|
||||
"Please use ArcaneFeatureFlagService instead.",
|
||||
)
|
||||
typedef ArcaneFeatureFlags = ArcaneFeatureFlagService;
|
||||
|
||||
/// A singleton class that manages feature flags in the Arcane architecture.
|
||||
///
|
||||
/// `ArcaneFeatureFlags` allows features to be dynamically enabled or disabled
|
||||
/// `ArcaneFeatureFlagService` allows features to be dynamically enabled or disabled
|
||||
/// at runtime. This can be useful for controlling access to experimental or
|
||||
/// conditional functionality without requiring an application restart.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// ArcaneFeatureFlags.I.enableFeature(MyFeature.example);
|
||||
/// if (ArcaneFeatureFlags.I.isEnabled(MyFeature.example)) {
|
||||
/// ArcaneFeatureFlagService.I.enableFeature(MyFeature.example);
|
||||
/// if (ArcaneFeatureFlagService.I.isEnabled(MyFeature.example)) {
|
||||
/// // Execute feature-specific logic
|
||||
/// }
|
||||
/// ```
|
||||
class ArcaneFeatureFlags extends ArcaneService {
|
||||
ArcaneFeatureFlags._internal();
|
||||
class ArcaneFeatureFlagService extends ArcaneService {
|
||||
ArcaneFeatureFlagService._internal();
|
||||
|
||||
/// The singleton instance of `ArcaneFeatureFlags`.
|
||||
static final ArcaneFeatureFlags _instance = ArcaneFeatureFlags._internal();
|
||||
/// The singleton instance of `ArcaneFeatureFlagService`.
|
||||
static final ArcaneFeatureFlagService _instance =
|
||||
ArcaneFeatureFlagService._internal();
|
||||
|
||||
/// Provides access to the singleton instance of `ArcaneFeatureFlags`.
|
||||
static ArcaneFeatureFlags get I => _instance;
|
||||
/// Provides access to the singleton instance of `ArcaneFeatureFlagService`.
|
||||
static ArcaneFeatureFlagService get I => _instance;
|
||||
|
||||
/// A list of enabled features.
|
||||
/// A list of enabled features as a snapshot value.
|
||||
///
|
||||
/// Reading this getter does not subscribe to changes and does not trigger
|
||||
/// widget rebuilds. Use [notifier] (for `ValueListenableBuilder`) or
|
||||
/// [enabledFeaturesChanges] (for streams) when you need reactive updates.
|
||||
///
|
||||
/// Each feature is represented as an `Enum`. The list holds the features that are
|
||||
/// currently enabled.
|
||||
@@ -37,6 +51,18 @@ class ArcaneFeatureFlags extends ArcaneService {
|
||||
/// A `ValueNotifier` that notifies listeners when the list of enabled features changes.
|
||||
ValueNotifier<List<Enum>> get notifier => _notifier;
|
||||
|
||||
StreamController<List<Enum>>? _enabledFeaturesStreamController;
|
||||
|
||||
StreamController<List<Enum>> get _enabledFeaturesController {
|
||||
_enabledFeaturesStreamController ??=
|
||||
StreamController<List<Enum>>.broadcast();
|
||||
return _enabledFeaturesStreamController!;
|
||||
}
|
||||
|
||||
/// Stream of enabled feature list updates.
|
||||
Stream<List<Enum>> get enabledFeaturesChanges =>
|
||||
I._enabledFeaturesController.stream;
|
||||
|
||||
/// Indicates whether the feature flags have been initialized.
|
||||
bool _initialized = false;
|
||||
|
||||
@@ -63,27 +89,26 @@ class ArcaneFeatureFlags extends ArcaneService {
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// ArcaneFeatureFlags.I.enableFeature(MyFeature.newFeature);
|
||||
/// ArcaneFeatureFlagService.I.enableFeature(MyFeature.newFeature);
|
||||
/// ```
|
||||
ArcaneFeatureFlags enableFeature(Enum feature) {
|
||||
ArcaneFeatureFlagService enableFeature(Enum feature) {
|
||||
if (!I._initialized) _init();
|
||||
|
||||
if (_enabledFeatures.contains(feature)) return I;
|
||||
|
||||
_enabledFeatures.add(feature);
|
||||
_notifier.value.add(feature);
|
||||
_notifier.value = [..._enabledFeatures, feature];
|
||||
_enabledFeaturesController.add(List<Enum>.from(_notifier.value));
|
||||
|
||||
if (Arcane.logger.initialized) {
|
||||
Arcane.logger.log(
|
||||
"Feature enabled: ${feature.name}",
|
||||
level: Level.debug,
|
||||
"Feature enabled: $feature",
|
||||
level: Level.info,
|
||||
metadata: {
|
||||
feature.name: "✅",
|
||||
feature.toString(): "✅",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
return I;
|
||||
}
|
||||
|
||||
@@ -94,26 +119,25 @@ class ArcaneFeatureFlags extends ArcaneService {
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// ArcaneFeatureFlags.I.disableFeature(MyFeature.oldFeature);
|
||||
/// ArcaneFeatureFlagService.I.disableFeature(MyFeature.oldFeature);
|
||||
/// ```
|
||||
ArcaneFeatureFlags disableFeature(Enum feature) {
|
||||
ArcaneFeatureFlagService disableFeature(Enum feature) {
|
||||
if (!I._initialized) _init();
|
||||
if (!_enabledFeatures.contains(feature)) return I;
|
||||
|
||||
_enabledFeatures.remove(feature);
|
||||
_notifier.value.remove(feature);
|
||||
_notifier.value = [..._enabledFeatures]..removeWhere((i) => i == feature);
|
||||
_enabledFeaturesController.add(List<Enum>.from(_notifier.value));
|
||||
|
||||
if (Arcane.logger.initialized) {
|
||||
Arcane.logger.log(
|
||||
"Feature disabled: ${feature.name}",
|
||||
level: Level.debug,
|
||||
"Feature disabled: $feature",
|
||||
level: Level.info,
|
||||
metadata: {
|
||||
feature.name: "❌",
|
||||
feature.toString(): "❌",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
return I;
|
||||
}
|
||||
|
||||
@@ -123,10 +147,34 @@ class ArcaneFeatureFlags extends ArcaneService {
|
||||
/// It is called automatically when enabling or disabling features if they haven't
|
||||
/// already been initialized.
|
||||
void _init() {
|
||||
_enabledFeatures.clear();
|
||||
_notifier.value.clear();
|
||||
|
||||
if (I._initialized) return;
|
||||
reset();
|
||||
I._initialized = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Resets the feature flags to their initial state.
|
||||
///
|
||||
/// This method clears all enabled features, resets notification values,
|
||||
/// marks the flags as uninitialized, and notifies listeners of the changes.
|
||||
void reset() {
|
||||
notifier
|
||||
..removeListener(_listener)
|
||||
..addListener(_listener);
|
||||
_notifier.value = [];
|
||||
_enabledFeaturesController.add(List<Enum>.from(_notifier.value));
|
||||
I._initialized = false;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
unawaited(_enabledFeaturesStreamController?.close());
|
||||
_enabledFeaturesStreamController = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _listener() {
|
||||
_enabledFeatures
|
||||
..clear()
|
||||
..addAll(notifier.value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
part of "logging_service.dart";
|
||||
|
||||
class LogEvent {
|
||||
static const Object _sentinel = Object();
|
||||
|
||||
const LogEvent({
|
||||
required this.message,
|
||||
this.metadata,
|
||||
this.level,
|
||||
this.stackTrace,
|
||||
this.extra,
|
||||
});
|
||||
|
||||
final String message;
|
||||
final Map<String, Object?>? metadata;
|
||||
final Level? level;
|
||||
final StackTrace? stackTrace;
|
||||
final Object? extra;
|
||||
|
||||
LogEvent copyWith({
|
||||
String? message,
|
||||
Object? metadata = _sentinel,
|
||||
Object? level = _sentinel,
|
||||
Object? stackTrace = _sentinel,
|
||||
Object? extra = _sentinel,
|
||||
}) {
|
||||
return LogEvent(
|
||||
message: message ?? this.message,
|
||||
metadata: identical(metadata, _sentinel)
|
||||
? this.metadata
|
||||
: metadata as Map<String, Object?>?,
|
||||
level: identical(level, _sentinel) ? this.level : level as Level?,
|
||||
stackTrace: identical(stackTrace, _sentinel)
|
||||
? this.stackTrace
|
||||
: stackTrace as StackTrace?,
|
||||
extra: identical(extra, _sentinel) ? this.extra : extra,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
part of "logging_service.dart";
|
||||
|
||||
/// Provides contextual information for a [LogInterceptor] invocation.
|
||||
///
|
||||
/// This context is passed to each log interceptor and can be used to provide
|
||||
/// additional data or interfaces that may influence how log events are processed.
|
||||
/// For example, it may contain a reference to the [LoggingInterface] that
|
||||
/// originated the log event.
|
||||
///
|
||||
/// Typically, you do not need to construct this class directly; it is created
|
||||
/// and managed by the logging framework.
|
||||
///
|
||||
/// See also:
|
||||
/// - [LogInterceptor], which uses this context when intercepting log events.
|
||||
/// - [LoggingInterface], which may be referenced by this context.
|
||||
|
||||
final class LogInterceptorContext {
|
||||
/// Creates a new [LogInterceptorContext].
|
||||
///
|
||||
/// The [interface] parameter may be used to provide a reference to the
|
||||
/// [LoggingInterface] that originated the log event, or may be null if not applicable.
|
||||
const LogInterceptorContext({
|
||||
this.interface,
|
||||
});
|
||||
|
||||
/// The [LoggingInterface] associated with this context, if any.
|
||||
///
|
||||
/// This can be used by interceptors to access additional logging features or
|
||||
/// metadata about the source of the log event.
|
||||
final LoggingInterface? interface;
|
||||
}
|
||||
|
||||
/// A function-like object that intercepts and optionally transforms log events.
|
||||
///
|
||||
/// [LogInterceptor] allows you to observe, modify, or suppress log events as
|
||||
/// they pass through the logging pipeline. You provide a callback that receives
|
||||
/// each [LogEvent] and its [LogInterceptorContext], and returns either a new
|
||||
/// (possibly modified) [LogEvent], or `null` to suppress the event.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// final interceptor = LogInterceptor((event, context) {
|
||||
/// // Filter out debug-level logs
|
||||
/// if (event.level == Level.debug) return null;
|
||||
/// return event;
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// See also:
|
||||
/// - [LogEvent], which represents a log entry.
|
||||
/// - [LogInterceptorContext], which provides context for the interception.
|
||||
class LogInterceptor {
|
||||
/// Creates a [LogInterceptor] with the given callback.
|
||||
///
|
||||
/// The [_callback] function will be invoked for each log event, with the
|
||||
/// event and its context. Return a [LogEvent] to continue processing, or
|
||||
/// `null` to suppress the event.
|
||||
const LogInterceptor(this._callback);
|
||||
|
||||
/// The callback function that processes each log event.
|
||||
///
|
||||
/// The function receives the [event] and its [context], and should return
|
||||
/// either a (possibly modified) [LogEvent], or `null` to suppress the event.
|
||||
final LogEvent? Function(
|
||||
LogEvent event,
|
||||
LogInterceptorContext context,
|
||||
) _callback;
|
||||
|
||||
/// Invokes the interceptor on the given [event] and [context].
|
||||
///
|
||||
/// Returns the (possibly modified) [LogEvent], or `null` to suppress the event.
|
||||
LogEvent? call(
|
||||
LogEvent event, {
|
||||
required LogInterceptorContext context,
|
||||
}) {
|
||||
return _callback(event, context);
|
||||
}
|
||||
}
|
||||
@@ -5,36 +5,49 @@ part of "logging_service.dart";
|
||||
/// Concrete implementations of this class should override the [log] method to provide
|
||||
/// platform-specific logging behavior.
|
||||
abstract class LoggingInterface {
|
||||
LoggingInterface._internal();
|
||||
static late final LoggingInterface _instance;
|
||||
|
||||
/// Provides access to the singleton instance of the `LoggingInterface`. This
|
||||
/// ensures that the logging interface, once configured, remains so.
|
||||
static LoggingInterface get I => _instance;
|
||||
|
||||
bool _initialized = false;
|
||||
|
||||
/// Whether the logging interface has been initialized.
|
||||
bool get initialized => I._initialized;
|
||||
|
||||
/// Initializes the logging interface.
|
||||
///
|
||||
/// If any configuration needs to be performed on the logging interface prior
|
||||
/// to use, this is where it should be done.
|
||||
/// This method should, at a minimum, set `I._initialized = true`.
|
||||
Future<LoggingInterface?> init() async {
|
||||
I._initialized = true;
|
||||
return I;
|
||||
}
|
||||
const LoggingInterface();
|
||||
|
||||
/// This method is called by the `ArcaneLogger` when a log message is
|
||||
/// received. See `ArcaneLogger.log` for further details on how logging
|
||||
/// works and what options are available.
|
||||
void log(
|
||||
String message, {
|
||||
Map<String, dynamic>? metadata,
|
||||
Map<String, Object?>? metadata,
|
||||
Level? level,
|
||||
StackTrace? stackTrace,
|
||||
Object? extra,
|
||||
});
|
||||
}
|
||||
|
||||
/// Optional lifecycle contract for logging interfaces that require setup.
|
||||
abstract interface class LoggingInitializable {
|
||||
/// Whether this logging destination has completed initialization.
|
||||
bool get initialized;
|
||||
|
||||
/// Initializes this logging destination.
|
||||
Future<void> init();
|
||||
}
|
||||
|
||||
/// Default initialization behavior for interfaces that opt into lifecycle.
|
||||
mixin LoggingInitialization implements LoggingInitializable {
|
||||
bool _initialized = false;
|
||||
|
||||
@override
|
||||
bool get initialized => _initialized;
|
||||
|
||||
@override
|
||||
Future<void> init() async {
|
||||
_initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Annotation used to tag a logging destination with a feature name.
|
||||
///
|
||||
/// Example:
|
||||
/// `@LoggingFeature("analytics")`
|
||||
final class LoggingFeature {
|
||||
const LoggingFeature(this.value);
|
||||
|
||||
/// The feature name associated with this logging destination.
|
||||
final String value;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import "dart:async";
|
||||
|
||||
import "package:arcane_helper_utils/arcane_helper_utils.dart";
|
||||
|
||||
part "log_event.dart";
|
||||
part "log_interceptor.dart";
|
||||
part "logging_enums.dart";
|
||||
part "logging_interface.dart";
|
||||
|
||||
@@ -19,16 +21,37 @@ class ArcaneLogger {
|
||||
/// Provides access to the singleton instance of `ArcaneLogger`.
|
||||
static ArcaneLogger get I => _instance;
|
||||
|
||||
final List<LoggingInterface> _interfaces = [];
|
||||
final List<_LoggingInterfaceRegistration> _interfaceRegistrations = [];
|
||||
|
||||
final List<LogInterceptor> _interceptors = [];
|
||||
|
||||
/// A list of registered logging interfaces.
|
||||
List<LoggingInterface> get interfaces => I._interfaces;
|
||||
List<LoggingInterface> get interfaces => [
|
||||
for (final _LoggingInterfaceRegistration registration
|
||||
in I._interfaceRegistrations)
|
||||
registration.interface,
|
||||
];
|
||||
|
||||
/// A list of globally registered interceptors.
|
||||
List<LogInterceptor> get interceptors => [
|
||||
...I._interceptors,
|
||||
];
|
||||
|
||||
final Map<String, String> _additionalMetadata = {};
|
||||
|
||||
/// Additional metadata that is included in all logs.
|
||||
Map<String, String> get additionalMetadata => I._additionalMetadata;
|
||||
|
||||
StreamController<String>? _logStreamController;
|
||||
|
||||
StreamController<String> get _logController {
|
||||
_logStreamController ??= StreamController<String>.broadcast();
|
||||
return _logStreamController!;
|
||||
}
|
||||
|
||||
/// Stream of log messages being received and sent to the registered interfaces.
|
||||
Stream<String> get logStream => I._logController.stream;
|
||||
|
||||
bool _initialized = false;
|
||||
|
||||
/// Whether the logger has been initialized.
|
||||
@@ -76,7 +99,7 @@ class ArcaneLogger {
|
||||
/// The stack trace associated with the log event. Useful for error and
|
||||
/// warning logs to trace the execution path leading to the log event.
|
||||
///
|
||||
/// - `metadata` ([Map<String, String>?], _optional_):
|
||||
/// - `metadata` ([Map<String, Object?>?], _optional_):
|
||||
/// Additional key-value pairs providing extra context for the log. Commonly
|
||||
/// used for custom information that can aid in diagnosing issues or
|
||||
/// understanding the log in context. If not provided, an empty map is used.
|
||||
@@ -114,95 +137,302 @@ class ArcaneLogger {
|
||||
/// ```
|
||||
///
|
||||
void log(
|
||||
/// The message to be logged
|
||||
String message, {
|
||||
/// The Dart class from which the `log` call was invoked. This is useful
|
||||
/// in determining which part of the code called the log event. If the
|
||||
/// [module] is not specified and [skipAutodetection] is set to [false],
|
||||
/// [ArcaneLogger] will attempt to derive this value automatically. However,
|
||||
/// this may fail in some cases and could potentially adversely impact
|
||||
/// performance.
|
||||
String? module,
|
||||
|
||||
/// The method from which the `log` call was invoked. This is useful
|
||||
/// in determining which part of the code called the log event. If the
|
||||
/// [method] is not specified and [skipAutodetection] is set to [false],
|
||||
/// [ArcaneLogger] will attempt to derive this value automatically. However,
|
||||
/// this may fail in some cases and could potentially adversely impact
|
||||
/// performance.
|
||||
String? method,
|
||||
|
||||
/// This value defines the severity of the log message. The default value is
|
||||
/// [Level.debug].
|
||||
Level level = Level.debug,
|
||||
|
||||
/// A [StackTrace] can be passed into the `log` call for further processing
|
||||
/// by the registered [LoggingInterface]s.
|
||||
StackTrace? stackTrace,
|
||||
Map<String, String>? metadata,
|
||||
|
||||
/// The provided [metadata] will be merged with any previously registered
|
||||
/// persistent metadata. If the [module] and/or [method] are provided, these
|
||||
/// values will be merged with the [metadata] as well, otherwise if one or
|
||||
/// none of these values are provided and [skipAutodetection] is set to
|
||||
/// [false] (default), the module, method, and/or [filenameAndLineNumber]
|
||||
/// will be automatically determined and added to the [metadata] if the
|
||||
/// values are not already present.
|
||||
Map<String, Object?>? metadata,
|
||||
|
||||
/// The [extra] parameter can be used to pass _any_ object into the
|
||||
/// registered [LoggingInterface]s.
|
||||
Object? extra,
|
||||
|
||||
/// If set to [true], this parameter will skip automatically trying to
|
||||
/// determine the current log message's [module], [method], and
|
||||
/// [filenameAndLineNumber].
|
||||
///
|
||||
/// If set to [true] and the [filenameAndLineNumber] are desired, they
|
||||
/// should be calculated by the [LoggingInterface] and added as as
|
||||
/// [metadata].
|
||||
///
|
||||
/// If this value is [false] (default), the [module] and [method] will only
|
||||
/// be added to the [metadata] if they are not otherwise provided. However,
|
||||
/// the [filenameAndLineNumber] will automatically be added _unless_ it is
|
||||
/// already in the [metadata] provided.
|
||||
///
|
||||
/// When set to [true], the automatic generation of these values _may_
|
||||
/// impact performance.
|
||||
bool skipAutodetection = false,
|
||||
}) {
|
||||
if (!I._initialized) {
|
||||
throw Exception("ArcaneLogger has not yet been initialized.");
|
||||
final Map<String, Object?> logMetadata = {
|
||||
...?metadata,
|
||||
};
|
||||
logMetadata.putIfAbsent(
|
||||
"timestamp",
|
||||
() => DateTime.now().toIso8601String(),
|
||||
);
|
||||
|
||||
String? filenameAndLineNumber;
|
||||
if (!skipAutodetection) {
|
||||
String? parts;
|
||||
try {
|
||||
parts = StackTrace.current
|
||||
.toString()
|
||||
.split("\n")[2]
|
||||
.split(RegExp("#2"))[1]
|
||||
.trim();
|
||||
} catch (_) {}
|
||||
|
||||
module ??= parts?.split(".").firstOrNull?.replaceFirst("new ", "");
|
||||
|
||||
method ??= ((parts?.split(".").length ?? 0) <= 1)
|
||||
? null
|
||||
: parts
|
||||
?.split(".")[1]
|
||||
.split(" ")
|
||||
.firstOrNull
|
||||
?.replaceAll("<anonymous", "");
|
||||
|
||||
final List<String> fileAndLineParts = [
|
||||
...?parts?.split("(package:").lastOrNull?.split(":"),
|
||||
];
|
||||
|
||||
if (fileAndLineParts.length < 2) {
|
||||
filenameAndLineNumber = fileAndLineParts.firstOrNull;
|
||||
} else {
|
||||
filenameAndLineNumber = "${fileAndLineParts[0]}:${fileAndLineParts[1]}";
|
||||
}
|
||||
}
|
||||
|
||||
metadata ??= <String, String>{};
|
||||
// Module management
|
||||
if (module.isNotEmptyOrNull) {
|
||||
logMetadata.putIfAbsent("module", () => module!);
|
||||
}
|
||||
|
||||
final String now = DateTime.now().toIso8601String();
|
||||
metadata.putIfAbsent("timestamp", () => now);
|
||||
// Method managmeent
|
||||
if (method.isNotEmptyOrNull) {
|
||||
logMetadata.putIfAbsent("method", () => method!);
|
||||
}
|
||||
|
||||
try {
|
||||
final List<String> parts = StackTrace.current
|
||||
.toString()
|
||||
.split("\n")[2]
|
||||
.split(RegExp("#2"))[1]
|
||||
.trimLeft()
|
||||
.split(".");
|
||||
|
||||
module ??= parts.first.replaceFirst("new ", "");
|
||||
method ??= parts[1].split(" ").first.replaceAll("<anonymous", "");
|
||||
|
||||
final List<String> fileAndLineParts = StackTrace.current
|
||||
.toString()
|
||||
.split("\n")[2]
|
||||
.split(RegExp("#2"))[1]
|
||||
.trim()
|
||||
.split("(package:")
|
||||
.last
|
||||
.split(":");
|
||||
|
||||
final String fileAndLine =
|
||||
"${fileAndLineParts[0]}:${fileAndLineParts[1]}";
|
||||
|
||||
metadata.putIfAbsent("module", () => module!);
|
||||
if (method.isNotNullOrEmpty)
|
||||
metadata.putIfAbsent("method", () => method!);
|
||||
metadata.putIfAbsent("filenameAndLineNumber", () => fileAndLine);
|
||||
} catch (_) {}
|
||||
|
||||
metadata.addAll(additionalMetadata);
|
||||
|
||||
// Send logs to registered interface(s)
|
||||
for (final LoggingInterface i in I._interfaces) {
|
||||
i.log(
|
||||
message,
|
||||
level: level,
|
||||
metadata: metadata,
|
||||
stackTrace: stackTrace,
|
||||
extra: extra,
|
||||
// Filename and line number management
|
||||
if (filenameAndLineNumber.isNotNullOrEmpty) {
|
||||
logMetadata.putIfAbsent(
|
||||
"filenameAndLineNumber",
|
||||
() => filenameAndLineNumber!,
|
||||
);
|
||||
}
|
||||
|
||||
logMetadata.addAll(additionalMetadata);
|
||||
|
||||
module ??= logMetadata.containsKey("module")
|
||||
? logMetadata["module"] as String?
|
||||
: null;
|
||||
method ??= logMetadata.containsKey("method")
|
||||
? logMetadata["method"] as String?
|
||||
: null;
|
||||
|
||||
final LogEvent event = LogEvent(
|
||||
message: message,
|
||||
metadata: logMetadata,
|
||||
level: level,
|
||||
stackTrace: stackTrace,
|
||||
extra: extra,
|
||||
);
|
||||
|
||||
// Send logs to registered interface(s)
|
||||
for (final _LoggingInterfaceRegistration registration
|
||||
in I._interfaceRegistrations) {
|
||||
if (initialized) {
|
||||
final LogEvent? interfaceEvent = _runInterceptors(
|
||||
event.copyWith(
|
||||
metadata: event.metadata == null
|
||||
? null
|
||||
: Map<String, Object?>.from(event.metadata!),
|
||||
),
|
||||
interceptors: [
|
||||
...I._interceptors,
|
||||
...registration.interceptors,
|
||||
],
|
||||
context: LogInterceptorContext(interface: registration.interface),
|
||||
);
|
||||
|
||||
if (interfaceEvent == null) continue;
|
||||
|
||||
registration.interface.log(
|
||||
interfaceEvent.message,
|
||||
level: interfaceEvent.level,
|
||||
metadata: interfaceEvent.metadata,
|
||||
stackTrace: interfaceEvent.stackTrace,
|
||||
extra: interfaceEvent.extra,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_logController.add(
|
||||
"${event.message} ${{
|
||||
"level": event.level,
|
||||
"metadata": event.metadata,
|
||||
"extra": event.extra,
|
||||
}}",
|
||||
);
|
||||
}
|
||||
|
||||
/// Registers a [LoggingInterface] with the [ArcaneLogger]. Due to iOS app
|
||||
/// tracking permissions, permission to track must first be checked for
|
||||
/// and (optionally) granted before the interface is automatically initialized.
|
||||
/// Registers a [LoggingInterface] with the [ArcaneLogger].
|
||||
///
|
||||
/// Once your [LoggingInterface] has been registered and initialized, logs
|
||||
/// will automatically be sent to the interface.
|
||||
/// Once your [LoggingInterface] has been registered, logs are eligible to be
|
||||
/// sent to the interface immediately.
|
||||
Future<ArcaneLogger> registerInterface(
|
||||
LoggingInterface loggingInterface, {
|
||||
List<LogInterceptor>? interceptors,
|
||||
}) async {
|
||||
if (!initialized) await _init();
|
||||
|
||||
I._interfaceRegistrations.add(
|
||||
_LoggingInterfaceRegistration(
|
||||
interface: loggingInterface,
|
||||
interceptors: interceptors,
|
||||
),
|
||||
);
|
||||
|
||||
return I;
|
||||
}
|
||||
|
||||
/// Registers a global [LogInterceptor] to run before interface fan-out.
|
||||
ArcaneLogger registerInterceptor(LogInterceptor interceptor) {
|
||||
I._interceptors.add(interceptor);
|
||||
return I;
|
||||
}
|
||||
|
||||
/// Registers a `List` of global [LogInterceptor]s.
|
||||
ArcaneLogger registerInterceptors(List<LogInterceptor> interceptors) {
|
||||
I._interceptors.addAll(interceptors);
|
||||
return I;
|
||||
}
|
||||
|
||||
/// Unregisters a previously registered global [LogInterceptor].
|
||||
ArcaneLogger unregisterInterceptor(LogInterceptor interceptor) {
|
||||
I._interceptors.remove(interceptor);
|
||||
return I;
|
||||
}
|
||||
|
||||
/// Removes all previously registered global interceptors.
|
||||
ArcaneLogger clearInterceptors() {
|
||||
I._interceptors.clear();
|
||||
return I;
|
||||
}
|
||||
|
||||
/// Registers a `List` of [LoggingInterface] with the [ArcaneLogger].
|
||||
///
|
||||
/// Once registered, logs are eligible to be sent to these interfaces
|
||||
/// immediately.
|
||||
Future<ArcaneLogger> registerInterfaces(
|
||||
List<LoggingInterface> interfaces,
|
||||
) async {
|
||||
List<LoggingInterface> interfaces, {
|
||||
Map<LoggingInterface, List<LogInterceptor>>? interceptors,
|
||||
}) async {
|
||||
if (!initialized) await _init();
|
||||
|
||||
for (final LoggingInterface i in interfaces) {
|
||||
I._interfaces.add(i);
|
||||
I._interfaceRegistrations.add(
|
||||
_LoggingInterfaceRegistration(
|
||||
interface: i,
|
||||
interceptors: interceptors?[i],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return I;
|
||||
}
|
||||
|
||||
/// Initializes all registered [LoggingInterface]s by calling their
|
||||
/// [LoggingInterface.init] methods.
|
||||
Future<ArcaneLogger> initializeInterfaces() async {
|
||||
assert(
|
||||
I._interfaces.isNotEmpty,
|
||||
"No logging interfaces have been registered.",
|
||||
/// Unregisters a [LoggingInterface] from the [ArcaneLogger], if it was
|
||||
/// previously registered.
|
||||
Future<ArcaneLogger> unregisterInterface(
|
||||
LoggingInterface interface,
|
||||
) async {
|
||||
if (!initialized) await _init();
|
||||
|
||||
I._interfaceRegistrations.removeWhere(
|
||||
(_LoggingInterfaceRegistration registration) =>
|
||||
identical(registration.interface, interface),
|
||||
);
|
||||
|
||||
if (!I._initialized) await _init();
|
||||
for (final LoggingInterface i in I._interfaces) {
|
||||
if (!i.initialized) await i.init();
|
||||
return I;
|
||||
}
|
||||
|
||||
/// Unregisters a `List` of [LoggingInterface] from the [ArcaneLogger], if
|
||||
/// they were previously registered.
|
||||
Future<ArcaneLogger> unregisterInterfaces(
|
||||
List<LoggingInterface> interfaces,
|
||||
) async {
|
||||
if (!initialized) await _init();
|
||||
|
||||
for (final LoggingInterface i in interfaces) {
|
||||
I._interfaceRegistrations.removeWhere(
|
||||
(_LoggingInterfaceRegistration registration) =>
|
||||
identical(registration.interface, i),
|
||||
);
|
||||
}
|
||||
|
||||
return I;
|
||||
}
|
||||
|
||||
/// Unregisters all previously registered [LoggingInterface] from the
|
||||
/// [ArcaneLogger], if any were previously registered.
|
||||
Future<ArcaneLogger> unregisterAllInterfaces() async {
|
||||
if (!initialized) await _init();
|
||||
I._interfaceRegistrations.clear();
|
||||
return I;
|
||||
}
|
||||
|
||||
/// Initializes registered interfaces that opt into [LoggingInitializable].
|
||||
///
|
||||
/// Interfaces that do not implement [LoggingInitializable] are skipped.
|
||||
Future<ArcaneLogger> initializeInterfaces() async {
|
||||
if (I._interfaceRegistrations.isEmptyOrNull) {
|
||||
throw Exception("No logging interfaces have been registered.");
|
||||
}
|
||||
|
||||
if (!initialized) await _init();
|
||||
|
||||
for (final _LoggingInterfaceRegistration registration
|
||||
in I._interfaceRegistrations) {
|
||||
final LoggingInterface loggingInterface = registration.interface;
|
||||
final LoggingInitializable? initializable =
|
||||
loggingInterface is LoggingInitializable
|
||||
? loggingInterface as LoggingInitializable
|
||||
: null;
|
||||
|
||||
if (initializable != null && !initializable.initialized) {
|
||||
await initializable.init();
|
||||
}
|
||||
}
|
||||
|
||||
return I;
|
||||
@@ -245,4 +475,50 @@ class ArcaneLogger {
|
||||
|
||||
/// Clears all persistent metadata.
|
||||
void clearPersistentMetadata() => _additionalMetadata.clear();
|
||||
|
||||
/// Resets the Arcane logging service by clearing all persistent metadata,
|
||||
/// clearing all registered [LoggingInterface]s and marking the logging
|
||||
/// service as no longer being initialized.
|
||||
void reset() {
|
||||
dispose();
|
||||
I._interfaceRegistrations.clear();
|
||||
I._interceptors.clear();
|
||||
I._initialized = false;
|
||||
I._additionalMetadata.clear();
|
||||
}
|
||||
|
||||
/// Closes logger streams and allows lazy recreation on subsequent access.
|
||||
void dispose() {
|
||||
unawaited(_logStreamController?.close());
|
||||
_logStreamController = null;
|
||||
}
|
||||
|
||||
LogEvent? _runInterceptors(
|
||||
LogEvent event, {
|
||||
required List<LogInterceptor> interceptors,
|
||||
required LogInterceptorContext context,
|
||||
}) {
|
||||
LogEvent? currentEvent = event;
|
||||
|
||||
for (final LogInterceptor interceptor in List<LogInterceptor>.from(
|
||||
interceptors,
|
||||
)) {
|
||||
if (currentEvent == null) return null;
|
||||
currentEvent = interceptor(currentEvent, context: context);
|
||||
}
|
||||
|
||||
return currentEvent;
|
||||
}
|
||||
}
|
||||
|
||||
final class _LoggingInterfaceRegistration {
|
||||
_LoggingInterfaceRegistration({
|
||||
required this.interface,
|
||||
List<LogInterceptor>? interceptors,
|
||||
}) : interceptors = [
|
||||
...?interceptors,
|
||||
];
|
||||
|
||||
final LoggingInterface interface;
|
||||
final List<LogInterceptor> interceptors;
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
part of "reactive_theme_service.dart";
|
||||
|
||||
/// An extension on `BuildContext` to check the current system dark mode setting.
|
||||
///
|
||||
/// This extension provides a convenient way to check whether the device is in dark mode.
|
||||
extension DarkMode on BuildContext {
|
||||
/// Returns `true` if the system is currently set to dark mode.
|
||||
///
|
||||
/// This uses `MediaQuery.of(this).platformBrightness` to check the system's brightness setting.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// if (context.isDarkMode) {
|
||||
/// // The system is in dark mode.
|
||||
/// }
|
||||
/// ```
|
||||
bool get isDarkMode {
|
||||
final brightness = MediaQuery.of(this).platformBrightness;
|
||||
return brightness == Brightness.dark;
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
import "package:arcane_framework/arcane_framework.dart";
|
||||
import "package:flutter/foundation.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
part "reactive_theme_extensions.dart";
|
||||
|
||||
/// A singleton service that manages theme switching and customization for the application.
|
||||
///
|
||||
/// `ArcaneReactiveTheme` allows switching between light and dark themes and provides
|
||||
/// methods to customize the themes. The current theme mode can be accessed, and the
|
||||
/// theme can be switched at runtime.
|
||||
class ArcaneReactiveTheme extends ArcaneService {
|
||||
/// The singleton instance of `ArcaneReactiveTheme`.
|
||||
static final ArcaneReactiveTheme _instance = ArcaneReactiveTheme._internal();
|
||||
|
||||
/// Provides access to the singleton instance of `ArcaneReactiveTheme`.
|
||||
static ArcaneReactiveTheme get I => _instance;
|
||||
|
||||
ArcaneReactiveTheme._internal();
|
||||
|
||||
/// Whether the current theme is dark.
|
||||
bool _isDark = false;
|
||||
|
||||
/// Returns the current theme mode based on `_isDark`.
|
||||
///
|
||||
/// If `_isDark` is true, it returns `ThemeMode.dark`, otherwise it returns `ThemeMode.light`.
|
||||
ThemeMode get currentMode => _isDark ? ThemeMode.dark : ThemeMode.light;
|
||||
|
||||
/// The `ThemeData` for the dark theme.
|
||||
ThemeData _darkTheme = ThemeData.dark();
|
||||
|
||||
/// The `ThemeData` for the light theme.
|
||||
ThemeData _lightTheme = ThemeData.light();
|
||||
|
||||
/// Returns the current dark theme `ThemeData`.
|
||||
ThemeData get dark => _darkTheme;
|
||||
|
||||
/// Returns the current light theme `ThemeData`.
|
||||
ThemeData get light => _lightTheme;
|
||||
|
||||
/// A listenable that notifies listeners when the syste theme mode changes.
|
||||
ValueListenable<ThemeMode> get systemTheme =>
|
||||
ValueNotifier<ThemeMode>(_isDark ? ThemeMode.dark : ThemeMode.light);
|
||||
|
||||
/// Switches the current theme between light and dark modes.
|
||||
///
|
||||
/// If the theme is currently light, it switches to dark, and vice versa. It also
|
||||
/// notifies listeners to update the UI accordingly.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// ArcaneReactiveTheme.I.switchTheme();
|
||||
/// ```
|
||||
ArcaneReactiveTheme switchTheme() {
|
||||
_isDark = !_isDark;
|
||||
notifyListeners();
|
||||
|
||||
return I;
|
||||
}
|
||||
|
||||
/// Switches the current theme between light and dark modes automatically
|
||||
/// based upon the system's current mode.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// ArcaneReactiveTheme.I.followSystemTheme(context);
|
||||
/// final ThemeMode mode = Arcane.theme.systemTheme.value;
|
||||
/// ```
|
||||
ArcaneReactiveTheme followSystemTheme(BuildContext context) {
|
||||
final ThemeMode systemMode =
|
||||
context.isDarkMode ? ThemeMode.dark : ThemeMode.light;
|
||||
|
||||
if (currentMode != systemMode) {
|
||||
switchTheme();
|
||||
}
|
||||
|
||||
return I;
|
||||
}
|
||||
|
||||
/// Sets a custom `ThemeData` for the dark theme.
|
||||
///
|
||||
/// This allows you to customize the dark theme and notify listeners to apply the
|
||||
/// changes immediately.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// ArcaneReactiveTheme.I.setDarkTheme(customDarkTheme);
|
||||
/// ```
|
||||
ArcaneReactiveTheme setDarkTheme(ThemeData theme) {
|
||||
_darkTheme = theme;
|
||||
notifyListeners();
|
||||
return I;
|
||||
}
|
||||
|
||||
/// Sets a custom `ThemeData` for the light theme.
|
||||
///
|
||||
/// This allows you to customize the light theme and notify listeners to apply the
|
||||
/// changes immediately.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// ArcaneReactiveTheme.I.setLightTheme(customLightTheme);
|
||||
/// ```
|
||||
ArcaneReactiveTheme setLightTheme(ThemeData theme) {
|
||||
_lightTheme = theme;
|
||||
notifyListeners();
|
||||
return I;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class ArcaneTheme extends InheritedWidget {
|
||||
final ThemeMode themeMode;
|
||||
final bool followSystem;
|
||||
final ThemeData? theme;
|
||||
|
||||
const ArcaneTheme({
|
||||
required super.child,
|
||||
super.key,
|
||||
this.themeMode = ThemeMode.light,
|
||||
this.followSystem = false,
|
||||
this.theme,
|
||||
});
|
||||
|
||||
static ArcaneTheme? of(BuildContext context) {
|
||||
return context.dependOnInheritedWidgetOfExactType<ArcaneTheme>();
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(ArcaneTheme oldWidget) {
|
||||
return themeMode != oldWidget.themeMode ||
|
||||
followSystem != oldWidget.followSystem ||
|
||||
theme != oldWidget.theme;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
import "arcane_theme.dart";
|
||||
import "theme_service.dart";
|
||||
|
||||
/// An extension on `BuildContext` to check the current effective dark mode.
|
||||
///
|
||||
/// This extension provides a convenient way to check whether the active
|
||||
/// `ThemeData` is dark in the current context.
|
||||
extension DarkMode on BuildContext {
|
||||
/// Returns `true` if the current effective theme is dark.
|
||||
///
|
||||
/// This uses `Theme.of(this).brightness`, so it reflects the app's active
|
||||
/// rendered theme rather than raw platform brightness.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// if (context.isDarkMode) {
|
||||
/// // The active app theme is dark.
|
||||
/// }
|
||||
/// ```
|
||||
bool get isDarkMode {
|
||||
final brightness = Theme.of(this).brightness;
|
||||
return brightness == Brightness.dark;
|
||||
}
|
||||
}
|
||||
|
||||
extension ArcaneThemeContext on BuildContext {
|
||||
/// Get the current theme mode from the nearest ArcaneThemeInherited widget
|
||||
ThemeMode get themeMode {
|
||||
return ArcaneTheme.of(this)?.themeMode ??
|
||||
ArcaneReactiveTheme.I.currentThemeMode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
import "dart:async";
|
||||
|
||||
import "package:arcane_framework/src/service/arcane_service.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
import "theme_extensions.dart";
|
||||
|
||||
@Deprecated(
|
||||
"Deprecated in 2.0.0. "
|
||||
"ArcaneReactiveTheme has been renamed to ArcaneThemeService for clarity. "
|
||||
"Please use ArcaneThemeService instead.",
|
||||
)
|
||||
typedef ArcaneReactiveTheme = ArcaneThemeService;
|
||||
|
||||
/// A singleton service that manages theme switching and customization for the application.
|
||||
///
|
||||
/// `ArcaneThemeService` allows switching between light and dark themes and provides
|
||||
/// methods to customize the themes. The current theme mode can be accessed, and the
|
||||
/// theme can be switched at runtime.
|
||||
///
|
||||
/// System theme changes are detected by the `ArcaneApp` widget, which ensures
|
||||
/// theme updates happen automatically when the device theme changes.
|
||||
class ArcaneThemeService extends ArcaneService {
|
||||
ArcaneThemeService._internal();
|
||||
static final ArcaneThemeService _instance = ArcaneThemeService._internal();
|
||||
static ArcaneThemeService get I => _instance;
|
||||
|
||||
// ************************************************************************ //
|
||||
// * MARK: System theme
|
||||
// ************************************************************************ //
|
||||
/// Whether the theme service is currently following the system theme.
|
||||
///
|
||||
/// When `true`, the theme will automatically switch between light and dark
|
||||
/// based on the system's brightness setting.
|
||||
bool get isFollowingSystemTheme => _followingSystemTheme;
|
||||
bool _followingSystemTheme = false;
|
||||
|
||||
/// Returns the `ThemeMode` corresponding to the current system theme
|
||||
ThemeMode get systemThemeMode => _currentSystemThemeMode;
|
||||
|
||||
/// Tracks the current system theme mode
|
||||
ThemeMode _currentSystemThemeMode = ThemeMode.system;
|
||||
|
||||
StreamController<ThemeMode>? _systemStreamController;
|
||||
|
||||
StreamController<ThemeMode> get _systemController {
|
||||
_systemStreamController ??= StreamController<ThemeMode>.broadcast();
|
||||
return _systemStreamController!;
|
||||
}
|
||||
|
||||
// ************************************************************************ //
|
||||
// * MARK: ThemeMode
|
||||
// ************************************************************************ //
|
||||
/// Returns the current `ThemeMode` being used by `ArcaneThemeService`.
|
||||
/// Will automatically update when the theme changes.
|
||||
ThemeMode currentModeOf(BuildContext context) => context.themeMode;
|
||||
|
||||
/// The currently active theme mode (light, dark, or system) as a snapshot value.
|
||||
///
|
||||
/// Reading this getter does not subscribe to changes and does not trigger
|
||||
/// widget rebuilds. Use [themeModeChanges] when you need reactive updates.
|
||||
///
|
||||
/// If `ThemeMode.system`, the effective theme is determined by the platform brightness.
|
||||
ThemeMode get currentThemeMode => _currentThemeMode;
|
||||
ThemeMode _currentThemeMode = ThemeMode.system;
|
||||
|
||||
/// Stream of `ThemeMode` changes that can be listened to for reactive UI updates.
|
||||
Stream<ThemeMode> get themeModeChanges => I._themeModeController.stream;
|
||||
|
||||
StreamController<ThemeMode>? _themeModeStreamController;
|
||||
|
||||
StreamController<ThemeMode> get _themeModeController {
|
||||
_themeModeStreamController ??= StreamController<ThemeMode>.broadcast();
|
||||
return _themeModeStreamController!;
|
||||
}
|
||||
|
||||
// ************************************************************************ //
|
||||
// * MARK: ThemeData
|
||||
// ************************************************************************ //
|
||||
/// The currently active theme style as a snapshot value.
|
||||
///
|
||||
/// Reading this getter does not subscribe to changes and does not trigger
|
||||
/// widget rebuilds. Use [themeDataChanges] when you need reactive updates.
|
||||
ThemeData get currentTheme => _currentTheme;
|
||||
ThemeData _currentTheme = ThemeData();
|
||||
|
||||
/// Stream of `ThemeData` changes that can be listened to for reactive UI updates.
|
||||
Stream<ThemeData> get themeDataChanges => I._themeController.stream;
|
||||
|
||||
/// Tracks whether a custom light/dark theme has been explicitly provided by the user.
|
||||
bool _themeOverriddenByUser = false;
|
||||
|
||||
StreamController<ThemeData>? _themeStreamController;
|
||||
|
||||
StreamController<ThemeData> get _themeController {
|
||||
_themeStreamController ??= StreamController<ThemeData>.broadcast();
|
||||
return _themeStreamController!;
|
||||
}
|
||||
|
||||
// ************************************************************************ //
|
||||
// * MARK: Light/Dark theme
|
||||
// ************************************************************************ //
|
||||
/// Returns the current dark theme `ThemeData` as a snapshot value.
|
||||
///
|
||||
/// Reading this getter does not subscribe to changes and does not trigger
|
||||
/// widget rebuilds. Use [darkTheme] when you need reactive updates.
|
||||
ThemeData get dark => _darkTheme.value;
|
||||
|
||||
/// Sets a custom dark `ThemeData`.
|
||||
///
|
||||
/// This is a convenience setter that delegates to [setDarkTheme].
|
||||
set dark(ThemeData theme) => setDarkTheme(theme);
|
||||
|
||||
/// ValueNotifier for the dark theme that can be observed for changes.
|
||||
ValueNotifier<ThemeData> get darkTheme => I._darkTheme;
|
||||
final ValueNotifier<ThemeData> _darkTheme = ValueNotifier(ThemeData.dark());
|
||||
|
||||
/// Returns the current light theme `ThemeData` as a snapshot value.
|
||||
///
|
||||
/// Reading this getter does not subscribe to changes and does not trigger
|
||||
/// widget rebuilds. Use [lightTheme] when you need reactive updates.
|
||||
ThemeData get light => _lightTheme.value;
|
||||
|
||||
/// Sets a custom light `ThemeData`.
|
||||
///
|
||||
/// This is a convenience setter that delegates to [setLightTheme].
|
||||
set light(ThemeData theme) => setLightTheme(theme);
|
||||
|
||||
/// ValueNotifier for the light theme that can be observed for changes.
|
||||
ValueNotifier<ThemeData> get lightTheme => I._lightTheme;
|
||||
final ValueNotifier<ThemeData> _lightTheme = ValueNotifier(ThemeData.light());
|
||||
|
||||
// ************************************************************************ //
|
||||
// * MARK: Methods
|
||||
// ************************************************************************ //
|
||||
/// Switches the current theme between light and dark modes.
|
||||
///
|
||||
/// If the theme is currently light, it switches to dark, and vice versa. It
|
||||
/// also notifies listeners to update the UI accordingly.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// ArcaneThemeService.I.switchTheme();
|
||||
/// // or
|
||||
/// ArcaneThemeService.I.switchTheme(themeMode: ThemeMode.dark);
|
||||
/// // or
|
||||
/// Arcane.theme.switchTheme(themeMode: ThemeMode.light);
|
||||
/// ```
|
||||
ArcaneThemeService switchTheme({ThemeMode? themeMode}) {
|
||||
_followingSystemTheme = false;
|
||||
|
||||
if (themeMode != null) {
|
||||
_updateTheme(themeMode);
|
||||
} else {
|
||||
final ThemeMode effectiveMode = _effectiveThemeMode;
|
||||
_updateTheme(
|
||||
effectiveMode == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark,
|
||||
);
|
||||
}
|
||||
|
||||
return I;
|
||||
}
|
||||
|
||||
/// Switches the current theme between light and dark modes automatically
|
||||
/// based upon the system's current mode.
|
||||
///
|
||||
/// This will also register for system theme changes, so the theme will
|
||||
/// automatically update when the system theme changes.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// ArcaneThemeService.I.followSystemTheme(context);
|
||||
/// // or
|
||||
/// Arcane.theme.followSystemTheme(context);
|
||||
/// ```
|
||||
ArcaneThemeService followSystemTheme(BuildContext context) {
|
||||
_followingSystemTheme = true;
|
||||
|
||||
_currentSystemThemeMode =
|
||||
MediaQuery.platformBrightnessOf(context) == Brightness.dark
|
||||
? ThemeMode.dark
|
||||
: ThemeMode.light;
|
||||
_systemController.add(_currentSystemThemeMode);
|
||||
_updateTheme(_currentSystemThemeMode);
|
||||
|
||||
final ThemeData theme = systemThemeMode == ThemeMode.dark ? dark : light;
|
||||
_themeController.add(theme);
|
||||
_currentTheme = theme;
|
||||
|
||||
return I;
|
||||
}
|
||||
|
||||
/// Sets a custom `ThemeData` for the dark theme.
|
||||
///
|
||||
/// This allows you to customize the dark theme and notify listeners to apply
|
||||
/// the changes immediately.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// ArcaneThemeService.I.setDarkTheme(customDarkTheme);
|
||||
/// ```
|
||||
ArcaneThemeService setDarkTheme(ThemeData theme) {
|
||||
_themeOverriddenByUser = true;
|
||||
_darkTheme.value = theme;
|
||||
// Only update the rendered theme if dark is the active mode.
|
||||
if (_effectiveThemeMode == ThemeMode.dark) {
|
||||
_themeController.add(theme);
|
||||
_currentTheme = theme;
|
||||
}
|
||||
return I;
|
||||
}
|
||||
|
||||
/// Sets a custom `ThemeData` for the light theme.
|
||||
///
|
||||
/// This allows you to customize the light theme and notify listeners to apply
|
||||
/// the changes immediately.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// ArcaneThemeService.I.setLightTheme(customLightTheme);
|
||||
/// ```
|
||||
ArcaneThemeService setLightTheme(ThemeData theme) {
|
||||
_themeOverriddenByUser = true;
|
||||
_lightTheme.value = theme;
|
||||
// Only update the rendered theme if light is the active mode.
|
||||
if (_effectiveThemeMode == ThemeMode.light) {
|
||||
_themeController.add(theme);
|
||||
_currentTheme = theme;
|
||||
}
|
||||
return I;
|
||||
}
|
||||
|
||||
/// Syncs the initial theme with the platform/effective mode snapshot.
|
||||
///
|
||||
/// This is invoked automatically by `ArcaneThemeSwitcher` during the first
|
||||
/// dependency pass when using `ArcaneApp`, so most apps do not need to call
|
||||
/// this directly.
|
||||
void setInitialTheme(BuildContext context) {
|
||||
// Only update when no custom theme was explicitly provided by the user.
|
||||
if (_themeOverriddenByUser) {
|
||||
return;
|
||||
}
|
||||
switch (_currentThemeMode) {
|
||||
case ThemeMode.system:
|
||||
final isDark =
|
||||
MediaQuery.platformBrightnessOf(context) == Brightness.dark;
|
||||
_currentTheme = isDark ? ThemeData.dark() : ThemeData.light();
|
||||
case ThemeMode.dark:
|
||||
_currentTheme = ThemeData.dark();
|
||||
case ThemeMode.light:
|
||||
_currentTheme = ThemeData.light();
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets the theme service to its default state.
|
||||
///
|
||||
/// This resets both light and dark themes to their default values and
|
||||
/// disables system theme following.
|
||||
@visibleForTesting
|
||||
void reset() {
|
||||
_darkTheme.value = ThemeData.dark();
|
||||
_lightTheme.value = ThemeData.light();
|
||||
_themeOverriddenByUser = false;
|
||||
_followingSystemTheme = false;
|
||||
_updateTheme(ThemeMode.light);
|
||||
_themeController.add(_lightTheme.value);
|
||||
_currentTheme = _lightTheme.value;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
unawaited(_systemStreamController?.close());
|
||||
unawaited(_themeModeStreamController?.close());
|
||||
unawaited(_themeStreamController?.close());
|
||||
|
||||
_systemStreamController = null;
|
||||
_themeModeStreamController = null;
|
||||
_themeStreamController = null;
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Updates the current theme mode and broadcasts the change.
|
||||
void _updateTheme(ThemeMode themeMode) {
|
||||
_currentThemeMode = themeMode;
|
||||
_themeModeController.add(themeMode);
|
||||
}
|
||||
|
||||
ThemeMode get _effectiveThemeMode {
|
||||
if (_currentThemeMode != ThemeMode.system) {
|
||||
return _currentThemeMode;
|
||||
}
|
||||
|
||||
return _currentTheme.brightness == Brightness.dark
|
||||
? ThemeMode.dark
|
||||
: ThemeMode.light;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import "dart:async";
|
||||
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
import "arcane_theme.dart";
|
||||
import "theme_service.dart";
|
||||
|
||||
class ArcaneThemeSwitcher extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const ArcaneThemeSwitcher({
|
||||
required this.child,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<ArcaneThemeSwitcher> createState() => _ArcaneThemeSwitcherState();
|
||||
}
|
||||
|
||||
class _ArcaneThemeSwitcherState extends State<ArcaneThemeSwitcher>
|
||||
with WidgetsBindingObserver {
|
||||
bool _initialized = false;
|
||||
late final StreamSubscription<ThemeMode> _themeModeSubscription;
|
||||
late final StreamSubscription<ThemeData> _themeSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Register as an observer to detect system theme changes
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
_themeModeSubscription = ArcaneThemeService.I.themeModeChanges.listen((_) {
|
||||
setState(() {});
|
||||
});
|
||||
_themeSubscription = ArcaneThemeService.I.themeDataChanges.listen((_) {
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
unawaited(_themeModeSubscription.cancel());
|
||||
unawaited(_themeSubscription.cancel());
|
||||
|
||||
// Clean up the observer when the widget is disposed
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (!_initialized) {
|
||||
ArcaneThemeService.I.setInitialTheme(context);
|
||||
|
||||
// ArcaneApp defaults to following the platform theme until the user
|
||||
// explicitly picks a manual light/dark mode.
|
||||
ArcaneThemeService.I.followSystemTheme(context);
|
||||
_initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ArcaneTheme(
|
||||
themeMode: ArcaneThemeService.I.currentThemeMode,
|
||||
followSystem: ArcaneThemeService.I.isFollowingSystemTheme,
|
||||
theme: ArcaneThemeService.I.currentTheme,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangePlatformBrightness() {
|
||||
// When system brightness changes, find the current builder context
|
||||
// and use it to check the system theme
|
||||
if (mounted) {
|
||||
// Use the current context from the key to check system theme
|
||||
if (ArcaneThemeService.I.isFollowingSystemTheme) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ArcaneThemeService.I.followSystemTheme(context);
|
||||
});
|
||||
}
|
||||
}
|
||||
super.didChangePlatformBrightness();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user