Skip to content

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

source/counter_display.dart
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.

Two equivalent surfaces. The SwiftUI-flavoured .environment<T>() extension on Widget, shipped by solid_annotations:

source/main.dart
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:

source/main.dart
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.

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:

source/counter.dart
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;