feat: adding assets manager (#163)

pull/167/head
Erick 4 years ago committed by GitHub
parent d7216bbe6d
commit 5fe5fc007d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,27 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
part 'assets_manager_state.dart';
/// {@template assets_manager_cubit}
/// Cubit responsable for pre loading any game assets
/// {@endtemplate}
class AssetsManagerCubit extends Cubit<AssetsManagerState> {
/// {@macro assets_manager_cubit}
AssetsManagerCubit(List<Future> loadables)
: super(
AssetsManagerState.initial(
loadables: loadables,
),
);
/// Loads the assets
Future<void> load() async {
final all = state.loadables.map((loadable) async {
await loadable;
emit(state.copyWith(loaded: [...state.loaded, loadable]));
}).toList();
await Future.wait(all);
}
}

@ -0,0 +1,41 @@
part of 'assets_manager_cubit.dart';
/// {@template assets_manager_state}
/// State used to load the game assets
/// {@endtemplate}
class AssetsManagerState extends Equatable {
/// {@macro assets_manager_state}
const AssetsManagerState({
required this.loadables,
required this.loaded,
});
/// {@macro assets_manager_state}
const AssetsManagerState.initial({
required List<Future> loadables,
}) : this(loadables: loadables, loaded: const []);
/// List of futures to load
final List<Future> loadables;
/// List of loaded futures
final List<Future> loaded;
/// Returns a value between 0 and 1 to indicate the loading progress
double get progress => loaded.length / loadables.length;
/// Returns a copy of this instance with the given parameters
/// updated
AssetsManagerState copyWith({
List<Future>? loadables,
List<Future>? loaded,
}) {
return AssetsManagerState(
loadables: loadables ?? this.loadables,
loaded: loaded ?? this.loaded,
);
}
@override
List<Object> get props => [loaded, loadables];
}

@ -1,3 +1,4 @@
export 'assets_manager/cubit/assets_manager_cubit.dart';
export 'bloc/game_bloc.dart'; export 'bloc/game_bloc.dart';
export 'components/components.dart'; export 'components/components.dart';
export 'game_assets.dart'; export 'game_assets.dart';

@ -4,9 +4,9 @@ import 'package:pinball_components/pinball_components.dart' as components;
/// Add methods to help loading and caching game assets. /// Add methods to help loading and caching game assets.
extension PinballGameAssetsX on PinballGame { extension PinballGameAssetsX on PinballGame {
/// Pre load the initial assets of the game. /// Returns a list of assets to be loaded
Future<void> preLoadAssets() async { List<Future> preLoadAssets() {
await Future.wait([ return [
images.load(components.Assets.images.ball.keyName), images.load(components.Assets.images.ball.keyName),
images.load(components.Assets.images.flutterSignPost.keyName), images.load(components.Assets.images.flutterSignPost.keyName),
images.load(components.Assets.images.flipper.left.keyName), images.load(components.Assets.images.flipper.left.keyName),
@ -47,6 +47,6 @@ extension PinballGameAssetsX on PinballGame {
images.load(components.Assets.images.chromeDino.mouth.keyName), images.load(components.Assets.images.chromeDino.mouth.keyName),
images.load(components.Assets.images.chromeDino.head.keyName), images.load(components.Assets.images.chromeDino.head.keyName),
images.load(Assets.images.components.background.path), images.load(Assets.images.components.background.path),
]); ];
} }
} }

@ -9,16 +9,41 @@ import 'package:pinball_audio/pinball_audio.dart';
import 'package:pinball_theme/pinball_theme.dart'; import 'package:pinball_theme/pinball_theme.dart';
class PinballGamePage extends StatelessWidget { class PinballGamePage extends StatelessWidget {
const PinballGamePage({Key? key, required this.theme}) : super(key: key); const PinballGamePage({
Key? key,
required this.theme,
required this.game,
}) : super(key: key);
final PinballTheme theme; final PinballTheme theme;
final PinballGame game;
static Route route({required PinballTheme theme}) { static Route route({
required PinballTheme theme,
bool isDebugMode = kDebugMode,
}) {
return MaterialPageRoute<void>( return MaterialPageRoute<void>(
builder: (_) { builder: (context) {
return BlocProvider( final audio = context.read<PinballAudio>();
create: (_) => GameBloc(),
child: PinballGamePage(theme: theme), final game = isDebugMode
? DebugPinballGame(theme: theme, audio: audio)
: PinballGame(theme: theme, audio: audio);
final pinballAudio = context.read<PinballAudio>();
final loadables = [
...game.preLoadAssets(),
pinballAudio.load(),
];
return MultiBlocProvider(
providers: [
BlocProvider(create: (_) => GameBloc()),
BlocProvider(
create: (_) => AssetsManagerCubit(loadables)..load(),
),
],
child: PinballGamePage(theme: theme, game: game),
); );
}, },
); );
@ -26,51 +51,19 @@ class PinballGamePage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PinballGameView(theme: theme); return PinballGameView(theme: theme, game: game);
} }
} }
class PinballGameView extends StatefulWidget { class PinballGameView extends StatelessWidget {
const PinballGameView({ const PinballGameView({
Key? key, Key? key,
required this.theme, required this.theme,
bool isDebugMode = kDebugMode, required this.game,
}) : _isDebugMode = isDebugMode, }) : super(key: key);
super(key: key);
final PinballTheme theme; final PinballTheme theme;
final bool _isDebugMode; final PinballGame game;
@override
State<PinballGameView> createState() => _PinballGameViewState();
}
class _PinballGameViewState extends State<PinballGameView> {
late PinballGame _game;
@override
void initState() {
super.initState();
final audio = context.read<PinballAudio>();
_game = widget._isDebugMode
? DebugPinballGame(theme: widget.theme, audio: audio)
: PinballGame(theme: widget.theme, audio: audio);
// TODO(erickzanardo): Revisit this when we start to have more assets
// this could expose a Stream (maybe even a cubit?) so we could show the
// the loading progress with some fancy widgets.
_fetchAssets();
}
Future<void> _fetchAssets() async {
final pinballAudio = context.read<PinballAudio>();
await Future.wait([
_game.preLoadAssets(),
pinballAudio.load(),
]);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -84,13 +77,41 @@ class _PinballGameViewState extends State<PinballGameView> {
builder: (_) { builder: (_) {
return GameOverDialog( return GameOverDialog(
score: state.score, score: state.score,
theme: widget.theme.characterTheme, theme: theme.characterTheme,
); );
}, },
); );
} }
}, },
child: Stack( child: _GameView(game: game),
);
}
}
class _GameView extends StatelessWidget {
const _GameView({
Key? key,
required PinballGame game,
}) : _game = game,
super(key: key);
final PinballGame _game;
@override
Widget build(BuildContext context) {
final loadingProgress = context.watch<AssetsManagerCubit>().state.progress;
if (loadingProgress != 1) {
return Scaffold(
body: Center(
child: Text(
loadingProgress.toString(),
),
),
);
}
return Stack(
children: [ children: [
Positioned.fill( Positioned.fill(
child: GameWidget<PinballGame>(game: _game), child: GameWidget<PinballGame>(game: _game),
@ -101,7 +122,6 @@ class _PinballGameViewState extends State<PinballGameView> {
child: GameHud(), child: GameHud(),
), ),
], ],
),
); );
} }
} }

