Toggle theme example
A simple toggle-theme feature built using flutter_solidart
See the code here.
This simple example uses a powerful feature of flutter_solidart, the Solid widget.
The Solid widget is used to provide signals to descendants without passing them as parameters.
First of all let's use the default light and dark themes by adding them to our MaterialApp:
MaterialApp(
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
)
themeMode is an Enum used by the MaterialApp to choose which theme to use: [light, dark, system].
To update the theme mode we've to rebuild our MaterialApp.
Let's wrap the MaterialApp with a Solid widget:
Solid(
child: MaterialApp(
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
),
)
Solid takes a providers list, let's create our themeMode signal:
Solid(
providers: [
Provider<Signal<ThemeMode>>(
create: () => Signal(ThemeMode.light),
),
],
child: MaterialApp(
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
),
)
Provider otherwise you may encounter unexpected errorsNow we've provided the themeMode signal to descendants, but we've to observe the themeMode signal in order to rebuilt the MaterialApp.
Since we need a BuildContext that is a descendant of Solid we wrap the MaterialApp inside a Builder.
Builder(
builder: (context) {
return MaterialApp(
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
);
}
)
Now using the context.observe method we effectively listen to the themeMode signal:
Builder(
builder: (context) {
final themeMode = context.observe<Signal<ThemeMode>>().value;
return MaterialApp(
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
themeMode: themeMode,
);
}
)
Provide the exact type of the signal value also to the observe() method.
This little line of code will subscribe the context we've used to the signal and will rebuild every time the themeMode signal changes.
Finally pass the themeMode value to the MaterialApp.
In this example we've a page called MyHomePage that displays a simple IconButton that toggles the theme mode.
The page retrieves the themeMode signal using the context.get() method:
@override
Widget build(BuildContext context) {
final themeMode = context.get<Signal<ThemeMode>>();
...
}
Signal or ReadSignal if you need to access a read-only signalThe get method obtains the signal for the given identifier (optional) without listening to it. You may use this method inside the initState(), build() methods and inside callbacks like onTap or onPressed.
To react to the signal we've used a SignalBuilder:
SignalBuilder(
builder: (_, __) {
return IconButton(
icon: Icon(
themeMode.value == ThemeMode.dark ? Icons.dark_mode : Icons.light_mode,
),
);
},
)
and to update our themeMode signal we just update the signal value inside the onPressed callback:
SignalBuilder(
builder: (_, __) {
final mode = themeMode.value;
return IconButton(
onPressed: () {
// toggle the theme mode
if (mode == ThemeMode.light) {
themeMode.value = ThemeMode.dark;
} else {
themeMode.value = ThemeMode.light;
}
},
icon: Icon(
mode == ThemeMode.dark ? Icons.dark_mode : Icons.light_mode,
),
);
},
)
Testing
Writing a test that toggles the dark mode on and off.
testWidgets(
'Check that when the app is in light mode the icon button shows a moon, while in dark mode it shows a sun',
(WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Icon finders
Finder lightModeIcon() => find.byIcon(Icons.light_mode);
Finder darkModeIcon() => find.byIcon(Icons.dark_mode);
// Given that our theme starts at light mode
// Verify that the toggle theme icon button shows the dark mode icon
expect(darkModeIcon(), findsOneWidget);
expect(lightModeIcon(), findsNothing);
// Tap the icon button to toggle the theme mode and trigger a frame.
await tester.tap(darkModeIcon());
await tester.pump();
// Verify that our theme has changed to 'dark' mode and the `light_mode` icon should be shown
expect(lightModeIcon(), findsOneWidget);
expect(darkModeIcon(), findsNothing);
// Tap the icon button to toggle the theme mode and trigger a frame.
await tester.tap(lightModeIcon());
await tester.pump();
// Verify that our theme has changed to 'light' mode and the `dark_mode` icon should be shown
expect(darkModeIcon(), findsOneWidget);
expect(lightModeIcon(), findsNothing);
});