Skip to content

Commit 52710ca

Browse files
authored
feat!: Making add,addAll sync methods (#3968)
Makes `add`, `addAll` and `addToParent` methods sync.
1 parent e291f1c commit 52710ca

91 files changed

Lines changed: 763 additions & 299 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/.cspell/dart_dictionary.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ dartdoc # documentation tool for dart
44
dartdocs # plural of dartdoc
55
endtemplate # Use @endtemplate to close a @template block in dartdoc
66
pubspec # dependency and configuration file of every Dart project
7+
unawaited # dart:async helper to mark a Future as intentionally not awaited

doc/bridge_packages/flame_behaviors/getting_started.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ For instance a `TimerComponent` can implement a time-based behavioral activity:
9494
class MyBehavior extends Behavior {
9595
@override
9696
Future<void> onLoad() async {
97-
await add(TimerComponent(period: 5, repeat: true, onTick: _onTick));
97+
add(TimerComponent(period: 5, repeat: true, onTick: _onTick));
9898
}
9999
100100
void _onTick() {

doc/bridge_packages/flame_bloc/bloc.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ We can do that by using `FlameBlocProvider` component:
2222
class MyGame extends FlameGame {
2323
@override
2424
Future<void> onLoad() async {
25-
await add(
25+
add(
2626
FlameBlocProvider<PlayerInventoryBloc, PlayerInventoryState>(
2727
create: () => PlayerInventoryBloc(),
2828
children: [
@@ -44,7 +44,7 @@ fashion:
4444
class MyGame extends FlameGame {
4545
@override
4646
Future<void> onLoad() async {
47-
await add(
47+
add(
4848
FlameMultiBlocProvider(
4949
providers: [
5050
FlameBlocProvider<PlayerInventoryBloc, PlayerInventoryState>(
@@ -72,7 +72,7 @@ By using `FlameBlocListener` component:
7272
class Player extends PositionComponent {
7373
@override
7474
Future<void> onLoad() async {
75-
await add(
75+
add(
7676
FlameBlocListener<PlayerInventoryBloc, PlayerInventoryState>(
7777
listener: (state) {
7878
updateGear(state);

doc/bridge_packages/flame_spine/flame_spine.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ class FlameSpineExample extends FlameGame {
4040
4141
// Set the "walk" animation on track 0 in looping mode
4242
spineboy.animationState.setAnimationByName(0, 'walk', true);
43-
await add(spineboy);
43+
add(spineboy);
4444
}
4545
4646
@override

doc/flame/components/components.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,57 @@ class MyGame extends FlameGame {
212212
The two approaches can be combined freely: the children specified within the constructor will be
213213
added first, and then any additional child components after.
214214

215+
The `add()`, `addAll()`, and `addToParent()` methods are synchronous: they return immediately
216+
without waiting for the child to load or mount. This makes them safe to call from anywhere,
217+
including inside `update()` or a loop that spawns many components, without having to `await` them
218+
or wrap them in `unawaited`. If you need to wait until a child has reached a given lifecycle stage,
219+
await its `loaded`, `mounted`, or `removed` future instead (see the lifecycle getters under
220+
[Component lifecycle](#component-lifecycle)):
221+
222+
```dart
223+
world.add(coin);
224+
await coin.mounted;
225+
// The coin is now guaranteed to be mounted.
226+
```
227+
228+
When you add a batch of children and only care that all of them made it into the tree, await
229+
`game.lifecycleEventsProcessed` once instead of collecting the individual futures:
230+
231+
```dart
232+
world.addAll(coins);
233+
await game.lifecycleEventsProcessed;
234+
// All the coins are now in world.children.
235+
```
236+
237+
The same three getters are also available on any `Iterable<Component>`, for when you need a
238+
specific stage for a specific group of children rather than for the whole tree:
239+
240+
```dart
241+
world.addAll(coins);
242+
await coins.loaded;
243+
// Every coin has finished loading.
244+
```
245+
246+
Awaiting `loaded` is safe from inside the parent's own `onLoad`, because the child starts loading as
247+
soon as it is added:
248+
249+
```dart
250+
class Inventory extends Component {
251+
@override
252+
Future<void> onLoad() async {
253+
final coin = Coin();
254+
add(coin);
255+
await coin.loaded;
256+
// Anything the coin's onLoad set up is now available here.
257+
}
258+
}
259+
```
260+
261+
Awaiting `mounted` or `removed` there is not safe: a child can only be mounted after its parent has
262+
been, and the parent is only mounted once its `onLoad` has completed, so those futures would
263+
deadlock. The same goes for `game.lifecycleEventsProcessed`, since the parent's own pending mount is
264+
part of the queue it waits for.
265+
215266
Note that the children added via either method are only guaranteed to be available eventually:
216267
after they are loaded and mounted. We can only assure that they will appear in the children list
217268
in the same order as they were scheduled for addition.

doc/flame/examples/lib/anchor.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ class AnchorGame extends FlameGame {
2525
paint: BasicPalette.blue.paint(),
2626
);
2727

28-
await _redComponent.addAll([
28+
_redComponent.addAll([
2929
_blueComponent,
3030
CircleComponent(radius: 2, anchor: Anchor.center),
3131
]);
3232

33-
await addAll([
33+
addAll([
3434
_redComponent,
3535
_parentAnchorText,
3636
_childAnchorText,

doc/flame/examples/lib/time_scale.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class TimeScaleGame extends FlameGame with HasTimeScale {
1010

1111
@override
1212
Future<void> onLoad() async {
13-
await add(
13+
add(
1414
EmberPlayer(
1515
position: size / 2,
1616
size: size / 4,

doc/flame/game.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ class MyCrate extends SpriteComponent {
3939
class MyWorld extends World {
4040
@override
4141
Future<void> onLoad() async {
42-
await add(MyCrate());
42+
await super.onLoad();
43+
add(MyCrate());
4344
}
4445
}
4546
@@ -236,8 +237,8 @@ application. This is a common scenario when building games: there is a single fu
236237

237238
Adding this mixin provides performance advantages in certain scenarios. In particular, a component's
238239
`onLoad` method is guaranteed to start when that component is added to its parent, even if the
239-
parent is not yet mounted itself. Consequently, `await`-ing on `parent.add(component)` is guaranteed
240-
to always finish loading the component.
240+
parent is not yet mounted itself. Consequently, awaiting `component.loaded` after
241+
`parent.add(component)` is guaranteed to finish loading the component.
241242

242243
Using this mixin is simple:
243244

doc/flame/migration.md

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,8 @@ would apply their action even though the drag never finished. This is not a rare
120120
with `MultiDragScaleDispatcher` every two finger pinch cancels the individual pointer drags.
121121

122122
The default implementation now only resets `isDragged`, which means that `onDragEnd` is no longer
123-
called when a drag is cancelled. If you were relying on the old behavior, override `onDragCancel` and
124-
forward the event yourself with `DragCancelEvent.toDragEnd`:
123+
called when a drag is cancelled. If you were relying on the old behavior, override `onDragCancel`
124+
and forward the event yourself with `DragCancelEvent.toDragEnd`:
125125

126126
```dart
127127
// Before
@@ -223,6 +223,80 @@ The equivalent field on the deprecated `*Info` event classes (`TapDownInfo.handl
223223
been removed as well.
224224

225225

226+
### `add`, `addAll` and `addToParent` are now synchronous
227+
228+
`Component.add`, `Component.addAll` and `Component.addToParent` used to return a future, which made
229+
it look like you could await the addition. That future only covered the child's loading, never its
230+
mounting, so awaiting it was misleading, and forgetting to await it (or to wrap it in `unawaited`)
231+
tripped the `discarded_futures` lint in a lot of games. All three methods now return `void`.
232+
233+
Drop the `await`:
234+
235+
```dart
236+
// Before
237+
await add(MyComponent());
238+
await addAll([MyComponent(), MyOtherComponent()]);
239+
240+
// After
241+
add(MyComponent());
242+
addAll([MyComponent(), MyOtherComponent()]);
243+
```
244+
245+
If you were relying on the returned future to know when the child had loaded, await the child's
246+
`loaded` future instead:
247+
248+
```dart
249+
// Before
250+
await add(crate);
251+
252+
// After
253+
add(crate);
254+
await crate.loaded;
255+
```
256+
257+
For a batch of children, `loaded`, `mounted` and `removed` are also available on any
258+
`Iterable<Component>`:
259+
260+
```dart
261+
// Before
262+
await addAll(crates);
263+
264+
// After
265+
addAll(crates);
266+
await crates.loaded;
267+
```
268+
269+
Or, when you need them to be present in `children` rather than just loaded, await
270+
`game.lifecycleEventsProcessed` once after adding them.
271+
272+
273+
#### Load errors are no longer reported by `GameWidget.errorBuilder`
274+
275+
`GameWidget.errorBuilder` shows a widget when the *game's* loading fails, and it used to catch a
276+
failing child's `onLoad` as well, because `await add(child)` chained the child's error onto the
277+
game's own `onLoad` future. Since `add` no longer returns a future, that chain is gone: a child that
278+
throws in `onLoad` no longer reaches `errorBuilder`.
279+
280+
The component itself is not added to the tree, and the rest of the game keeps running. The error is
281+
reported through the child's `loaded` future, and if nothing is awaiting it, it is handed to the
282+
current `Zone` as an uncaught error.
283+
284+
To get the old behavior for a specific child, await its `loaded` future inside the parent's
285+
`onLoad`, which puts the error back onto the future `errorBuilder` watches:
286+
287+
```dart
288+
class MyGame extends FlameGame {
289+
@override
290+
Future<void> onLoad() async {
291+
final level = Level();
292+
world.add(level);
293+
// Throws here if Level.onLoad fails, so errorBuilder is shown.
294+
await level.loaded;
295+
}
296+
}
297+
```
298+
299+
226300
### `GameWidget.controlled` renamed to `GameWidget.managed`
227301

228302
The `GameWidget.controlled` constructor has been renamed to `GameWidget.managed`. The behavior is

doc/tutorials/platformer/app/lib/overlays/hud.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class Hud extends PositionComponent with HasGameReference<EmberQuestGame> {
4444

4545
for (var i = 1; i <= game.health; i++) {
4646
final positionX = 40 * i;
47-
await add(
47+
add(
4848
HeartHealthComponent(
4949
heartNumber: i,
5050
position: Vector2(positionX.toDouble(), 20),

0 commit comments

Comments
 (0)