@ -0,0 +1,35 @@
import 'dart:async';
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
void main() {
group('AssetsManagerCubit', () {
final completer1 = Completer<void>();
final completer2 = Completer<void>();
final future1 = completer1.future;
final future2 = completer2.future;
blocTest<AssetsManagerCubit, AssetsManagerState>(
'emits the loaded on the order that they load',
build: () => AssetsManagerCubit([future1, future2]),
act: (cubit) {
cubit.load();
completer2.complete();
completer1.complete();
},
expect: () => [
AssetsManagerState(
loadables: [future1, future2],
loaded: [future2],
),
AssetsManagerState(
loadables: [future1, future2],
loaded: [future2, future1],
),
],
);
});
}

@ -0,0 +1,145 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
void main() {
group('AssetsManagerState', () {
test('can be instantiated', () {
expect(
AssetsManagerState(loadables: const [], loaded: const []),
isNotNull,
);
});
test('has the correct initial state', () {
final future = Future<void>.value();
expect(
AssetsManagerState.initial(loadables: [future]),
equals(
AssetsManagerState(
loadables: [future],
loaded: const [],
),
),
);
});
group('progress', () {
final future1 = Future<void>.value();
final future2 = Future<void>.value();
test('returns 0 when no future is loaded', () {
expect(
AssetsManagerState(
loadables: [future1, future2],
loaded: const [],
).progress,
equals(0),
);
});
test('returns the correct value when some of the futures are loaded', () {
expect(
AssetsManagerState(
loadables: [future1, future2],
loaded: [future1],
).progress,
equals(0.5),
);
});
test('returns the 1 when all futures are loaded', () {
expect(
AssetsManagerState(
loadables: [future1, future2],
loaded: [future1, future2],
).progress,
equals(1),
);
});
});
group('copyWith', () {
final future = Future<void>.value();
test('returns a copy with the updated loadables', () {
expect(
AssetsManagerState(
loadables: const [],
loaded: const [],
).copyWith(loadables: [future]),
equals(
AssetsManagerState(
loadables: [future],
loaded: const [],
),
),
);
});
test('returns a copy with the updated loaded', () {
expect(
AssetsManagerState(
loadables: const [],
loaded: const [],
).copyWith(loaded: [future]),
equals(
AssetsManagerState(
loadables: const [],
loaded: [future],
),
),
);
});
});
test('supports value comparison', () {
final future1 = Future<void>.value();
final future2 = Future<void>.value();
expect(
AssetsManagerState(
loadables: const [],
loaded: const [],
),
equals(
AssetsManagerState(
loadables: const [],
loaded: const [],
),
),
);
expect(
AssetsManagerState(
loadables: [future1],
loaded: const [],
),
isNot(
equals(
AssetsManagerState(
loadables: [future2],
loaded: const [],
),
),
),
);
expect(
AssetsManagerState(
loadables: const [],
loaded: [future1],
),
isNot(
equals(
AssetsManagerState(
loadables: const [],
loaded: [future2],
),
),
),
);
});
});
}

