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:

dart
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:

dart
Solid(
  child: MaterialApp(
    theme: ThemeData.light(),
    darkTheme: ThemeData.dark(),
  ),
)

Solid takes a providers list, let's create our themeMode signal:

dart
Solid(
  providers: [
    Provider<Signal<ThemeMode>>(
      create: () => Signal(ThemeMode.light),
    ),
  ],
  child: MaterialApp(
    theme: ThemeData.light(),
    darkTheme: ThemeData.dark(),
  ),
)

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

dart
Builder(
  builder: (context) {
    return MaterialApp(
      theme: ThemeData.light(),
      darkTheme: ThemeData.dark(),
    );
  }
)

Now using the context.observe method we effectively listen to the themeMode signal:

dart
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:

dart
 @override
  Widget build(BuildContext context) {
    final themeMode = context.get<Signal<ThemeMode>>();
    ...
}

The 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:

dart
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:

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

dart
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);
});

On this page