Skip to content

dimassfeb-09/themely

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Themely 🎨

pub package GitHub stars License: MIT Flutter

A modular, highly customizable Flutter theme manager supporting dark, light and custom named themes with semantic color tokens, animated transitions, scheduling, and more.

Themely Preview

Table of Contents

  1. Features
  2. Requirements
  3. Installation
  4. Getting Started
  5. Quick Start
  6. Customization
  7. API Reference
  8. Cookbook & FAQ
  9. Contributing
  10. Support
  11. Contributors
  12. Changelog
  13. License

Features

  • Core Toggle: Switch between light, dark and system modes effortlessly.
  • Persistence: Automatically saves user preferences using SharedPreferences.
  • Semantic Tokens: Define and use colors semantically via type-safe context extensions.
  • Smooth Animations: Built-in AnimatedTheme transitions 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 BuildContext extensions (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.

  1. Install package
dependencies:
  themely: ^1.0.0
  1. 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));
}
  1. 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(),
      ),
    );
  }
}
  1. 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'),
)
  1. Access color tokens in widgets
Container(
  // Automatically adapts based on active mode
  color: context.themeColors.cardSurface, 
)
  1. 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',
)
  1. 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 active AppThemeMode.
  • activeNamedTheme: Returns the active named theme string (nullable).

AppThemeMode (Enum)

  • light, dark, amoled, system

BuildContext Extensions

  • context.isDark: bool
  • context.isLight: bool
  • context.isAmoled: bool
  • context.currentMode: AppThemeMode
  • context.themeColors: AppThemeTokens
  • context.themeTokens<T>(): T extends AppThemeTokens
  • context.themeController: ThemeController
  • context.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: ImageProvider per mode.
  • ThemeIcon: IconData per mode.
  • ThemeText: String per mode.
  • ThemeLottie: Renders .json animations per mode via lottie package (.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 ThemeAnimatedColor for 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:

  1. Fork the repository.
  2. Create a new branch.
  3. Make your changes.
  4. Submit a pull request.

Support

If you find this library useful and want to support its development, you can support me on Ko-fi!

Support me on Ko-fi

Contributors

Changelog

v1.0.1

  • Updated README with a centered, responsive preview GIF.
  • Improved documentation layout.

v1.0.0

  • Initial core release.
  • Core: Added ThemeController with persistence and AppThemeMode (light, dark, amoled, system).
  • Architecture: Implemented ThemeExtension with generic support for type-safe custom tokens.
  • Tokens: Added Semantic Color Tokens, Typography (Font) tokens, and AppThemeIcons.
  • UI: Integrated AnimatedTheme for smooth global theme transitions.
  • Widgets: Introduced ThemeBuilder, ThemeAsset, ThemeIcon, ThemeText, and ThemeLottie.
  • Logic: Added ThemeScheduler for automatic time-based switching.
  • Tools: Built-in DebugThemeOverlay for visual token debugging.
  • Utilities: Added contrastOn helper for automatic WCAG-compliant text color selection.

License

MIT License. See LICENSE file for details.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors