themely 1.0.1
themely: ^1.0.1 copied to clipboard
A modular Flutter theme manager supporting dark, light, amoled, and custom named themes with semantic color tokens, animated transitions, scheduling, and more.
Themely 🎨 #
A modular, highly customizable Flutter theme manager supporting dark, light and custom named themes with semantic color tokens, animated transitions, scheduling, and more.
Table of Contents #
- Features
- Requirements
- Installation
- Getting Started
- Quick Start
- Customization
- API Reference
- Cookbook & FAQ
- Contributing
- Support
- Contributors
- Changelog
- License
Features #
- Core Toggle: Switch between
light,darkandsystemmodes effortlessly. - Persistence: Automatically saves user preferences using SharedPreferences.
- Semantic Tokens: Define and use colors semantically via type-safe context extensions.
- Smooth Animations: Built-in
AnimatedThemetransitions when switching modes. - Widget Switchers: Swap widgets (
ThemeAsset,ThemeIcon,ThemeText,ThemeLottie,ThemeBuilder) based on the current mode automatically. - Preview Mode: Let users preview a theme before saving it permanently.
- Named Themes: Support for infinite custom themes (e.g., "sepia", "high_contrast").
- Auto Schedule: Automatically switch modes based on the time of day.
- No Boilerplate: Access everything easily via
BuildContextextensions (context.isDark,context.themeColors). - Debug Overlay: Built-in overlay to visually debug your active theme tokens.
Requirements #
- Flutter >= 3.10.0
- Dart >= 3.0.0
Installation #
Add the dependency to your pubspec.yaml:
dependencies:
themely: ^1.0.0
Getting Started #
To get started with Themely, you need to configure your ThemeController and wrap your app with ThemelyApp. Themely handles the ThemeData injection automatically via AnimatedTheme for smooth transitions.
Quick Start #
A brief tutorial from zero to running.
- Install package
dependencies:
themely: ^1.0.0
- Initialize ThemeController in main.dart
import 'package:flutter/material.dart';
import 'package:themely/themely.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize the controller with your ThemeData
final controller = ThemeController(
lightTheme: ThemeData.light(),
darkTheme: ThemeData.dark(),
);
// Await initialization to load saved preferences
await controller.initialize();
runApp(MyApp(controller: controller));
}
- Wrap MaterialApp with package
class MyApp extends StatelessWidget {
final ThemeController controller;
const MyApp({super.key, required this.controller});
@override
Widget build(BuildContext context) {
// Inject ThemeScope and listen to stream
return ThemelyApp(
controller: controller,
builder: (context, theme, child) => MaterialApp(
theme: theme,
home: const Home(),
),
);
}
}
- Toggle mode from a button
ElevatedButton(
// Toggles between dark and light mode
onPressed: () => context.themeController.toggleDark(),
child: Text(context.isDark ? 'Switch to Light' : 'Switch to Dark'),
)
- Access color tokens in widgets
Container(
// Automatically adapts based on active mode
color: context.themeColors.cardSurface,
)
- Use a widget switcher
// Renders different icons based on mode without manual ternary checks
ThemeIcon(
light: Icons.wb_sunny,
dark: Icons.nightlight_round,
)
// Seamlessly switch Lottie animations
ThemeLottie.asset(
light: 'assets/animations/sun.json',
dark: 'assets/animations/moon.json',
)
- Activate scheduled auto switch
final controller = ThemeController(
lightTheme: ThemeData.light(),
darkTheme: ThemeData.dark(),
autoSchedule: true, // Enable scheduling
darkFrom: const TimeOfDay(hour: 18, minute: 0), // Dark starts at 6 PM
darkUntil: const TimeOfDay(hour: 6, minute: 0), // Dark ends at 6 AM
);
Customization #
Themely is built to be extended. Here is how you can customize every aspect of it.
Custom Color Palette #
To define a custom color palette, use ThemeData and pass it to the controller.
final myLightTheme = ThemeData(
brightness: Brightness.light,
colorSchemeSeed: Colors.green,
);
Custom Semantic Tokens #
You can add your own custom tokens by subclassing AppThemeTokens and overriding lerp.
class MyCustomTokens extends AppThemeTokens {
final Color brandColor;
const MyCustomTokens({
required super.buttonBackground,
// ... other super fields
required this.brandColor,
});
@override
MyCustomTokens lerp(AppThemeTokens? other, double t) {
if (other is! MyCustomTokens) return this;
return MyCustomTokens(
buttonBackground: Color.lerp(buttonBackground, other.buttonBackground, t)!,
// ... lerp other super fields
brandColor: Color.lerp(brandColor, other.brandColor, t)!,
);
}
}
// Pass it via ThemeExtension
final theme = ThemeData(
extensions: [
AppThemeExtension<MyCustomTokens>(tokens: myTokens),
]
);
// Access it
final brand = context.themeTokens<MyCustomTokens>().brandColor;
Per-token Opacity #
You can adjust opacity on any token independently using .withValues(alpha: ...).
Container(
color: context.themeColors.buttonBackground.withValues(alpha: 0.5),
)
Gradient Support #
While tokens natively hold Color, you can build gradients using your tokens.
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
context.themeColors.buttonBackground,
context.themeColors.cardSurface,
],
),
),
)
Custom Font per Mode #
Define fonts differently per mode using ThemeData or via semantic tokens.
Text(
'Hello',
style: TextStyle(fontFamily: context.themeColors.primaryFont),
)
Custom TextStyle per Mode #
You can define entire TextTheme per mode in ThemeData.
final lightTheme = ThemeData(
textTheme: const TextTheme(
bodyLarge: TextStyle(fontSize: 16, fontWeight: FontWeight.normal, letterSpacing: 0.5),
),
);
Custom Border Radius per Mode #
Override shapes per mode in ThemeData.
final amoledTheme = ThemeData(
cardTheme: CardTheme(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0)), // Sharp edges
),
);
Custom Spacing Scale per Mode #
Use ThemeValue to return different dimensions based on mode.
Padding(
padding: EdgeInsets.all(
context.themeValue<double>(
light: 16.0,
dark: 24.0, // More breathing room in dark mode
)
),
child: const Text('Spaced Content'),
)
Custom Elevation per Mode #
Card(
elevation: context.themeValue<double>(
light: 2.0,
dark: 0.0, // Flat design in dark mode
),
)
Custom Overlay & Splash Color #
Modify ripple and highlight colors globally in ThemeData.
final theme = ThemeData(
splashColor: Colors.blue.withValues(alpha: 0.2),
highlightColor: Colors.transparent,
);
Custom Transition Builder #
Customize the cross-fade animation between modes via ThemeController.
final controller = ThemeController(
animationDuration: const Duration(milliseconds: 500),
animationCurve: Curves.easeInOutBack,
);
Custom Scheduler Logic #
Bypass the built-in autoSchedule and trigger mode changes via your own logic (e.g., location, battery level).
Battery().onBatteryStateChanged.listen((state) {
if (state == BatteryState.powerSave) {
context.themeController.setMode(AppThemeMode.amoled);
}
});
Custom Storage Adapter #
Create your own adapter by implementing ThemeStorage.
class HiveThemeStorage implements ThemeStorage {
@override
Future<String?> loadMode(String key) async {
return Hive.box('settings').get(key);
}
@override
Future<void> saveMode(String key, String mode) async {
await Hive.box('settings').put(key, mode);
}
}
// Inject it
final controller = ThemeController(storage: HiveThemeStorage());
Custom Persistence Key #
If you need to avoid collisions, build a custom adapter that prefixes the keys.
// Inside your custom storage adapter:
final String prefix = 'my_app_v2_';
await prefs.setString('$prefix$key', mode);
Export/Import Konfigurasi Tema #
You can read current active values and serialize them to JSON.
// Example serialized output
{
"mode": "dark",
"named": "sepia"
}
final currentMode = context.currentMode.name;
final namedTheme = context.themeController.activeNamedTheme;
final jsonExport = jsonEncode({'mode': currentMode, 'named': namedTheme});
Theme Reset #
Clear all named themes and revert to initial configuration.
await context.themeController.clearNamedTheme();
await context.themeController.setMode(AppThemeMode.system);
API Reference #
ThemeController #
setMode(AppThemeMode mode): Sets the global theme mode.toggleDark(): Toggles between Light and Dark mode.cycleMode(): Cycles through all available AppThemeModes.registerTheme(String name, {ThemeData? light, ThemeData? dark}): Registers a new custom named theme.setNamedTheme(String name): Activates a registered named theme.clearNamedTheme(): Reverts to standard mode.preview(AppThemeMode mode): Temporarily changes the theme without saving.confirmPreview(): Saves the currently previewed theme.cancelPreview(): Reverts to the previously saved theme.modeStream:Stream<AppThemeMode>of mode changes.currentMode: Returns activeAppThemeMode.activeNamedTheme: Returns the active named theme string (nullable).
AppThemeMode (Enum) #
light,dark,amoled,system
BuildContext Extensions #
context.isDark:boolcontext.isLight:boolcontext.isAmoled:boolcontext.currentMode:AppThemeModecontext.themeColors:AppThemeTokenscontext.themeTokens<T>():T extends AppThemeTokenscontext.themeController:ThemeControllercontext.themeValue<T>({required T light, T? dark, T? amoled, T? orElse}): Returns specific value based on mode.
Widget Switchers #
ThemeBuilder:Widget Function(BuildContext)for different modes.ThemeAsset:ImageProviderper mode.ThemeIcon:IconDataper mode.ThemeText:Stringper mode.ThemeLottie: Renders.jsonanimations per mode vialottiepackage (.asset()or.network()).LocalTheme: Force a subtree to a specific mode.
Cookbook & FAQ #
Here are the most common questions from developers building with Themely.
1. How do I change the default colors? #
You don't need to rewrite anything. Just use .copyWith() on the default tokens when registering your theme.
final myLightTokens = AppThemeTokens.light.copyWith(
cardSurface: Colors.white,
buttonBackground: Colors.blueAccent,
);
// Register it in your ThemeController/ThemeData extensions
2. How do I add my own color names (e.g., brandColor)? #
If the default attributes aren't enough, just extend the class.
class MyColors extends AppThemeTokens {
final Color brandColor;
const MyColors({required this.brandColor, ...super_fields});
// Override lerp to support animations (see Customization section)
}
// Access it anywhere:
final brand = context.themeTokens<MyColors>().brandColor;
3. How do I make text color automatic (Contrast Magic)? #
Stop guessing if text should be black or white. Use the contrastOn helper which automatically picks the best contrast based on WCAG luminance.
Text(
'I adapt to my background!',
style: TextStyle(
// If buttonBackground is dark, this returns white. If light, returns black.
color: context.themeColors.contrastOn(context.themeColors.buttonBackground),
),
)
4. How do I make a widget change color automatically? #
You have two ways:
- Stateless/Standard: Use
context.themeColors.x. When the theme changes, the widget rebuilds with the new color. - Animated: Use
ThemeAnimatedColorfor a smooth transition.
ThemeAnimatedColor(
color: context.themeColors.buttonBackground,
duration: Duration(milliseconds: 500),
builder: (context, color, child) => Container(color: color),
)
5. Can I swap entire widgets based on theme? #
Yes! Use our built-in switchers for zero boilerplate:
ThemeIcon(light: Icons.sunny, dark: Icons.moon)ThemeAsset(light: 'day.png', dark: 'night.png')ThemeText(light: 'Good Morning', dark: 'Good Evening')
Contributing #
Contributions are welcome! If you find a bug or have a feature request, please open an issue. If you cannot contribute code yet, giving this repository a ⭐ Star is also a great way to support the project!
If you want to contribute code, please:
- Fork the repository.
- Create a new branch.
- Make your changes.
- Submit a pull request.
Support #
If you find this library useful and want to support its development, you can support me on Ko-fi!
Contributors #
- Dimas Febriano (@dimassfeb-09) - Creator & Lead Developer
- Website: dimassfeb.com
Changelog #
v1.0.1
- Updated README with a centered, responsive preview GIF.
- Improved documentation layout.
v1.0.0
- Initial core release.
- Core: Added
ThemeControllerwith persistence andAppThemeMode(light, dark, amoled, system). - Architecture: Implemented
ThemeExtensionwith generic support for type-safe custom tokens. - Tokens: Added Semantic Color Tokens, Typography (Font) tokens, and
AppThemeIcons. - UI: Integrated
AnimatedThemefor smooth global theme transitions. - Widgets: Introduced
ThemeBuilder,ThemeAsset,ThemeIcon,ThemeText, andThemeLottie. - Logic: Added
ThemeSchedulerfor automatic time-based switching. - Tools: Built-in
DebugThemeOverlayfor visual token debugging. - Utilities: Added
contrastOnhelper for automatic WCAG-compliant text color selection.
License #
MIT License. See LICENSE file for details.