From 3804f52434cf1bcaf28b501bf96858ecd3636164 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Sun, 28 Jan 2024 19:54:35 +0100 Subject: [PATCH] fix: Lifecycle completers to be called for FlameGame (#3007) Previously the `mounted`, `loaded` and `removed` completers for `FlameGame` weren't called since `FlameGame` doesn't go through the normal component lifecycle flow, this PR adds so that the completers are completed properly. Closes #3003 --- .../lib/src/components/core/component.dart | 26 ++++++++-- packages/flame/lib/src/game/flame_game.dart | 14 ++++++ packages/flame/lib/src/game/game.dart | 10 +++- .../lib/src/game/game_widget/game_widget.dart | 7 +-- packages/flame/test/game/flame_game_test.dart | 47 +++++++++++++++++++ .../flame_test/lib/src/test_flame_game.dart | 3 +- 6 files changed, 96 insertions(+), 11 deletions(-) diff --git a/packages/flame/lib/src/components/core/component.dart b/packages/flame/lib/src/components/core/component.dart index b595ef45cd6..0097139dea6 100644 --- a/packages/flame/lib/src/components/core/component.dart +++ b/packages/flame/lib/src/components/core/component.dart @@ -3,11 +3,10 @@ import 'dart:math' as math; import 'package:collection/collection.dart'; import 'package:flame/components.dart'; +import 'package:flame/game.dart'; import 'package:flame/src/cache/value_cache.dart'; import 'package:flame/src/components/core/component_tree_root.dart'; import 'package:flame/src/effects/provider_interfaces.dart'; -import 'package:flame/src/game/flame_game.dart'; -import 'package:flame/src/game/game.dart'; import 'package:flutter/painting.dart'; import 'package:meta/meta.dart'; @@ -927,13 +926,34 @@ class Component { } } + /// Used by the [FlameGame] to set the loaded state of the component, since + /// the game isn't going through the whole normal component life cycle. @internal - void setMounted() { + void setLoaded() { _setLoadedBit(); + _loadCompleter?.complete(); + _loadCompleter = null; + } + + /// Used by the [FlameGame] to set the mounted state of the component, since + /// the game isn't going through the whole normal component life cycle. + @internal + void setMounted() { _setMountedBit(); + _mountCompleter?.complete(); + _mountCompleter = null; _reAddChildren(); } + /// Used by the [FlameGame] to set the removed state of the component, since + /// the game isn't going through the whole normal component life cycle. + @internal + void setRemoved() { + _setRemovedBit(); + _removeCompleter?.complete(); + _removeCompleter = null; + } + void _remove() { assert(_parent != null, 'Trying to remove a component with no parent'); diff --git a/packages/flame/lib/src/game/flame_game.dart b/packages/flame/lib/src/game/flame_game.dart index 9f18fe460c5..bd8127f118c 100644 --- a/packages/flame/lib/src/game/flame_game.dart +++ b/packages/flame/lib/src/game/flame_game.dart @@ -92,6 +92,13 @@ class FlameGame extends ComponentTreeRoot @override Vector2 get size => camera.viewport.virtualSize; + @override + @internal + FutureOr load() async { + await super.load(); + setLoaded(); + } + @override @internal void mount() { @@ -99,6 +106,13 @@ class FlameGame extends ComponentTreeRoot setMounted(); } + @override + @internal + void finalizeRemoval() { + super.finalizeRemoval(); + setRemoved(); + } + /// This implementation of render renders each component, making sure the /// canvas is reset for each one. /// diff --git a/packages/flame/lib/src/game/game.dart b/packages/flame/lib/src/game/game.dart index be983fd4b72..d044c996a21 100644 --- a/packages/flame/lib/src/game/game.dart +++ b/packages/flame/lib/src/game/game.dart @@ -73,7 +73,7 @@ abstract mixin class Game { bool _debugOnLoadStarted = false; @internal - FutureOr get onLoadFuture { + FutureOr load() async { assert( () { _debugOnLoadStarted = true; @@ -101,6 +101,12 @@ abstract mixin class Game { onMount(); } + @mustCallSuper + @internal + void finalizeRemoval() { + onRemove(); + } + /// Current game viewport size, updated every resize via the [onGameResize] /// method hook. Vector2 get size { @@ -221,7 +227,7 @@ abstract mixin class Game { } /// Called when the game is about to be removed from the Flutter widget tree, - /// but before it is actually removed. See the docs for an example on how to + /// but before it is actually removed. See the docs for an example on how to /// do cleanups to avoid memory leaks. void onRemove() {} diff --git a/packages/flame/lib/src/game/game_widget/game_widget.dart b/packages/flame/lib/src/game/game_widget/game_widget.dart index a3346d51ab4..6ebe3126ef4 100644 --- a/packages/flame/lib/src/game/game_widget/game_widget.dart +++ b/packages/flame/lib/src/game/game_widget/game_widget.dart @@ -191,10 +191,7 @@ class GameWidgetState extends State> { Future get loaderFuture => _loaderFuture ??= (() async { final game = currentGame; assert(game.hasLayout); - final onLoad = game.onLoadFuture; - if (onLoad != null) { - await onLoad; - } + await game.load(); game.mount(); if (!game.paused) { game.update(0); @@ -277,7 +274,7 @@ class GameWidgetState extends State> { void disposeCurrentGame({bool callGameOnDispose = false}) { currentGame.removeGameStateListener(_onGameStateChange); currentGame.lifecycleStateChange(AppLifecycleState.paused); - currentGame.onRemove(); + currentGame.finalizeRemoval(); if (callGameOnDispose) { currentGame.onDispose(); } diff --git a/packages/flame/test/game/flame_game_test.dart b/packages/flame/test/game/flame_game_test.dart index 188d07f3c5f..297e3dbab37 100644 --- a/packages/flame/test/game/flame_game_test.dart +++ b/packages/flame/test/game/flame_game_test.dart @@ -216,6 +216,41 @@ void main() { }, ); + group('completers', () { + testWidgets( + 'game calls loaded completer', + (WidgetTester tester) async { + final game = _CompleterGame(); + + await tester.pumpWidget(GameWidget(game: game)); + expect(game.loadedCompleterCount, 1); + expect(game.mountedCompleterCount, 1); + }, + ); + + testWithGame( + 'game calls mount completer', + _CompleterGame.new, + (game) async { + await game.mounted; + expect(game.mountedCompleterCount, 1); + }, + ); + + testWidgets( + 'game calls loaded completer', + (WidgetTester tester) async { + final game = _CompleterGame(); + + await tester.pumpWidget(GameWidget(game: game)); + expect(game.loadedCompleterCount, 1); + expect(game.mountedCompleterCount, 1); + await tester.pumpWidget(Container()); + expect(game.removedCompleterCount, 1); + }, + ); + }); + group('world and camera', () { testWithFlameGame( 'game world setter', @@ -440,3 +475,15 @@ class _OnAttachGame extends FlameGame { return Future.delayed(const Duration(seconds: 1)); } } + +class _CompleterGame extends FlameGame { + int loadedCompleterCount = 0; + int mountedCompleterCount = 0; + int removedCompleterCount = 0; + + _CompleterGame() { + loaded.whenComplete(() => loadedCompleterCount++); + mounted.whenComplete(() => mountedCompleterCount++); + removed.whenComplete(() => removedCompleterCount++); + } +} diff --git a/packages/flame_test/lib/src/test_flame_game.dart b/packages/flame_test/lib/src/test_flame_game.dart index f7e3d11d63f..1a245469e27 100644 --- a/packages/flame_test/lib/src/test_flame_game.dart +++ b/packages/flame_test/lib/src/test_flame_game.dart @@ -92,7 +92,8 @@ Future testWithGame( Future initializeGame(CreateFunction create) async { final game = create(); game.onGameResize(Vector2(800, 600)); - await game.onLoad(); + // ignore: invalid_use_of_internal_member + await game.load(); // ignore: invalid_use_of_internal_member game.mount(); game.update(0);