@ -11,6 +11,7 @@ import '../../helpers/helpers.dart';
void main() { void main() {
const theme = PinballTheme(characterTheme: DashTheme()); const theme = PinballTheme(characterTheme: DashTheme());
final game = PinballGameTest();
group('PinballGamePage', () { group('PinballGamePage', () {
testWidgets('renders PinballGameView', (tester) async { testWidgets('renders PinballGameView', (tester) async {
@ -22,21 +23,72 @@ void main() {
); );
await tester.pumpApp( await tester.pumpApp(
PinballGamePage(theme: theme), PinballGamePage(theme: theme, game: game),
gameBloc: gameBloc, gameBloc: gameBloc,
); );
expect(find.byType(PinballGameView), findsOneWidget); expect(find.byType(PinballGameView), findsOneWidget);
}); });
testWidgets('route returns a valid navigation route', (tester) async { testWidgets(
'renders the loading indicator while the assets load',
(tester) async {
final gameBloc = MockGameBloc();
whenListen(
gameBloc,
Stream.value(const GameState.initial()),
initialState: const GameState.initial(),
);
final assetsManagerCubit = MockAssetsManagerCubit();
final initialAssetsState = AssetsManagerState(
loadables: [Future<void>.value()],
loaded: const [],
);
whenListen(
assetsManagerCubit,
Stream.value(initialAssetsState),
initialState: initialAssetsState,
);
await tester.pumpApp(
PinballGamePage(theme: theme, game: game),
gameBloc: gameBloc,
assetsManagerCubit: assetsManagerCubit,
);
expect(find.text('0.0'), findsOneWidget);
final loadedAssetsState = AssetsManagerState(
loadables: [Future<void>.value()],
loaded: [Future<void>.value()],
);
whenListen(
assetsManagerCubit,
Stream.value(loadedAssetsState),
initialState: loadedAssetsState,
);
await tester.pump();
expect(find.byType(PinballGameView), findsOneWidget);
},
);
group('route', () {
Future<void> pumpRoute({
required WidgetTester tester,
required bool isDebugMode,
}) async {
await tester.pumpApp( await tester.pumpApp(
Scaffold( Scaffold(
body: Builder( body: Builder(
builder: (context) { builder: (context) {
return ElevatedButton( return ElevatedButton(
onPressed: () { onPressed: () {
Navigator.of(context) Navigator.of(context).push<void>(
.push<void>(PinballGamePage.route(theme: theme)); PinballGamePage.route(
theme: theme,
isDebugMode: isDebugMode,
),
);
}, },
child: const Text('Tap me'), child: const Text('Tap me'),
); );
@ -51,8 +103,27 @@ void main() {
// which is an infinity animation, so it will timeout // which is an infinity animation, so it will timeout
await tester.pump(); // Runs the button action await tester.pump(); // Runs the button action
await tester.pump(); // Runs the navigation await tester.pump(); // Runs the navigation
}
expect(find.byType(PinballGamePage), findsOneWidget); testWidgets('route creates the correct non debug game', (tester) async {
await pumpRoute(tester: tester, isDebugMode: false);
expect(
find.byWidgetPredicate(
(w) => w is PinballGameView && w.game is! DebugPinballGame,
),
findsOneWidget,
);
});
testWidgets('route creates the correct debug game', (tester) async {
await pumpRoute(tester: tester, isDebugMode: true);
expect(
find.byWidgetPredicate(
(w) => w is PinballGameView && w.game is DebugPinballGame,
),
findsOneWidget,
);
});
}); });
}); });
@ -66,7 +137,7 @@ void main() {
); );
await tester.pumpApp( await tester.pumpApp(
PinballGameView(theme: theme), PinballGameView(theme: theme, game: game),
gameBloc: gameBloc, gameBloc: gameBloc,
); );
@ -99,7 +170,7 @@ void main() {
); );
await tester.pumpApp( await tester.pumpApp(
const PinballGameView(theme: theme), PinballGameView(theme: theme, game: game),
gameBloc: gameBloc, gameBloc: gameBloc,
); );
await tester.pump(); await tester.pump();
@ -107,45 +178,5 @@ void main() {
expect(find.byType(GameOverDialog), findsOneWidget); expect(find.byType(GameOverDialog), findsOneWidget);
}, },
); );
testWidgets('renders the real game when not in debug mode', (tester) async {
final gameBloc = MockGameBloc();
whenListen(
gameBloc,
Stream.value(const GameState.initial()),
initialState: const GameState.initial(),
);
await tester.pumpApp(
const PinballGameView(theme: theme, isDebugMode: false),
gameBloc: gameBloc,
);
expect(
find.byWidgetPredicate(
(w) => w is GameWidget<PinballGame> && w.game is! DebugPinballGame,
),
findsOneWidget,
);
});
testWidgets('renders the debug game when on debug mode', (tester) async {
final gameBloc = MockGameBloc();
whenListen(
gameBloc,
Stream.value(const GameState.initial()),
initialState: const GameState.initial(),
);
await tester.pumpApp(
const PinballGameView(theme: theme),
gameBloc: gameBloc,
);
expect(
find.byWidgetPredicate(
(w) => w is GameWidget<PinballGame> && w.game is DebugPinballGame,
),
findsOneWidget,
);
});
}); });
} }

