Skip to content

Commit 3183d42

Browse files
committed
Add Injectable section and handler lifecycle warnings to docs
- Add "Want Less Boilerplate? Try Injectable!" section to get_it getting_started - Move "Naming Your GetIt Instance" section earlier in the doc flow - Add handler lifecycle warning to watch_it observing_commands, handlers, and debugging docs - Create handler_lifecycle_example.dart code sample - Apply all changes to both English and Spanish documentation
1 parent 0d443bb commit 3183d42

File tree

9 files changed

+398
-51
lines changed

9 files changed

+398
-51
lines changed
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// ignore_for_file: unused_local_variable
2+
import 'package:flutter/material.dart';
3+
import 'package:watch_it/watch_it.dart';
4+
import '_shared/stubs.dart';
5+
6+
// #region bad
7+
// ❌ BAD: Handler inside widget that gets destroyed on parent rebuild
8+
class ParentWidget extends StatefulWidget {
9+
const ParentWidget({super.key});
10+
11+
@override
12+
State<ParentWidget> createState() => _ParentWidgetState();
13+
}
14+
15+
class _ParentWidgetState extends State<ParentWidget> {
16+
bool _isHovered = false;
17+
18+
@override
19+
Widget build(BuildContext context) {
20+
return MouseRegion(
21+
onEnter: (_) => setState(() => _isHovered = true),
22+
onExit: (_) => setState(() => _isHovered = false),
23+
child: Container(
24+
color: _isHovered ? Colors.blue.shade100 : Colors.white,
25+
child: SaveButtonWithHandler(), // ❌ Destroyed on every hover!
26+
),
27+
);
28+
}
29+
}
30+
31+
class SaveButtonWithHandler extends WatchingWidget {
32+
@override
33+
Widget build(BuildContext context) {
34+
// ❌ Handler is registered here - if parent rebuilds,
35+
// this widget is destroyed and handler is lost!
36+
registerHandler(
37+
select: (TodoManager m) => m.createTodoCommand.results,
38+
handler: (context, result, cancel) {
39+
if (result.hasData) {
40+
Navigator.of(context).pop(); // May never execute!
41+
}
42+
},
43+
);
44+
45+
return ElevatedButton(
46+
onPressed: () => di<TodoManager>().createTodoCommand(
47+
CreateTodoParams(title: 'New', description: 'Task'),
48+
),
49+
child: const Text('Save'),
50+
);
51+
}
52+
}
53+
// #endregion bad
54+
55+
// #region good
56+
// ✅ GOOD: Handler in stable parent widget
57+
class StableParentWidget extends WatchingStatefulWidget {
58+
const StableParentWidget({super.key});
59+
60+
@override
61+
State<StableParentWidget> createState() => _StableParentWidgetState();
62+
}
63+
64+
class _StableParentWidgetState extends State<StableParentWidget> {
65+
bool _isHovered = false;
66+
67+
@override
68+
Widget build(BuildContext context) {
69+
// ✅ Handler registered in parent - survives child rebuilds
70+
registerHandler(
71+
select: (TodoManager m) => m.createTodoCommand.results,
72+
handler: (context, result, cancel) {
73+
if (result.hasData) {
74+
Navigator.of(context).pop(); // Always executes!
75+
}
76+
},
77+
);
78+
79+
return MouseRegion(
80+
onEnter: (_) => setState(() => _isHovered = true),
81+
onExit: (_) => setState(() => _isHovered = false),
82+
child: Container(
83+
color: _isHovered ? Colors.blue.shade100 : Colors.white,
84+
child: const SaveButtonOnly(), // Child can rebuild safely
85+
),
86+
);
87+
}
88+
}
89+
90+
class SaveButtonOnly extends StatelessWidget {
91+
const SaveButtonOnly({super.key});
92+
93+
@override
94+
Widget build(BuildContext context) {
95+
// Just the button - no handler here
96+
return ElevatedButton(
97+
onPressed: () => di<TodoManager>().createTodoCommand(
98+
CreateTodoParams(title: 'New', description: 'Task'),
99+
),
100+
child: const Text('Save'),
101+
);
102+
}
103+
}
104+
// #endregion good
105+
106+
void main() {
107+
setupDependencyInjection();
108+
}

docs/documentation/get_it/getting_started.md

Lines changed: 113 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,31 @@ dependencies:
6565

