Skip to content

Commit 04ba026

Browse files
committed
Add rocket list screen
1 parent 7dc6585 commit 04ba026

31 files changed

+824
-4
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,4 @@ jobs:
133133
flutter test integration_test/launches_mock_test.dart --flavor dev
134134
flutter test integration_test/launches_test.dart --flavor dev
135135
flutter test integration_test/settings_test.dart --flavor dev
136+
flutter test integration_test/rockets_screen_integration_test.dart --flavor dev
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_bloc_app_template/features/rockets/bloc/rockets_bloc.dart';
3+
import 'package:flutter_bloc_app_template/features/rockets/rockets_screen.dart';
4+
import 'package:flutter_bloc_app_template/features/rockets/widget/rocket_item/rocket_item.dart';
5+
import 'package:flutter_test/flutter_test.dart';
6+
import 'package:integration_test/integration_test.dart';
7+
import 'package:mocktail/mocktail.dart';
8+
9+
import '../test/bloc/utils.dart';
10+
import '../test/features/rockets/rockets_screen_test.dart';
11+
import '../test/repository/rocket_repository_test.dart';
12+
13+
void main() {
14+
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
15+
16+
late RocketsBloc rocketsBloc;
17+
18+
setUp(() {
19+
rocketsBloc = MockRocketsBloc();
20+
});
21+
22+
group('Rockets Screen Tests', () {
23+
testWidgets(
24+
'renders CircularProgressIndicator '
25+
'when rocket list state is initial', (tester) async {
26+
when(() => rocketsBloc.state).thenReturn(const RocketsLoadingState());
27+
28+
await tester.pumpLocalizedWidgetWithBloc<RocketsBloc>(
29+
bloc: rocketsBloc,
30+
child: const RocketsBlocContent(),
31+
locale: const Locale('en'),
32+
);
33+
34+
await tester.pump();
35+
36+
expect(find.byType(CircularProgressIndicator), findsOneWidget);
37+
});
38+
39+
testWidgets(
40+
'renders Empty list text '
41+
'when rocket list state is success but with 0 items', (tester) async {
42+
when(() => rocketsBloc.state).thenReturn(const RocketsEmptyState());
43+
await tester.pumpLocalizedWidgetWithBloc<RocketsBloc>(
44+
bloc: rocketsBloc,
45+
child: const RocketsBlocContent(),
46+
locale: const Locale('en'),
47+
);
48+
await tester.pumpAndSettle();
49+
50+
expect(find.text('Empty list'), findsOneWidget);
51+
});
52+
53+
testWidgets('renders 1 item', (tester) async {
54+
when(() => rocketsBloc.state).thenReturn(
55+
RocketsSuccessState(
56+
rockets: [
57+
getRocketResource(),
58+
],
59+
),
60+
);
61+
await tester.pumpLocalizedWidgetWithBloc<RocketsBloc>(
62+
bloc: rocketsBloc,
63+
child: const RocketsBlocContent(),
64+
locale: const Locale('en'),
65+
);
66+
await tester.pumpAndSettle();
67+
68+
expect(find.byType(RocketItemWidget), findsNWidgets(1));
69+
});
70+
71+
testWidgets('renders error text', (tester) async {
72+
when(() => rocketsBloc.state).thenReturn(const RocketsErrorState());
73+
await tester.pumpLocalizedWidgetWithBloc<RocketsBloc>(
74+
bloc: rocketsBloc,
75+
child: const RocketsBlocContent(),
76+
locale: const Locale('en'),
77+
);
78+
await tester.pumpAndSettle();
79+
80+
expect(find.text('Try Again'), findsOneWidget);
81+
});
82+
});
83+
}

