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 Guide.

DarkModeApp​

In able to initialize and read hook providers whole application needs to be wrapped with HookProviderContainerWidget. Declared Global States can be accessed GlobalStates can be accessed either through BuildContext and get() function or 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 implements useState hook determining current theme mode and two useMemoized hooks. They are responsible for caching and updating 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_text.dart';

class ThemeState {
final bool darkMode;
final AppColors colors;
final AppText texts;
final void Function() onModeChanged;

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

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

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

return ThemeState(
darkMode: darkModeState.value,
onModeChanged: 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;

AppText 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() onThemeChanged;

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

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

return DarkModePageState(
onThemeChanged: themeState.onModeChanged,
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 πŸ”—β€‹

FormValidationPageView is responsible for displaying UI and Button changing current theme. It uses the DarkModePageState passed by DarkModePage.

Keep in mind that the View gets 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.onThemeChanged,
backgroundColor: context.colors.field,
tooltip: 'Change mode',
child: Icon(
state.darkMode ? Icons.nightlight_outlined : Icons.sunny,
color: context.colors.icon,
),
),
);
}
}

AppColors & AppText​

AppColors and AppText 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);
}


Crafted specially for you by UtopiaSoftware πŸ‘½