6666
<strong>That's it!</strong> No Provider wrappers, no InheritedWidgets, no BuildContext needed.
6767

68+
---
69+
70+
## Naming Your GetIt Instance
71+
72+
The standard pattern is to create a global variable:
73+
74+
75+
<<< @/../code_samples/lib/get_it/code_sample_866f5818.dart#example
76+
77+
<strong>Alternative names you might see:</strong>
78+
- `final sl = GetIt.instance;` (service locator)
79+
- `final locator = GetIt.instance;`
80+
- `final di = GetIt.instance;` (dependency injection)
81+
- `GetIt.instance` or `GetIt.I` (use directly without variable)
82+
83+
<strong>Recommendation:</strong> Use `getIt` or `di` - both are clear and widely recognized in the Flutter community.
84+
85+
::: tip Using with `watch_it`
86+
If you're using the [`watch_it`](https://pub.dev/packages/watch_it) package, you already have a global `di` instance available - no need to create your own. Just import `watch_it` and use `di` directly.
87+
:::
88+
89+
::: tip Cross-Package Usage
90+
`GetIt.instance` returns the same singleton across all packages in your project. Create your global variable once in your main app and import it elsewhere.
91+
:::
92+
6893
::: warning Isolate Safety
6994
GetIt instances are not thread-safe and cannot be shared across isolates. Each isolate will get its own GetIt instance. This means objects registered in one isolate can't be accessed from another isolate.
7095
:::
@@ -73,7 +98,7 @@ GetIt instances are not thread-safe and cannot be shared across isolates. Each i
7398

7499
## When to Use Which Registration Type
75100

76-
get_it offers three main registration types:
101+
`get_it` offers three main registration types:
77102

