mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-01 19:12:31 +08:00
feat: Adding ComponentNotifier API (#1889)
This adds a proposal for a new API for flame, the ComponentNotifier.
This API offers the user change notifiers classes that are tied to FlameGame and its components so the user can be notified when a component is added, removed or updated.
This will enable users to:
Take the benefit of reactive programming inside the game
Have a simple way of watching certain states from the game, on Flutter Widgets
One important note here is that this proposal does not mean to replace integrations like flame_bloc, but rather provider an simple and out of the box solution, without any need of additional packages, since change notifiers are provided by flutter itself.
Opening this as draft for now to get feedback on the implementation, will write tests and docs once we have the final implementation.
This commit is contained in:
@ -1085,6 +1085,94 @@ Check the example app
|
||||
for details on how to use it.
|
||||
|
||||
|
||||
## ComponentsNotifier
|
||||
|
||||
Most of the time just accessing children and their attributes is enough to build the logic of
|
||||
your game.
|
||||
|
||||
But sometimes, reactivity can help the developer to simplify and write better code, to help with
|
||||
that Flame provides the `ComponentsNotifier`, which is an implementation of a
|
||||
`ChangeNotifier` that notifies listeners every time a component is added, removed or manually
|
||||
changed.
|
||||
|
||||
For example, lets say that we want to show a game over text when the player's lives reach zero.
|
||||
|
||||
To make the component automatically report when new instances are added or removed, the `Notifier`
|
||||
mixin can be applied to the component class:
|
||||
|
||||
```dart
|
||||
class Player extends SpriteComponent with Notifier {}
|
||||
```
|
||||
|
||||
Then to listen to changes on that component the `componentsNotifier` method from `FlameGame` can
|
||||
be used:
|
||||
|
||||
```dart
|
||||
class MyGame extends FlameGame {
|
||||
int lives = 2;
|
||||
|
||||
Future<void> onLoad() {
|
||||
final playerNotifier = componentsNotifier<Player>()
|
||||
..addListener(() {
|
||||
final player = playerNotifier.single;
|
||||
if (player == null) {
|
||||
lives--;
|
||||
if (lives == 0) {
|
||||
add(GameOverComponent());
|
||||
} else {
|
||||
add(Player());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
A `Notifier` component can also manually notify its listeners that something changed. Lets expand
|
||||
the example above to make a hud component to blink when the player has half of their health. In
|
||||
order to do so, we need that the `Player` component notify a change manually, example:
|
||||
|
||||
```dart
|
||||
class Player extends SpriteComponent with Notifier {
|
||||
double health = 1;
|
||||
|
||||
void takeHit() {
|
||||
health -= .1;
|
||||
if (health == 0) {
|
||||
removeFromParent();
|
||||
} else if (health <= .5) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then our hud component could look like:
|
||||
|
||||
```dart
|
||||
class Hud extends PositionComponent with HasGameRef {
|
||||
|
||||
Future<void> onLoad() {
|
||||
final playerNotifier = gameRef.componentsNotifier<Player>()
|
||||
..addListener(() {
|
||||
final player = playerNotifier.single;
|
||||
if (player != null) {
|
||||
if (player.health <= .5) {
|
||||
add(BlinkEffect());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`ComponentsNotifier`s can also come in handy to rebuild widgets when state changes inside a
|
||||
`FlameGame`, to help with that Flame provides a `ComponentsNotifierBuilder` widget.
|
||||
|
||||
To see an example of its use check the running example
|
||||
[here](https://github.com/flame-engine/flame/tree/main/examples/lib/stories/components/components_notifier_example.dart);
|
||||
|
||||
|
||||
## ClipComponent
|
||||
|
||||
A `ClipComponent` is a component that will clip the canvas to its size and shape. This means that
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import 'package:dashbook/dashbook.dart';
|
||||
import 'package:examples/commons/commons.dart';
|
||||
import 'package:examples/stories/components/clip_component_example.dart';
|
||||
import 'package:examples/stories/components/components_notifier_example.dart';
|
||||
import 'package:examples/stories/components/components_notifier_provider_example.dart';
|
||||
import 'package:examples/stories/components/composability_example.dart';
|
||||
import 'package:examples/stories/components/debug_example.dart';
|
||||
import 'package:examples/stories/components/game_in_game_example.dart';
|
||||
@ -52,5 +54,17 @@ void addComponentsStories(Dashbook dashbook) {
|
||||
(_) => GameWidget(game: LookAtSmoothExample()),
|
||||
codeLink: baseLink('components/look_at_smooth_example.dart'),
|
||||
info: LookAtExample.description,
|
||||
)
|
||||
..add(
|
||||
'Component Notifier',
|
||||
(_) => const ComponentsNotifierExampleWidget(),
|
||||
codeLink: baseLink('components/component_notifier_example.dart'),
|
||||
info: ComponentsNotifierExampleWidget.description,
|
||||
)
|
||||
..add(
|
||||
'Component Notifier (with provider)',
|
||||
(_) => const ComponentsNotifierProviderExampleWidget(),
|
||||
codeLink: baseLink('components/component_notifier_provider_example.dart'),
|
||||
info: ComponentsNotifierProviderExampleWidget.description,
|
||||
);
|
||||
}
|
||||
|
||||
111
examples/lib/stories/components/components_notifier_example.dart
Normal file
111
examples/lib/stories/components/components_notifier_example.dart
Normal file
@ -0,0 +1,111 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ComponentsNotifierExampleWidget extends StatefulWidget {
|
||||
const ComponentsNotifierExampleWidget({super.key});
|
||||
|
||||
static String description = '''
|
||||
Showcases how the components notifier can be used between
|
||||
a flame game instance and widgets.
|
||||
|
||||
Tap the red dots to defeat the enemies and see the hud being updated
|
||||
to reflect the current state of the game.
|
||||
''';
|
||||
|
||||
@override
|
||||
State<ComponentsNotifierExampleWidget> createState() =>
|
||||
_ComponentsNotifierExampleWidgetState();
|
||||
}
|
||||
|
||||
class _ComponentsNotifierExampleWidgetState
|
||||
extends State<ComponentsNotifierExampleWidget> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
game = ComponentNotifierExample();
|
||||
}
|
||||
|
||||
late final ComponentNotifierExample game;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: GameWidget(game: game),
|
||||
),
|
||||
Positioned(
|
||||
left: 16,
|
||||
top: 16,
|
||||
child: ComponentsNotifierBuilder<Enemy>(
|
||||
notifier: game.componentsNotifier<Enemy>(),
|
||||
builder: (context, notifier) {
|
||||
return GameHud(
|
||||
remainingEnemies: notifier.components.length,
|
||||
onReplay: game.replay,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GameHud extends StatelessWidget {
|
||||
const GameHud({
|
||||
super.key,
|
||||
required this.remainingEnemies,
|
||||
required this.onReplay,
|
||||
});
|
||||
|
||||
final int remainingEnemies;
|
||||
final VoidCallback onReplay;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: remainingEnemies == 0
|
||||
? ElevatedButton(
|
||||
onPressed: onReplay,
|
||||
child: const Text('Play again'),
|
||||
)
|
||||
: Text('Remaining enemies: $remainingEnemies'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Enemy extends CircleComponent with Tappable, Notifier {
|
||||
Enemy({super.position})
|
||||
: super(
|
||||
radius: 20,
|
||||
paint: Paint()..color = const Color(0xFFFF0000),
|
||||
);
|
||||
|
||||
@override
|
||||
bool onTapUp(_) {
|
||||
removeFromParent();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class ComponentNotifierExample extends FlameGame with HasTappables {
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
replay();
|
||||
}
|
||||
|
||||
void replay() {
|
||||
add(Enemy(position: Vector2(100, 100)));
|
||||
add(Enemy(position: Vector2(200, 100)));
|
||||
add(Enemy(position: Vector2(300, 100)));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,105 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ComponentsNotifierProviderExampleWidget extends StatefulWidget {
|
||||
const ComponentsNotifierProviderExampleWidget({super.key});
|
||||
|
||||
static String description = '''
|
||||
Similar to the Components Notifier example, but uses provider
|
||||
instead of the built in ComponentsNotifierBuilder widget.
|
||||
''';
|
||||
|
||||
@override
|
||||
State<ComponentsNotifierProviderExampleWidget> createState() =>
|
||||
_ComponentsNotifierProviderExampleWidgetState();
|
||||
}
|
||||
|
||||
class _ComponentsNotifierProviderExampleWidgetState
|
||||
extends State<ComponentsNotifierProviderExampleWidget> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
game = ComponentNotifierExample();
|
||||
}
|
||||
|
||||
late final ComponentNotifierExample game;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: MultiProvider(
|
||||
providers: [
|
||||
Provider<ComponentNotifierExample>.value(value: game),
|
||||
ChangeNotifierProvider<ComponentsNotifier<Enemy>>(
|
||||
create: (_) => game.componentsNotifier<Enemy>(),
|
||||
),
|
||||
],
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: GameWidget(game: game),
|
||||
),
|
||||
const Positioned(
|
||||
left: 16,
|
||||
top: 16,
|
||||
child: GameHud(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GameHud extends StatelessWidget {
|
||||
const GameHud({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final enemies = context.watch<ComponentsNotifier<Enemy>>().components;
|
||||
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: enemies.isEmpty
|
||||
? ElevatedButton(
|
||||
child: const Text('Play again'),
|
||||
onPressed: () {
|
||||
context.read<ComponentNotifierExample>().replay();
|
||||
},
|
||||
)
|
||||
: Text('Remaining enemies: ${enemies.length}'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Enemy extends CircleComponent with Tappable, Notifier {
|
||||
Enemy({super.position})
|
||||
: super(
|
||||
radius: 20,
|
||||
paint: Paint()..color = const Color(0xFFFF0000),
|
||||
);
|
||||
|
||||
@override
|
||||
bool onTapUp(_) {
|
||||
removeFromParent();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class ComponentNotifierExample extends FlameGame with HasTappables {
|
||||
@override
|
||||
Future<void> onLoad() async {
|
||||
replay();
|
||||
}
|
||||
|
||||
void replay() {
|
||||
add(Enemy(position: Vector2(100, 100)));
|
||||
add(Enemy(position: Vector2(200, 100)));
|
||||
add(Enemy(position: Vector2(300, 100)));
|
||||
}
|
||||
}
|
||||
@ -23,6 +23,7 @@ dependencies:
|
||||
google_fonts: ^2.3.2
|
||||
meta: ^1.8.0
|
||||
padracing: ^1.0.0
|
||||
provider: ^6.0.3
|
||||
rogue_shooter: ^0.1.0
|
||||
trex_game: ^0.1.0
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ export 'src/anchor.dart';
|
||||
export 'src/collisions/has_collision_detection.dart';
|
||||
export 'src/collisions/hitboxes/screen_hitbox.dart';
|
||||
export 'src/components/clip_component.dart';
|
||||
export 'src/components/components_notifier.dart';
|
||||
export 'src/components/core/component.dart';
|
||||
export 'src/components/core/component_set.dart';
|
||||
export 'src/components/core/position_type.dart';
|
||||
@ -21,6 +22,7 @@ export 'src/components/mixins/has_game_ref.dart' show HasGameRef;
|
||||
export 'src/components/mixins/has_paint.dart';
|
||||
export 'src/components/mixins/hoverable.dart';
|
||||
export 'src/components/mixins/keyboard_handler.dart';
|
||||
export 'src/components/mixins/notifier.dart';
|
||||
export 'src/components/mixins/parent_is_a.dart';
|
||||
export 'src/components/mixins/single_child_particle.dart';
|
||||
export 'src/components/mixins/tappable.dart';
|
||||
|
||||
49
packages/flame/lib/src/components/components_notifier.dart
Normal file
49
packages/flame/lib/src/components/components_notifier.dart
Normal file
@ -0,0 +1,49 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// A [ChangeNotifier] that notifies its listeners when a [Component] is
|
||||
/// added or removed, or updated. The meaning of an updated component
|
||||
/// will vary depending on the component implementation, this is something
|
||||
/// defined and executed by the component itself.
|
||||
///
|
||||
/// For example, in a Player component, that holds a health variable
|
||||
/// you may want to notify changes when that variable has changed.
|
||||
class ComponentsNotifier<T extends Component> extends ChangeNotifier {
|
||||
ComponentsNotifier(List<T> initial) : _components = initial;
|
||||
|
||||
final List<T> _components;
|
||||
|
||||
/// The list of components.
|
||||
List<T> get components => UnmodifiableListView(_components);
|
||||
|
||||
/// Returns a single element of the components on the game.
|
||||
///
|
||||
/// Returns null if there is no component.
|
||||
T? get single {
|
||||
if (_components.isNotEmpty) {
|
||||
return _components.first;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@internal
|
||||
bool applicable(Component component) => component is T;
|
||||
|
||||
@internal
|
||||
void add(T component) {
|
||||
_components.add(component);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@internal
|
||||
void remove(T component) {
|
||||
_components.remove(component);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@internal
|
||||
void notify() => notifyListeners();
|
||||
}
|
||||
49
packages/flame/lib/src/components/mixins/notifier.dart
Normal file
49
packages/flame/lib/src/components/mixins/notifier.dart
Normal file
@ -0,0 +1,49 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// Makes a component capable of notifying listeners of changes.
|
||||
///
|
||||
/// Notifier components will automatically notify when
|
||||
/// new instances are added or removed to the game instance.
|
||||
///
|
||||
/// To notify internal changes of a component instance, the component
|
||||
/// should call [notifyListeners].
|
||||
mixin Notifier on Component {
|
||||
FlameGame get _gameRef {
|
||||
final game = findGame();
|
||||
assert(
|
||||
game == null || game is FlameGame,
|
||||
"Notifier can't be used without FlameGame",
|
||||
);
|
||||
return game! as FlameGame;
|
||||
}
|
||||
|
||||
@override
|
||||
@mustCallSuper
|
||||
void onMount() {
|
||||
super.onMount();
|
||||
|
||||
_gameRef.propagateToApplicableNotifiers(this, (notifier) {
|
||||
notifier.add(this);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@mustCallSuper
|
||||
void onRemove() {
|
||||
_gameRef.propagateToApplicableNotifiers(this, (notifier) {
|
||||
notifier.remove(this);
|
||||
});
|
||||
|
||||
super.onRemove();
|
||||
}
|
||||
|
||||
/// When called, will notify listeners that a change happened on
|
||||
/// this component's class notifier.
|
||||
void notifyListeners() {
|
||||
_gameRef.propagateToApplicableNotifiers(this, (notifier) {
|
||||
notifier.notify();
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flame/src/components/core/component.dart';
|
||||
import 'package:flame/src/extensions/vector2.dart';
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/src/game/camera/camera.dart';
|
||||
import 'package:flame/src/game/camera/camera_wrapper.dart';
|
||||
import 'package:flame/src/game/game.dart';
|
||||
@ -30,6 +29,9 @@ class FlameGame extends Component with Game {
|
||||
|
||||
late final CameraWrapper _cameraWrapper;
|
||||
|
||||
@internal
|
||||
late final List<ComponentsNotifier> notifiers = [];
|
||||
|
||||
/// The camera translates the coordinate space after the viewport is applied.
|
||||
Camera get camera => _cameraWrapper.camera;
|
||||
|
||||
@ -150,4 +152,36 @@ class FlameGame extends Component with Game {
|
||||
|
||||
@override
|
||||
Projector get projector => camera.combinedProjector;
|
||||
|
||||
/// Returns a [ComponentsNotifier] for the given type [T].
|
||||
///
|
||||
/// This method handles duplications, so there will never be
|
||||
/// more than one [ComponentsNotifier] for a given type, meaning
|
||||
/// that this method can be called as many times as needed for a type.
|
||||
ComponentsNotifier<T> componentsNotifier<T extends Component>() {
|
||||
for (final notifier in notifiers) {
|
||||
if (notifier is ComponentsNotifier<T>) {
|
||||
return notifier;
|
||||
}
|
||||
}
|
||||
|
||||
final notifier = ComponentsNotifier<T>(
|
||||
descendants().whereType<T>().toList(),
|
||||
);
|
||||
notifiers.add(notifier);
|
||||
|
||||
return notifier;
|
||||
}
|
||||
|
||||
@internal
|
||||
void propagateToApplicableNotifiers(
|
||||
Component component,
|
||||
void Function(ComponentsNotifier) callback,
|
||||
) {
|
||||
for (final notifier in notifiers) {
|
||||
if (notifier.applicable(component)) {
|
||||
callback(notifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A widget that rebuilds every time the given [notifier] changes.
|
||||
class ComponentsNotifierBuilder<T extends Component> extends StatefulWidget {
|
||||
const ComponentsNotifierBuilder({
|
||||
super.key,
|
||||
required this.notifier,
|
||||
required this.builder,
|
||||
});
|
||||
|
||||
final ComponentsNotifier<T> notifier;
|
||||
final Widget Function(BuildContext, ComponentsNotifier<T>) builder;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _ComponentsNotifierBuilderState<T>();
|
||||
}
|
||||
}
|
||||
|
||||
class _ComponentsNotifierBuilderState<T extends Component>
|
||||
extends State<ComponentsNotifierBuilder<T>> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
widget.notifier.addListener(_listener);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.notifier.removeListener(_listener);
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _listener() {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) =>
|
||||
widget.builder(context, widget.notifier);
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
export 'src/anchor.dart';
|
||||
export 'src/widgets/animation_widget.dart';
|
||||
export 'src/widgets/components_notifier_builder.dart';
|
||||
export 'src/widgets/nine_tile_box.dart';
|
||||
export 'src/widgets/sprite_button.dart';
|
||||
export 'src/widgets/sprite_widget.dart';
|
||||
|
||||
94
packages/flame/test/components/components_notifier_test.dart
Normal file
94
packages/flame/test/components/components_notifier_test.dart
Normal file
@ -0,0 +1,94 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class Enemy extends PositionComponent with Notifier {}
|
||||
|
||||
void main() {
|
||||
group('ComponentsNotifier', () {
|
||||
testWithFlameGame('correctly have the initial value', (game) async {
|
||||
await game.ensureAdd(Enemy());
|
||||
await game.ensureAdd(Enemy());
|
||||
|
||||
final notifier = game.componentsNotifier<Enemy>();
|
||||
|
||||
expect(notifier.components.length, equals(2));
|
||||
});
|
||||
|
||||
testWithFlameGame('notifies when a component is added', (game) async {
|
||||
var called = 0;
|
||||
|
||||
game.componentsNotifier<Enemy>().addListener(() => called++);
|
||||
|
||||
await game.ensureAdd(Enemy());
|
||||
|
||||
expect(called, equals(1));
|
||||
});
|
||||
|
||||
testWithFlameGame('notifies when a component is added', (game) async {
|
||||
var called = 0;
|
||||
|
||||
final component = Enemy();
|
||||
await game.ensureAdd(component);
|
||||
|
||||
game.componentsNotifier<Enemy>().addListener(() => called++);
|
||||
|
||||
component.removeFromParent();
|
||||
await game.ready();
|
||||
|
||||
expect(called, equals(1));
|
||||
});
|
||||
|
||||
testWithFlameGame(
|
||||
'notifies when a component is manually notified',
|
||||
(game) async {
|
||||
var called = 0;
|
||||
|
||||
final component = Enemy();
|
||||
await game.ensureAdd(component);
|
||||
|
||||
game.componentsNotifier<Enemy>().addListener(() => called++);
|
||||
|
||||
component.notifyListeners();
|
||||
|
||||
expect(called, equals(1));
|
||||
},
|
||||
);
|
||||
|
||||
testWithFlameGame(
|
||||
'lazy initializes the notifier',
|
||||
(game) async {
|
||||
expect(game.notifiers, isEmpty);
|
||||
game.componentsNotifier<Enemy>();
|
||||
expect(game.notifiers, isNotEmpty);
|
||||
},
|
||||
);
|
||||
|
||||
testWithFlameGame(
|
||||
'do not add the same type',
|
||||
(game) async {
|
||||
expect(game.notifiers, isEmpty);
|
||||
game.componentsNotifier<Enemy>();
|
||||
game.componentsNotifier<Enemy>();
|
||||
expect(game.notifiers.length, equals(1));
|
||||
},
|
||||
);
|
||||
|
||||
testWithFlameGame('can listen to parent classes', (game) async {
|
||||
var parentCalled = 0;
|
||||
var called = 0;
|
||||
|
||||
game.componentsNotifier<PositionComponent>().addListener(
|
||||
() => parentCalled++,
|
||||
);
|
||||
game.componentsNotifier<Enemy>().addListener(() => called++);
|
||||
|
||||
expect(game.notifiers.length, equals(2));
|
||||
|
||||
await game.ensureAdd(Enemy());
|
||||
|
||||
expect(called, equals(1));
|
||||
expect(parentCalled, equals(1));
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
class Enemy extends PositionComponent with Notifier {}
|
||||
|
||||
void main() {
|
||||
group('ComponentsNotifierBuilder', () {
|
||||
testWidgets('renders the initial value', (tester) async {
|
||||
final notifier = ComponentsNotifier<Enemy>([Enemy()]);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: ComponentsNotifierBuilder(
|
||||
notifier: notifier,
|
||||
builder: (context, notifier) {
|
||||
return Text(
|
||||
'Enemies: ${notifier.components.length}',
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Enemies: 1'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('rebuilds when an enemy is added', (tester) async {
|
||||
final notifier = ComponentsNotifier<Enemy>([Enemy()]);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: ComponentsNotifierBuilder(
|
||||
notifier: notifier,
|
||||
builder: (context, notifier) {
|
||||
return Text(
|
||||
'Enemies: ${notifier.components.length}',
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
notifier.add(Enemy());
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Enemies: 2'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('rebuilds when an enemy is added', (tester) async {
|
||||
final enemy = Enemy();
|
||||
final notifier = ComponentsNotifier<Enemy>([enemy]);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: ComponentsNotifierBuilder(
|
||||
notifier: notifier,
|
||||
builder: (context, notifier) {
|
||||
return Text(
|
||||
'Enemies: ${notifier.components.length}',
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
notifier.remove(enemy);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.text('Enemies: 0'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user