Environment
The @SolidEnvironment() annotation injects a value from the widget tree into your widget. It mirrors SwiftUI’s @Environment property wrapper: type-keyed, lookup happens on first access, and any reactive @SolidState fields on the injected type stay reactive in your build method.
Annotate a late field with @SolidEnvironment() on a StatelessWidget or an existing State<X>:
class CounterDisplay extends StatelessWidget { CounterDisplay({super.key});
@SolidEnvironment() late Counter counter;
@override Widget build(BuildContext context) { return Center(child: Text('Counter is ${counter.value}')); }}counter is bound to the nearest ancestor Provider<Counter> in the widget tree on first access. Because Counter.value is a @SolidState field, the read is reactive: only the Text widget rebuilds when counter.value changes — the same fine-grained reactivity you’d get from a same-class @SolidState read.
Providing the instance
Section titled “Providing the instance”Two equivalent surfaces. The SwiftUI-flavoured .environment<T>() extension on Widget, shipped by solid_annotations:
class MyApp extends StatelessWidget { const MyApp({super.key});
@override Widget build(BuildContext context) { return MaterialApp( home: CounterDisplay().environment((_) => Counter()), ); }}The type argument is inferred from the closure’s return type. Pass it explicitly only when you want consumers to read by a supertype:
HomePage().environment<AuthService>((_) => RealAuthService())Chained calls nest providers in declaration order:
HomePage() .environment((_) => Counter()) .environment((_) => Logger())Or Provider<T> directly from package:provider:
import 'package:provider/provider.dart';
class MyApp extends StatelessWidget { const MyApp({super.key});
@override Widget build(BuildContext context) { return MaterialApp( home: Provider( create: (_) => Counter(), child: CounterDisplay(), ), ); }}To compose multiple providers, use MultiProvider from package:provider:
MultiProvider( providers: [ Provider(create: (_) => Counter()), Provider(create: (_) => Logger()), ], child: CounterDisplay(),)Pass dispose: to either form when the injected instance owns resources that need to be torn down — see the next section.
Disposing the injected instance
Section titled “Disposing the injected instance”The host widget never disposes the injected instance — the providing scope owns disposal via the dispose: callback you pass to .environment<T>() or Provider<T>. Most of the time that callback wires an existing cleanup method on the injected type:
HomePage().environment( (_) => DatabaseClient(), dispose: (_, c) => c.close(),)Solid does not constrain the cleanup-method name. close(), cancel(), shutdown(), or any other method already declared on your type works — you don’t need to add anything to your source class.
The uncommon case is when the injected type is itself a Solid class — one carrying @SolidState, @SolidEffect, or @SolidQuery declarations. Solid synthesizes a dispose() on the generated lib/ output that tears down all reactive declarations, but Dart’s analyzer reads source/, not the generated output. To make dispose: (_, c) => c.dispose() typecheck, your source class needs to declare void dispose() itself. The minimal pattern is an empty stub:
class Counter { @SolidState() int value = 0; void dispose() {}}Solid merges the disposal of every reactive declaration on the class (value.dispose();, etc.) into the body — you never write value.dispose() yourself.
Most apps never reference the generated Disposable marker directly — it documents which generated classes carry a dispose() method. If you want to consume it (for example, a runtime is Disposable check inside your own helper), import it from solid_annotations:
import 'package:solid_annotations/solid_annotations.dart' show Disposable;