juice_theme

App theme selection — mode + optional flavor — as a Juice bloc, with optional persistence behind a swappable seam.

pub package License: MIT

What it owns

The theme selection: ThemeMode (light/dark/system) and an optional named flavor (e.g. 'ocean'). It does not own the ThemeData — feed state.mode to MaterialApp.themeMode and map state.flavor to your themes.

Install

dependencies:
  juice_theme: ^0.1.0

Use

import 'package:juice/juice.dart';
import 'package:juice_theme/juice_theme.dart';

final theme = ThemeBloc.withConfig(
  ThemeConfig(persistence: StorageThemePersistence(storageBloc)),
);

class App extends StatelessJuiceWidget<ThemeBloc> {
  App({super.key}) : super(groups: {ThemeGroups.mode});

  @override
  Widget onBuild(BuildContext context, StreamStatus status) {
    return MaterialApp(
      themeMode: bloc.state.mode,
      theme: ThemeData.light(),
      darkTheme: ThemeData.dark(),
      home: const HomePage(),
    );
  }
}

// elsewhere: theme.toggle();  theme.setMode(ThemeMode.system);

Persistence seam (and why it's testable)

ThemeBloc depends on the ThemePersistence interface, not on storage. The default StorageThemePersistence stores the selection in StorageBloc (SharedPreferences). Inject a fake in tests, or pass persistence: null for in-memory only:

class FakeThemePersistence implements ThemePersistence {
  ThemeSelection? saved;
  @override Future<ThemeSelection?> load() async => saved;
  @override Future<void> save(ThemeSelection s) async => saved = s;
}

State

Field / getter Meaning
mode ThemeMode — feed to MaterialApp.themeMode
flavor optional named theme key
isDarkMode / isLightMode / isSystemMode mode checks

For the resolved brightness under system, read MediaQuery.platformBrightnessOf(context) in the UI — the bloc owns the selection, not the platform reading.

License

MIT License — see LICENSE.

Libraries

juice_theme
App theme selection as a Juice bloc.