Solid

The Flutter framework works like a Tree. There are ancestors and there are descendants.

You may incur the need to pass a Signal deep into the tree, this is discouraged. You should never pass a signal as a parameter.

To avoid this there's the Solid widget.

With this widget you can pass a signal down the tree to anyone who needs it.

You will have already seen Theme.of(context) or MediaQuery.of(context), the procedure is practically the same.

Let's see an example to grasp the concept.

You're going to see how to build a toggle theme feature using Solid, this example is present also here

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    // Provide the theme mode signal to descendats
    return Solid(
      providers: [
        Provider<Signal<ThemeMode>>(
          create: () => Signal(ThemeMode.light),
        ),
      ],
      // using the builder method to immediately access the signal
      builder: (context) {
        // observe the theme mode value this will rebuild every time the themeMode signal changes.
        final themeMode = context.observe<Signal<ThemeMode>>().value;
        return MaterialApp(
          title: 'Toggle theme',
          themeMode: themeMode,
          theme: ThemeData.light(),
          darkTheme: ThemeData.dark(),
          home: const MyHomePage(),
        );
      },
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    // retrieve the theme mode signal
    final themeMode = context.get<Signal<ThemeMode>>();
    return Scaffold(
      appBar: AppBar(
        title: const Text('Toggle theme'),
      ),
      body: Center(
        child:
            // Listen to the theme mode signal rebuilding only the IconButton
            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,
              ),
            );
          },
        ),
      ),
    );
  }
}

The Solid widgets takes a List of providers: The Provider has a create function that returns the signal. You may create a signal or a derived signal. The value is a function because the signal is created lazily only when used for the first time, if you never access the signal it never gets created. In the Provider you can also specify an identifier for having multiple signals of the same type.

The context.observe() method listen to the signal value and rebuilds the widget when the value changes. It takes an optional id that is the signal identifier that you want to use. This method must be called only inside the build method.

The context.get() method doesn't listen to the signal value. You may use this method inside the initState and build methods.

It is mandatory to set the type of signal to the Provider otherwise you're going to encounter an error, for example:

  1. Provider<Signal<ThemeMode>>(create: () => Signal(ThemeMode.light))
  2. context.observe<Signal<ThemeMode>> and context.get<Signal<ThemeMode>> where Signal<ThemeMode> is the type of signal with its type value.

Providers

You can also pass Providers to descendants:

class NameProvider {
  const NameProvider(this.name);
  final String name;

  void dispose() {
    // put your dispose logic here
    // ignore: avoid_print
    print('dispose name provider');
  }
}

class NumberProvider {
  const NumberProvider(this.number);
  final int number;
}

class ProvidersPage extends StatelessWidget {
  const ProvidersPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Providers'),
      ),
      body: Solid(
        providers: [
          Provider<NameProvider>(
            create: () => const NameProvider('Ale'),
            // the dispose method is fired when the [Solid] widget above is removed from the widget tree.
            dispose: (provider) => provider.dispose(),
          ),
          Provider<NumberProvider>(
            create: () => const NumberProvider(1),
            // Do not create the provider lazily, but immediately
            lazy: false,
            id: 1,
          ),
          Provider<NumberProvider>(
            create: () => const NumberProvider(10),
            id: 2,
          ),
        ],
        child: const SomeChildThatNeedsProviders(),
      ),
    );
  }
}

class SomeChildThatNeedsProviders extends StatelessWidget {
  const SomeChildThatNeedsProviders({super.key});
  @override
  Widget build(BuildContext context) {
    final nameProvider = context.get<NameProvider>();
    final numberProvider = context.get<NumberProvider>(1);
    final numberProvider2 = context.get<NumberProvider>(2);
    return Center(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          Text('name: ${nameProvider.name}'),
          const SizedBox(height: 8),
          Text('number: ${numberProvider.number}'),
          const SizedBox(height: 8),
          Text('number2: ${numberProvider2.number}'),
        ],
      ),
    );
  }
}
You can have multiple providers of the same type in the same Solid widget specifying a different id to each one.

Solid.value

The Solid.value factory is useful for passing providers to modals, because they are spawned in a new tree. This is necessary because modals are spawned in a new tree. Solid.value takes a list of ProviderElements.

Access providers in modals

Future<void> openDialog(BuildContext context) {
   return showDialog(
     context: context,
     builder: (_) =>
      // using `Solid.value` we provide the existing provider(s) to the dialog
      Solid.value(
       elements: [
         context.getElement<NameProvider>(),
         context.getElement<NumberProvider>(ProviderId.firstNumber),
         context.getElement<NumberProvider>(ProviderId.secondNumber),
       ],
       child: Dialog(
         child: Builder(builder: (innerContext) {
           final nameProvider = innerContext.get<NameProvider>();
           final numberProvider1 =
               innerContext.get<NumberProvider>(ProviderId.firstNumber);
           final numberProvider2 =
               innerContext.get<NumberProvider>(ProviderId.secondNumber);
           return SizedBox.square(
             dimension: 100,
             child: Center(
               child: Text('''
 name: ${nameProvider.name}
 number1: ${numberProvider1.number}
 number2: ${numberProvider2.number}
 '''),
            ),
          );
        }),
      ),
    ),
  );
}