mirror of
https://github.com/hanskokx/arcane_framework.git
synced 2026-05-14 10:29:06 +02:00
[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>
This commit is contained in:
+1
-8
@@ -6,14 +6,7 @@ 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();
|
||||
|
||||
/// Creates a singleton instance of `Arcane`.
|
||||
///
|
||||
/// This factory constructor always returns the same instance of `Arcane`.
|
||||
factory Arcane() => Arcane._internal();
|
||||
|
||||
abstract class Arcane {
|
||||
/// Provides access to the singleton instance of the logger service.
|
||||
///
|
||||
/// The `ArcaneLogger` is used for logging messages throughout the app.
|
||||
|
||||
@@ -1,54 +1,109 @@
|
||||
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.
|
||||
/// An `InheritedWidget` that provides access to the application environment.
|
||||
///
|
||||
/// 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);
|
||||
/// The `ArcaneEnvironment` widget holds the current environment (`debug` or `normal`)
|
||||
/// and allows descendant widgets to access it.
|
||||
class ArcaneEnvironment extends InheritedWidget {
|
||||
/// The current application environment.
|
||||
final Environment environment;
|
||||
|
||||
final ValueChanged<Environment> onEnvironmentChanged;
|
||||
|
||||
/// Creates an `ArcaneEnvironment` widget.
|
||||
const ArcaneEnvironment({
|
||||
required this.environment,
|
||||
required Widget child,
|
||||
required this.onEnvironmentChanged,
|
||||
Key? key,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
/// 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 enableDebugMode() => onEnvironmentChanged(Environment.debug);
|
||||
void disableDebugMode() => onEnvironmentChanged(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;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_environment = widget.environment;
|
||||
}
|
||||
|
||||
/// Enables debug mode by setting the environment to `Environment.debug`.
|
||||
void enableDebugMode() {
|
||||
if (state == Environment.debug) return;
|
||||
|
||||
emit(Environment.debug);
|
||||
if (_environment == Environment.debug) return;
|
||||
setState(() {
|
||||
_environment = Environment.debug;
|
||||
});
|
||||
}
|
||||
|
||||
/// Disables debug mode by setting the environment to `Environment.normal`.
|
||||
void disableDebugMode() {
|
||||
if (state == Environment.normal) return;
|
||||
|
||||
emit(Environment.normal);
|
||||
if (_environment == Environment.normal) return;
|
||||
setState(() {
|
||||
_environment = 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,
|
||||
return ArcaneEnvironment(
|
||||
environment: _environment,
|
||||
onEnvironmentChanged: (Environment environment) {
|
||||
setState(() {
|
||||
_environment = environment;
|
||||
});
|
||||
},
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,14 +53,12 @@ class ArcaneServiceProvider extends InheritedNotifier {
|
||||
/// final provider = ArcaneServiceProvider.of(context);
|
||||
/// ```
|
||||
static ArcaneServiceProvider of(BuildContext context) {
|
||||
final ArcaneServiceProvider? result =
|
||||
context.dependOnInheritedWidgetOfExactType<ArcaneServiceProvider>();
|
||||
|
||||
if (result == null) {
|
||||
try {
|
||||
return context
|
||||
.dependOnInheritedWidgetOfExactType<ArcaneServiceProvider>()!;
|
||||
} catch (e) {
|
||||
throw Exception("ArcaneServiceProvider not found in context");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
@@ -59,12 +58,15 @@ class ArcaneAuthenticationService extends ArcaneService {
|
||||
Future<String?> get refreshToken =>
|
||||
authInterface?.refreshToken ?? Future.value("");
|
||||
|
||||
AuthenticationStatus? _previousModeWhenSettingDebug;
|
||||
|
||||
/// Removes any registered `ArcaneAuthInterface` and resets all values to
|
||||
/// default.
|
||||
Future<void> reset() async {
|
||||
_authInterface = null;
|
||||
_notifier.value = AuthenticationStatus.unauthenticated;
|
||||
_isSignedIn.value = isAuthenticated;
|
||||
_previousModeWhenSettingDebug = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -85,17 +87,18 @@ class ArcaneAuthenticationService extends ArcaneService {
|
||||
BuildContext context, {
|
||||
Future<void> Function()? onDebugModeSet,
|
||||
}) async {
|
||||
ArcaneEnvironment? environment;
|
||||
|
||||
try {
|
||||
environment = context.read<ArcaneEnvironment>();
|
||||
final Environment previousEnvironment = environment.state;
|
||||
final ArcaneEnvironment arcaneEnvironment = ArcaneEnvironment.of(context);
|
||||
|
||||
final Environment previousEnvironment = arcaneEnvironment.environment;
|
||||
|
||||
if (previousEnvironment == Environment.debug) return;
|
||||
|
||||
environment.enableDebugMode();
|
||||
_previousModeWhenSettingDebug = status;
|
||||
|
||||
final Environment currentEnvironment = environment.state;
|
||||
arcaneEnvironment.enableDebugMode();
|
||||
|
||||
final Environment currentEnvironment = arcaneEnvironment.environment;
|
||||
|
||||
if (previousEnvironment == currentEnvironment) {
|
||||
throw Exception("Unable to switch to debug mode.");
|
||||
@@ -103,8 +106,8 @@ class ArcaneAuthenticationService extends ArcaneService {
|
||||
|
||||
_setStatus(AuthenticationStatus.debug);
|
||||
if (onDebugModeSet != null) await onDebugModeSet();
|
||||
} catch (_) {
|
||||
throw Exception("No ArcaneEnvironment found in BuildContext");
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,23 +118,24 @@ class ArcaneAuthenticationService extends ArcaneService {
|
||||
BuildContext context, {
|
||||
Future<void> Function()? onDebugModeUnset,
|
||||
}) async {
|
||||
ArcaneEnvironment? environment;
|
||||
|
||||
try {
|
||||
environment = context.read<ArcaneEnvironment>();
|
||||
final Environment previousEnvironment = environment.state;
|
||||
final ArcaneEnvironment arcaneEnvironment = ArcaneEnvironment.of(context);
|
||||
|
||||
final Environment previousEnvironment = arcaneEnvironment.environment;
|
||||
|
||||
if (previousEnvironment == Environment.normal) return;
|
||||
|
||||
environment.disableDebugMode();
|
||||
arcaneEnvironment.disableDebugMode();
|
||||
|
||||
final Environment currentEnvironment = environment.state;
|
||||
final Environment currentEnvironment = arcaneEnvironment.environment;
|
||||
|
||||
if (previousEnvironment == currentEnvironment) {
|
||||
throw Exception("Unable to switch to normal mode.");
|
||||
}
|
||||
|
||||
_setStatus(AuthenticationStatus.debug);
|
||||
_setStatus(
|
||||
_previousModeWhenSettingDebug ?? AuthenticationStatus.unauthenticated,
|
||||
);
|
||||
if (onDebugModeUnset != null) await onDebugModeUnset();
|
||||
} catch (_) {
|
||||
throw Exception("No ArcaneEnvironment found in BuildContext");
|
||||
@@ -174,6 +178,8 @@ class ArcaneAuthenticationService extends ArcaneService {
|
||||
if (onLoggedOut != null) await onLoggedOut();
|
||||
}
|
||||
|
||||
_previousModeWhenSettingDebug = null;
|
||||
|
||||
return loggedOut;
|
||||
}
|
||||
|
||||
|
||||
@@ -129,4 +129,16 @@ class ArcaneFeatureFlags extends ArcaneService {
|
||||
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() {
|
||||
_enabledFeatures.clear();
|
||||
_notifier.value.clear();
|
||||
|
||||
I._initialized = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import "dart:async";
|
||||
|
||||
import "package:arcane_helper_utils/arcane_helper_utils.dart";
|
||||
import "package:flutter/foundation.dart";
|
||||
|
||||
part "logging_enums.dart";
|
||||
part "logging_interface.dart";
|
||||
@@ -174,9 +175,27 @@ class ArcaneLogger {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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].
|
||||
/// Due to iOS app tracking permissions, permission to track must first be
|
||||
/// checked for and (optionally) granted before the interface is automatically
|
||||
/// initialized.
|
||||
///
|
||||
/// Once your [LoggingInterface] has been registered and initialized, logs
|
||||
/// will automatically be sent to the interface.
|
||||
Future<ArcaneLogger> registerInterface(
|
||||
LoggingInterface loggingInterface,
|
||||
) async {
|
||||
if (!initialized) await _init();
|
||||
|
||||
I._interfaces.add(loggingInterface);
|
||||
|
||||
return I;
|
||||
}
|
||||
|
||||
/// Registers a `List` of [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.
|
||||
///
|
||||
/// Once your [LoggingInterface] has been registered and initialized, logs
|
||||
/// will automatically be sent to the interface.
|
||||
@@ -192,15 +211,37 @@ class ArcaneLogger {
|
||||
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._interfaces.remove(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._interfaces.clear();
|
||||
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.",
|
||||
);
|
||||
if (!initialized) await _init();
|
||||
|
||||
if (I._interfaces.isEmptyOrNull) {
|
||||
throw Exception("No logging interfaces have been registered.");
|
||||
}
|
||||
|
||||
if (!I._initialized) await _init();
|
||||
for (final LoggingInterface i in I._interfaces) {
|
||||
if (!i.initialized) await i.init();
|
||||
}
|
||||
@@ -245,4 +286,11 @@ class ArcaneLogger {
|
||||
|
||||
/// Clears all persistent metadata.
|
||||
void clearPersistentMetadata() => _additionalMetadata.clear();
|
||||
|
||||
@visibleForTesting
|
||||
void reset() {
|
||||
I._interfaces.clear();
|
||||
I._initialized = false;
|
||||
I._additionalMetadata.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,29 +18,30 @@ class ArcaneReactiveTheme extends ArcaneService {
|
||||
|
||||
ArcaneReactiveTheme._internal();
|
||||
|
||||
/// Whether the current theme is dark.
|
||||
bool _isDark = false;
|
||||
final ValueNotifier<ThemeMode> _systemThemeNotifier =
|
||||
ValueNotifier(ThemeMode.light);
|
||||
|
||||
/// 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;
|
||||
ThemeMode get currentMode => I._systemThemeNotifier.value;
|
||||
|
||||
/// The `ThemeData` for the dark theme.
|
||||
ThemeData _darkTheme = ThemeData.dark();
|
||||
final ValueNotifier<ThemeData> _darkTheme = ValueNotifier(ThemeData.dark());
|
||||
|
||||
/// The `ThemeData` for the light theme.
|
||||
ThemeData _lightTheme = ThemeData.light();
|
||||
final ValueNotifier<ThemeData> _lightTheme = ValueNotifier(ThemeData.light());
|
||||
|
||||
/// Returns the current dark theme `ThemeData`.
|
||||
ThemeData get dark => _darkTheme;
|
||||
ThemeData get dark => _darkTheme.value;
|
||||
ValueNotifier<ThemeData> get darkTheme => I._darkTheme;
|
||||
|
||||
/// Returns the current light theme `ThemeData`.
|
||||
ThemeData get light => _lightTheme;
|
||||
ThemeData get light => _lightTheme.value;
|
||||
ValueNotifier<ThemeData> get lightTheme => I._lightTheme;
|
||||
|
||||
/// A listenable that notifies listeners when the syste theme mode changes.
|
||||
ValueListenable<ThemeMode> get systemTheme =>
|
||||
ValueNotifier<ThemeMode>(_isDark ? ThemeMode.dark : ThemeMode.light);
|
||||
ValueListenable<ThemeMode> get systemTheme => I._systemThemeNotifier;
|
||||
|
||||
/// Switches the current theme between light and dark modes.
|
||||
///
|
||||
@@ -52,7 +53,9 @@ class ArcaneReactiveTheme extends ArcaneService {
|
||||
/// ArcaneReactiveTheme.I.switchTheme();
|
||||
/// ```
|
||||
ArcaneReactiveTheme switchTheme() {
|
||||
_isDark = !_isDark;
|
||||
_systemThemeNotifier.value = _systemThemeNotifier.value == ThemeMode.light
|
||||
? ThemeMode.dark
|
||||
: ThemeMode.light;
|
||||
notifyListeners();
|
||||
|
||||
return I;
|
||||
@@ -87,7 +90,7 @@ class ArcaneReactiveTheme extends ArcaneService {
|
||||
/// ArcaneReactiveTheme.I.setDarkTheme(customDarkTheme);
|
||||
/// ```
|
||||
ArcaneReactiveTheme setDarkTheme(ThemeData theme) {
|
||||
_darkTheme = theme;
|
||||
_darkTheme.value = theme;
|
||||
notifyListeners();
|
||||
return I;
|
||||
}
|
||||
@@ -102,8 +105,16 @@ class ArcaneReactiveTheme extends ArcaneService {
|
||||
/// ArcaneReactiveTheme.I.setLightTheme(customLightTheme);
|
||||
/// ```
|
||||
ArcaneReactiveTheme setLightTheme(ThemeData theme) {
|
||||
_lightTheme = theme;
|
||||
_lightTheme.value = theme;
|
||||
notifyListeners();
|
||||
return I;
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
void reset() {
|
||||
_darkTheme.value = ThemeData.dark();
|
||||
_lightTheme.value = ThemeData.light();
|
||||
_systemThemeNotifier.value = ThemeMode.light;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user