Skip to main content

Dark Mode

How to write a Dark Mode Flutter application with UtopiaHooks.

To visit Dark Mode repository press HERE.

Project Structure

|- dark_mode.dart
|- theme
| |- app_colors.dart
| |- app_texts.dart
|- global_state
| |- theme_state.dart
|- ui
| |- dark_mode_page.dart - Coordinator between state & view layers
| |- state
| | |- dark_mode_page_state.dart - Layer that definies State and Hook responsible for business-logic
| |- view
| | |- dark_mode_page_view.dart
|- util
| |- context_extension.dart

For more info about our recommended directory structure visit our Guides.

DarkModeApp

To initialize and read hook providers, the whole application needs to be wrapped with HookProviderContainerWidget. Declared global states can then be accessed either through BuildContext and the get() function, or the useProvided<T>() hook.

Learn more about Global States.

class DarkModeApp extends StatelessWidget {
const DarkModeApp();


Widget build(BuildContext context) {
return const HookProviderContainerWidget(
{ThemeState: useThemeState},
child: MaterialApp(
title: 'Flutter Demo',
home: DarkModePage(),
),
);
}
}

ThemeState 🔗

theme_state.dart is responsible for providing and managing theme. It consists of two segments: ThemeState object and useThemeState Hook responsible for state management.

useThemeState uses a useState hook determining the current theme mode and two useMemoized hooks. They are responsible for caching and updating the themes currently used by the application.

import 'package:utopia_hooks/utopia_hooks.dart';
import 'package:utopia_hooks_example/dark_mode/theme/app_colors.dart';
import 'package:utopia_hooks_example/dark_mode/theme/app_texts.dart';

class ThemeState {
final bool darkMode;
final AppColors colors;
final AppTexts texts;
final void Function() changeType;

const ThemeState({
required this.colors,
required this.darkMode,
required this.texts,
required this.changeType,
});
}

ThemeState useThemeState() {
final darkModeState = useState<bool>(false);

final colors = useMemoized(() => darkModeState.value ? AppColors.light : AppColors.dark, [darkModeState.value]);
final texts = useMemoized(() => AppTexts(colors: colors), [colors]);

return ThemeState(
darkMode: darkModeState.value,
changeType: darkModeState.toggle,
colors: colors,
texts: texts,
);
}

BuildContextExtension 🔗

Extension for easier access to theme

extension BuildContextExtension on BuildContext {
ThemeState get theme => get<ThemeState>();

AppColors get colors => theme.colors;

AppTexts get texts => theme.texts;
}

DarkModePageState 🔗

DarkModePageState handles local business-logic of DarkModePage. In this case it serves as a bridge between View layer and the ThemeState, which is accessed via the useProvided hook.

class DarkModePageState {
final bool darkMode;
final void Function() onModeChanged;

const DarkModePageState({
required this.onModeChanged,
required this.darkMode,
});
}

DarkModePageState useDarkModePageState() {
final themeState = useProvided<ThemeState>();

return DarkModePageState(
onModeChanged: themeState.changeType,
darkMode: themeState.darkMode,
);
}

DarkModePage - Coordinator 🔗

We start with a simple wrapper that serves as a bridge between our state and view. In this simple case it's only responsible for initializing DarkModePageState.

In more complex structures, it also provides Flutter-related functions, such as Navigator.of(context).push to useDarkModePageState Hook.

class DarkModePage extends StatelessWidget {
const DarkModePage();


Widget build(BuildContext context) {
return const HookCoordinator(
use: useDarkModePageState,
builder: DarkModePageView.new,
);
}
}

DarkModePageView 🔗

DarkModePageView is responsible for displaying the UI and a button changing the current theme. It uses the DarkModePageState passed by DarkModePage.

Keep in mind that the View gets the current theme via our BuildContextExtension.

class DarkModePageView extends StatelessWidget {
final DarkModePageState state;

const DarkModePageView(this.state);


Widget build(BuildContext context) {
return Scaffold(
backgroundColor: context.colors.canvas,
appBar: AppBar(
backgroundColor: context.colors.field,
title: Text("Flutter demo dark mode", style: context.texts.body),
),
floatingActionButton: FloatingActionButton(
onPressed: state.onModeChanged,
backgroundColor: context.colors.field,
tooltip: 'Change mode',
child: Icon(
state.darkMode ? Icons.nightlight_outlined : Icons.sunny,
color: context.colors.icon,
),
),
);
}
}

AppColors & AppTexts

AppColors and AppTexts are simple classes containing parts of the application styles.

AppColors

class AppColors {
final Color text;
final Color canvas;
final Color field;
final Color icon;

const AppColors._({
required this.text,
required this.canvas,
required this.icon,
required this.field,
});

static AppColors light = AppColors._(
text: const Color(0xFF060542),
canvas: const Color(0xFFE2EAF6),
field: Colors.grey[200]!,
icon: const Color(0xFF04266C),
);

static AppColors dark = AppColors._(
text: const Color(0xFFFFFFFF),
canvas: const Color(0xFF00030E),
field: Colors.grey[900]!,
icon: Colors.yellow,
);
}

AppTexts

class AppTexts {
final AppColors colors;

const AppTexts({required this.colors});

TextStyle get _base => TextStyle(fontFamily: "Roboto", color: colors.text);

TextStyle get body => _base.copyWith(fontWeight: FontWeight.w500, fontSize: 12);
}

See also



Crafted for you by UtopiaSoftware 👾