lib/di/app_bloc_providers.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import 'package:flutter_bloc_app_template/bloc/email_list/email_list_bloc.dart';
33
import 'package:flutter_bloc_app_template/bloc/init/init_bloc.dart';
44
import 'package:flutter_bloc_app_template/bloc/theme/theme_cubit.dart';
55
import 'package:flutter_bloc_app_template/features/launches/bloc/launches_bloc.dart';
6+
import 'package:flutter_bloc_app_template/features/rockets/bloc/rockets_bloc.dart';
67
import 'package:flutter_bloc_app_template/repository/email_list_repository.dart';
78
import 'package:flutter_bloc_app_template/repository/launches_repository.dart';
9+
import 'package:flutter_bloc_app_template/repository/rocket_repository.dart';
810
import 'package:flutter_bloc_app_template/repository/theme_repository.dart';
911
import 'package:provider/single_child_widget.dart' show SingleChildWidget;
1012

@@ -32,6 +34,13 @@ abstract class AppBlocProviders {
3234
const LaunchesEvent.load(),
3335
),
3436
),
37+
BlocProvider(
38+
create: (context) => RocketsBloc(
39+
RepositoryProvider.of<RocketRepository>(context),
40+
)..add(
41+
const RocketsEvent.load(),
42+
),
43+
),
3544
BlocProvider<InitBloc>(
3645
create: (_) => InitBloc()
3746
..add(

lib/di/app_repository_providers.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:flutter_bloc/flutter_bloc.dart';
22
import 'package:flutter_bloc_app_template/repository/email_list_repository.dart';
33
import 'package:flutter_bloc_app_template/repository/launches_repository.dart';
4+
import 'package:flutter_bloc_app_template/repository/rocket_repository.dart';
45
import 'package:flutter_bloc_app_template/routes/router.dart';
56
import 'package:provider/single_child_widget.dart' show SingleChildWidget;
67

@@ -18,6 +19,9 @@ abstract class AppRepositoryProviders {
1819
RepositoryProvider<LaunchesRepository>(
1920
create: (context) => diContainer.get<LaunchesRepository>(),
2021
),
22+
RepositoryProvider<RocketRepository>(
23+
create: (context) => diContainer.get<RocketRepository>(),
24+
),
2125
];
2226
}
2327
}

lib/di/di_initializer.config.dart

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/di/di_network_module.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import 'package:dio/dio.dart';
22
import 'package:flutter_bloc_app_template/data/network/data_source/launches_network_data_source.dart';
3+
import 'package:flutter_bloc_app_template/data/network/data_source/rocket_network_data_source.dart';
34
import 'package:flutter_bloc_app_template/data/network/service/launch/launch_service.dart';
5+
import 'package:flutter_bloc_app_template/data/network/service/rocket/rocket_service.dart';
46
import 'package:injectable/injectable.dart';
57
import 'package:talker_dio_logger/talker_dio_logger_interceptor.dart';
68
import 'package:talker_dio_logger/talker_dio_logger_settings.dart';
@@ -27,8 +29,18 @@ abstract class NetworkModule {
2729
return LaunchService(dio);
2830
}
2931

32+
@factoryMethod
33+
RocketService provideRocketService(Dio dio) {
34+
return RocketService(dio);
35+
}
36+
3037
@factoryMethod
3138
LaunchesDataSource provideLaunchesDataSource(LaunchService service) {
3239
return LaunchesNetworkDataSource(service);
3340
}
41+
42+
@factoryMethod
43+
RocketDataSource provideRocketDataSource(RocketService service) {
44+
return RocketNetworkDataSource(service);
45+
}
3446
}

lib/di/di_repository_module.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import 'package:flutter_bloc_app_template/data/network/data_source/launches_network_data_source.dart';
2+
import 'package:flutter_bloc_app_template/data/network/data_source/rocket_network_data_source.dart';
23
import 'package:flutter_bloc_app_template/data/theme_storage.dart';
34
import 'package:flutter_bloc_app_template/repository/launches_repository.dart';
5+
import 'package:flutter_bloc_app_template/repository/rocket_repository.dart';
46
import 'package:flutter_bloc_app_template/repository/theme_repository.dart';
57
import 'package:injectable/injectable.dart';
68

