Skip to content

Commit

Permalink
feat: create top level challenge bloc to track the challenge progress (
Browse files Browse the repository at this point in the history
…#230)

* feat: rename welcome bloc to challenge bloc

* feat: convert board info retrieval to stream

* test: board info repo
  • Loading branch information
jsgalarraga authored Apr 9, 2024
1 parent 4ef2cac commit 6042856
Show file tree
Hide file tree
Showing 23 changed files with 220 additions and 189 deletions.
22 changes: 16 additions & 6 deletions lib/app/view/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:crossword_repository/crossword_repository.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:game_domain/game_domain.dart';
import 'package:io_crossword/challenge/challenge.dart';
import 'package:io_crossword/crossword/crossword.dart';
import 'package:io_crossword/game_intro/game_intro.dart';
import 'package:io_crossword/l10n/l10n.dart';
Expand Down Expand Up @@ -37,12 +38,21 @@ class App extends StatelessWidget {
Provider.value(value: crosswordRepository),
Provider.value(value: boardInfoRepository),
],
child: BlocProvider(
create: (_) => CrosswordBloc(
crosswordRepository: crosswordRepository,
boardInfoRepository: boardInfoRepository,
crosswordResource: crosswordResource,
),
child: MultiBlocProvider(
providers: [
BlocProvider(
create: (_) => CrosswordBloc(
crosswordRepository: crosswordRepository,
boardInfoRepository: boardInfoRepository,
crosswordResource: crosswordResource,
),
),
BlocProvider(
create: (context) => ChallengeBloc(
boardInfoRepository: context.read(),
)..add(const ChallengeDataRequested()),
),
],
child: const AppView(),
),
);
Expand Down
40 changes: 40 additions & 0 deletions lib/challenge/bloc/challenge_bloc.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import 'package:bloc/bloc.dart';
import 'package:board_info_repository/board_info_repository.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';

part 'challenge_event.dart';
part 'challenge_state.dart';

class ChallengeBloc extends Bloc<ChallengeEvent, ChallengeState> {
ChallengeBloc({
required BoardInfoRepository boardInfoRepository,
}) : _boardInfoRepository = boardInfoRepository,
super(const ChallengeState.initial()) {
on<ChallengeDataRequested>(_onDataRequested);
}

final BoardInfoRepository _boardInfoRepository;

Future<void> _onDataRequested(
ChallengeDataRequested event,
Emitter<ChallengeState> emit,
) async {
return emit.forEach(
Rx.combineLatest2(
_boardInfoRepository.getSolvedWordsCount(),
_boardInfoRepository.getTotalWordsCount(),
(solved, total) => state.copyWith(
solvedWords: solved,
totalWords: total,
),
),
onData: (state) => state,
onError: (error, stackTrace) {
addError(error, stackTrace);
return state;
},
);
}
}
13 changes: 13 additions & 0 deletions lib/challenge/bloc/challenge_event.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
part of 'challenge_bloc.dart';

sealed class ChallengeEvent extends Equatable {
const ChallengeEvent();

@override
List<Object> get props => [];
}

/// Requests the data needed for the challenge progress.
class ChallengeDataRequested extends ChallengeEvent {
const ChallengeDataRequested();
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
part of 'welcome_bloc.dart';
part of 'challenge_bloc.dart';

class WelcomeState extends Equatable {
const WelcomeState({
class ChallengeState extends Equatable {
const ChallengeState({
required this.solvedWords,
required this.totalWords,
});

/// Creates a [WelcomeState] with the initial status.
const WelcomeState.initial({
/// Creates a [ChallengeState] with the initial status.
const ChallengeState.initial({
this.solvedWords = fallbackSolvedWords,
this.totalWords = fallbackTotalWords,
});
Expand All @@ -30,11 +30,11 @@ class WelcomeState extends Equatable {
/// default to [fallbackTotalWords].
final int totalWords;

WelcomeState copyWith({
ChallengeState copyWith({
int? solvedWords,
int? totalWords,
}) {
return WelcomeState(
return ChallengeState(
solvedWords: solvedWords ?? this.solvedWords,
totalWords: totalWords ?? this.totalWords,
);
Expand Down
1 change: 1 addition & 0 deletions lib/challenge/challenge.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export 'bloc/challenge_bloc.dart';
2 changes: 1 addition & 1 deletion lib/game_intro/view/game_intro_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import 'package:io_crossword/about/about.dart';
import 'package:io_crossword/crossword/crossword.dart';
import 'package:io_crossword/game_intro/game_intro.dart';
import 'package:io_crossword/initials/view/initials_page.dart';
import 'package:io_crossword/welcome/view/welcome_page.dart';
import 'package:io_crossword/welcome/welcome.dart';

class GameIntroPage extends StatelessWidget {
const GameIntroPage({super.key});
Expand Down
39 changes: 0 additions & 39 deletions lib/welcome/bloc/welcome_bloc.dart

This file was deleted.

13 changes: 0 additions & 13 deletions lib/welcome/bloc/welcome_event.dart

This file was deleted.

10 changes: 3 additions & 7 deletions lib/welcome/view/welcome_page.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:flow_builder/flow_builder.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:io_crossword/challenge/challenge.dart';
import 'package:io_crossword/game_intro/game_intro.dart';
import 'package:io_crossword/l10n/l10n.dart';
import 'package:io_crossword/welcome/welcome.dart';
Expand All @@ -15,12 +16,7 @@ class WelcomePage extends StatelessWidget {

@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => WelcomeBloc(
boardInfoRepository: context.read(),
)..add(const WelcomeDataRequested()),
child: const WelcomeView(),
);
return const WelcomeView();
}
}

Expand Down Expand Up @@ -69,7 +65,7 @@ class WelcomeView extends StatelessWidget {
textAlign: TextAlign.center,
),
const SizedBox(height: 48),
BlocSelector<WelcomeBloc, WelcomeState, (int, int)>(
BlocSelector<ChallengeBloc, ChallengeState, (int, int)>(
selector: (state) => (state.solvedWords, state.totalWords),
builder: (context, words) {
return ChallengeProgress(
Expand Down
1 change: 0 additions & 1 deletion lib/welcome/welcome.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,5 @@
/// of all the players.
library;

export 'bloc/welcome_bloc.dart';
export 'view/view.dart';
export 'widgets/widgets.dart';
4 changes: 2 additions & 2 deletions lib/welcome/widgets/welcome_header_image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import 'package:io_crossword/assets/assets.gen.dart';
import 'package:io_crossword/welcome/welcome.dart';

/// {@template welcome_header_image}
/// An image that is displayed at the top of the [WelcomePage].
/// An image that is displayed at the top of the [WelcomeView].
///
/// See also:
///
/// * [WelcomePage], a page that displays the [WelcomeHeaderImage]
/// * [WelcomeView], a page that displays the [WelcomeHeaderImage]
/// on smaller screens, such as mobile devices.
/// {@endtemplate}
class WelcomeHeaderImage extends StatelessWidget
Expand Down
20 changes: 8 additions & 12 deletions packages/board_info_repository/lib/src/board_info_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,28 +35,24 @@ class BoardInfoRepository {
late final CollectionReference<Map<String, dynamic>> boardInfoCollection;

/// Returns the total words count available in the crossword.
Future<int> getTotalWordsCount() async {
Stream<int> getTotalWordsCount() {
try {
final results = await boardInfoCollection
return boardInfoCollection
.where('type', isEqualTo: 'total_words_count')
.get();

final data = results.docs.first.data();
return data['value'] as int;
.snapshots()
.map((event) => event.docs.first.data()['value'] as int);
} catch (error, stackStrace) {
throw BoardInfoException(error, stackStrace);
}
}

/// Returns the solved words count in the crossword.
Future<int> getSolvedWordsCount() async {
Stream<int> getSolvedWordsCount() {
try {
final results = await boardInfoCollection
return boardInfoCollection
.where('type', isEqualTo: 'solved_words_count')
.get();

final data = results.docs.first.data();
return data['value'] as int;
.snapshots()
.map((event) => event.docs.first.data()['value'] as int);
} catch (error, stackStrace) {
throw BoardInfoException(error, stackStrace);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ void main() {
).thenReturn(collection);

when(collection.get).thenAnswer((_) async => query);
when(collection.snapshots).thenAnswer((_) => Stream.value(query));
when(() => query.docs).thenReturn([doc]);
when(doc.data).thenReturn({'value': value});
}
Expand All @@ -61,8 +62,8 @@ void main() {
group('getTotalWordsCount', () {
test('returns total words count from firebase', () async {
mockQueryResult(123000);
final result = await boardInfoRepository.getTotalWordsCount();
expect(result, equals(123000));
final result = boardInfoRepository.getTotalWordsCount();
expect(result, emits(123000));
});

test('throws BoardInfoException when fetching the info fails', () {
Expand All @@ -79,8 +80,8 @@ void main() {
group('getSolvedWordsCount', () {
test('returns solved words count from firebase', () async {
mockQueryResult(66000);
final result = await boardInfoRepository.getSolvedWordsCount();
expect(result, equals(66000));
final result = boardInfoRepository.getSolvedWordsCount();
expect(result, emits(66000));
});

test('throws BoardInfoException when fetching the info fails', () {
Expand Down
8 changes: 8 additions & 0 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
rxdart:
dependency: "direct main"
description:
name: rxdart
sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb"
url: "https://pub.dev"
source: hosted
version: "0.27.7"
shelf:
dependency: transitive
description:
Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ dependencies:
io_crossword_ui:
path: packages/io_crossword_ui
provider: ^6.1.2
rxdart: ^0.27.7
url_launcher: ^6.2.5

dev_dependencies:
Expand Down
4 changes: 2 additions & 2 deletions test/app/view/app_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ void main() {
() => crosswordRepository.watchSectionFromPosition(0, 0),
).thenAnswer((_) => Stream.value(null));
when(boardInfoRepository.getSolvedWordsCount)
.thenAnswer((_) => Future.value(25));
.thenAnswer((_) => Stream.value(25));
when(boardInfoRepository.getTotalWordsCount)
.thenAnswer((_) => Future.value(100));
.thenAnswer((_) => Stream.value(100));
when(boardInfoRepository.getSectionSize)
.thenAnswer((_) => Future.value(20));
when(boardInfoRepository.getZoomLimit)
Expand Down
53 changes: 53 additions & 0 deletions test/challenge/bloc/challenge_bloc_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import 'package:bloc_test/bloc_test.dart';
import 'package:board_info_repository/board_info_repository.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:io_crossword/challenge/challenge.dart';
import 'package:mocktail/mocktail.dart';

class _MockBoardInfoRepository extends Mock implements BoardInfoRepository {}

void main() {
group('$ChallengeBloc', () {
late BoardInfoRepository boardInfoRepository;

setUp(() {
boardInfoRepository = _MockBoardInfoRepository();
});

test('initial state is ChallengeState.initial', () {
expect(
ChallengeBloc(boardInfoRepository: boardInfoRepository).state,
equals(const ChallengeState.initial()),
);
});

blocTest<ChallengeBloc, ChallengeState>(
'remains the same when retrieval fails',
seed: () => const ChallengeState(solvedWords: 1, totalWords: 2),
build: () => ChallengeBloc(boardInfoRepository: boardInfoRepository),
act: (bloc) {
when(() => boardInfoRepository.getSolvedWordsCount())
.thenAnswer((_) => Stream.error(Exception()));
when(() => boardInfoRepository.getTotalWordsCount())
.thenAnswer((_) => Stream.error(Exception()));
bloc.add(const ChallengeDataRequested());
},
expect: () => <ChallengeState>[],
);

blocTest<ChallengeBloc, ChallengeState>(
'emits ChallengeState with updated values when data is requested',
build: () => ChallengeBloc(boardInfoRepository: boardInfoRepository),
act: (bloc) {
when(() => boardInfoRepository.getSolvedWordsCount())
.thenAnswer((_) => Stream.value(1));
when(() => boardInfoRepository.getTotalWordsCount())
.thenAnswer((_) => Stream.value(2));
bloc.add(const ChallengeDataRequested());
},
expect: () => <ChallengeState>[
const ChallengeState(solvedWords: 1, totalWords: 2),
],
);
});
}
Loading

0 comments on commit 6042856

Please sign in to comment.