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.

You are discouraged in passing signals as parameters, even inside modals you may use Solid.value

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(),
  ),
)
Don't forget to define type of Provider otherwise you may encounter unexpected errors

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.

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>>();
    ...
}
Note that the type passed to the get method contains also the signal type, it can be Signal or ReadSignal if you need to access a read-only signal

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:

SignalBuilder(
  builder: (_, __) {
    return IconButton(
      icon: Icon(
        themeMode.value == ThemeMode.light ? 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.light ? 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);
});