@@ -13,4 +15,8 @@ abstract class RepositoryModule {
1315
@factoryMethod
1416
LaunchesRepository provideLaunchesRepository(LaunchesDataSource dataSource) =>
1517
LaunchesRepositoryImpl(dataSource);
18+
19+
@factoryMethod
20+
RocketRepository provideRocketRepository(RocketDataSource dataSource) =>
21+
RocketRepositoryImpl(dataSource);
1622
}

lib/features/main/navigation/destinations.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:flutter/material.dart';
22
import 'package:flutter_bloc_app_template/features/launches/launches_screen.dart';
3+
import 'package:flutter_bloc_app_template/features/rockets/rockets_screen.dart';
34
import 'package:flutter_bloc_app_template/index.dart';
45

56
List<NavDestination> getDestinations(BuildContext context) {
@@ -15,7 +16,7 @@ List<NavDestination> getDestinations(BuildContext context) {
1516
label: context.rocketsTab,
1617
icon: const Icon(Icons.rocket_outlined),
1718
selectedIcon: const Icon(Icons.rocket),
18-
screen: Container(),
19+
screen: const RocketsScreen(),
1920
key: const Key('rockets'),
2021
),
2122
NavDestination(
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_bloc/flutter_bloc.dart';
3+
import 'package:flutter_bloc_app_template/features/rockets/bloc/rockets_bloc.dart';
4+
import 'package:flutter_bloc_app_template/features/rockets/widget/rocket_item/rocket_item.dart';
5+
import 'package:flutter_bloc_app_template/generated/l10n.dart';
6+
import 'package:flutter_bloc_app_template/index.dart';
7+
8+
class RocketsScreen extends StatelessWidget {
9+
const RocketsScreen({super.key});
10+
11+
@override
12+
Widget build(BuildContext context) => Scaffold(
13+
appBar: AppBar(
14+
title: Text(context.rocketsTitle),
15+
),
16+
body: RefreshIndicator(
17+
onRefresh: () async {
18+
await Future<void>.delayed(const Duration(seconds: 1));
19+
},
20+
child: SizedBox(
21+
height: MediaQuery.of(context).size.height,
22+
child: const RocketsBlocContent(),
23+
),
24+
),
25+
);
26+
}
27+
28+
class RocketsBlocContent extends StatelessWidget {
29+
const RocketsBlocContent({super.key});
30+
31+
@override
32+
Widget build(BuildContext context) => BlocBuilder<RocketsBloc, RocketsState>(
33+
builder: (context, state) {
34+
switch (state) {
35+
case RocketsLoadingState _:
36+
return const LoadingContent();
37+
case RocketsSuccessState _:
38+
return RocketListContent(rockets: state.rockets);
39+
case RocketsErrorState _:
40+
return ErrorContent(
41+
onTryAgainClick: () {
42+
context.read<RocketsBloc>().add(const RocketsEvent.load());
43+
},
44+
);
45+
case RocketsEmptyState _:
46+
return EmptyContent(
47+
content: S.of(context).emptyList,
48+
);
49+
}
50+
51+
return EmptyWidget();
52+
},
53+
);
54+
}
55+
56+
class RocketListContent extends StatelessWidget {
57+
const RocketListContent({super.key, required this.rockets});
58+
59+
final List<RocketResource> rockets;
60+
61+
@override
62+
Widget build(BuildContext context) => ListView.builder(
63+
physics: const BouncingScrollPhysics(),
64+
padding: EdgeInsets.zero,
65+
shrinkWrap: true,
66+
primary: false,
67+
itemBuilder: (context, index) => RocketItemWidget(
68+
key: Key('${rockets[index].rocketName}${rockets[index].rocketType}'),
69+
rocket: rockets[index],
70+
onClick: (launch) {
71+
//todo
72+
},
73+
),
74+
itemCount: rockets.length,
75+
);
76+
}

0 commit comments

Comments
 (0)