78103
| Registration Type | When Created | Lifetime | Use When |
79104
|-------------------|--------------|----------|----------|
@@ -161,6 +186,93 @@ See [Where should I put my get_it setup code?](/documentation/get_it/faq#where-s
161186

162187
---
163188

189+
## Want Less Boilerplate? Try Injectable!
190+
191+
**Injectable** is a code generation package that automates your `get_it` setup. If you find yourself writing lots of registration code, injectable might be for you.
192+
193+
### How It Works
194+
195+
Instead of manually registering each service:
196+
197+
```dart
198+
void configureDependencies() {
199+
getIt.registerLazySingleton<ApiClient>(() => ApiClient());
200+
getIt.registerLazySingleton<Database>(() => Database());
201+
getIt.registerLazySingleton<AuthService>(() => AuthService(getIt(), getIt()));
202+
}
203+
```
204+
205+
Just annotate your classes:
206+
207+
```dart
208+
@lazySingleton
209+
class ApiClient { }
210+
211+
@lazySingleton
212+
class Database { }
213+
214+
@lazySingleton
215+
class AuthService {
216+
AuthService(ApiClient api, Database db); // Auto-injected!
217+
}
218+
```
219+
220+
Run build_runner and injectable generates all the registration code for you.
221+
222+
### Quick Setup
223+
224+
1. Add dependencies:
225+
```yaml
226+
dependencies:
227+
get_it: ^8.3.0
228+
injectable: ^2.3.2
229+
230+
dev_dependencies:
231+
injectable_generator: ^2.6.1
232+
build_runner: ^2.4.0
233+
```
234+
235+
2. Annotate your main configuration:
236+
```dart
237+
@InjectableInit()
238+
void configureDependencies() => getIt.init();
239+
```
240+
241+
3. Run code generation:
242+
```bash
243+
flutter pub run build_runner build
244+
```
245+
246+
### When to Use Injectable
247+
248+
**Use injectable if:**
249+
- You have many services to register
250+
- You want automatic dependency resolution
251+
- You prefer annotations over manual setup
252+
- You're comfortable with code generation
253+
254+
**Stick with plain `get_it` if:**
255+
- You prefer explicit, visible registration
256+
- You want to avoid build_runner in your workflow
257+
- You have a small number of services
258+
- You want full control over registration order and logic
259+
260+
Both approaches use the same `GetIt` instance and have identical runtime performance!
261+
262+
### Learn More
263+
264+
Check out [injectable on pub.dev](https://pub.dev/packages/injectable) for full documentation and advanced features like:
265+
- Environment-specific registrations (dev/prod)
266+
- Pre-resolved dependencies
267+
- Module registration
268+
- Custom annotations
269+
270+
::: tip Injectable Support
271+
Injectable is a separate package maintained by a different author. If you encounter issues with injectable, please file them on the [injectable GitHub repository](https://github.com/Milad-Akarie/injectable/issues).
272+
:::
273+
274+
---
275+
164276
## Next Steps
165277

166278
Now that you understand the basics, explore these topics:
@@ -228,27 +340,3 @@ get_it implements the Service Locator pattern - it decouples interface (abstract
228340
For deeper understanding, read Martin Fowler's classic article: [Inversion of Control Containers and the Dependency Injection pattern](https://martinfowler.com/articles/injection.html)
229341

230342
</details>
231-
232-
---
233-
234-
## Naming Your GetIt Instance
235-
236-
The standard pattern is to create a global variable:
237-
238-
239-
<<< @/../code_samples/lib/get_it/code_sample_866f5818.dart#example
240-
241-
<strong>Alternative names you might see:</strong>
242-
- `final sl = GetIt.instance;` (service locator)
243-
- `final locator = GetIt.instance;`
244-
- `final di = GetIt.instance;` (dependency injection)
245-
- `GetIt.instance` or `GetIt.I` (use directly without variable)
246-
247-
<strong>Recommendation:</strong> Use `getIt` or `di` - both are clear and widely recognized in the Flutter community.
248-
249-
::: tip Using with `watch_it`
250-
If you're using the [`watch_it`](https://pub.dev/packages/watch_it) package, you already have a global `di` instance available - no need to create your own. Just import `watch_it` and use `di` directly.
251-
:::
252-
253-
::: tip Cross-Package Usage
254-
`GetIt.instance` returns the same singleton across all packages in your project. Create your global variable once in your main app and import it elsewhere.

docs/documentation/watch_it/debugging_tracing.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,18 @@ See [listen_it Collections](/documentation/listen_it/collections/introduction.md
136136

137137
<<< @/../code_samples/lib/watch_it/debugging_registerhandler.dart#handler_registered_before_return_good
138138

139+
#### 2. Widget destroyed during command execution
140+
141+
If the widget containing the handler is destroyed and rebuilt while the command is running, the handler will be re-registered and may miss state changes.
142+
143+
**Example:** A button inside a widget that rebuilds on hover:
144+
145+
<<< @/../code_samples/lib/watch_it/handler_lifecycle_example.dart#bad
146+
147+
**Solution:** Move the handler to a stable parent widget:
148+
149+
<<< @/../code_samples/lib/watch_it/handler_lifecycle_example.dart#good
150+
139151
## Debugging Techniques
140152

141153
### Enable `watch_it` Tracing

docs/documentation/watch_it/handlers.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,16 @@ When `true`, the handler is called on the first build with the current value of
202202

203203
<<< @/../code_samples/lib/watch_it/handler_patterns.dart#mistake_good
204204

205+
### ❌ Handler in widget that gets destroyed
206+
207+
If the widget containing the handler is destroyed during command execution, the handler is lost and misses state changes:
208+
209+
<<< @/../code_samples/lib/watch_it/handler_lifecycle_example.dart#bad
210+
211+
### ✅ Move handler to stable parent widget
212+
213+
<<< @/../code_samples/lib/watch_it/handler_lifecycle_example.dart#good
214+
205215
## What's Next?
206216

207217
Now you know when to rebuild (watch) vs when to run side effects (handlers). Next:

docs/documentation/watch_it/observing_commands.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,16 @@ While `watch` is for rebuilding UI, use `registerHandler` for side effects like
6767
- Log error to crash reporting
6868
- Retry logic
6969

70+
::: warning Handler Lifecycle
71+
If you rely on your handler reacting to all state changes, ensure the widget where the handler is registered isn't destroyed and rebuilt during command execution.
72+
73+
**Common pitfall:** A button that registers a handler and calls a command, but the parent widget rebuilds on an `onHover` event - this destroys and recreates the button (and its handler), causing missed state changes.
74+
75+
**Solution:** Move the handler to a parent widget that will live for the entire duration of the command execution.
76+
77+
**Note:** This doesn't apply to `watch` functions - their results are only used in the same build function, so widget rebuilds don't cause issues.
78+
:::
79+
7080
## Watching Command Results
7181

7282
The `results` property provides a `CommandResult` object containing all command state in one place:

0 commit comments

Comments
 (0)