Breaking up example into smaller widgets

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2025-04-30 15:04:14 +02:00
parent 2f18c4213f
commit 68ce73abf5
2 changed files with 517 additions and 489 deletions
+13
View File
@@ -1,3 +1,5 @@
import "package:flutter/material.dart";
enum Feature { enum Feature {
logging(true), logging(true),
authentication(true), authentication(true),
@@ -6,3 +8,14 @@ enum Feature {
final bool enabledAtStartup; final bool enabledAtStartup;
const Feature(this.enabledAtStartup); const Feature(this.enabledAtStartup);
} }
// Some colors we'll use for our example
const List<MaterialColor> colors = [
Colors.red,
Colors.orange,
Colors.yellow,
Colors.green,
Colors.blue,
Colors.purple,
Colors.deepPurple,
];
+471 -456
View File
@@ -74,20 +74,47 @@ class MainApp extends StatelessWidget {
appBar: AppBar( appBar: AppBar(
title: const Text("Arcane Framework Example"), title: const Text("Arcane Framework Example"),
), ),
body: const HomeScreen(), body: Column(
children: [
Expanded(
child: GridView.extent(
maxCrossAxisExtent: 300,
padding: const EdgeInsets.all(16),
children: const [
ArcaneThemeExample(),
ArcaneAuthExample(),
ArcaneFeatureFlagsExample(),
ArcaneEnvironmentExample(),
ArcaneServicesExample(),
],
),
),
const ArcaneLoggingExample(),
],
),
), ),
); );
} }
} }
class HomeScreen extends StatefulWidget { // * Logging
const HomeScreen({super.key}); // Arcane's logging system gives developers the power to dynamically add and
// remove logging interfaces on-the-fly: try enabling a debug logging interface
// when the app is running in debug mode, adding a third-party logging interface
// when in production, and waiting until after the user has gone through the
// login process to ask them for permission to track. Include useful metadata,
// including persistent metadata, in your log messages. All of these things, and
// more, are possible when using Arcane's logging system.
class ArcaneLoggingExample extends StatefulWidget {
const ArcaneLoggingExample({
super.key,
});
@override @override
State<HomeScreen> createState() => _HomeScreenState(); State<ArcaneLoggingExample> createState() => _ArcaneLoggingExampleState();
} }
class _HomeScreenState extends State<HomeScreen> { class _ArcaneLoggingExampleState extends State<ArcaneLoggingExample> {
// Set up a subscriber that we can use to listen to logs in realtime. // Set up a subscriber that we can use to listen to logs in realtime.
// Note: this is completely optional and does _not_ impact whether logs are // Note: this is completely optional and does _not_ impact whether logs are
// sent to any registered logging interfaces. // sent to any registered logging interfaces.
@@ -118,457 +145,9 @@ class _HomeScreenState extends State<HomeScreen> {
super.dispose(); super.dispose();
} }
// Some colors we'll use for our example
static const List<MaterialColor> colors = [
Colors.red,
Colors.orange,
Colors.yellow,
Colors.green,
Colors.blue,
Colors.purple,
Colors.deepPurple,
];
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Padding(
children: [
Expanded(
child: GridView.extent(
maxCrossAxisExtent: 300,
padding: const EdgeInsets.all(16),
children: [
// * Theme
// Arcane enables easy, dynamic theme switching. Themes can be switched
// at any time between light mode and dark mode, or set to follow the
// system theme. In addition, themes can be swapped out on-the-fly,
// enabling dynamic customizations and remote theme fetching.
Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"Theme",
style: Theme.of(context).textTheme.headlineSmall,
),
Column(
children: [
Switch(
value:
Arcane.theme.currentThemeMode == ThemeMode.dark,
thumbIcon:
WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return const Icon(Icons.dark_mode);
}
return const Icon(Icons.light_mode);
}),
onChanged: (_) {
final ThemeMode oldTheme =
Arcane.theme.currentThemeMode;
Arcane.theme.switchTheme();
Arcane.log(
"Switching theme",
metadata: {
"followingSystemTheme":
"${Arcane.theme.isFollowingSystemTheme}",
"newMode": Arcane.theme.currentThemeMode.name,
"oldMode": oldTheme.name,
},
);
},
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Checkbox(
value: Arcane.theme.isFollowingSystemTheme,
onChanged: (value) {
final ThemeMode oldTheme =
Arcane.theme.currentThemeMode;
if (value == true) {
Arcane.theme.followSystemTheme(context);
Arcane.log(
"Switching theme",
metadata: {
"followingSystemTheme":
"${Arcane.theme.isFollowingSystemTheme}",
"newMode":
Arcane.theme.currentThemeMode.name,
"oldMode": oldTheme.name,
},
);
} else {
Arcane.theme.switchTheme(
themeMode: Arcane.theme.systemThemeMode,
);
Arcane.log(
"Switching theme",
metadata: {
"followingSystemTheme":
"${Arcane.theme.isFollowingSystemTheme}",
"newMode":
Arcane.theme.currentThemeMode.name,
"oldMode": oldTheme.name,
},
);
}
},
),
const Text("Follow system"),
],
),
],
),
SizedBox(
height: 20,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
spacing: 8,
children: [
const Text("Color"),
Expanded(
child: ListView.separated(
itemCount: colors.length,
scrollDirection: Axis.horizontal,
separatorBuilder: (_, __) =>
const SizedBox(width: 4),
itemBuilder: (context, index) {
return InkWell(
onTap: () {
if (Arcane.theme.currentThemeMode ==
ThemeMode.dark) {
Arcane.theme.setDarkTheme(
ThemeData(
brightness: Brightness.dark,
colorSchemeSeed: colors[index],
),
);
} else if (Arcane
.theme.currentThemeMode ==
ThemeMode.light) {
Arcane.theme.setLightTheme(
ThemeData(
brightness: Brightness.light,
colorSchemeSeed: colors[index],
),
);
}
Arcane.log(
"Setting ${Arcane.theme.currentThemeMode.name} theme color to ${colors[index].name}",
);
},
child: Container(
decoration: BoxDecoration(
color: colors[index],
border: Arcane.theme.currentTheme
.colorScheme.primary.name ==
colors[index].name
? Border.all(width: 2)
: null,
),
width: 20,
height: 20,
),
);
},
),
),
],
),
),
Text(
"The current theme mode is ${Arcane.theme.currentModeOf(context).name} and "
"is ${Arcane.theme.isFollowingSystemTheme ? "" : "not "}"
"following the system theme.",
),
],
),
),
),
// * Authentication
// Arcane's authentication system provides a simple, standard interface
// for common authentication tasks - including registration and account
// management, logging in and out, etc. Authentication status is reflected
// in realtime within the application as changes happen, so you can focus
// on what's most important.
Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"Authentication",
style: Theme.of(context).textTheme.headlineSmall,
),
ElevatedButton(
onPressed: Feature.authentication.enabled
? () async {
if (Arcane.auth.isSignedIn.value) {
await Arcane.auth.logOut(
onLoggedOut: () async {
setState(() {});
},
);
} else {
await Arcane.auth.login<Credentials>(
input: (
email: "email",
password: "password",
),
onLoggedIn: () async {
setState(() {});
},
);
}
}
: null,
child: Text(
Arcane.auth.isSignedIn.value ? "Sign out" : "Sign in",
),
),
Center(
child: Text("Status: ${Arcane.auth.status.name}"),
),
],
),
),
),
// * Feature flags
// Arcane's feature flag system is extremely simple and flexible to use.
// By registering _any_ enum (or even multiple enums!), features can be
// toggled on and off at any point. The feature flag system even offers
// a notifier, so you can listen to changes as they happen. Fetch your
// remote config and use it to dynamically enable and disable features
// with ease!
Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"Feature Flags",
style: Theme.of(context).textTheme.headlineSmall,
),
Expanded(
child: ListView.builder(
itemCount: Feature.values.length,
itemBuilder: (context, i) {
final Feature feature = Feature.values[i];
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(feature.name),
Switch(
value: feature.enabled,
onChanged: (_) {
feature.enabled
? feature.disable()
: feature.enable();
},
),
],
);
},
),
),
],
),
),
),
// * Environment
// Quickly and easily toggle between a "normal" and "debug" environment
// within your application. This is particularly useful during development
// when you may want to change the behavior of the application under
// certain conditions.
Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"Environment",
style: Theme.of(context).textTheme.headlineSmall,
),
ElevatedButton(
onPressed: () {
final Environment currentEnvironment =
ArcaneEnvironment.of(context).environment;
if (currentEnvironment == Environment.normal) {
ArcaneEnvironment.of(context).enableDebugMode();
Arcane.log(
"Environment changed.",
metadata: {
"previous": ArcaneEnvironment.of(context)
.environment
.name,
"current": Environment.debug.name,
},
);
} else {
ArcaneEnvironment.of(context).disableDebugMode();
Arcane.log(
"Environment changed.",
metadata: {
"previous": ArcaneEnvironment.of(context)
.environment
.name,
"current": Environment.normal.name,
},
);
}
},
child: const Text("Switch environment"),
),
Text(
"Environment: ${ArcaneEnvironment.of(context).environment.name}",
textAlign: TextAlign.center,
),
],
),
),
),
// * Services
// Arcane's services system is flexible and minimal, leaving the power
// and control in developers' hands. This system powers much of Arcane
// internally, so you know it's reliable.
Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"Services",
style: Theme.of(context).textTheme.headlineSmall,
),
ValueListenableBuilder(
valueListenable:
ArcaneService.ofType<FavoriteColorService>(
context,
)?.notifier ??
ValueNotifier(null),
builder: (context, color, _) {
return Text(
color != null
? "Favorite color: ${color.name}"
: "",
);
},
),
ElevatedButton(
onPressed: ArcaneServiceProvider.serviceOfType<
FavoriteColorService>(context) ==
null
? () {
ArcaneServiceProvider.of(context).addService(
FavoriteColorService.I,
);
Arcane.log(
"Service registered.",
metadata: {
"service": "FavoriteColorService",
},
);
}
: () {
ArcaneServiceProvider.of(context)
.removeService<FavoriteColorService>();
Arcane.log(
"Service removed.",
metadata: {
"service": "FavoriteColorService",
},
);
},
child: Text(
'${ArcaneServiceProvider.serviceOfType<FavoriteColorService>(context) == null ? 'Register' : 'Remove'} service',
),
),
SizedBox(
height: 20,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
spacing: 8,
children: [
const Text("Color"),
Expanded(
child: ListView.separated(
itemCount: colors.length,
scrollDirection: Axis.horizontal,
separatorBuilder: (_, __) =>
const SizedBox(width: 4),
itemBuilder: (context, index) {
return InkWell(
onTap: () {
ArcaneService.ofType<
FavoriteColorService>(
context,
)?.setMyFavoriteColor(colors[index]);
Arcane.log(
"Set a color in FavoriteColorService",
metadata: {
"color":
colors[index].name ?? "Unknown",
},
);
},
child: Container(
decoration: BoxDecoration(
color: colors[index],
border: ArcaneService.ofType<
FavoriteColorService>(
context,
)?.myFavoriteColor?.name ==
colors[index].name
? Border.all(width: 2)
: null,
),
width: 20,
height: 20,
),
);
},
),
),
],
),
),
Text(
"Service is ${ArcaneService.ofType<FavoriteColorService>(context) != null ? "" : "not "}registered",
),
],
),
),
),
],
),
),
// * Logging
// Arcane's logging system gives developers the power to dynamically add and
// remove logging interfaces on-the-fly: try enabling a debug logging interface
// when the app is running in debug mode, adding a third-party logging interface
// when in production, and waiting until after the user has gone through the
// login process to ask them for permission to track. Include useful metadata,
// including persistent metadata, in your log messages. All of these things, and
// more, are possible when using Arcane's logging system.
Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: SizedBox( child: SizedBox(
height: 200, height: 200,
@@ -604,8 +183,444 @@ class _HomeScreenState extends State<HomeScreen> {
], ],
), ),
), ),
), );
], }
}
// * Authentication
// Arcane's authentication system provides a simple, standard interface
// for common authentication tasks - including registration and account
// management, logging in and out, etc. Authentication status is reflected
// in realtime within the application as changes happen, so you can focus
// on what's most important.
class ArcaneAuthExample extends StatelessWidget {
const ArcaneAuthExample({
super.key,
});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ValueListenableBuilder(
valueListenable: Arcane.auth.isSignedIn,
builder: (context, isSignedIn, _) {
return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"Authentication",
style: Theme.of(context).textTheme.headlineSmall,
),
ElevatedButton(
onPressed: Feature.authentication.enabled
? () async {
if (isSignedIn) {
await Arcane.auth.logOut();
} else {
await Arcane.auth.login<Credentials>(
input: (
email: "email",
password: "password",
),
);
}
}
: null,
child: Text(
isSignedIn ? "Sign out" : "Sign in",
),
),
Center(
child: Text("Status: ${Arcane.auth.status.name}"),
),
],
);
},
),
),
);
}
}
// * Theme
// Arcane enables easy, dynamic theme switching. Themes can be switched
// at any time between light mode and dark mode, or set to follow the
// system theme. In addition, themes can be swapped out on-the-fly,
// enabling dynamic customizations and remote theme fetching.
class ArcaneThemeExample extends StatelessWidget {
const ArcaneThemeExample({
super.key,
});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"Theme",
style: Theme.of(context).textTheme.headlineSmall,
),
Column(
children: [
Switch(
value: Arcane.theme.currentThemeMode == ThemeMode.dark,
thumbIcon: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return const Icon(Icons.dark_mode);
}
return const Icon(Icons.light_mode);
}),
onChanged: (_) {
final ThemeMode oldTheme = Arcane.theme.currentThemeMode;
Arcane.theme.switchTheme();
Arcane.log(
"Switching theme",
metadata: {
"followingSystemTheme":
"${Arcane.theme.isFollowingSystemTheme}",
"newMode": Arcane.theme.currentThemeMode.name,
"oldMode": oldTheme.name,
},
);
},
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Checkbox(
value: Arcane.theme.isFollowingSystemTheme,
onChanged: (value) {
final ThemeMode oldTheme =
Arcane.theme.currentThemeMode;
if (value == true) {
Arcane.theme.followSystemTheme(context);
Arcane.log(
"Switching theme",
metadata: {
"followingSystemTheme":
"${Arcane.theme.isFollowingSystemTheme}",
"newMode": Arcane.theme.currentThemeMode.name,
"oldMode": oldTheme.name,
},
);
} else {
Arcane.theme.switchTheme(
themeMode: Arcane.theme.systemThemeMode,
);
Arcane.log(
"Switching theme",
metadata: {
"followingSystemTheme":
"${Arcane.theme.isFollowingSystemTheme}",
"newMode": Arcane.theme.currentThemeMode.name,
"oldMode": oldTheme.name,
},
);
}
},
),
const Text("Follow system"),
],
),
],
),
SizedBox(
height: 20,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
spacing: 8,
children: [
const Text("Color"),
Expanded(
child: ListView.separated(
itemCount: colors.length,
scrollDirection: Axis.horizontal,
separatorBuilder: (_, __) => const SizedBox(width: 4),
itemBuilder: (context, index) {
return InkWell(
onTap: () {
if (Arcane.theme.currentThemeMode ==
ThemeMode.dark) {
Arcane.theme.setDarkTheme(
ThemeData(
brightness: Brightness.dark,
colorSchemeSeed: colors[index],
),
);
} else if (Arcane.theme.currentThemeMode ==
ThemeMode.light) {
Arcane.theme.setLightTheme(
ThemeData(
brightness: Brightness.light,
colorSchemeSeed: colors[index],
),
);
}
Arcane.log(
"Setting ${Arcane.theme.currentThemeMode.name} theme color to ${colors[index].name}",
);
},
child: Container(
decoration: BoxDecoration(
color: colors[index],
border: Arcane.theme.currentTheme.colorScheme
.primary.name ==
colors[index].name
? Border.all(width: 2)
: null,
),
width: 20,
height: 20,
),
);
},
),
),
],
),
),
Text(
"The current theme mode is ${Arcane.theme.currentModeOf(context).name} and "
"is ${Arcane.theme.isFollowingSystemTheme ? "" : "not "}"
"following the system theme.",
),
],
),
),
);
}
}
// * Feature flags
// Arcane's feature flag system is extremely simple and flexible to use.
// By registering _any_ enum (or even multiple enums!), features can be
// toggled on and off at any point. The feature flag system even offers
// a notifier, so you can listen to changes as they happen. Fetch your
// remote config and use it to dynamically enable and disable features
// with ease!
class ArcaneFeatureFlagsExample extends StatelessWidget {
const ArcaneFeatureFlagsExample({
super.key,
});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"Feature Flags",
style: Theme.of(context).textTheme.headlineSmall,
),
Expanded(
child: ListView.builder(
itemCount: Feature.values.length,
itemBuilder: (context, i) {
final Feature feature = Feature.values[i];
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(feature.name),
Switch(
value: feature.enabled,
onChanged: (_) {
feature.enabled
? feature.disable()
: feature.enable();
},
),
],
);
},
),
),
],
),
),
);
}
}
// * Environment
// Quickly and easily toggle between a "normal" and "debug" environment
// within your application. This is particularly useful during development
// when you may want to change the behavior of the application under
// certain conditions.
class ArcaneEnvironmentExample extends StatelessWidget {
const ArcaneEnvironmentExample({
super.key,
});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"Environment",
style: Theme.of(context).textTheme.headlineSmall,
),
ElevatedButton(
onPressed: () {
final Environment currentEnvironment =
ArcaneEnvironment.of(context).environment;
if (currentEnvironment == Environment.normal) {
ArcaneEnvironment.of(context).enableDebugMode();
Arcane.log(
"Environment changed.",
metadata: {
"previous":
ArcaneEnvironment.of(context).environment.name,
"current": Environment.debug.name,
},
);
} else {
ArcaneEnvironment.of(context).disableDebugMode();
Arcane.log(
"Environment changed.",
metadata: {
"previous":
ArcaneEnvironment.of(context).environment.name,
"current": Environment.normal.name,
},
);
}
},
child: const Text("Switch environment"),
),
Text(
"Environment: ${ArcaneEnvironment.of(context).environment.name}",
textAlign: TextAlign.center,
),
],
),
),
);
}
}
/// * Services
/// Arcane's services system is flexible and minimal, leaving the power
/// and control in developers' hands. This system powers much of Arcane
/// internally, so you know it's reliable.
class ArcaneServicesExample extends StatelessWidget {
const ArcaneServicesExample({
super.key,
});
@override
Widget build(BuildContext context) {
final FavoriteColorService? service =
ArcaneServiceProvider.serviceOfType<FavoriteColorService>(context);
return Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
"Services",
style: Theme.of(context).textTheme.headlineSmall,
),
ValueListenableBuilder(
valueListenable: ArcaneService.ofType<FavoriteColorService>(
context,
)?.notifier ??
ValueNotifier(null),
builder: (context, color, _) {
return Text(
color != null ? "Favorite color: ${color.name}" : "",
);
},
),
ElevatedButton(
onPressed: () {
if (service == null) {
ArcaneServiceProvider.of(context).addService(
FavoriteColorService.I,
);
Arcane.log(
"Service registered.",
metadata: {"service": "FavoriteColorService"},
);
} else {
ArcaneServiceProvider.of(context)
.removeService<FavoriteColorService>();
Arcane.log(
"Service removed.",
metadata: {"service": "FavoriteColorService"},
);
}
},
child: Text('${service == null ? 'Register' : 'Remove'} service'),
),
SizedBox(
height: 20,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
spacing: 8,
children: [
const Text("Color"),
Expanded(
child: ListView.separated(
itemCount: colors.length,
scrollDirection: Axis.horizontal,
separatorBuilder: (_, __) => const SizedBox(width: 4),
itemBuilder: (context, index) {
return InkWell(
onTap: () {
service?.setMyFavoriteColor(colors[index]);
Arcane.log(
"Set a color in FavoriteColorService",
metadata: {
"color": colors[index].name ?? "Unknown",
},
);
},
child: Container(
decoration: BoxDecoration(
color: colors[index],
border: service?.myFavoriteColor?.name ==
colors[index].name
? Border.all(width: 2)
: null,
),
width: 20,
height: 20,
),
);
},
),
),
],
),
),
Text(
"Service is ${service != null ? "" : "not "}registered",
),
],
),
),
); );
} }
} }