@ -74,3 +74,5 @@ class MockComponentSet extends Mock implements ComponentSet {}
class MockDashNestBumper extends Mock implements DashNestBumper {} class MockDashNestBumper extends Mock implements DashNestBumper {}
class MockPinballAudio extends Mock implements PinballAudio {} class MockPinballAudio extends Mock implements PinballAudio {}
class MockAssetsManagerCubit extends Mock implements AssetsManagerCubit {}

@ -5,6 +5,7 @@
// license that can be found in the LICENSE file or at // license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT. // https://opensource.org/licenses/MIT.
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
@ -26,11 +27,31 @@ PinballAudio _buildDefaultPinballAudio() {
return audio; return audio;
} }
MockAssetsManagerCubit _buildDefaultAssetsManagerCubit() {
final cubit = MockAssetsManagerCubit();
final state = AssetsManagerState(
loadables: [Future<void>.value()],
loaded: [
Future<void>.value(),
],
);
whenListen(
cubit,
Stream.value(state),
initialState: state,
);
return cubit;
}
extension PumpApp on WidgetTester { extension PumpApp on WidgetTester {
Future<void> pumpApp( Future<void> pumpApp(
Widget widget, { Widget widget, {
MockNavigator? navigator, MockNavigator? navigator,
GameBloc? gameBloc, GameBloc? gameBloc,
AssetsManagerCubit? assetsManagerCubit,
ThemeCubit? themeCubit, ThemeCubit? themeCubit,
LeaderboardRepository? leaderboardRepository, LeaderboardRepository? leaderboardRepository,
PinballAudio? pinballAudio, PinballAudio? pinballAudio,
@ -54,6 +75,9 @@ extension PumpApp on WidgetTester {
BlocProvider.value( BlocProvider.value(
value: gameBloc ?? MockGameBloc(), value: gameBloc ?? MockGameBloc(),
), ),
BlocProvider.value(
value: assetsManagerCubit ?? _buildDefaultAssetsManagerCubit(),
),
], ],
child: MaterialApp( child: MaterialApp(
localizationsDelegates: const [ localizationsDelegates: const [

Loading…
Cancel
Save