diff --git a/CHANGELOG.md b/CHANGELOG.md index 66492f7..90207ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,27 @@ +## 1.2.3 + +- Added `ValueNotifier`s to both the `ArcaneAuthenticationService` and + `ArcaneFeatureFlags`. This enables the possibility of listening for changes to + either service. + +### Example + +```dart +// Listen to changes in the authentication status +Arcane.auth.isSignedIn.addListener(() { + if (Arcane.auth.isSignedIn.value) { + Arcane.log("User is signed in"); + } else { + Arcane.log("User is signed out"); + } +}); + +// Listen to changes in the enabled/disabled features +Arcane.features.notifier.addListener(() { + Arcane.log("Enabled features have been updated: ${Arcane.features.notifier.value}"); +}); +``` + ## 1.2.2 - Lowered minimum required collection dependency version to prevent forcing diff --git a/README.md b/README.md index 82b2d88..83ac62c 100644 --- a/README.md +++ b/README.md @@ -54,27 +54,27 @@ A service's purpose is to facilitate cross-feature communication of small pieces ```dart class FavoriteColorService extends ArcaneService { - static bool _mocked = false; static final FavoriteColorService _instance = FavoriteColorService._internal(); static FavoriteColorService get I => _instance; FavoriteColorService._internal(); - Color? _myFavoriteColor; - Color? get myFavoriteColor => _myFavoriteColor; + Color? get myFavoriteColor => _notifier.value; + + final ValueNotifier _notifier = ValueNotifier(null); + + ValueNotifier get notifier => _notifier; void setMyFavoriteColor(Color? newValue) { - if (_mocked) return; - - _myFavoriteColor = newValue; + if (_notifier.value != newValue) { + _notifier.value = newValue; + } notifyListeners(); } - - @visibleForTesting - static void setMocked() => _mocked = true; } + ``` To register a service with Arcane, simply add the instance of the `ArcaneService` to your list of services when initializing the `ArcaneApp`. @@ -88,7 +88,14 @@ ArcaneApp( ), ``` -Service properties can be accessed either directly (e.g., `FavoriteColorService.I.myFavoriteColor`) or via `BuildContext` (e.g., `context.serviceOfType()?.myFavoriteColor`). If the `notifyListeners()` method is included within your service, any widgets that are referencing the service property through `BuildContext` will automatically be notified of the change. +Service properties can be accessed either directly (e.g., `FavoriteColorService.I.myFavoriteColor`) or via `BuildContext` (e.g., `context.serviceOfType()?.myFavoriteColor`). If the `notifyListeners()` method is included within your service, any widgets that are referencing the service property through `BuildContext` will automatically be notified of the change. Additionally, a listener can be added to watch the value for changes. + +```dart +FavoriteColorService.I.notifier.addListener(() { + final Color? color = FavoriteColorService.I.myFavoriteColor; + // Do something with the value +}); +``` ### Feature Flags @@ -154,6 +161,14 @@ To get a list of the currently enabled features, simply ask the Arcane feature f final List enabledFeatures = Arcane.features.enabledFeatures; ``` +It is also possible to add a listener to watch for changes in the enabled features. + +```dart +Arcane.features.notifier.addListener(() { + print("Features changed: ${Arcane.features.enabledFeatures}"); +}); +``` + Note that it is possible to register multiple different `Enum` types in the feature flag service, should one have a need to do so. ### Logging @@ -166,17 +181,13 @@ To get started, first create one or more logging interfaces, extending the `Logg class DebugConsole implements LoggingInterface { static final DebugConsole _instance = DebugConsole._internal(); static DebugConsole get I => _instance; + DebugConsole._internal(); final bool _initialized = true; @override bool get initialized => I._initialized; - DebugConsole._internal(); - - @visibleForTesting - void setMocked() => _mocked = true; - bool _mocked = false; @override void log( @@ -192,11 +203,7 @@ class DebugConsole implements LoggingInterface { } @override - Future init() async { - if (_mocked) return null; - - return I; - } + Future init() async => I; } ``` diff --git a/lib/src/services/authentication/authentication_service.dart b/lib/src/services/authentication/authentication_service.dart index 554f1d7..d4409cd 100644 --- a/lib/src/services/authentication/authentication_service.dart +++ b/lib/src/services/authentication/authentication_service.dart @@ -1,7 +1,6 @@ import "dart:async"; import "package:arcane_framework/arcane_framework.dart"; -import "package:flutter/foundation.dart"; import "package:flutter/widgets.dart"; import "package:flutter_bloc/flutter_bloc.dart"; @@ -15,13 +14,16 @@ part "authentication_interface.dart"; class ArcaneAuthenticationService extends ArcaneService { ArcaneAuthenticationService._internal(); - static final ArcaneAuthenticationService _instance = - ArcaneAuthenticationService._internal(); + static final ArcaneAuthenticationService _instance = ArcaneAuthenticationService._internal(); /// Provides access to the singleton instance of this service. static ArcaneAuthenticationService get I => _instance; - AuthenticationStatus _status = AuthenticationStatus.unauthenticated; + final ValueNotifier _notifier = + ValueNotifier(AuthenticationStatus.unauthenticated); + + /// A `ValueNotifier` that emits the current `AuthenticationStatus`. + ValueNotifier get notifier => _notifier; /// Returns the current `AuthenticationStatus`. /// @@ -29,7 +31,7 @@ class ArcaneAuthenticationService extends ArcaneService { /// - `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 => _status; + AuthenticationStatus get status => _notifier.value; static ArcaneAuthInterface? _authInterface; @@ -40,25 +42,26 @@ class ArcaneAuthenticationService extends ArcaneService { /// A shortcut to `status != AuthenticationStatus.unauthenticated`. bool get isAuthenticated => status != AuthenticationStatus.unauthenticated; - /// Expose the ValueListenable so other widgets can listen to changes. - ValueListenable get isSignedIn => ValueNotifier(isAuthenticated); + final ValueNotifier _isSignedIn = ValueNotifier(false); + + /// A `ValueNotifier` that emits `true` if the user is currently signed in. + ValueNotifier get isSignedIn => _isSignedIn; /// 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 get accessToken => - authInterface?.accessToken ?? Future.value(""); + Future get accessToken => authInterface?.accessToken ?? Future.value(""); /// Returns a JWT refresh token if the registered `ArcaneAuthInterface` /// provides one. - Future get refreshToken => - authInterface?.refreshToken ?? Future.value(""); + Future get refreshToken => authInterface?.refreshToken ?? Future.value(""); /// Removes any registered `ArcaneAuthInterface` and resets all values to /// default. Future reset() async { _authInterface = null; - _status = AuthenticationStatus.unauthenticated; + _notifier.value = AuthenticationStatus.unauthenticated; + _isSignedIn.value = isAuthenticated; notifyListeners(); } @@ -143,7 +146,10 @@ class ArcaneAuthenticationService extends ArcaneService { } void _setStatus(AuthenticationStatus newStatus) { - _status = newStatus; + if (_notifier.value != newStatus) { + _notifier.value = newStatus; + _isSignedIn.value = isAuthenticated; + } notifyListeners(); } @@ -267,8 +273,7 @@ class ArcaneAuthenticationService extends ArcaneService { final auth = authInterface as ArcaneAuthAccountRegistration; - final Future>? result = - auth.resendVerificationCode(input: email); + final Future>? result = auth.resendVerificationCode(input: email); if (result == null) { return Result.error( diff --git a/lib/src/services/feature_flags/feature_flags_service.dart b/lib/src/services/feature_flags/feature_flags_service.dart index 99a38c2..fa06c3d 100644 --- a/lib/src/services/feature_flags/feature_flags_service.dart +++ b/lib/src/services/feature_flags/feature_flags_service.dart @@ -1,4 +1,5 @@ import "package:arcane_framework/arcane_framework.dart"; +import "package:flutter/foundation.dart"; part "feature_flags_extensions.dart"; @@ -31,6 +32,11 @@ class ArcaneFeatureFlags extends ArcaneService { List get enabledFeatures => _enabledFeatures; final List _enabledFeatures = []; + final ValueNotifier> _notifier = ValueNotifier>([]); + + /// A `ValueNotifier` that notifies listeners when the list of enabled features changes. + ValueNotifier> get notifier => _notifier; + /// Indicates whether the feature flags have been initialized. bool _initialized = false; @@ -65,6 +71,7 @@ class ArcaneFeatureFlags extends ArcaneService { if (_enabledFeatures.contains(feature)) return I; _enabledFeatures.add(feature); + _notifier.value.add(feature); if (Arcane.logger.initialized) { Arcane.logger.log( @@ -94,6 +101,7 @@ class ArcaneFeatureFlags extends ArcaneService { if (!_enabledFeatures.contains(feature)) return I; _enabledFeatures.remove(feature); + _notifier.value.remove(feature); if (Arcane.logger.initialized) { Arcane.logger.log( @@ -116,6 +124,7 @@ class ArcaneFeatureFlags extends ArcaneService { /// already been initialized. void _init() { _enabledFeatures.clear(); + _notifier.value.clear(); I._initialized = true; notifyListeners(); diff --git a/pubspec.yaml b/pubspec.yaml index 97d7d28..5f3ca24 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: arcane_framework description: "Agnostic Reusable Component Architecture for New Ecosystems: a modern framework for bootstrapping new applications" -version: 1.2.2 +version: 1.2.3 repository: https://github.com/hanskokx/arcane_framework issue_tracker: https://github.com/hanskokx/arcane_framework/issues