From 722013eb7e38ec0666b1d780dd95ab03ad4e1574 Mon Sep 17 00:00:00 2001 From: Nino Gjoni <49320058+ngjoni@users.noreply.github.com> Date: Tue, 16 Nov 2021 15:08:56 +0100 Subject: [PATCH 01/26] feat: added launch repository and launch model --- packages/launch_repository/.gitignore | 7 ++ packages/launch_repository/README.md | 11 +++ .../launch_repository/analysis_options.yaml | 1 + .../lib/launch_repository.dart | 3 + .../lib/src/launch_repository.dart | 20 +++++ packages/launch_repository/pubspec.yaml | 17 ++++ .../test/src/launch_repository_test.dart | 11 +++ .../lib/src/models/crew_member.g.dart | 5 +- .../spacex_api/lib/src/models/launch.dart | 77 +++++++++++++++++++ .../spacex_api/lib/src/models/launch.g.dart | 41 ++++++++++ .../spacex_api/lib/src/models/rocket.g.dart | 2 +- 11 files changed, 191 insertions(+), 4 deletions(-) create mode 100644 packages/launch_repository/.gitignore create mode 100644 packages/launch_repository/README.md create mode 100644 packages/launch_repository/analysis_options.yaml create mode 100644 packages/launch_repository/lib/launch_repository.dart create mode 100644 packages/launch_repository/lib/src/launch_repository.dart create mode 100644 packages/launch_repository/pubspec.yaml create mode 100644 packages/launch_repository/test/src/launch_repository_test.dart create mode 100644 packages/spacex_api/lib/src/models/launch.dart create mode 100644 packages/spacex_api/lib/src/models/launch.g.dart diff --git a/packages/launch_repository/.gitignore b/packages/launch_repository/.gitignore new file mode 100644 index 0000000..526da15 --- /dev/null +++ b/packages/launch_repository/.gitignore @@ -0,0 +1,7 @@ +# See https://www.dartlang.org/guides/libraries/private-files + +# Files and directories created by pub +.dart_tool/ +.packages +build/ +pubspec.lock \ No newline at end of file diff --git a/packages/launch_repository/README.md b/packages/launch_repository/README.md new file mode 100644 index 0000000..eb7ef81 --- /dev/null +++ b/packages/launch_repository/README.md @@ -0,0 +1,11 @@ +# launch_repository + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] +[![License: MIT][license_badge]][license_link] + +A Dart package to manage the launches + +[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg +[license_link]: https://opensource.org/licenses/MIT +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis diff --git a/packages/launch_repository/analysis_options.yaml b/packages/launch_repository/analysis_options.yaml new file mode 100644 index 0000000..3742fc3 --- /dev/null +++ b/packages/launch_repository/analysis_options.yaml @@ -0,0 +1 @@ +include: package:very_good_analysis/analysis_options.2.4.0.yaml \ No newline at end of file diff --git a/packages/launch_repository/lib/launch_repository.dart b/packages/launch_repository/lib/launch_repository.dart new file mode 100644 index 0000000..9e320bd --- /dev/null +++ b/packages/launch_repository/lib/launch_repository.dart @@ -0,0 +1,3 @@ +library launch_repository; + +export 'src/launch_repository.dart'; diff --git a/packages/launch_repository/lib/src/launch_repository.dart b/packages/launch_repository/lib/src/launch_repository.dart new file mode 100644 index 0000000..e169a50 --- /dev/null +++ b/packages/launch_repository/lib/src/launch_repository.dart @@ -0,0 +1,20 @@ +import 'package:spacex_api/spacex_api.dart'; + +///Thrown when an error occurs while looking up for launches +class LaunchException implements Exception {} + +/// {@template launches_repository} +/// A Dart package to manage the launches +/// {@endtemplate} +class LaunchRepository { + /// {@macro launches_repository} + LaunchRepository({SpaceXApiClient? spaceXApiClient}) + : _spaceXApiClient = spaceXApiClient ?? SpaceXApiClient(); + + final SpaceXApiClient _spaceXApiClient; + + ///Returns the latest launch + /// + ///Throws a [LaunchesException] if an error occurs. + +} diff --git a/packages/launch_repository/pubspec.yaml b/packages/launch_repository/pubspec.yaml new file mode 100644 index 0000000..84419c4 --- /dev/null +++ b/packages/launch_repository/pubspec.yaml @@ -0,0 +1,17 @@ +name: launch_repository +description: A Dart package to manage the launches +version: 1.0.0+1 +publish_to: none + +environment: + sdk: '>=2.14.0 <3.0.0' + +dependencies: + spacex_api: + path: ../spacex_api + +dev_dependencies: + coverage: ^1.0.2 + mocktail: ^0.1.1 + test: ^1.19.2 + very_good_analysis: ^2.4.0 diff --git a/packages/launch_repository/test/src/launch_repository_test.dart b/packages/launch_repository/test/src/launch_repository_test.dart new file mode 100644 index 0000000..9799c78 --- /dev/null +++ b/packages/launch_repository/test/src/launch_repository_test.dart @@ -0,0 +1,11 @@ +// ignore_for_file: prefer_const_constructors +import 'package:launch_repository/launch_repository.dart'; +import 'package:test/test.dart'; + +void main() { + group('LaunchesRepository', () { + test('can be instantiated', () { + expect(LaunchRepository(), isNotNull); + }); + }); +} diff --git a/packages/spacex_api/lib/src/models/crew_member.g.dart b/packages/spacex_api/lib/src/models/crew_member.g.dart index 20ffd06..62a1cfc 100644 --- a/packages/spacex_api/lib/src/models/crew_member.g.dart +++ b/packages/spacex_api/lib/src/models/crew_member.g.dart @@ -14,9 +14,8 @@ CrewMember _$CrewMemberFromJson(Map json) { agency: json['agency'] as String, image: json['image'] as String, wikipedia: json['wikipedia'] as String, - launches: (json['launches'] as List) - .map((dynamic e) => e as String) - .toList(), + launches: + (json['launches'] as List).map((e) => e as String).toList(), ); } diff --git a/packages/spacex_api/lib/src/models/launch.dart b/packages/spacex_api/lib/src/models/launch.dart new file mode 100644 index 0000000..ff63fdb --- /dev/null +++ b/packages/spacex_api/lib/src/models/launch.dart @@ -0,0 +1,77 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:spacex_api/spacex_api.dart'; + +part 'launch.g.dart'; + +/// {@template launch} +/// A model containing data about a launch +/// {@endtemplate} +@JsonSerializable(fieldRename: FieldRename.snake) +class Launch extends Equatable { + /// {@macro launch} + const Launch({ + required this.id, + required this.name, + required this.details, + required this.crew, + required this.flightNumber, + this.rocket, + this.success, + this.dateUtc, + this.dateLocal, + }); + + ///The ID of the launch. + final String id; + + ///The name of the launch. + final String name; + + ///The details of the launch. + final String details; + + /// A List of crew members + /// + /// May be empty. + final List crew; + + final int? flightNumber; + + ///The name of the rocket + final Rocket? rocket; + + ///If launch succeeded + final bool? success; + + ///The launch date in UTC + final DateTime? dateUtc; + + ///The launch date + final DateTime? dateLocal; + + @override + List get props => [ + id, + name, + details, + crew, + flightNumber, + rocket, + success, + dateUtc, + dateLocal + ]; + + /// Converts a JSON [Map] into a [Launch] instance + static Launch fromJson(Map json) => _$LaunchFromJson(json); + + /// Converts this [Launch] instance into a JSON [Map] + Map toJson() => _$LaunchToJson(this); + + @override + bool get stringify => true; + + @override + String toString() => 'Launch($id, $name)'; +} diff --git a/packages/spacex_api/lib/src/models/launch.g.dart b/packages/spacex_api/lib/src/models/launch.g.dart new file mode 100644 index 0000000..5391c1d --- /dev/null +++ b/packages/spacex_api/lib/src/models/launch.g.dart @@ -0,0 +1,41 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'launch.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Launch _$LaunchFromJson(Map json) { + return Launch( + id: json['id'] as String, + name: json['name'] as String, + details: json['details'] as String, + crew: (json['crew'] as List) + .map((e) => CrewMember.fromJson(e as Map)) + .toList(), + flightNumber: json['flight_number'] as int?, + rocket: json['rocket'] == null + ? null + : Rocket.fromJson(json['rocket'] as Map), + success: json['success'] as bool?, + dateUtc: json['date_utc'] == null + ? null + : DateTime.parse(json['date_utc'] as String), + dateLocal: json['date_local'] == null + ? null + : DateTime.parse(json['date_local'] as String), + ); +} + +Map _$LaunchToJson(Launch instance) => { + 'id': instance.id, + 'name': instance.name, + 'details': instance.details, + 'crew': instance.crew, + 'flight_number': instance.flightNumber, + 'rocket': instance.rocket, + 'success': instance.success, + 'date_utc': instance.dateUtc?.toIso8601String(), + 'date_local': instance.dateLocal?.toIso8601String(), + }; diff --git a/packages/spacex_api/lib/src/models/rocket.g.dart b/packages/spacex_api/lib/src/models/rocket.g.dart index f4e360d..866d647 100644 --- a/packages/spacex_api/lib/src/models/rocket.g.dart +++ b/packages/spacex_api/lib/src/models/rocket.g.dart @@ -15,7 +15,7 @@ Rocket _$RocketFromJson(Map json) { diameter: Length.fromJson(json['diameter'] as Map), mass: Mass.fromJson(json['mass'] as Map), flickrImages: (json['flickr_images'] as List) - .map((dynamic e) => e as String) + .map((e) => e as String) .toList(), active: json['active'] as bool?, stages: json['stages'] as int?, From 4fb6fbd402f82a5048c899e80fe72a1b12ea4ac5 Mon Sep 17 00:00:00 2001 From: Nino Gjoni <49320058+ngjoni@users.noreply.github.com> Date: Tue, 16 Nov 2021 16:08:36 +0100 Subject: [PATCH 02/26] feat: add launch example and launches empty cubit and view --- lib/app/app.dart | 5 +++++ lib/launches/cubit/launches_cubit.dart | 8 ++++++++ lib/launches/cubit/launches_state.dart | 6 ++++++ lib/launches/view/launches_page.dart | 10 ++++++++++ lib/main_development.dart | 3 +++ lib/main_production.dart | 3 +++ lib/main_staging.dart | 3 +++ packages/launch_repository/example/.gitignore | 7 +++++++ .../example/analysis_options.yaml | 5 +++++ packages/launch_repository/example/lib/main.dart | 16 ++++++++++++++++ packages/launch_repository/example/pubspec.yaml | 14 ++++++++++++++ .../lib/src/launch_repository.dart | 10 ++++++++-- packages/spacex_api/lib/src/models/launch.dart | 3 ++- pubspec.lock | 7 +++++++ pubspec.yaml | 4 +++- 15 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 lib/launches/cubit/launches_cubit.dart create mode 100644 lib/launches/cubit/launches_state.dart create mode 100644 lib/launches/view/launches_page.dart create mode 100644 packages/launch_repository/example/.gitignore create mode 100644 packages/launch_repository/example/analysis_options.yaml create mode 100644 packages/launch_repository/example/lib/main.dart create mode 100644 packages/launch_repository/example/pubspec.yaml diff --git a/lib/app/app.dart b/lib/app/app.dart index 6c8533a..4d61344 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -2,6 +2,7 @@ import 'package:crew_member_repository/crew_member_repository.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:launch_repository/launch_repository.dart'; import 'package:rocket_repository/rocket_repository.dart'; import 'package:spacex_demo/home/home.dart'; import 'package:spacex_demo/l10n/l10n.dart'; @@ -11,12 +12,15 @@ class App extends StatelessWidget { Key? key, required RocketRepository rocketRepository, required CrewMemberRepository crewMemberRepository, + required LaunchRepository launchRepository, }) : _rocketRepository = rocketRepository, _crewMemberRepository = crewMemberRepository, + _launchRepository = launchRepository, super(key: key); final RocketRepository _rocketRepository; final CrewMemberRepository _crewMemberRepository; + final LaunchRepository _launchRepository; @override Widget build(BuildContext context) { @@ -24,6 +28,7 @@ class App extends StatelessWidget { providers: [ RepositoryProvider.value(value: _rocketRepository), RepositoryProvider.value(value: _crewMemberRepository), + RepositoryProvider.value(value: _launchRepository) ], child: const AppView(), ); diff --git a/lib/launches/cubit/launches_cubit.dart b/lib/launches/cubit/launches_cubit.dart new file mode 100644 index 0000000..d0c4d84 --- /dev/null +++ b/lib/launches/cubit/launches_cubit.dart @@ -0,0 +1,8 @@ +import 'package:bloc/bloc.dart'; +import 'package:meta/meta.dart'; + +part 'launches_state.dart'; + +class LaunchesCubit extends Cubit { + LaunchesCubit() : super(LaunchesInitial()); +} diff --git a/lib/launches/cubit/launches_state.dart b/lib/launches/cubit/launches_state.dart new file mode 100644 index 0000000..e29c73c --- /dev/null +++ b/lib/launches/cubit/launches_state.dart @@ -0,0 +1,6 @@ +part of 'launches_cubit.dart'; + +@immutable +abstract class LaunchesState {} + +class LaunchesInitial extends LaunchesState {} diff --git a/lib/launches/view/launches_page.dart b/lib/launches/view/launches_page.dart new file mode 100644 index 0000000..9a47736 --- /dev/null +++ b/lib/launches/view/launches_page.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class LaunchesPage extends StatelessWidget { + const LaunchesPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container(); + } +} diff --git a/lib/main_development.dart b/lib/main_development.dart index 58cb9f4..989c8c4 100644 --- a/lib/main_development.dart +++ b/lib/main_development.dart @@ -3,6 +3,7 @@ import 'dart:developer'; import 'package:crew_member_repository/crew_member_repository.dart'; import 'package:flutter/widgets.dart'; +import 'package:launch_repository/launch_repository.dart'; import 'package:rocket_repository/rocket_repository.dart'; import 'package:spacex_demo/app/app.dart'; @@ -13,12 +14,14 @@ void main() { final rocketRepository = RocketRepository(); final crewMemberRepository = CrewMemberRepository(); + final launchRepository = LaunchRepository(); runZonedGuarded( () => runApp( App( rocketRepository: rocketRepository, crewMemberRepository: crewMemberRepository, + launchRepository: launchRepository, ), ), (error, stackTrace) => log(error.toString(), stackTrace: stackTrace), diff --git a/lib/main_production.dart b/lib/main_production.dart index 58cb9f4..989c8c4 100644 --- a/lib/main_production.dart +++ b/lib/main_production.dart @@ -3,6 +3,7 @@ import 'dart:developer'; import 'package:crew_member_repository/crew_member_repository.dart'; import 'package:flutter/widgets.dart'; +import 'package:launch_repository/launch_repository.dart'; import 'package:rocket_repository/rocket_repository.dart'; import 'package:spacex_demo/app/app.dart'; @@ -13,12 +14,14 @@ void main() { final rocketRepository = RocketRepository(); final crewMemberRepository = CrewMemberRepository(); + final launchRepository = LaunchRepository(); runZonedGuarded( () => runApp( App( rocketRepository: rocketRepository, crewMemberRepository: crewMemberRepository, + launchRepository: launchRepository, ), ), (error, stackTrace) => log(error.toString(), stackTrace: stackTrace), diff --git a/lib/main_staging.dart b/lib/main_staging.dart index 58cb9f4..989c8c4 100644 --- a/lib/main_staging.dart +++ b/lib/main_staging.dart @@ -3,6 +3,7 @@ import 'dart:developer'; import 'package:crew_member_repository/crew_member_repository.dart'; import 'package:flutter/widgets.dart'; +import 'package:launch_repository/launch_repository.dart'; import 'package:rocket_repository/rocket_repository.dart'; import 'package:spacex_demo/app/app.dart'; @@ -13,12 +14,14 @@ void main() { final rocketRepository = RocketRepository(); final crewMemberRepository = CrewMemberRepository(); + final launchRepository = LaunchRepository(); runZonedGuarded( () => runApp( App( rocketRepository: rocketRepository, crewMemberRepository: crewMemberRepository, + launchRepository: launchRepository, ), ), (error, stackTrace) => log(error.toString(), stackTrace: stackTrace), diff --git a/packages/launch_repository/example/.gitignore b/packages/launch_repository/example/.gitignore new file mode 100644 index 0000000..526da15 --- /dev/null +++ b/packages/launch_repository/example/.gitignore @@ -0,0 +1,7 @@ +# See https://www.dartlang.org/guides/libraries/private-files + +# Files and directories created by pub +.dart_tool/ +.packages +build/ +pubspec.lock \ No newline at end of file diff --git a/packages/launch_repository/example/analysis_options.yaml b/packages/launch_repository/example/analysis_options.yaml new file mode 100644 index 0000000..f116a5a --- /dev/null +++ b/packages/launch_repository/example/analysis_options.yaml @@ -0,0 +1,5 @@ +include: package:very_good_analysis/analysis_options.2.4.0.yaml +linter: + rules: + public_member_api_docs: false + avoid_print: false diff --git a/packages/launch_repository/example/lib/main.dart b/packages/launch_repository/example/lib/main.dart new file mode 100644 index 0000000..d7e079c --- /dev/null +++ b/packages/launch_repository/example/lib/main.dart @@ -0,0 +1,16 @@ +import 'dart:io'; + +import 'package:launch_repository/launch_repository.dart'; + +Future main() async { + final launchRepository = LaunchRepository(); + + try { + final dynamic latestLaunch = await launchRepository.fetchLatestLaunch(); + print(latestLaunch); + } on Exception catch (e) { + print(e); + } + + exit(0); +} diff --git a/packages/launch_repository/example/pubspec.yaml b/packages/launch_repository/example/pubspec.yaml new file mode 100644 index 0000000..65316ff --- /dev/null +++ b/packages/launch_repository/example/pubspec.yaml @@ -0,0 +1,14 @@ +name: launch_repository_example +description: A small example package showcasing the launch_repository. +version: 1.0.0+1 +publish_to: none + +environment: + sdk: '>=2.14.0 <3.0.0' + +dependencies: + launch_repository: + path: ../ + +dev_dependencies: + very_good_analysis: ^2.4.0 diff --git a/packages/launch_repository/lib/src/launch_repository.dart b/packages/launch_repository/lib/src/launch_repository.dart index e169a50..f742ff5 100644 --- a/packages/launch_repository/lib/src/launch_repository.dart +++ b/packages/launch_repository/lib/src/launch_repository.dart @@ -15,6 +15,12 @@ class LaunchRepository { ///Returns the latest launch /// - ///Throws a [LaunchesException] if an error occurs. - + ///Throws a [LaunchException] if an error occurs. + Future fetchLatestLaunch() { + try { + return _spaceXApiClient.fetchLatestLaunch(); + } on Exception { + throw LaunchException(); + } + } } diff --git a/packages/spacex_api/lib/src/models/launch.dart b/packages/spacex_api/lib/src/models/launch.dart index ff63fdb..f7dee72 100644 --- a/packages/spacex_api/lib/src/models/launch.dart +++ b/packages/spacex_api/lib/src/models/launch.dart @@ -15,7 +15,7 @@ class Launch extends Equatable { required this.name, required this.details, required this.crew, - required this.flightNumber, + this.flightNumber, this.rocket, this.success, this.dateUtc, @@ -36,6 +36,7 @@ class Launch extends Equatable { /// May be empty. final List crew; + /// The flightNumber of the launch final int? flightNumber; ///The name of the rocket diff --git a/pubspec.lock b/pubspec.lock index 78aa478..c829f55 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -224,6 +224,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.1.0" + launch_repository: + dependency: "direct main" + description: + path: "packages/launch_repository" + relative: true + source: path + version: "1.0.0+1" logging: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2a8dcc1..00ca1ea 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ homepage: https://github.com/VGVentures/spacex_demo publish_to: none environment: - sdk: ">=2.14.0-0 <3.0.0" + sdk: '>=2.14.0-0 <3.0.0' dependencies: bloc: ^7.2.1 @@ -18,6 +18,8 @@ dependencies: flutter_localizations: sdk: flutter intl: ^0.17.0 + launch_repository: + path: packages/launch_repository rocket_repository: path: packages/rocket_repository spacex_api: From bc0c092c290a79cb0702ad7074564d2aea086334 Mon Sep 17 00:00:00 2001 From: Nino Gjoni <49320058+ngjoni@users.noreply.github.com> Date: Tue, 16 Nov 2021 16:09:12 +0100 Subject: [PATCH 03/26] feat: add _getOne method in client --- .../spacex_api/lib/src/spacex_api_client.dart | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/packages/spacex_api/lib/src/spacex_api_client.dart b/packages/spacex_api/lib/src/spacex_api_client.dart index 524fd7e..8455407 100644 --- a/packages/spacex_api/lib/src/spacex_api_client.dart +++ b/packages/spacex_api/lib/src/spacex_api_client.dart @@ -74,6 +74,20 @@ class SpaceXApiClient { } } + /// Fetch latest launch. + /// + /// REST call: `GET /launches/latest` + Future fetchLatestLaunch() async { + final uri = Uri.https(authority, '/v4/launches/latest/'); + final dynamic responseBody = await _getOne(uri); + + try { + return responseBody; + } catch (_) { + throw JsonDeserializationException(); + } + } + Future> _get(Uri uri) async { http.Response response; @@ -93,4 +107,24 @@ class SpaceXApiClient { throw JsonDecodeException(); } } + + Future _getOne(Uri uri) async { + http.Response response; + + try { + response = await _httpClient.get(uri); + } catch (_) { + throw HttpException(); + } + + if (response.statusCode != 200) { + throw HttpRequestFailure(response.statusCode); + } + + try { + return json.decode(response.body); + } catch (_) { + throw JsonDecodeException(); + } + } } From fdcf41333b8e319c2be87590079620882559f9c6 Mon Sep 17 00:00:00 2001 From: Nino Gjoni <49320058+ngjoni@users.noreply.github.com> Date: Tue, 16 Nov 2021 16:09:56 +0100 Subject: [PATCH 04/26] feat: add launchRepository in test --- test/app/app_test.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/app/app_test.dart b/test/app/app_test.dart index 651da3c..b87f29e 100644 --- a/test/app/app_test.dart +++ b/test/app/app_test.dart @@ -1,5 +1,6 @@ import 'package:crew_member_repository/crew_member_repository.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:launch_repository/launch_repository.dart'; import 'package:mockingjay/mockingjay.dart'; import 'package:rocket_repository/rocket_repository.dart'; import 'package:spacex_demo/app/app.dart'; @@ -14,10 +15,12 @@ class MockCrewMemberRepository extends Mock implements CrewMemberRepository {} void main() { late RocketRepository rocketRepository; late CrewMemberRepository crewMemberRepository; + late LaunchRepository launchRepository; setUp(() { rocketRepository = MockRocketRepository(); crewMemberRepository = MockCrewMemberRepository(); + launchRepository = LaunchRepository(); }); group('App', () { @@ -26,6 +29,7 @@ void main() { App( rocketRepository: rocketRepository, crewMemberRepository: crewMemberRepository, + launchRepository: launchRepository, ), ); expect(find.byType(AppView), findsOneWidget); From 4a767b909efa1e8199a54d03085716feb045467e Mon Sep 17 00:00:00 2001 From: Nino Gjoni <49320058+ngjoni@users.noreply.github.com> Date: Tue, 16 Nov 2021 16:52:53 +0100 Subject: [PATCH 05/26] fix: update launch model --- .../launch_repository/example/lib/main.dart | 2 +- .../lib/src/launch_repository.dart | 2 +- .../spacex_api/lib/src/models/launch.dart | 28 ++++++++++--------- .../spacex_api/lib/src/models/launch.g.dart | 10 +++---- .../spacex_api/lib/src/models/models.dart | 1 + .../spacex_api/lib/src/spacex_api_client.dart | 4 +-- 6 files changed, 24 insertions(+), 23 deletions(-) diff --git a/packages/launch_repository/example/lib/main.dart b/packages/launch_repository/example/lib/main.dart index d7e079c..1af8d75 100644 --- a/packages/launch_repository/example/lib/main.dart +++ b/packages/launch_repository/example/lib/main.dart @@ -6,7 +6,7 @@ Future main() async { final launchRepository = LaunchRepository(); try { - final dynamic latestLaunch = await launchRepository.fetchLatestLaunch(); + final latestLaunch = await launchRepository.fetchLatestLaunch(); print(latestLaunch); } on Exception catch (e) { print(e); diff --git a/packages/launch_repository/lib/src/launch_repository.dart b/packages/launch_repository/lib/src/launch_repository.dart index f742ff5..74c3202 100644 --- a/packages/launch_repository/lib/src/launch_repository.dart +++ b/packages/launch_repository/lib/src/launch_repository.dart @@ -16,7 +16,7 @@ class LaunchRepository { ///Returns the latest launch /// ///Throws a [LaunchException] if an error occurs. - Future fetchLatestLaunch() { + Future fetchLatestLaunch() { try { return _spaceXApiClient.fetchLatestLaunch(); } on Exception { diff --git a/packages/spacex_api/lib/src/models/launch.dart b/packages/spacex_api/lib/src/models/launch.dart index f7dee72..69b5207 100644 --- a/packages/spacex_api/lib/src/models/launch.dart +++ b/packages/spacex_api/lib/src/models/launch.dart @@ -13,8 +13,8 @@ class Launch extends Equatable { const Launch({ required this.id, required this.name, - required this.details, - required this.crew, + this.details, + this.crew, this.flightNumber, this.rocket, this.success, @@ -22,33 +22,35 @@ class Launch extends Equatable { this.dateLocal, }); - ///The ID of the launch. + /// The ID of the launch. final String id; - ///The name of the launch. + /// The name of the launch. final String name; - ///The details of the launch. - final String details; + /// The details of the launch. + /// + /// May be null + final String? details; /// A List of crew members /// /// May be empty. - final List crew; + final List? crew; /// The flightNumber of the launch final int? flightNumber; - ///The name of the rocket - final Rocket? rocket; + /// The ID of the rocket + final String? rocket; - ///If launch succeeded + /// If launch succeeded final bool? success; - ///The launch date in UTC + /// The launch date in UTC final DateTime? dateUtc; - ///The launch date + /// The launch date final DateTime? dateLocal; @override @@ -74,5 +76,5 @@ class Launch extends Equatable { bool get stringify => true; @override - String toString() => 'Launch($id, $name)'; + String toString() => 'Latest Launch($id, $name)'; } diff --git a/packages/spacex_api/lib/src/models/launch.g.dart b/packages/spacex_api/lib/src/models/launch.g.dart index 5391c1d..303fdd3 100644 --- a/packages/spacex_api/lib/src/models/launch.g.dart +++ b/packages/spacex_api/lib/src/models/launch.g.dart @@ -10,14 +10,12 @@ Launch _$LaunchFromJson(Map json) { return Launch( id: json['id'] as String, name: json['name'] as String, - details: json['details'] as String, - crew: (json['crew'] as List) - .map((e) => CrewMember.fromJson(e as Map)) + details: json['details'] as String?, + crew: (json['crew'] as List?) + ?.map((e) => CrewMember.fromJson(e as Map)) .toList(), flightNumber: json['flight_number'] as int?, - rocket: json['rocket'] == null - ? null - : Rocket.fromJson(json['rocket'] as Map), + rocket: json['rocket'] as String?, success: json['success'] as bool?, dateUtc: json['date_utc'] == null ? null diff --git a/packages/spacex_api/lib/src/models/models.dart b/packages/spacex_api/lib/src/models/models.dart index 50f49d2..1374ca9 100644 --- a/packages/spacex_api/lib/src/models/models.dart +++ b/packages/spacex_api/lib/src/models/models.dart @@ -1,2 +1,3 @@ export 'crew_member.dart'; +export 'launch.dart'; export 'rocket.dart'; diff --git a/packages/spacex_api/lib/src/spacex_api_client.dart b/packages/spacex_api/lib/src/spacex_api_client.dart index 8455407..a3f5fd4 100644 --- a/packages/spacex_api/lib/src/spacex_api_client.dart +++ b/packages/spacex_api/lib/src/spacex_api_client.dart @@ -77,12 +77,12 @@ class SpaceXApiClient { /// Fetch latest launch. /// /// REST call: `GET /launches/latest` - Future fetchLatestLaunch() async { + Future fetchLatestLaunch() async { final uri = Uri.https(authority, '/v4/launches/latest/'); final dynamic responseBody = await _getOne(uri); try { - return responseBody; + return Launch.fromJson(responseBody as Map); } catch (_) { throw JsonDeserializationException(); } From 8e7df5ce983f054483b75110adcec78ea4d4c57e Mon Sep 17 00:00:00 2001 From: Nino Gjoni <49320058+ngjoni@users.noreply.github.com> Date: Tue, 16 Nov 2021 19:12:02 +0100 Subject: [PATCH 06/26] feat: implemented launches cubit and state --- lib/launches/cubit/launches_cubit.dart | 37 ++++++++++++++++++++++++-- lib/launches/cubit/launches_state.dart | 16 ++++++++--- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/lib/launches/cubit/launches_cubit.dart b/lib/launches/cubit/launches_cubit.dart index d0c4d84..f698af9 100644 --- a/lib/launches/cubit/launches_cubit.dart +++ b/lib/launches/cubit/launches_cubit.dart @@ -1,8 +1,41 @@ import 'package:bloc/bloc.dart'; -import 'package:meta/meta.dart'; +import 'package:equatable/equatable.dart'; +import 'package:launch_repository/launch_repository.dart'; +import 'package:spacex_api/spacex_api.dart'; part 'launches_state.dart'; class LaunchesCubit extends Cubit { - LaunchesCubit() : super(LaunchesInitial()); + LaunchesCubit({ + required LaunchRepository launchRepository, + }) : _launchRepository = launchRepository, + super(const LaunchesState()); + + final LaunchRepository _launchRepository; + + Future fetchLatestLaunch() async { + emit( + LaunchesState( + status: LaunchesStatus.loading, + latestLaunch: state.latestLaunch, + ), + ); + + try { + final latestLaunch = await _launchRepository.fetchLatestLaunch(); + emit( + LaunchesState( + status: LaunchesStatus.success, + latestLaunch: latestLaunch, + ), + ); + } on Exception { + emit( + LaunchesState( + status: LaunchesStatus.failure, + latestLaunch: state.latestLaunch, + ), + ); + } + } } diff --git a/lib/launches/cubit/launches_state.dart b/lib/launches/cubit/launches_state.dart index e29c73c..8dddc84 100644 --- a/lib/launches/cubit/launches_state.dart +++ b/lib/launches/cubit/launches_state.dart @@ -1,6 +1,16 @@ part of 'launches_cubit.dart'; -@immutable -abstract class LaunchesState {} +enum LaunchesStatus { initial, loading, success, failure } -class LaunchesInitial extends LaunchesState {} +class LaunchesState extends Equatable { + const LaunchesState({ + this.status = LaunchesStatus.initial, + this.latestLaunch, + }); + + final LaunchesStatus status; + final Launch? latestLaunch; + + @override + List get props => [status, latestLaunch]; +} From 5605391b589d415988dbab48c1a0983a2eafdc6d Mon Sep 17 00:00:00 2001 From: Nino Gjoni <49320058+ngjoni@users.noreply.github.com> Date: Tue, 16 Nov 2021 19:12:27 +0100 Subject: [PATCH 07/26] feat: add launch page --- lib/launches/launches.dart | 2 + lib/launches/view/launches_page.dart | 85 +++++++++++++++++++++++++++- 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 lib/launches/launches.dart diff --git a/lib/launches/launches.dart b/lib/launches/launches.dart new file mode 100644 index 0000000..74f074c --- /dev/null +++ b/lib/launches/launches.dart @@ -0,0 +1,2 @@ +export 'cubit/launches_cubit.dart'; +export 'view/launches_page.dart'; diff --git a/lib/launches/view/launches_page.dart b/lib/launches/view/launches_page.dart index 9a47736..552e531 100644 --- a/lib/launches/view/launches_page.dart +++ b/lib/launches/view/launches_page.dart @@ -1,10 +1,93 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:launch_repository/launch_repository.dart'; +import 'package:spacex_demo/l10n/l10n.dart'; +import 'package:spacex_demo/launches/cubit/launches_cubit.dart'; class LaunchesPage extends StatelessWidget { const LaunchesPage({Key? key}) : super(key: key); + static Route route() { + return MaterialPageRoute( + builder: (context) => const LaunchesPage(), + ); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => LaunchesCubit( + launchRepository: context.read(), + )..fetchLatestLaunch(), + child: const LaunchesView(), + ); + } +} + +class LaunchesView extends StatelessWidget { + const LaunchesView({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { - return Container(); + final l10n = context.l10n; + + return Scaffold( + appBar: AppBar( + title: Text(l10n.rocketsAppBarTitle), + ), + body: const Center( + child: _Content(), + ), + ); + } +} + +class _Content extends StatelessWidget { + const _Content({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final status = context.select((LaunchesCubit cubit) => cubit.state.status); + + switch (status) { + case LaunchesStatus.initial: + return const SizedBox( + key: Key('launchesView_initial_sizedBox'), + ); + case LaunchesStatus.loading: + return const Center( + key: Key('launchesView_loading_indicator'), + child: CircularProgressIndicator.adaptive(), + ); + case LaunchesStatus.failure: + return Center( + key: const Key('launchesView_failure_text'), + child: Text(l10n.rocketsFetchErrorMessage), + ); + case LaunchesStatus.success: + return const _LatestLaunch( + key: Key('launchesView_success_rocketList'), + ); + } + } +} + +class _LatestLaunch extends StatelessWidget { + const _LatestLaunch({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final latestLaunch = + context.select((LaunchesCubit cubit) => cubit.state.latestLaunch!); + + return Container( + alignment: Alignment.center, + child: Column( + children: [ + Text(latestLaunch.name), + Text('${latestLaunch.flightNumber}'), + ], + )); } } From d76e6e80dd60a6ab08f188823c05b6d72897649b Mon Sep 17 00:00:00 2001 From: Nino Gjoni <49320058+ngjoni@users.noreply.github.com> Date: Tue, 16 Nov 2021 19:13:52 +0100 Subject: [PATCH 08/26] feat: add launch text --- lib/l10n/arb/app_en.arb | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 315c73a..a07d6c7 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -4,15 +4,19 @@ "@homeAppBarTitle": { "description": "Text shown in the AppBar of the Home Page" }, - "rocketSpaceXTileTitle": "Rockets", - "@rocketSpaceXTileTitle": { - "description": "Text included as title on the rockets tile on the home page" - }, - "crewSpaceXTileTitle": "Crew", - "@crewSpaceXTileTitle": { - "description": "Text included as title on the crew tile on the home page" - }, - "rocketsAppBarTitle": "Rockets", + "rocketSpaceXTileTitle": "Rockets", + "@rocketSpaceXTileTitle": { + "description": "Text included as title on the rockets tile on the home page" + }, + "crewSpaceXTileTitle": "Crew", + "@crewSpaceXTileTitle": { + "description": "Text included as title on the crew tile on the home page" + }, + "latestLaunchSpaceXTileTitle": "Latest Launch", + "@latestLaunchSpaceXTileTitle": { + "description": "Text included as title on the latest launch tile on the home page" + }, + "rocketsAppBarTitle": "Rockets", "@rocketsAppBarTitle": { "description": "Text shown in the AppBar of the Rockets Page" }, @@ -33,7 +37,7 @@ "@openWikipediaButtonText": { "description": "Button text shown on the Rocket Details Page that opens the corresponding Wikipedia page." }, - "crewAppBarTitle": "Crew", + "crewAppBarTitle": "Crew", "@crewAppBarTitle": { "description": "Text shown in the AppBar of the Crew Page" }, @@ -41,20 +45,20 @@ "@crewFetchErrorMessage": { "description": "Error text shown on the Home Page when an error occurred while fetching crew members." }, - "crewMemberDetailsAgency": "Agency", + "crewMemberDetailsAgency": "Agency", "@crewMemberDetailsAgency": { "description": "Prefix word placed in the 1st subtitle of the crew member details page" }, - "crewMemberDetailsParticipatedLaunches": "Has participated in", + "crewMemberDetailsParticipatedLaunches": "Has participated in", "@crewMemberDetailsParticipatedLaunches": { "description": "Prefix text placed in the 2nd subtitle of the crew member details page" }, - "crewMemberDetailsLaunch": "launch", + "crewMemberDetailsLaunch": "launch", "@crewMemberDetailsLaunch": { "description": "Singular suffix word placed in the 2nd subtitle of the crew member details page" }, - "crewMemberDetailsLaunches": "launches", + "crewMemberDetailsLaunches": "launches", "@crewMemberDetailsLaunches": { "description": "Plural suffix word placed in the 2nd subtitle of the crew member details page" } -} +} \ No newline at end of file From 4c3ac2e7d11105940f7f88de5cbca07016b1316b Mon Sep 17 00:00:00 2001 From: Nino Gjoni <49320058+ngjoni@users.noreply.github.com> Date: Tue, 16 Nov 2021 19:14:29 +0100 Subject: [PATCH 09/26] feat: add launch widget in home and launch image --- assets/images/img_spacex_launch.jpeg | Bin 0 -> 96227 bytes lib/home/widgets/home_page_content.dart | 16 ++++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 assets/images/img_spacex_launch.jpeg diff --git a/assets/images/img_spacex_launch.jpeg b/assets/images/img_spacex_launch.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..28b8af4fb63a0921e337c5abff02f354105f4b12 GIT binary patch literal 96227 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 y73Y`coPS8T8&|EL-tr_q2>D-peFan-!PhS} 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
    C$WfKz`o`MCcAN8NkR z{f2i>^6IboYEqTpv9eW7_f?;g92;O30>vg?v%w{$DGIvRQ&W{RwlX`bmrEMCgRxmy zHd`0#8&6Q{S(xS-)lYn`(4VB7-olxoYQ)9G+}tra2i-gWGw5lRuo;)o_dDR)K(;|L zP(J7`<1o#&&N|!G#B8G_$wJb3c*p10!s%O|z5Z)Qf7^@y@57HLxOpxiViimBK%9jp zyqPQUUm zl3H2{xK~#2;9#&l{2~zt^JLKUf`-V_YelnHYia$t3aZ8ZqIdwxmcA+svHN42$jA=w z+}zmyldqMgr7ue?nht7chdg+g8oCH!FFf>RlfR?AK$sDsjC0^hV<_L>Wb8e)=vew% z?V^LEa48|?Pxs~nkISB+&-)Klln*zEV||fK20SpeZQXB#QxWm|Cz(P`$hMoKa&`A*D{wmjw^Jqm{U4p%|NeH7nl3!o{?m^yL^7bAff!rY z%``@2S}F(4(^^Pr>Ujg8r<6{yW)6~dT!Azuos|zHohv7LNehB+UL~fwH@=E`E|% zIy{MK|Lxn4CN_*o3>cZZh7GlV-dm;RlBkJnea`vco<)}Vo*NR$bX7q_atsLGiSBtp}n6SCsd-GCw27r7n~ z0jB4e7pd^*#Gn)!fGmr~8c0Xd*g&!#NFNL|fcK^?anAp%U400SLjRXj1?hp16DYDg{(pjpxjk~>5U=jTZ)KGBTx7RNNX zx9YN8Wg#pl(&E%4rfEFP)WFcKjts~@4=^dg3CY^bKyxtnJV6Z9wGfF&FS$Qp>*nT~ z;#4N8)=}$^bLvcl7%_Q(>nR{c62bj~R=c9Ifm6O0QxR2;DsR!Ef_oUYJjSUal;XaH zGkyuNo1xsilt&GGIpB8?4n39-1K_6wpc9IRAQ?~NKCsOP!labDtKwo5E_92&z3wZY zE{H(fZ2rFc@ZFV9FI-)HQNAyg@~S;F*Q^TTNoF^BuQcQk19N(=-ifm^5o=lA3_(@R z>v(3!gx~#A{>B#LSeV+gKynpBWaW}7k@iKaPUl^M0MJ>{n5ydNBH)ceC%~$=y>=1i z5-?Tbr4yw#!72ku=l_eMyi>+IAra9uA+In~Uw*3=z$cg;q%r^85)XUuqCS?@Cp zO^^uNP{Ims9?wZ$>8N%-xbkevZR)eM-!g?~mVA@!j(W+t>9JQ?;-m4`R&XFMg_O7W zNWW8pYQ*WimmeuB^C=f+Z@5<#<+trFZ+V2qu;oeYNp%>L9bs8ZeKaK{@-@5qwB{~? z#?D+z=K-^ZtVVPGTKZZ;GT5e2T#13D^AZ%##d3FYMt3H`7Nf4({wk9VphI(yR6`TJ zn8tdn^;gWs`w}XwtGx;}Vk!?0wijR1Ou!EMGuS>r%&aH_H$)3o3Tq8l=AgQyd+rRu z+f$b-^kgz^2mBJ6HW?D^ww`45O)y0HrE#q8%7(nk0PcIw#oKz0+(znJ>E+(Bf0a7@ybc zAw^ZWk3a*RMd)EpgE}z&;u>A+H|+Be-Dj6q$ZkI-S~FHX*}Fz(`zRmq*HKE<=Md}#04@) zthNH`q?(yiuHO!NYYND#oBP%XXe8W;pG;geM3{GN={%}sb+>U3nPh+@-RV8D!w{^Q z>$*^iUjaJDdOF_~m^Lx%95jkZq(zQNgJdH8wbEsCEjLln51(NwwbzI)^zxdZ@)uu1 zJlUNmYOOAzEpSze;r@B>Nj4-Y1!~for=?&m^)u4ctyiDOGzQUye|cm%zb?61^DXs% zev3C>r$_=802f_U<;5(c3Bf*Kw`jj_1&VJd^eN-*2SfKr7*1^}*XOUinHV*`lnZ$m z+xDppOe-5mX8=9(isw|hLqE(VgBcHi70w=gk(~_K>_zc6k}KXP#6y|8>fV__xybIs z15GWVTFzsUSlw*mVm%3TL*OMPlmnyb8W0E#A^N5@a}?;-R2`iq5n&I&AL@r4`DDvbCxQ^MBkZI16)Jh1Vl`7OUJ>F`=%Ix#g*au}dHO8dg)Z&U z_h6QW$q|C_mF!OQ0(Hbvgb62@JKtd!1_gTo9TJD?Oo0l{d>P5e)dcny)^^IE7owW$ zkq93lo@Lt$1QsHK>v(@m)FThT`DDI;v6WzKh_|>DnGS@!%Xz>P`}Z_S0mairy=|9d zsfwQdTi3y~6GH@QdPvE=1OG%tC5W?l9K6naXwPD)o&55}wg?$lAqQ32T9CF#v=jLfA0*3}}a8 zB~tHZM>tv-ntrRCu6HYgPyk-S!^wg*k0NzU@7+WMFlz5p&Qu zNvR45<=?73Ak3O!?d!dwxR;GX4YJDYH~wEqXqu%zvHumrlj4{dEh0_(iLg3|Qe4b` zI565I4=ak}pT6?ST^QE35M8Q$4w`p?sEsD5DsS;iLo`5Jf;kn^qC>TnXznedo&ZZ|(A z7&j~K+9qJbDoBD6Bpp5EKY*Au=?ZI83rfStAsD-adzPOxo*_wHh}g+z(VPZe|679c z{{W;&47?rd@M~LG5u670fj8F{k#bt61{=S0t=xF|=Ue-qS%&e<{MTm>lsQ~HeL*Zg z7@tE5rnrSlGy+6TCC%)T>QfPEPzT`VEtAUuO45|ht4FYKr%Mqk8KGUzTxbf6&%Jp)^@%5HC)uL1Te#|M;oT26z&%l2~d zvKd_tXshgsPD(TCT9AuI_Jn03dm!Au#tNX~SY_Oz0(xQOTnWL{%SvL>%I{N4%|2)( z#U=HG#COx-yX|ON<74B@n|5=oDlaBJCIEPTdILog8)3=ZYI9_FV)694AAHaR5~p=U zFTG#ltEfzew=#1ST#^ETXUvp<@HOh{T{f)Na!tsS!s>NpQscsqUayJ5XG}(2xH~S0 zVea8nleE@x(K{aay_B=i(LsT*p^#GC%C6>_O~~8}p(55p{6C~qkXXnOUjv=4+|H2~ z*F|ajB_C+28iM9024G^>@(vz6i0>9>NlVgl`wwb;3_p`oeii(Zf7O~+&_lg3h#k^G z8mH?tmCaZMU>D*5SRTp+RkRX^7Gs%gJ!NF*>HCN{#z5g#p_(whnltG#-#AcETe_ks z#x~3|4=0f9`GQUXLfktZ<(lq!q8ZBnYlr&cf3&^8p`yh}1i;7Kx+G~jju_X=QM%h|XrPiW z&b2`Iv2E18Su%wH=LXXmxxj(>zTV`i38$&Z+& zE2bJ71f21mLguPRiS!FBG42ObvmH~W9r0(Ilty}eVgxZ?AN;7|y?<{eJ^lXcf|x#+ z68+hcEdBGQI|!w#S)5gxg|11y<2r*{1z6D5Tt%~DD|Akrqz29k_c$W$0)$17#tOYm zqByPA&3e~oT^JzpUCiyt7Sa-_eW+CfaQ+edB<<53d}7KG(6x6k2wh!UvfInt#FSSf z9jwkrr8p%^f~k(304@67`rr-D*X(pP#pS~v(OhM&V5~LSh<}L1Qr=reGg?7C2#{P4 zoFNjagibL|_9K!x;zMG1O3YG%UMU=^BgPs<^W?%CZtairTz`7Fbxv*D4bc%K-7zK- z=sPk1uJLN8hwxAf)pw0blFXP)i9;bH?*9QrPw7jjOM}arW(SKE?*5ecA)u| zUMwg@c%0Uk_Q?k~T*#EWJ+aVT@MVxNk?$H0kM6rL-_Lj{+QM*;m6xS4iE!#tu}U`_ zJi>t2IE}wpp5Ph6zmWjHjuz{0ss?uOqH53e`Hq`nn7#{BHLt{mbji9kEkw|ZsGCJ< zuKBu8tw;x`a5FQ$#*Ic1JxmX5K65Mcpxv6jUHztX_gl8JS*2?aeov% zRYlleEbAU2-Xq6i+jpJUodoL~hwIk);x8J9p?2C&R)b>uk$T_z?;|NbS-C;?3JbhMh0&fwB z0xV2<5ckbj7HP&u-e(oehv=3hmrw>gqi;=OKebh{L8UCv8N5QZqfwfrn*05Pvf@Qm z%`u?+X46ZAPz{jlSm4#f`GdCCvR42vZEXTG==Z#{Vghk7-32rAc(R_qcWM>i4wi_Y zj6XorJj021Wo$4p9YK|QwV%f?m8wGupq!P}8EebzCB2wV*GZT@`i(SSqxu46kkUYD zu0@l^JJ}nbKgj~g)8%R*8~PA>!on@DJmk_I3e)10MoF6(%F z-Em8{sMJ^uP>W=hFO?%f6&JWW60-)>Wmkh84CJnV#Q4mX}|cWzVHjN$FZ@Vy>LcB_Y4Bfkx)~ zN10b8K-vb{{=CL5JJ!}oG>=(LXnk4_W=-cJ(UzX0*^yHX3FcNz+ix)2Yn`>qbpYMF zM1+LnmV&S96Z6Xkod>3>lgIn}iP=u53k|nw-Midy7Pb^bcQBo?Y<$n096wA_$cHd7X5>vtIdIP`l*(JQoDdi*FaN^*|gk>=fXXAVnt5z@X$jtXsTLvn_eKotRRn4 z+uNG&m|XEjLFrBLN_p&(uEEW7S>)mOb{#{`lm;|xGe1m9Z7Bp9X0 z^5R+86Q5PS^EREvP$+O zF%KSX|0s0~&A5;{Szpt!r)_C13?)>QqXKSAs8MRK4V)nRW|t<&uHN9s^ptHSfISU0g;ycy}+CWy|c|bH`+tF1OiBAB3Vg5+C!+X ze+jfmehS&hRr+&E1=pcBjIx>1*r8**|l$0}4yFut!YrJZn=x zRc6Et$0VEr68Xq`tCcCy(0TyuE1MsCGVm;ZNtO|#{ZY*3np4EXzyj7v9Rj+44c@N! zo84!16*Iq2J5)|JWFswqZ>5rxEl)Xfe?>aC|*`SI5z<%?>n$iP%vt0@K! zZzTgm8d~m!v~Y&%XKj~M9@T500de?`$X;L`Hk>&uI2kDzNb3+F`+x`UBH-2pcrfh( z;AZoc<_vp8g91ASFnCwv!C zIW**e$erxb~m;T9POG4QXaQ^g@*qNj3A18m4-1(yGZW{sV)YXT&eZyOf*N;|x z6fn`P6$45$o)QJpPe@7hrRr}R!WJZ3FZX6vlxeBv7g^$u30+X&UAO<6xMMSr$|ZSo zBot|CXli<>o@$!Troo{!oP|r=od9p29>?2JkZLX`t=|$YiH810+@;Ext{OhhfmENp8$VcfJj#4w9lyjc%`O- zm?A zq()f215z4wQAHQ&6Vt$Gtj>-wlHk$PGYt?rWx`0R%RavjWqwLS(;!ZD|)!7wvC1lO=h#4AbX~KDl$(go54lcM*m(WW~jMY*HneZfRNy#H)hJx33b(_-o+&0b5mNgy|WaVCBw6#U!NqjWp3wRdyH!DtglEfbZ zy&{g}g!$r+C5CU}aNDF%j@(W#(AC!Yg_6rDeX@(XHc#E-rU~JcB zsIV*cDn|U{^zWrs7Ox5to($tE5s;4SY$27;qjBRg zYv?IC+es1TL=hj7I(#J#rSvjJT=%Q?sk}VA?4x63*Uh{=Xx6p0sT5kENEK zP$MHBNmD7>UD~}b`>6bfg%SwlaNW^faWggsUd5-$)qm6zQF>8CkEP7ZEc zuRoKkDIU|#P5BEb4K{ZQO&|Y#%FOV4QGh2aJH#C+*ZiIF+^3!ZV1G$+FPly2z3n4r zzYAn1@hZf*O16eSRLV|QsgVYq#FPy+w_QGa@YH~SWaZ^q@&dRXvfC`~P7lm#`NSr{ zzqhB9c`9}Ez3eE{J;Pq=9zX-qfHYZzWVI;#kIdpWUe?P3O_J98OAKeY>XjUnyMwQ8 z2AcZ!t=e9Y_nR|nCOyr`=~`4k&4wpcM)|NfFMdh;wb=CIBHyHL!R$WXiwBjT5_5gN zSZkZxCIx%w zy%vx(FdHiD{iy@kcdnQv?~e(4zLl2>gkH+Jahh#5&MxG&Kpq~lD`c7RMKDK96!@&A zF`(9$<9wuWEvM1hjhlD>Zhm?EyKxwa5l4;`6X8OnBaWCOJ4hyjOfaor6>qzeg+ zj@x(E{VAvF=3d2rdxkz@iBK?hvk>^e0idN6r_AAFtub1vvl@Sf%q`T8;k7g*wy#vZX#2z@;HopEDQ>}0>@sfdI9nPz3;8%bX2D0*EAW>$< zp(MvhA5j@MG3g=R`&$gUp9eLX)2**?ua4Xqdg^4oX_78|n2?)+dl5B357~E*u$XS9gk1~<$-giJX zAN@3o7$yHt+$fq^C%A_SaG6b6I)-4_XB{p=MsoEne@8TKkYUq-W?~oyVdl6^d zBp>7i;81H%5SA^_*Jrb5hjJ)c;amHRU1Bw>r_7mIgtB`n%P5;+jIsT*>t5M=-@4uR ze;agkd#YjI!D;zkoLb)uz1V{~?!!rxX0g!vYe?TJP*8)p*-3F)ft*FnV+TSjYKml0 zEfQKD<};aIFDr|b#Ir`i{c8N0m%pcdnkBz1#niRwF#NN5ezz)lJ9$BOkP*oxjWwJQ za4(c>aKzCy9)@5m*_BNb?%`as-V4%H%pRFOnrqPzXMw&GEEb2#N-#CxEb#=KCAl8Z zcX@;aV#phEU?-vpO-Ii=4Sdlh>G3%|73({!Wh(0UsD1oU_I}r2sCDtqHB}3&h>K|x z_utR{`dX{pDU{I4Z%qK}I+dC;`v6%1ho<8hu_UTU51vuoEDsdO*$Re}+tX9`d#(r% z52T(V*$c;OtV)Pvq3DU*7JjWdEO7MP_1Pd^*>#(dQ8%zxpkiH!j+oKrrTL~Bk}X1Y z-!b-l3zBAl{j;!<+d#r%HB5l$DsTl3RP{inMeoE|nDZyw+h?+M-#%r+1xlBE?@}x5 zrP)fA%ySbNx#`P4>m>Ah*I!2WcnnptPnA6*@}o;=w)BRk)oKZO!NQW;H>Ti4cy+Apf- zrKy&gCJ669WmZE_G->KOnI^}*i-Fa&@UKO-AFosD>l=);AgxnzDqrsLe728-wN)S! zi4V1mYB2`;VgCJ^e@X7pUhsIlIaH9=PqwqypQx>W`uKi|C_jB>Q|vQAKpdmcOTaag z^ioEmTRixlh!M^8g(=V0B%v0Q>nP0FUsa!jsJZX6dQ;Qf?s(VgL=*Q`?i!NEr}dQ{ z1-;jrF7`0@+&%f$du+vY?H_S#_W$ZdTGA|*Yx|g> zDfrD=%raP$%Pk57s^4~i8WdEv$aYS@EPEl+GT*H}TC5`*pj)^O1OvBUN(QPpGyGtRj20++Mq@>wg4iLvU`hVcV6Vv$QMa?VR|KqE^;H&~O zKJ<3BP)*WHmtUgWCY&X+K|1QU|_Du34KF5m&?skclNS4jj1j`)Gp+d zYt!T)8qaA;nSgvXHmqO-J;?UF2PiTlo<5fz%W6xxwsI1t!N?ot5DbSOr6H)>To(+L z7;J5q+xa%P;5%pC354+Jw!RCR2C0foJqko7x*Ql&YeK_!Z(yFe?uJ1=soaWtm|r1>6OlF~swD;vN-R#_OZ+o_3J|JS-k7v4%CpUTWzzbapz z{xy^`A%BzdNA^y2qd4VQ+T_qCr|IKSG!5|%k@}eZek5V!?RVjA6F2Mmm4_}DxlCJo zi}NwH9+LD>FZ<^KfTR<<XL3&RP-HanvgKw z`e(W3S~0x@)_b=-0|=VyoRtOh=RFq*_Y&NT}L z0oVd?4pSQ91t@@uY$a);$w}i?QLi^YV^+@A+|M(+^v_gT2)3mUVEqerr`0 z!Lx2WfI=&UdsqIq;m@y5-E%Haq9$G$PX9fp>BYn0kkTT`g|f~b5E5d0jy6S!;ZWhr z_)Fjz+~^3UPnA(Vr&fgeH02bwxj9Bx^7Jo_v0l4x3q4nj4(6GI`|bQ=N*`Y?wKa_F zxM^;7W!WJgss={=sO!Nw*XSJxA$h>u&Kx8qFS1>7`yD2@Swtj0D$~Du&Mg5P!RYdH3CBZ2L%kXNRcngz zmDCWTtB3gf+XdCt!re#IUBca&ALlpDzF@jB!@mtWUhO?7yj$~Ox0gpL^GT@My~9&| zC$}-pe7erY+ijQ5Fx9FZDoWR;om9r#Oul8VS4h*7ru;;+vEW<_#>O-(_1s7sd8U0p z1cEE72}BFb4EKHwj47XUv-cX4GvD{=Z$ani?LTGC@=Qxo`wncHQ7~RSXk*$T)mv&+ zxmf*%ulAW`2WK1Aa{i`-^-w^y8u_>bnmg%S9Wi67P`Czkxj3}!9&$T;COVF1jNaMJ zs;zr7;9%a@KYemkSmT|s@^bG0;-Kz^K|T?P=Tk!_o64LQ?RmfotY{MSgn&a|w#l+D zyV@KzXT#VE#Ap;}zt_zc4UuyZar|_puqdnP(r`fcvTcM24zsEF2Eq9jL@AsByHtKj<86XuLkD6N<|ahy2# zpQvoUf4m*zlv}K#zHa%}m2q#{R((KVfAo>5iGn)2zyZra+aVJY?h%yMp4S+3WuXJ2 zd4(7v=AlfN@1JSt*Ufr-Ig;6f=PlhF(2RKlc0PwDJc!RU8`m#s5y?cQykFORia32X z`{D5a-ftdx^y_$(xMPKCd*obLgn|`#WTV;n0ik_*q;0P0>RpZ7N4MG5thQgcwkq}L zCf)F5TbOaHr<=Q$Flca{vkMhZxl>JxMh5JWoN_U3|B+W%|ZEj!IFHF*a zozHwdW6(T(reiX?rQ>91{DHCUC}OfPXKW0dV-yT1vxT=rr8aH_!Xcm9*1E8iv^8rI z89My~o3y8!4ZWy;jx-}^0QI61g_y)EQD$~Pt>8EUW%E`~Fqex6mrv(A-}=8B+t)9? z_S!uS0F5RlX^B&#=5QM@Qivn_}IwX)s! zOckxK7wmAeO3iKP-hHA1l>299Ohjf>fC#BS9RUtvYMPG(aD$F`C?w&7!^52*cpm@v zO653mC*xTT)JQ+tA2uBS*AB*}^1t_-ZwJS&J%39k#W*ds9JBCh(3*%>GHud@`5$00 zu@BKg=6Ty6rgyC%^W4mw@bqaoEq8+*(q8AUh!Q7(j`+3;`zow>oR!-X;aq}+q zar5rg9lAg!`8&!3CQexshc0IBIUpTQeA^C-nt^oxVzwgeD0HTq^*K7WBxjs zUZ87pqp2ldO0GPoRE~?&*%8J0y$^Bb=dqN<=-GmOcqlwtTiMN)Afg7OIoKr_i}hL0 zeJ_C)KSTOYjrG~0??XM@IbSz^b?w@ZzX&%N!!o-31wHo^yBqhNNCziFgAqMAr7hGX zuzWphQq1>@0-Fhk;wq=N_q*6X=*H7F2*Rqx(NNd@ZXAc9lU`HR8v{F!*>|mH9?uum zb~A&+mvjq7%B&ZXvZf(RSAI5wrLc6JT5I$7*FUw~Rd=95%wL$tTG<;>vzVgjVk>8%%FSVQMg^Djq|Uw z!rwJPq;zg=PF3AjA*MErNhwC-k{0Q~Rx68Ae1uA;3OlnsKUUw+d8L*58TbDqCbxTn zEyz67L5LJwNiD1l&mPyGjiNaG?7HrpQF#nItaZOVSID7hJTqDghbNEpABc$fvuzi&Ac%o(@kJ0EvCm+LvqOR$bLg!OQDgsKc#m7|-GE zskcA<`r+uGufw^WD~gok(P}j5&mwN(@+Kw4vK$C5-Vy4448=Q6!6WLP|?nj)-SL=%0T&n428i;C9 z>aFRF_<2#(iCb3+s;(6DcvgB^){1a_@=s8cJy@D5Xwj8XF$~E3GTSgaQ z|E66QY2bs5%5kAj#^kYG-E+qZY7T8%qfA=H&F!G3^;lchO~WLbtqm<~3(+nVtzCX} zNmkqP`q=0L^eD_09-A8L#^1!!?}TNN{Wz<2FCPmhGTIoix!29!yv~eSczfZ=l1R*H zVfJtl$5;|&S=K@2rc)Y0qPXr^enzvtnVEsE`;AIeLf)}{`J7+p9lQ6c%la!v`EK7i zete*_Q*pXs!d7AGoEr1o8QUO)68saF!uIF7GX{6xE+0?r21pn>K?vL5%llS1ehY<< z?Yc}RgmPJ1=@asioQLZ&Gp^el=E~l(onumJR~xbEA2?{*;2H&!IQ&DC981HW5|*(| zo>39{&tytKy*BXqcEgver{}iXN68?{h+u5rEHPk&Z*9hIWzV-Jd?b%EN+a&MSu2nc zLC4mulDb_dJ!4Y$XC7T~&ht>%A%fG<<(C-p)x@Ed!wT&#>^EJwXN6V31c6~ZML1MY z>2}aDykXNmwQ9wwZfiWlYdrB6+T;2?F{y2Yk->%YeslAST1{R`o)(4mn-Mkg5o>00 zLV_=Ma87g^hL8Th{bKs4)L#(qPBo{`U)Z|H+T8JU|98e3#fTxj>f`Et`H0$(JB-Q;muP*JoY3n&@#2s(NXT(@Jh!p+UL=nvz=2{+WLyhS5CTa&hFrcs1G~B6Tt-x6?lNS!eW>rAQ0(g7 z7`nc5njD+aJ&}t()DG0jSKnV^T7<;Nq&pz*a zHaS6@zq@iUvN{&D|F@X#F=f)AD{1^6D;%PNGmro4#=esdpZz1%PZ^R_b!Dw?(u5EU z9V!zN0vdkZPhfqJp>!^|Rjjbj=zcPhaqEhpwR>E+;t*uao+!9gd#j?!|AU7qWWURQ zzuB!_3%c2jZ>no}-*+3Y<2>UrPB+L$s`YE3^nvCKX7I#V-~NFcUs21W%~t=f>-?*~ zYfobPZ7A)b-oE^6!;kug|40&Z+g@ccxx$posF35QZ{Ch_1}n7Gx$?h;&aJARRL|;& zyjxeE+t?+}-1w;2f7dU>ah=US(u)7_Ma%WA&qMg89-A3c-}mtL?ZUcu`&9;IR8&r! zgaRgZbuahw!jAC*CTx#o^d_1I=>eKy%f}ky>glN-Hf* z8|Rcp?m;GqW(npXb7yL1IWqN_nswX@95`_=a^yl0+*@#>BL2VMp9`-Gxi0-ac;EMV z?q}WigCvIddEqb#Cnu-KswMCP-phMJmw|o7*CELp!PBD-8+t{RE9K25z70j_;OKE2 z)0zWkOgxgBBczZOxY0E5eInxW4qwN;Zl{iTb0iMtM8hSKJ%HVC6uS@qAo*;M>@Jm z4KXKa(I)Ut&vK`Wgo{LD9eYWFcMttBtodU%f-+ZU&F@xcS;iP5@Luzy8WCl(A|wJ$ z1unpx5h^`q&yi-&(R#vfN1fIU^|<9veM}e_O)+iMY3Ja>=Z*jC+(mF?Fw-e-Cr5NN4Z$OTL}z=;KuxQV5iBGziD%=Rzhma_J!Uq z;t^!ko+pt3Nn>dk--Sq^dEWqMFFog#i<720sp$y12(EBr(VLdPw zd!Jb16V(ko94{H|_Arra3k_roV)cN&(0Ga{HD9b4x8(D<~FB#taOMx#?k?zoB?S z-P|f+a5&N_${Ar8p(alqb)w#s*o;t4qLZBtqh5VB+7zDFZx1t(I3H9}btXser1a>f z@L@odx48nnyK%3@1 zG-8NiId@T7Je+wFXNyb@5u=Z}9pc=3$e0PFH~yEqDlQFyBYXAmrh#2`i0b&C@AaVg zp%bXMQ4I|27;^s(KoowbseFd2B$U9q-*r+i8VX=^+*GYS(r^!Lh4)l(-A%}pO<=4w z4A(ISN01SkQAnH|QAlprTbyHF{@zVJK$}xce%fSS{tngD$c$^y-`+cm8CPo6PZQEJ ze>F3AMw4RD&e0N7C84%*CNh|7{T5hyA+ycim?fay8NimQ-GhF-r@v1fTys&Lfn(od z&JvMHWH&p0j4FvP#9{JoqrTvoo+S-U0NawekYa3A->eBEN+(Q0)UQ;A$X>VFN6EbeRhh<OHm{AD_|7}70+X^Tu6bITM}tLtv-Rx2S-=#V{#$OsCT@i(2( zwZ^p<|8rBK4woq+&B4M(QM~KkddW!qpL!+z7I`7<#0yk-N^UO=tRp|%DeY4HEVc5K zk8}WS!R0N}>nduVd%sq*Q%`E3<Vo*X3}-sml7pnWCJ%iK9Yk8!Hl6L+gYk zNy_(GFC$Si_7J1uZWP$#LnWxzKGXYzu2_x8$x>-1Z*A&m$z)7&ye!EuM_K5I+Es4uq#G(I@3keAjqb;6HoEwM5WA`|6Q=(AGu1Ow0_Z8@l6n{l zjhrF?`sB&=D64gX{Wk}~(dwr9-*lrNgnO^ogGV;hNhFBJ42I&z=6JkDj3A>f>@gK? zlp}wLZs?!JY|d=W+FVXPnW;5ThlzD-B9&+a2`#c(9SlKMy$yq50O)HNVT{CwL+-N`rPWC%O^U7Oq15*?ThEfZUjQGA!zj!M6l(9+xy4c${^sZ#Y*oHVd z(|CF%K6)Z(TEfL9DrelJ(ipj*ibgg3$C+=urBY4DK^I%SHH1$Jd>a)2)Z%zxG;HAC#jf=WzxDD&nlK~@^ve6?|{ zL&QArR8x-yydS&)EhD+3B?;0;N?PO(tx04bJb=1!y|}KYFjWGgFO5k=bn)tOD0^&y-~*SaO|WDx!I6G-Zb7A&MZ2 zO7q&8Y!R+vDAN2iZZK&EC8QBR>m|McY~Y9GYK^kgJmoF6bpe09d)@uE=TVlcdfK(SwlRpD9J(Q} z*SnvI0bn*DLof}Av&H<+o1i#lF_dC)pP2{|O+g$PSL)TSZz{H@syR>Ll-8|v!bu?G zlIRqPaPob+l-Jt%H?YFeaj6G+Bk?i>wQ^mEcD4PvEe(ga17j_8mk6?`GxX!B$Ov-q;5fyItcF`qdH#+{n*6L1 zeJ(g7rRn)s$ka)pOUeL1!gXsrq~pliU6g7>8IZ^*BWn-USr{S+(qd$=6hY)8x$)!9 z|6cL~TZHH)j}u4YB#}6&mlN_8&L|Gx6-MgqD;rN~Mu~V+q>$Cb?nFN_MjJ^h2fn|Q-0|Tt zj8R_5eKLZs4RhWIM%arrCyP5>LXW*U2gmo>Yi^U~W2OjFeZET70)l+_ZS?vYwO^m5P762gJmHnYjl9*E2{p>OGv=Lcr{ncx_!w zm5sUP8^dP1JI?L$O~_>WSa~dFco>O=J+*I=2=o=!Qc)Ec%XxkOqfvtMU+xL*3Q#RfWe7v$< zpTcs~DVP9mr;9C&>w!mdQ2o^^q88)_o~<=R7)pyUg$eLLu!87JXfuULMuy>Qo#J?J zI7J1_JkAblJ)o1_*Cms(L-!J#-g}XMcIWf+iiz8;{TTtDdiwOK9Z*xoNJdADRpLX1 z2FSoldcib!$Zt1W$+2n`xPM}2UtdDvCM`68O$hsg>tdfH6PINt3ycBpz2Tr9=)SHjDE-<_)}Tt41i!WbW_)Vs&NK;x{n)r zTAg+q87E{ed$QXGhOEUYMItLHz%oq81KQ*~&GG{yg7#v#KegyduPMm5Ka7ZU1I~%P z7QtE-So^T5w^Uzotf=?VeC%n^ApmZ9XGZgZ7_kcV@~)>qxh7jMW+B91H^$)EmRn zV3=VnH~9_{)(jjI0*+o##y%+)jTj)mq2UtHqD?%ZJn^Xd?i$L!2g{?~*^qptKXW~} zZVzqbmt=-VTQ?K%`(zIwrV=AJ!5kXZu8*||=a!6bMJh#dLPDCFnmQY8#i~x8p-l|0 zB293$l;R>i(syIAgQ8*)Vuo!-@~=WKlA_07L%)=phvxb9)YtcyA7h{p&Gy5|1GZ+}A&*6cg;cFb*G94O6GPsOSl-60TRi|*-U4P9Q+ZGt1_YSP<6<@2> zR(}I{W*veSTj*{Ya>kB^qZqv$YujsU#sL8~ZlL4q)7w6O2s|?%$4Z;31q@Mv9v+-; zwFFaiU-_CvwmfLQV%BxrK6^1f|APix8Q?7}3mkd!b02|rE1Ns)GVTO_a)N3p-q~k? zPnJ$jP8J=M&eTE$Xe`LS-{_X1C`!eWfO<$y+AQWlBH_r0VHGc+c^-+6JHV|wkxRYy zJ8-CLBb)IFWl^@nH*#@f>8~#tQ zx`|c$=lKsE>)J_e-a+=rD&z|?>Sb%S#E$4r64fKfZ&GZ4?53W^Z%`I!$u>ao8w%tS z2yg)c;gF?+8B}=dO`>@>lQeXbQxCnDmz!5Gl(jzbT8knbSq=q9@|n=`b#GnRw*FW1 zU#x4+SreNNfhJ2^#LNA$Qe+I^g-bm=R0$rJJPq;nep4+}T6DA*Q;!T7)FSQ?48`nc z9QUYP?05d<@IA#z^i*@(fiA4k$tbFtlV0)ik>%ClCQO`u2ae)EM4|JGnQ=NBst9{Y z7Z+mtMqE(?H~Cp*Jp$7ipSD-2&`dS}jQ5^u-M9s^T=Uaun-VJN*Gy3mBS-h)H=SkR znxjC*WwreeBCjNP#{D~ZzR6Vl{kO-qz`EqA=cpGoC{K0LkSv`QjGms_I&uaEyI&X? z(m^#$=p#cS7&yAxFLFa#t?`pBSFmzYGr8UGvJPu)e=2AuKI5uEEn({Qo>`}-np*+P z1EKrmfxw57!}3kr)cyJuh%UJ~@mCBIpRT62F@1zUx;_iEVFGlitpqP=9T+lX3t7%b z(3q#sUqm#|Hu#Y})NqWP-kFyGTxKvm4835JgbG|Mj#2GJ7Ak|w&t01wT7ScR^6HD= zTG4F+1v#e$Ckb_{5sGKZCcD0%W(XQWDE@ESjl@o{FOq-+{*;XJqmNpMXnHueD=P$R z$BxBV*)E~Zwdl};Wi4D{lPFoUGv;ak1FaQd7|PY!+S>gz*gk5W?2uwRJ3AvjbWuuH zq_jp9i6eNRo~@gZrBVDNQX~AxXrPEebSp=ek@s~T50mZ16WwrR3=Elyq}6q6JMPcC z6wnX4vuq-?Vjd4UTM`;0iBYSwv;-0(1Z1%kQFK1wf2rpfA9WE>Zeb~lmt{;Lp) zBfp8FyQr~lK9l;wxS9_UUpfDfjI1bmGwaep(HR~lzbU=QRI4+K6aO(88H&uwMx6v4 zrJ9<~t(@Gvb8g#DpWe09ehnNje)P+0Y6Lay1Bu@+@JtlP6|A*V?&vri{o;PaS}gE2%{dw}+x?k%x4Mf+gd`Hlhc~A;OR+He%EdK+ZF;S1~pi zvcG-=uoH(zBFHK}hDaO(-%cGv;*{jo08}6k+)EM}LpI0QdKDf5zBoA?*tRo>yOwVN zcY>$uB9lfR>1>kkv_ytj_>rxy;K**#qAlJ|JI=s=w#B*R|AqOybvZ*^Aa18$hyCF; z92eBa)2t{a{K;*`s|qWF6-0+uP~FLxF?hRL;fPcp5doBo_(j^tV$CpQNjt|f*xRl!sMIO2}+d*#(NCofeO@-5^m>CD#QWCBiSjl1#pDg zNF2RVt*nQPTuFRlVkU@l(V^U<($xSw(b9YK^D5S)66n2cyf1eq%-Z}2b>H&U&VksZ zQX_sOY?%bVhubs=z+V%yF?pR@G?`H$eX}P+$8+jaPlO)?8ERTBr(cQwSHF*?LQmYu zyOqJEV{wT20tsb(Gj94Ya<4x%Nif#8$XJ&_sd8Z6qY-r&WprGhYG4vskdUDUWYrNc zWVJLhH@A-XF^P;u5>8OEfZB~8a|2@peyW%KX$L1yRcU1n%US ztq+5IIuJ7!r#L)35M?OPtlQ$lgEZC;o(#vw9+8^szwB%j1_i8O(IfTvy&x%k`(eXw z@sS5}kNxEslp7?qGIe{Mkr{*Cus#M=LjU(>C3fw0dgl7U-XMG77&P3lmos%(G0Blzmv72R(5N8qW+pVmRDAcrsXDxYJO@#1F7H zvV%N9--z2GI&edJTq3s zJ&%Up2+kFXwKU-A!CVj+3{mlf+vxt(dv!-QH-6SCO^_j@>ry-GBh~To)`iy_R}#-3 zgU(;=U`aABm*($Uy>9oEI(j0NT^J3qAYqH52^+ORzv8E;M=={yeuod)dy{P7ot1U7KK4dkVE1AD z&Thlx|39brS5ak$nd`vQ*blx@zk5h7?%iW-@3XWIUE-9cdfJRWun2HD!>-M0X0fAa zuLD{K*%?Ft_cqmT)UOK$l6r&aoC{L5sG7@LLCNGs)&w+=wYCfR13JM4pGmYXJ7ij5 zu(2b}~udk(|exT?6hQ=)f zZ^zDSwmZ7#xJ$b~8aF~rMpjj+jIH6-nXk-NyK~osq)*3xxW@j#U;Jsus&zYn)+K`DcJ{8Vb6?)P&3-|V6K_6n z7&->cXR^HiDQuGw)1c#0x89{61b3SGKms%i^rliOrkhOL*1ZzLl2*BPnMe6 z;h^k045W8xbE&2c%5s9Xh1+(4>qc)w z@P*o>J*@%Y)WeTKUx7RQP9C?N4W?W}0Vj zQJ_jCETS{&wN{B!F2UhSaX38M8;|zG5zwCKQcuq|JT|Um3y3R_g@w4TI6+Hs!Kalk z#`2$?inxT3Y~aV9=;K~R(t#9?_Hy%)B4%WfP|a3f*_Uz9SmkH&m)^GCU>DqTD1CDm zpjuq3SCXufJ?f3$7#dl5CR;2}ZfqHv6+G-{3Y8W8Xb~TfQRH;6ni!VZydJP9%WneK zW+ewV%mV6}bCs`0Mi6uHd)DP$`vHI6Ww>frJ7|p>HCEBj^;eD-)_9s~54-G$22|I= z#un>YReXc27#9>G&usa*Zg`MIsDSW?J)37%Y?RK!`c%{jE>4Zxkx49XEQ(j~GfuM_=|H0v1uRK-i z`yztsM3Vjy5xSNUS$w{CB9e1Z`{kR@{Qd9joj!xV-#I_8m1wch*~udbW7Bt{UvM9aKy} z`c+4tNjmal5w(tN?MGakEBHCwB-zu`6Is&SOa@A<<7_FElqWD#LFbhM_+i_+>$SMs z|EJ?(%jp^7_Du_jB_&TL`6P)yLmz{_n_VR(slwIc{Rn4|!@|Tm0C_-YG#m>3kUcEL#e9S!v87yK~5M z9Fb^O5-$)}f(#1Nc&m}#Emanz_2W^wBDc#)Mj2RfG70Wda&4THAgQg&d@9S}Shuh1 ze$=q0zeFj@d-@T*=44gY?zPHAB~S(8EsXm*WrxBYp#^VOSUGZLrDfW@|4(&$qP#~X6$~%G_1B|;sccuP&0~-ltI+yHE5IL`hxN9;1hcG#-?zZ} z4&_;DhqAlH-Urz>{QD>8$|lE90&kpB)pbSBzdaa3il7a9$bMqNM$y5yYZ`&rQ~)Ck zaSlfOSWntnd4J>zA4B`Asi~9ruwp7VrqZqAmHd`#l=5t~7Dg4{ENc2aJj_fxvVj=y z)rm1R+}oX^mwHnpmoMhi*nHhV5QDbaZ3p@MgL1auf~-NDi6g>meFpQt*-oK_53_Rw zZ)r4y5os`D0Qu;)48#fOZ^2R$HT zVBLV^`1A}12`1qu$3`PNs!p&a0lWOiWS^MXb=N7N{mB6$@&;>T|Ka(dHt;rCMZfM> zZLc&_+=rc~^zV~>w_bq?O0Hc`Fzr0CnPT)|B`Y)VG-XR(_gU?u4e$@)fVb&#uQa~H zXf+*0p~h6%$1W}-+yPFM1iYVX8=W0<>y3$lP|=VqTa9u*yvc0$!w=mzkG|j7Z0-QB z%Vo4UEi_EP3zEJeHqyps@&ihPu=0A_$DoH9V&ei916%U?-7;_Z;CGIirk)?_GBb{* za^nNl&~VpBTk{(!O;Wq(K8|xT-Q__48dbYo?l*kVbtWj)|M|j@>)|D#S$%8Nfu!Qi z{g8W*;LOSV?`*LJv%@-<`j07=GlcZ$hTq6QNKxkWKF_A2crl%#e*WYJ%tIo{q}Zl> zSadHS#1Yfn`fRixP5Gnm8DlDi z>^mcGV*L=}l+w{(8}~By^M?V3rB_^mTotDwW)GFKnDP@c%*M#EiH~|ROMMHK|LwP{ z%ADQq%w~N9Q@wq@1+e0YUJamG4DW1rkxp|5lrw>4A*XtNB~~qh0#i^usANE7cG{a1 z0ME)F@{`UUg>Y+b2Av>DK>qNZ|I5Gg?Nn3x;L*$BC(zuSgGUU$-Qf($>?9-PCzr`W zY@RK${B=Hbd@Z4l1JH@S1hS+pf)gBzGOm+U*X#tK-RUA3HD3kd!;eAw$#g;r?MN&= zxVs>hAuql5>dLzQcBdb5gR~p~cbUo0*Sd2InyeFKy9Z^qbWV&51)w3rH#pOV1!o7v z!)}&Hyt4_)md~SSrYg!XgPnrn|4gpmwaW}Vj2~pGKHxJ9O}=ej7eD*?hjrxXLziOx zx|>D-l*=u)3(lUvMwM)4woalU-ytzrTdzR1iU$w+$_shcku8g>6A^l`o5->65VgD# zs7Rr9poz@uEk3HX%NyhFg4@ssR`EtoYaN9itU!PoM*kG^Vor#Mz>H>J6M3)v^};>T4_@8pa@$ zVk|72X2iJV&5KVtgsV7Z{0vojrLgi6vRF>y+~bYBwVABKLrj-%Jk-qcUKP+!+A#!4AHn2;C!u2+S;6E>mmAzEn=36P{{|;$K~J7a6}(6sD7n%-b>BuR zGHto{6z#fnkv2efHqROb&)$ixWW$@JFXK;u=_l3OPdY4DnXFtoxb-cN3 z>9~4wYmRYP$5N|6Rk!)+gYQTf`U(B3n?rZ?Tfx6*Xn)uiKj|@_@fxJ)@Ww5zG??l> zI>K)AzGBb%6n6~jjr!^^-e^ZlX9c(&@vlNF9!$(cJ&#R>{%+kYk?SbroB_nruzHZO znp!7_@>63e_!X_7I=SGRr#J%0c@6SoirZKhsPSs4V$CtAo2$%ebrPqRnUnX}e=Ojd;a4#_Y>?}Im)^TXnE6|x`wE9MD9y%WKeP^v;=UhMoa2wCJPqZ;te3ZG|Z zFMI?_@`nQ&CiE0l<-gK_QesKN@@=@0A2#*%F-ZN!1oS(PcokhEH|S{!|D%zuBx76d zNOU*S_J~hlDDNJMpjqsxH|X@tifUM9fZvuow|^M@88xJzjtE)o*(e;s|f2->oMqHMufm8#OUmmvkr+-v71n z4T+AvsIQse+w#io1q9)>J1@4gF(h*ZUONdEe6eweItIlkjO+r)xiTYeYJw;7sE^_> zNnSYW%RI8~WzQUCg)ojz1325V9ytAg&ib_dL$g(tT|3Y*s0DA!n#27$K#XEjgIZ4f zoLs#kM|xC*8*a#Uc3RzZ_>Yz*?3t>gpD7}k7p(I%RvjJ#Dfg98;ooiCLxr!xEz#S$ z%i#Ql8Fbg<2b+!K1n$n6;t3vFn5%a^Fs+V*ejmZuY-3a4Uw_`G63(5GX(qjW;*@js zOxns%jl|v4E(OVX9?|fsB2x!x)zx5wS(hjTd&c};SOpQJXyJqa( z8&mHjpcG~gD;1muuOFdHYETY7wHGgcKw6t^X8t$q{VM5^M^FBpOLlr>45Z!Z!whl( zL$*N2M#o@~lVxO2gt(vK3EnW{gzbtQA*PF$HF=cY~Ce!?Lq6}XrI-_@L&oMl+ zH}u@hoZ4?|bDn*tn5%T+S|<;-z&|2L_qvS!{e-uj^w(^c`bPKnwoAF2m9=%q=<2fF zC4T&ppaG=126LE!3om|>J^i#0Sf8;)qXId7B|*!ucCV)8N9PBp88yFB+J%U^sQ0IT z8(5bs8j4Qa;}zml0JQT;?ZAZv@I?AyV-s(prc#|aN6Nn&IgR|D)v(H}Esgh!$5v%o zGxtuNIP47_)F`w$f^E#$POZE zp@m8#E2nvlHfdAqx5^-r;0WJg8((WFOy+{ysLH^LM(`o`q7|lJGGR?d(>0+^{QlnS z6Nc$~2N8aU1JTK*nSZrm#ShK?Wc`5VB&iDfc1{VOi=RKo_n}^3uXkex{wsYOS@6rb z_kAwr>R&M1ua%XRXWG9DD;~T`-o}cRju=KK?N)E<2%yt57fZBj7w#AN8_tzWE>W~l z4^hZr4V-buX&;s&Xvt;2(UTI0>~H_Q zX_wi|Q5m~E6_>6}wWT22*5CkltB9SqcD?uZGnaQuDaqK0FT>$f1y4$S=WmqJa&UB~ z(oPB9puPAQ6w&OwRUgU{Ezw=e+ehdLOqIyuCf4U$yxS=xK?s&K$GBzX5~=I!|L)y9#@vDu^1hR)F|`!(E) z*aFmg+i7(S_>)=0eIu>lXVRk9F&4!!?p6lW2JRySlO!AH$|Q}vTU)Jp=`krvf%VOz z%$UlP*3)Z{Kj!`XGp_sALt01W*Ca*{^p>YngY)QGN8S#lCi5pmMcIdz#gJ{@=+jnF!PaU+6ihV1OL8VgI zF`m3b4PR{U@BbRR#ZK$kKcKAtvuD|(6VFhyj(I7kL5IBl;CX`N@rwSbns_~pp-e0;j#nQHh zFnF;(^3d)n;!*loR(t5``s=Va6J*K!ZS3Sh(2J9?lNmnPiKJ_?jqw}DAcLE}b2<*5 zfivqHXB+GtH=Ib|`?qunb|J4IwV}MYPw~&>dQvGf5z%_C0AgEdHh)x1+8hh#~`0fkcEC?hhy@??D~Qa+stGW%!-_E z16LkSnZdF^^KB_8(a{dby(zcdB$n8Jo|_ug84Amli!89Q(A9n4&0P2kIYs+k=jSfd zlWf{j%}N%FVw`Dbbk|6z#^B2Yx?{0IKv&e!o{l{z;*Ils!30n*&d z%Fwa%qu1g$r5*_dmbg#ZkFg!CmzNYn+v1_E?;EF*tct})2)i~#(7TdC$Dos$w8~5yKC%B)d$xPy;{42| zLdP923RRK0_(SssHrczjCE&^9%U+Sr^9%lZ%cs6KJ%x%$?90q9vz^kV>x?0n!wmT-G|VK8|8hN7(uu=(qtjQK z;`f|I*6`NgjDwFQx)WX>XBUMllV|N)jo?pin~fiofWn%BeT^RnV_4e7#GoSR??gs2 z9j9k}80^PdBc%nU)opoxdw1utj>liC3;D&Vhu8OvQ?f_Kb)E7a6^4w>`@Q){NrL2d zSd6Z?t4!cHR7f27cvD|h+kf4cfjrD?UhN$#)v-BiF~g^ow8G+ZEeI_k)`09 zJ2QEs{w0sole49@T$^WX<*$nNlP?2p1^Q5q-HEV|gJlO^ntyi%^3HCC`p9B%KbUvj=BXAn)jQgTffGW?GsXejhbbZe+>HT$ua1Hdi)MCpSw1S=|)%v zb?}5Zy+Y>TK4qSkkrp{mk-CI--pc8o04o0ktXP9M0yjQBj^{EtcP3C{X~{j7;m$BuY6`xMX=KU=nCS$auQeu*bb+`=2mog;ibg zGS9?n@QqJn7CPO3{asL4t|5gww^c|$$&A)NhSUyCMuxlYyxb<`n5CG-9HB$mKUIQ) z8GN+gEe!^Kl(1y$t?tQ_hYd%UTz#2;lF{A2k`3xtWE-;Kwx)}s8P@B9wu_!#&`!nP z&bm_DvEQhqj^`8Q`@S3ne6w?E*QUO~N5eJ5J9^3K=43n1ZYNJqEjD71W1-P&a_UWLH}&H&Xi4SZ z>k3x%^HvnpM90|0e_m|=OMh7mmGlJd+3fGrqyS61RuNmXSz?m^^GjwuKsFJa*`*eVmpIl1N{8t~&ES29Df8QnulrTx?mA||S15JM-Oyttzg-D+uswcx!8sX*hcB2CN z^KrYuSFW22MYD%H&uDGrwOh_hjqrRYnU&HbOJgT--3sBG^OT^_kqG&TSAA=m0zGe} zdPdI8IDckU5noSEBh-l#oc}$oxv^&3!gG7KiL)SQ%qp&i?Zo_*M=@4#BiWCcd1FON z9m+gcg))qHm?hnPZZ{FODPH*X}+y1;kd*Dww$zWFp0R7R-?--mD^ukoqRiQwe;ip z8<>m-Py@B1q-dBw-EXbnD>1xWYXzfe{em6_w6HK z%mUJ}*!aZ9Z&>4F{lMv3YrETYo1FoGU}pAoEKsTN7`&&r*1k2CxhEp_pxJP0?VYLg zS>GXq{K^`T53T<)p)`-}=_R*5#Y*z4Yt_^F59!yY@)mg z$@N-&(eK5}8-5ZZ0*yJ*dWc?jIJJihieRv>mD_{6R|O(CnDO#Csf} zs}`aHZOH-qAByB7O&(4j+RKHo(bY3TZJAoU+Ub7V<3ED}%~tl8ETdTA|$q#Yj9+T7<-6L^JY*u z(2T{Iswy4-D`9k_85Q7588bkIxM4=tmSqFt^K%!C+G8p&?AH&kTi%F&H~E^)&jVKb zclpikBy@4&jC|2F-=Z<(AftUNjSMY<9KP+HO3n~~zm*mw)MuQ-5{z>c^NaH+qZQ_M?ww6K$72>{wOX5T>sx;8M}Na)d#gIp z1dBcS&5+$WDo=y&!8z9H(LZlDZfyE#oAt>Y?5q0KUy@=qCYnOBni~GcC?q^J*KpXv zv-j3CBB<#$_JM-;rEX@;nLL0A&Q+z1{kYh3-*geRTXy;4)_2Yv$h{P3r(fTdqm*-x zK+oAJ(B78JwmqI=n52X+*k=?5b?Qief;+yx+o>IF9omHV`LD z|EET!r3H=ftbM(8=^vX)`{ivI5Lbws1JUD_>;e;W`zIg+COM6+K6Bx~$yLV;j-K9_ z-?ry)@#nw^w0iIh{Y8W+iaTPcjOVCNG^TDU~TZw;i);|0m zbyZYM{DPZmVo6oa%^bIYE{cFSK_~wI!N~zUKp+r@h_Jkz=8XrJoqR&BCuEg2^)qPn z<8cr##|aJ&sBly!QPqSmF!4iAiy@e~`26FG4dFE9{hV9$t6zFH8G0IDz6*+;qMOpR z^?DQaRU}hceicehTMe9TQCF`S%Fhd1F!_b;Vui!xk{sz#eeXh7JwGVRjqu6?(9W>n zigh?k!1=hDwLU)Hdyb*}S>H?gx$-2Nou6Bn*u`9ova+Vy*c1R=q8heH*^{ZszzlQX zx?H_vn`q2a({0t$nde(n{b{qED|u=|@q=Ie z+8;KFj+CTSp6I$#se25%CChaG??@`Qu9^Yx{ z(Kj*8-VR*KVP$2M?lyOCSEj7JMsrlTVC+9%QoEdm!)~4y6EmEC-+#wptD=TgZvR&U z^Ks_v?cAl&H_J!o^*8v<{o3JSdf!b#@5JWkTtVhG9Pm!PXw&%{pm-&}t}5+nC1-~WNwTfz*izYjzRDP(bp!(T2qgB{?N9Fh=}Y3 z-PAr7m|)na@u{oXq=os3mgSbLdJE8&#*w!ZUJOnujIfVETz3M6PI)wQi42Iz>#4>5 z-Rt?!hWN^$?3dq#=4Y1rY0l3*L5V*k6kZSL3FI+uNEJU0GC^m4r0F$hY3Z2Lo5wdg*Lk6v-5R9kTFi+o))>oO)V z`J`4oU&>XkWh3XUY&=VxECMSe`?1}_^0+N6D^&B{&bj2nWRHqT?3nnb=P5U$p7-BQ zQh}&sw3RWn1MMLyNzp``o6Q^3DJkFOPjzljQ4cW$(hk#mw(gybt6Z(GsT;x!PD4N&9rJpb{^~0EuQmC16~IK<-6dows`(Khgj>* zMUU}W<5rAG$Dw9u5T2X<*>{CBZ%ghbvo}}-~+J1fhWV6A+ zY=<4=mN#HLIQ=Bi#U}8)MCcsv6|=1g)uPfTja3s9S*+w|$vpHP6Tvf$Uq3t=tNv@V zjy)2xvlPuJx5Xe;jqeExzT7mL7S#@)5C1H`Lod4UJ;m-!4rHsC+in0HFaiVsERE9V zuZ>ToN1+6!h_r~RdtaG=A-Sa|T-;MK|N3P5G3X*bHFjd@>qN1jP)FFp98DvS3bUg} zg#{?I^h25+LnIT$29w9HfB@*7`!bk%Kkm{cg(%r5r^?j zMb8+cSa(%pCdN=uJn>xEd}m)W)h@P1C5$-`6W-f@M(TG&S6}!gQT=B3(te*?baknD zT*rMkJ5>KcfVf$XkCE1dv(zY^Iwk%);zoQT>>QO5%w^?yky&m{|Okrx?6L|UTj*?w#O?`j4_}5oQ zR}_9+JgF|xvTGddJxbq~y!#-QMvyn?nx0PP>pN9?^RK_3=Y#n^s9Z+S;d%)K+e2+#I&?IY|jAP^`Uj0-i}?c!Z6LS8Wc=PrA` z&$(``Y7*rF|D82CrSs2Ml`x7rUyg1VU-V{!f|&)kQuN;dhd|N)wR9!mY-ZiR?Vw3j zh1eQni6yZ$wxH8mM#LU8X=^L;3EC(fbd9A<36h|;w27rOA*HRa8d}wLwZ+VHDtTHo|Ag{SEQq4>zE+Dui1C9rvh9)ufAo}p(+Ru&_e1U$ugnPIpbxHKjV z`tqQQd1)Xy{PNWHoR7p|PnNU5&~*rbqVt!+`m~%P)ce%gPn|~%y%t;TTaIRRGn-I` z7d!%k2zRqM&ET15>Ybf47vKJ7OSg$QhR<=6Q+Mb@1b7{1J6)IT~2 zrrjCMucB@sQzMMqZLo#__bl)t@DjeR4iJN(=N6X0Y-~(YtNbe&w%{PA;qARTJ1bsL zO`j_AMXMzpydn_grG;`hH)KQiJspVU8SrBnCJF_>_(1)60zfJ(E(4C2#wyFbIJt#P zKXzUNsy9~gb61+3BYNo0BZoR>jMCMK>1lpzP7Wl>z*~-G6clptU$P0z03L}F%>!gX zU@*B9LL(ysiej`NW;$W|$Gyzc?Z36Qu7lK7_7SG*0GcT_LiyCFL-X9FvZXDdd? zejHET(ANm+e}AIMbYGB35Fv-@qtuAEsOPxGjaQIZviMciMVs=z=<^AURy)L|JB3l= zlgd@_7TE!~MuT<^27*|;M(ydN)TWA#>w+i7ZwZXHr2&%g<`ztDSbW%3Mn>VWK(BSl z%b&Y?PI1g_2`rj0T~Nn-8aC;H%&ME`q70K^!7MN{<`y7UzQt53mquV#1AKWvqOmIJ zveI%79m!UD`39Zkx(25w-tl!kp2duz*2xYX}+ z!!Q<5we;bUIM4JQs;y4f-mLrI2gx9?_7SE+866^xgLf_|RRogR$x+Lv#~P@Qhs&Tm zQXwsG6J}9m*6t=M{M_+n=im8n9qRjG92TD@YN>h91@g`V2P$+FmzD(`2Lc z*CT-*b{frL^7>R=3T(fU+@hni9i_}1eSFKm^Tp)9z;qfx2cBIIy$wv-2Hjg5_f zVJZP^QiX&GfF&l^P6|@D4l>fN<_S<%j8j?N{JQVTuovC8N9D|Avb`#0U**n@L=X9m zRf$h&ax9oUXeSv5bZdSXk+L~GIR79o7QfB0m_jem`iz(Q9fnRZT^rG39; zx{pwexTBn)j$$+`tD>8n_;~3%{EAnGz87)!(0zH7;~6<}oNS-o%Iq_iA0=`+v$8te z8*zpd*X}R%Df@)eW*N37ZUp8tYr+jBTqA*r;{k&y1V*zz28|D$4u#wML1rovuH?pAB#7Fw@?uQ3^TcRcv%`2ZY*ZJhez( zMs#InIMxIw;doO5nj@0>Fkj^l$ZMG;j~Jz=Jg0EE!I`k`-Thn7d^%@yh@Ilv*~gp8 z{ps(K$eg93FjyBHIJ|&eE??RK=Nt;HDHi>P0iTArv8}P%`BLc_G~6e`66MYk=dl6K z1>Ft9bVSwN+3MZ)N+G^8Cztc33MVapvnG5|&^QdjKtZ@XDiValVWO$psZLmg+cJh` zfB?ZE?~t8SGc}GYdcfA%7dVW%5N>A2?VT_Ge$`b&^>*=AcI=1q{-vD54bfD%R3iuv z!sW`g@9P=BwsRrC5j!~DgDWnWdQ8LxsxBOahVO%lNTL5Bp(qcp-F{Ws{SbApwCHnU z$@;JaOFo6k&7h%Lu-)(4!9{c$MhBAhXS@;|Bur~#&b_>hcXMsynG~uE%>6HRNclm# zdR5%ej&v5tDnoELC%{IF<+Si*a8oh!1I98g*~6yubXNvD-(~>R9I6W8GCIKP@@-A; zW{3p-md|q=QP-`DCKmP%p@?_~1z$J3GuRf<&5yUpVMh3qL;{-*Af>@*9bwg#5YI8T zN0dC}jyLH5`r#(P?zi-8N8GPHfO!&z3_IO#|4ars%P@hd2L2wJN|m z1?r&5?h1q`P!A5#gSDrbka^BVXe)Z=;RYf4slAJBgFz&{DusDc_2YQKk-V2~ZiqTZ z4}1QF*bM@G+A)GY&ogb~;A4voJ-I-{Q}@+TRxcGKKta3&YTEZ<0z=O-0nIlEa1Tw| zk!fKEJu;k@QANN0+yyKo-U{W3rLEum-$2{V*hTRc!1Btu)4!UVWat9rIXfGA0+ued z$W!BKX`H8J-Ewwz#zL^|H+TF(&cxTAQMnw>os6X#InMQ(bMJ7t@Q)X^bW@^v?>N%_ z-e4NC_=H97cx@`h1ajg;(7@qjeU+2O(h2ID>YMhdn)0`kn;d?Z1n}meV&$w~C!LYP zlj+X>A!xuG0`p4r$c7xNFzu+?42hs5zlZ^l2H%ikbMt*V* zz3~c>b5+`f;jm}3{JNQB@7t?;RzSD)a^jfm6E#iOJO~paJRa{6l#|m+8LLlzDemqT z3OW_WZ-Cg3=ew96ql=2&wM2Y*OtX=~ckPN~gojkuAcny3cY(ZkjVC9||IIt+u_Ga>8XalPCy zg4>#F&&Me-qtK_S{)g^>nwv1qd+@T;5@edB`H^9a`&Mx|Gvwzkv-kk5I=xAo!2}6y zn<)pgD?`TA*(cm2^*5U*y}a3XoHH2mG17H)I(xN|3vAd9n6GsPuN@O-t@#?fdE&fj zv=Ri&)+~L%ON;!EJtXuQ04Oi;!(-wjm;oNf#s)PtJl)}esi@_n8{4_J&&_O_=~j-B z(ow9KSTT0{bx&&;`I@eg?uZl!M)9lOj5GEALrizScTZBx?KKz@&p6*9JYFdbPVU;O z)cAHX#n8DQJGAxi=Cw?ykIf?;s)9vny8~AO160m|TPN~q zE0=x=*aHwy?1XdwivXBSe|sH!yLmk@e4QkbZ`PfcyTun9lo=EoHRo;RnSE5I8H=)O z9{NU)>Iz?UY$_hcD~UKVO@k@olS$Oaf)6}~DjD5pQc-KBG% z)cqJ4a@p=49Xa0XWX>uOQ&%5(FS|sCRqIonUO&p5EkBkxOBb6*4zq_#Zb|!HD;{u` z=M05wI|W-9TU$#=7g^-@c4fybvIDK}|8V_qc3^4qgfcR!BT^zA%Ml~f6hr*CzlfCA zbo5@I@WfV13KzaSZNvd<4b-;GfCqs#GIhQfeK~W+vg3HyRgicv#dXwlzP>%|>P=5> z(o=d{tFvcxR$EO5mO6iNrg+{e_nBOoOIgu)_G)hLTE%z7xq0$z#V~d5NovctEpBbo z(3{+E_FRw(tOBERwfwaH{&Q>H{zaQA{Y3{$B`{^+2 z4b=bAaZxXf0eRb9%!EE=&I1EU=Sp=>r+h1ba{rwyty#*eNC07w^JM& zzNzI!4PO&m{%yZz`p9$EVj@&$@esv;Q`cne>l;jxlo@DMqSv+9tG~G4Yx5|$z0KPQ zt2D9M%gSUGM%(_<>E;?vD)Yh9Edu%FbTo8SAs^_rj@oYVF(KDvJ} z9&LBTiHsJX>PU0|;nx*E!4z_?g1!u@Cukh^AF6vqY;exfh|fPK$1(cmY-g-h8T{CJ zM!BJ*BpWH`c?$NI5t1+Y8fRs9KsfT_vgw7DZv1@YY=^x^3vV>9cHBpMOJv$WT88Vd z__Vd8V?frvcn?39zYWTb>>K|&duig~b9`(NUUpv@=G;b@V`aRPzPoPQtsMSkE47Z# zC&K#gXS$Mb_p@svRffs-f(O?-&%+F*`5X`?8r#JT>=??|>$pFloS=M+4sT=^)gLQB zAxEQ(jRg^;=ySOy0i;f?w)G>?p3fpQQ}{3|_WA*n2gb4${|_YZ{vVROTl!xdc?U?= zh66xys9D^#{NDi}S+qE?^e>PMK+O)2$rS15f^#aC1D+%CpOgMbQi_j%U{-$fD|aF> z&fb#I)%uI-y5>{!{luwDtQaM@(`ZK&d~f^tIaTobU*ow+H^o~vkab|vBW^_bsh9+b zUx)R0lUs|X6#koevhvIGO$M2Q<#^|fH>NVd zO{UYW*Cw}W)5ocYezad$%_8n)CY?H%0u=`F-mV|wp{$WCE}vpL&WkW8-v?ZBX73pz zEyyZV@p%DMiTC9Nj-+(>D5OZMaHmyO>LBGAH_lS%FDc`#vRX0@otNyKp@vw8UXTTZ z?3pS=p##~g4uZokBICHz4i=gyo1E8}=k0Mn1eOovz{h7q`u-Vrf5;G@=UX|H{oK_e z3lx%jq;T%&CpwUU&7TKi2Umzg+iFeQ>vIy^8WHy+-xrD$no|$ zDb#pn*TseXNi@&G-%Uv~vd$hbWZODVbD? z2ftdd`-hmm6i>1Bfr%)=(N%$hYV6R+;i@dHcY~mD4czJTkAMA^D{$A{NVgw4HJ^wz zOrPwp)O^hu4mc}B7<>yu|Dg$8-Aq1KQ?T)LOLOe?8uGM$R7Y(QP(ja`bEi?^=5%t-A#ot~nar?5epXWn;UpgQ8 zvU*O-0AAK`X+p84V&#n9{LeIhAvi!4>JfEyR&2eS+7w)a`|-v7*- zKbbF{1#(Z#b$WYi&n=IRTv{-oK9@T5=|tW~6e0){OA5QUaEWh$eE`(4r||1!%;gH9 z=@X3{Qat{$4up!7rUg~)tz8aEdeE-fgG@X*7!sV|G(AKu-!pYk=IASvFcb&GQ}AyY z?!V1W@!7U-cl!m9Y!ICu zRq`Apf=bI})?%7SLcA>w1LbyzW2)9HH_i-3lPT*#P2@Tf3Ke-J2U^fqOPs`ICK>cI zF4g>Kum4eNDizX~n=W7`aIjqgkP6&8bd8LYyT@pV8JKM$bKHfVU zPiYTF=#Dn0+>9j++Z)P7#ZMx`d-F=QGbrz`yr9kmMy{n_Iu&f!TJ_<_auVmHCTs&z zL>an88fQ1E=5%%Su^{g^Mb^;l*Fi&4EHEwUpnO=68a- zX!WUDki8vsM0EH;B%O&tIYJHNLt@S#D3g~4^5&himnX-UI7Bw%lL;L=x)NiH7S zg_Jg@W0_ZN`o69}x+1fWGluECvZ<8D7fmR~DNU=6sG(P3{{20xHCD}&xWLbpI!JUy zPyOqCC`cf~?8oJca6D$GfNEvUie8w?JNkCRB(Py3o?_ZQV!n>tI~8x8j9SV{;#Xr- zv%9^ouOoW&b73=XZ2tbh3~I&PvwvivKa_v&qDka&XCqXlb7)Pf($&td8f}ASk3R1X z6Pf2xCzc-c%t|$4plK&kfg4UsNl)kglbQPm!sHE?Q0??{m#a90R`zb$1oo~yqOTpq zT;8!)%SkakVtC_%YHpMUP!eZr*_ z)){y+Q48FWWx%eB4MI55bHcE23lajChIpLS(@%5Mi57wMJ0O60gY+==GMMN|aGsNk zuX{q_jMK^PE5I(&n8KQ2zS>jcS6M1LI#3mzOC8~p7V>EUAhZrtw-N^H@qV6%!$87; z=pr0}#z4qv0p`ojnmy+S;%hrwPR(Wy{_}LfAyUG5AhejDD5BIjccCDd*q{J&u3TKW zj_62Nk;NHo0FcBAM>y7GwhdT>i>&3K55T=;jVgf}g@(qP#w*Ec0)sbcPG9JSkfyvK z$dew{!F&bq{wOK}SWiPrKt?#~3_(E-LUtp&vGXCc>zQDBFd5rRODYqOQ3eLU?;oA{ zoyD;~4*lic6^ZGO*__jL@~N=ic9JTEFAHe^LkW{0v?CW%v^THlXgAFfo|ifZgM;rL zO6`4EG+!808YUd2M5=$8z8s)HBLzI`up0$G!y?iC~|@dmjUns9NH0q zfiS7_>}BAD+ce!C3<+)xq3U-A4jrsV6fe(mllG=mUT$@yqxF*flLIG)dlNEM z`{%B;+<=O3*^2w~z5dH& zc}#IxRefpz$dTioBX`lUhT4OpYWH|UeNmQF7~DE~C>JhM2Prbo3jtUECtY>?&Fbd% z{+KRX7^FY5Fu|^{4pMy6geTNfm7-yQL@-MV4$i){FE1@%PD@k*&I9xU?Z}~p!@>WJ zjY6Q&I)I4%zm=bRLo}q57T>0rW^Pd;UXZE4rABVlI8fS12c6EW|Fcxn8NiIRyXo|J zxD*?o{7Ui}svt236H5-J3N38}dpoj-hKbht5-tAw20mXnJ zX;=jS$O;7mz!OyjE(GX9m;r<^jBS|n*ZvA?N5lo5=>Wf@)r-W~l5Q_R*}YX$g(1pJ zCoWtn!^H&X=pu#$hJ?o-?to^K}u z`**ORe^QZOo%}bD!|xu?gfRRzvud|;_ayuKyavSIL`$3ae zOv6#bwFf{rDsntJY&tGpHJ@3j7z3QI0T&fC;xJ$xD8Qm(ubT<=dGqQPTDEZzYOn~M;9P2z`sonb?m5u3|xwutD|w!zSxdE;=>x&WDop;1vH_Bd)briZ&NnONTz~W_+1^)XcE}zbQ)Io~tZwC+*Z`24i|}${U&r~9 z2M$5?r{0|3$0dCgAAyOZ%_QvTege4N|aF%kn&D8B>vV~b~tPL#n%_{k2U4G z9pY-V#!P|Tnp4^RFK0chgE4Org2ErbSMeVd<{ds}7lr{)I1Uqrz`0T3Hr{>?17-Uj zU32Bfu5`Ra#Ha+ET2%qd4=;Af8)^;5EGJKjS<5z!#Av*;$TcWrg(vhSObdg!5nXVR z-&MN)R&q=0Ku(Nn?0{v{cfgzh&wzs8V3mN+uW`j+f4shitJxOhmn*bH*Ucs-KcC0@ z*9qC1bwa~{k&yuGv}%tMZ;-_h!j&*8q_l1d5vT%7yd-KGpe=Exx~X~?P^sNq_wu=Do5;i5R#Yd(9ZC==W<8kMzt_4(&6Bbsn1Ho)1?SGR(r=cpj1oeyes32+8O+}u(z zH*Y_B_Q&>t{xS(C1)q(J!DbvJv#AJ`bgJIO)DayRBnq~5EFOdg=84>NlGIo)Q9tR% zvR|+BR@Rr!`3{D!->oS`77Y`woqt%*kwof)Drpz0=(}^jE`XB%QeK5#Xq8t9h^o{P z&?f>E(Aovc%{N01mgY!7mtA`ucKh9Ci3;r9z!eMjZ~pEMTje^gIKjEDY^xK8JLc(Aj!kw3|eIJ9i;LZ>$;Ol2qQ*UXH4qT z#sa1Fb~BE>)VfBJ0??a#(moL5BKgdtmK*z^wNrsd%e)|I{^MUTCe-lWdMd2Pr)$r` zfj4#az=yAVgXy^xX7&bLjxiG_0L-Js0+6iG^x?@xE?ySPzp7UN@z18TeZfB8y}Lo$mqO z>1~f8!|7LLPGf)Wy8Ck%aH7WkvnlbnM_hu;7Zg0ZCk;~$p$NH#mv^%BK#rcJ#54Y2 z9YiQ&5SUI8JD!G{q;SJVweE)ExwY$#@t8eQpU$7WuP}0W%Sh*Rft2;&7F?Kb6rwJP zSI|92JbAI3+RvUtTQ<75E&^zCH?#?~;jG%7`-w~I5$YN%`5HXfot~M}W~GL_QQ)Yj z=qRP=5?%n(%S{Do=Uzj$ZMzj*k;H|~C^tFGZ2DAxU(`7>Xf8ZV?D_As>T>Q|s{pM~ z_z}pnt8zfbOlFO9*&}(6?e3A?QYS;=BlFCB`ffM>#SOdju3GD!m`pRpo*e6$e)7C( zJVnP|`GEV-ikUd00tA;b4RdpSh3qNq@#)y@uxsDXU3=UAUcdOvQOb1@hTV+z`JviP z>?!%VizKiJT2jgEOL?;e(utpK0tp)S_nHZ z^Ex*Puq}+7PHYh{0qkg`0)$2b9yyTPk0aNFr%N;$}|-( z(32IC^DorQq!q?sw&^2vY7?`;bODf_wPaGUQ`R_&}gd-O`T9H=1t zNq@QS-46#fD}gymXJ^KNgL`XcV*VK8(0vhf@sYRF_iQTAi52x+J!&E* z`TnNrd}Mgmik^cn(uMktD?V{S>}0J86r)PshK?49&uFbVKImC;s;}>?v~PiwBzKR! zQcjNuf7=i~4diE@eb!w!SxYBornuo!79FiFSIr|oZYzmfJ1`p`Hx!eQ|M(ZI`z3tFCtoGS3;DW4Lw4v!c+F(QfvQYwYLsJha4RSi9O9CeE_aFMr`@H>-0{6Su}RB zcJhc3)z$UdSkLk~E*ut63p=ch``2 zS-x03YRjH=nKMqcUBA0bLD!WxP%}AMQZSg>&s`cq1|w_bb^rYLX{}!Mjg&p1rrE90 zo4XSoxT=q-lJW(*Pn*x6?2`s$Z98j?TFd`K9quJBJPR)HC|ERm1gw)d@Uhg^e7_gb zO%IvLyd>qt5Mrg+C8jBQ?Fv2Yfh_gR+paOpx_q;S?d`nTGt=`PukUp^O+A2vq6>Yj z#w|s*CI)FvPT1bg_m!FUYAxj4&4C4N*04B*zX~Q8RJAQ0Ew6A$Yb~9b8^5e^YrG`k z`^??MaDYvYzt23Xq1}&z#`M`=d)}`0dZPTIZi4!@yGDpi?kCf!TC!=PeV386rC-}! zQ)%zK(fc#*3CzD1Z<-DVg(u*XJWsbGE@R}PC)PQni4rKfvs06>IDdk;=lw)NuI|A6 z8PAZhe8WvRMKyTgbn^X$^$!I{1=#!wa&uRCLV^4&{e$+Z) z#uy)TikYg-e`0%s+oTol+T>buYW@tLwq_o-42KqX!8I|4g|niF(OmhfxgnN6NB$2> Ci3 Navigator.of(context).push(LaunchesPage.route()), + title: Text(l10n.latestLaunchSpaceXTileTitle), + imageUrl: 'assets/images/img_spacex_launch.jpeg', + ), + ), + ), Expanded( child: Padding( padding: const EdgeInsets.symmetric( From 6c33f52c906bcd3ab7bad9f1312f867a37e5612c Mon Sep 17 00:00:00 2001 From: Nino Gjoni <49320058+ngjoni@users.noreply.github.com> Date: Tue, 16 Nov 2021 19:34:47 +0100 Subject: [PATCH 10/26] feat: add links and patch in launch model --- .../spacex_api/lib/src/models/launch.dart | 69 +++++++++++++++++++ .../spacex_api/lib/src/models/launch.g.dart | 26 +++++++ 2 files changed, 95 insertions(+) diff --git a/packages/spacex_api/lib/src/models/launch.dart b/packages/spacex_api/lib/src/models/launch.dart index 69b5207..eaab1e4 100644 --- a/packages/spacex_api/lib/src/models/launch.dart +++ b/packages/spacex_api/lib/src/models/launch.dart @@ -78,3 +78,72 @@ class Launch extends Equatable { @override String toString() => 'Latest Launch($id, $name)'; } + +/// {@template links} +/// A model that represents available links to images, videos and articles. +/// {@endtemplate} +@JsonSerializable( + fieldRename: FieldRename.snake, +) +class Links extends Equatable { + /// {@macro length} + const Links({ + required this.patch, + required this.webcast, + required this.wikipedia, + }); + + /// The Patch for the launch mission + final Patch patch; + + /// The launch video link + final String webcast; + + /// The latest launch information on wikipedia + final String wikipedia; + + @override + List get props => [patch, webcast, wikipedia]; + + /// Converts a JSON [Map] into a [Links] instance. + static Links fromJson(Map json) => _$LinksFromJson(json); + + /// Converts this [Links] instance into a JSON [Map]. + Map toJson() => _$LinksToJson(this); + + @override + String toString() => + 'Links(Patch($patch), Webcast: $webcast, Wikipedia: $wikipedia)'; +} + +/// {@template Patch} +/// A model that represents small and large images of the mission patch. +/// {@endtemplate} +@JsonSerializable( + fieldRename: FieldRename.snake, +) +class Patch extends Equatable { + /// {@macro patch} + const Patch({ + required this.small, + this.large, + }); + + /// A small patch image link + final String small; + + /// A large patch image link + final String? large; + + @override + List get props => [small, large]; + + /// Converts a JSON [Map] into a [Patch] instance. + static Patch fromJson(Map json) => _$PatchFromJson(json); + + /// Converts this [Patch] instance into a JSON [Map]. + Map toJson() => _$PatchToJson(this); + + @override + String toString() => 'Patch(Small: $small , Large: $large)'; +} diff --git a/packages/spacex_api/lib/src/models/launch.g.dart b/packages/spacex_api/lib/src/models/launch.g.dart index 303fdd3..985a116 100644 --- a/packages/spacex_api/lib/src/models/launch.g.dart +++ b/packages/spacex_api/lib/src/models/launch.g.dart @@ -37,3 +37,29 @@ Map _$LaunchToJson(Launch instance) => { 'date_utc': instance.dateUtc?.toIso8601String(), 'date_local': instance.dateLocal?.toIso8601String(), }; + +Links _$LinksFromJson(Map json) { + return Links( + patch: Patch.fromJson(json['patch'] as Map), + webcast: json['webcast'] as String, + wikipedia: json['wikipedia'] as String, + ); +} + +Map _$LinksToJson(Links instance) => { + 'patch': instance.patch, + 'webcast': instance.webcast, + 'wikipedia': instance.wikipedia, + }; + +Patch _$PatchFromJson(Map json) { + return Patch( + small: json['small'] as String, + large: json['large'] as String?, + ); +} + +Map _$PatchToJson(Patch instance) => { + 'small': instance.small, + 'large': instance.large, + }; From 1d66dda1168901eea908c63d23d4d11432729337 Mon Sep 17 00:00:00 2001 From: Nino Gjoni <49320058+ngjoni@users.noreply.github.com> Date: Tue, 16 Nov 2021 19:38:57 +0100 Subject: [PATCH 11/26] feat: add links in launch --- packages/spacex_api/lib/src/models/launch.dart | 4 ++++ packages/spacex_api/lib/src/models/launch.g.dart | 2 ++ 2 files changed, 6 insertions(+) diff --git a/packages/spacex_api/lib/src/models/launch.dart b/packages/spacex_api/lib/src/models/launch.dart index eaab1e4..3c48210 100644 --- a/packages/spacex_api/lib/src/models/launch.dart +++ b/packages/spacex_api/lib/src/models/launch.dart @@ -13,6 +13,7 @@ class Launch extends Equatable { const Launch({ required this.id, required this.name, + required this.links, this.details, this.crew, this.flightNumber, @@ -53,6 +54,9 @@ class Launch extends Equatable { /// The launch date final DateTime? dateLocal; + /// Available source links + final Links links; + @override List get props => [ id, diff --git a/packages/spacex_api/lib/src/models/launch.g.dart b/packages/spacex_api/lib/src/models/launch.g.dart index 985a116..dded7cd 100644 --- a/packages/spacex_api/lib/src/models/launch.g.dart +++ b/packages/spacex_api/lib/src/models/launch.g.dart @@ -10,6 +10,7 @@ Launch _$LaunchFromJson(Map json) { return Launch( id: json['id'] as String, name: json['name'] as String, + links: Links.fromJson(json['links'] as Map), details: json['details'] as String?, crew: (json['crew'] as List?) ?.map((e) => CrewMember.fromJson(e as Map)) @@ -36,6 +37,7 @@ Map _$LaunchToJson(Launch instance) => { 'success': instance.success, 'date_utc': instance.dateUtc?.toIso8601String(), 'date_local': instance.dateLocal?.toIso8601String(), + 'links': instance.links, }; Links _$LinksFromJson(Map json) { From 23847f2edf10aad32f0197173839d5632763774c Mon Sep 17 00:00:00 2001 From: Nino Gjoni <49320058+ngjoni@users.noreply.github.com> Date: Tue, 16 Nov 2021 21:06:00 +0100 Subject: [PATCH 12/26] feat: add launch intl --- lib/l10n/arb/app_en.arb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index a07d6c7..eaef4fd 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -16,6 +16,10 @@ "@latestLaunchSpaceXTileTitle": { "description": "Text included as title on the latest launch tile on the home page" }, + "latestLaunchAppBarTitle": "Latest Launch", + "@latestLaunchAppBarTitle": { + "description": "Text shown in the AppBar of the latest Launch Page" + }, "rocketsAppBarTitle": "Rockets", "@rocketsAppBarTitle": { "description": "Text shown in the AppBar of the Rockets Page" @@ -33,6 +37,19 @@ } } }, + "latestLaunchSubtitle": "Launched: {date}", + "@latestLaunchSubtitle": { + "description": "Subtitle text shown on the Launches Page that indicates the latest launch.", + "placeholders": { + "date": { + "example": "31-12-2021" + } + } + }, + "openWebcastButtonText": "Open Webcast", + "@openWebcastButtonText": { + "description": "Button text shown on the Rocket Details Page that opens the corresponding Webcast page." + }, "openWikipediaButtonText": "Open Wikipedia", "@openWikipediaButtonText": { "description": "Button text shown on the Rocket Details Page that opens the corresponding Wikipedia page." From 41bb55b8032fe0186a2fbf95eccd19436ed9ac5b Mon Sep 17 00:00:00 2001 From: Nino Gjoni <49320058+ngjoni@users.noreply.github.com> Date: Tue, 16 Nov 2021 21:06:31 +0100 Subject: [PATCH 13/26] feat: add latest launch page --- lib/home/widgets/home_page_content.dart | 3 +- lib/launches/view/launches_page.dart | 108 ++++++++++++++++++++++-- 2 files changed, 102 insertions(+), 9 deletions(-) diff --git a/lib/home/widgets/home_page_content.dart b/lib/home/widgets/home_page_content.dart index 1a151ea..0f76082 100644 --- a/lib/home/widgets/home_page_content.dart +++ b/lib/home/widgets/home_page_content.dart @@ -24,7 +24,8 @@ class HomePageContent extends StatelessWidget { ), child: SpaceXCategoryCard( key: const Key( - 'homePageContent_latestLaunch_spaceXCategoryCard'), + 'homePageContent_latestLaunch_spaceXCategoryCard', + ), onTap: () => Navigator.of(context).push(LaunchesPage.route()), title: Text(l10n.latestLaunchSpaceXTileTitle), imageUrl: 'assets/images/img_spacex_launch.jpeg', diff --git a/lib/launches/view/launches_page.dart b/lib/launches/view/launches_page.dart index 552e531..d5a7dd8 100644 --- a/lib/launches/view/launches_page.dart +++ b/lib/launches/view/launches_page.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:intl/intl.dart'; import 'package:launch_repository/launch_repository.dart'; import 'package:spacex_demo/l10n/l10n.dart'; import 'package:spacex_demo/launches/cubit/launches_cubit.dart'; +import 'package:url_launcher/url_launcher.dart'; class LaunchesPage extends StatelessWidget { const LaunchesPage({Key? key}) : super(key: key); @@ -33,7 +35,7 @@ class LaunchesView extends StatelessWidget { return Scaffold( appBar: AppBar( - title: Text(l10n.rocketsAppBarTitle), + title: Text(l10n.latestLaunchAppBarTitle), ), body: const Center( child: _Content(), @@ -78,16 +80,106 @@ class _LatestLaunch extends StatelessWidget { @override Widget build(BuildContext context) { + final l10n = context.l10n; + final latestLaunch = context.select((LaunchesCubit cubit) => cubit.state.latestLaunch!); return Container( - alignment: Alignment.center, - child: Column( - children: [ - Text(latestLaunch.name), - Text('${latestLaunch.flightNumber}'), - ], - )); + padding: const EdgeInsets.only(top: 10), + child: Column( + children: [ + //Latest Launch Row, following widget could be a list of launches + ListTile( + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + CircleAvatar( + backgroundImage: NetworkImage(latestLaunch.links.patch.small), + radius: 50, + ), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Row( + children: [ + // const Text('Name: '), + Text(latestLaunch.name), + ], + ), + Row( + children: [ + // const Text('Flight Number:'), + Text('${latestLaunch.flightNumber}'), + ], + ), + ], + ), + ) + ], + ), + subtitle: latestLaunch.dateUtc == null + ? null + : Text( + l10n.latestLaunchSubtitle( + DateFormat('dd-MM-yyyy hh:mm') + .format(latestLaunch.dateUtc!), + ), + ), + ), + const SizedBox( + height: 35, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Positioned( + left: 16, + bottom: 16, + right: 16, + child: SizedBox( + height: 64, + child: ElevatedButton( + key: const Key( + 'launchesPage_openWebcast_elevatedButton', + ), + onPressed: () async { + final url = latestLaunch.links.webcast; + + if (await canLaunch(url)) { + await launch(url); + } + }, + child: Text(l10n.openWebcastButtonText), + ), + ), + ), + Positioned( + left: 16, + bottom: 16, + right: 16, + child: SizedBox( + height: 64, + child: ElevatedButton( + key: const Key( + 'launchesPage_openWikipedia_elevatedButton', + ), + onPressed: () async { + final url = latestLaunch.links.wikipedia; + + if (await canLaunch(url)) { + await launch(url); + } + }, + child: Text(l10n.openWikipediaButtonText), + ), + ), + ), + ], + ), + ], + ), + ); } } From 9b1fd1468e9964a596cf6b028c67ebfaec8df3c5 Mon Sep 17 00:00:00 2001 From: Nino Gjoni <49320058+ngjoni@users.noreply.github.com> Date: Tue, 16 Nov 2021 22:03:03 +0100 Subject: [PATCH 14/26] feat: add test for launches --- .../test/src/launch_repository_test.dart | 62 ++++++- test/helpers/pump_app.dart | 7 + test/launches/cubit/launches_cubit_test.dart | 70 ++++++++ test/launches/cubit/launches_state_test.dart | 42 +++++ test/launches/view/launches_page_test.dart | 165 ++++++++++++++++++ 5 files changed, 343 insertions(+), 3 deletions(-) create mode 100644 test/launches/cubit/launches_cubit_test.dart create mode 100644 test/launches/cubit/launches_state_test.dart create mode 100644 test/launches/view/launches_page_test.dart diff --git a/packages/launch_repository/test/src/launch_repository_test.dart b/packages/launch_repository/test/src/launch_repository_test.dart index 9799c78..a1b0b78 100644 --- a/packages/launch_repository/test/src/launch_repository_test.dart +++ b/packages/launch_repository/test/src/launch_repository_test.dart @@ -1,11 +1,67 @@ // ignore_for_file: prefer_const_constructors import 'package:launch_repository/launch_repository.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:spacex_api/spacex_api.dart'; import 'package:test/test.dart'; +class MockSpaceXApiClient extends Mock implements SpaceXApiClient {} + void main() { - group('LaunchesRepository', () { - test('can be instantiated', () { - expect(LaunchRepository(), isNotNull); + group('LaunchRepository', () { + late SpaceXApiClient spaceXApiClient; + late LaunchRepository subject; + + final latestLaunch = Launch( + id: '0', + name: 'mock-launch-name', + links: const Links( + patch: Patch( + small: 'https://avatars.githubusercontent.com/u/2918581?v=4', + large: 'https://avatars.githubusercontent.com/u/2918581?v=4', + ), + webcast: 'https://www.youtube.com', + wikipedia: 'https://www.wikipedia.org/', + ), + ); + + setUp(() { + spaceXApiClient = MockSpaceXApiClient(); + when(() => spaceXApiClient.fetchLatestLaunch()) + .thenAnswer((_) async => latestLaunch); + + subject = LaunchRepository(spaceXApiClient: spaceXApiClient); + }); + + test('constructor returns normally', () { + expect( + () => LaunchRepository(), + returnsNormally, + ); + }); + + group('.fetchLatestLaunch', () { + test('throws LaunchException when api throws an exception', () async { + when(() => spaceXApiClient.fetchLatestLaunch()).thenThrow(Exception()); + + expect( + () => subject.fetchLatestLaunch(), + throwsA(isA()), + ); + + verify(() => spaceXApiClient.fetchLatestLaunch()).called(1); + }); + + test('makes correct request', () async { + await subject.fetchLatestLaunch(); + + verify(() => spaceXApiClient.fetchLatestLaunch()).called(1); + }); + }); + + test('makes correct request', () async { + await subject.fetchLatestLaunch(); + + verify(() => spaceXApiClient.fetchLatestLaunch()).called(1); }); }); } diff --git a/test/helpers/pump_app.dart b/test/helpers/pump_app.dart index ff82dd3..9f6dfe2 100644 --- a/test/helpers/pump_app.dart +++ b/test/helpers/pump_app.dart @@ -4,11 +4,14 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:launch_repository/launch_repository.dart'; import 'package:mockingjay/mockingjay.dart'; import 'package:mocktail/mocktail.dart'; import 'package:rocket_repository/rocket_repository.dart'; import 'package:spacex_demo/l10n/l10n.dart'; +import '../launches/cubit/launches_cubit_test.dart'; + class MockRocketRepository extends Mock implements RocketRepository {} class MockCrewMemberRepository extends Mock implements CrewMemberRepository {} @@ -19,6 +22,7 @@ extension PumpApp on WidgetTester { MockNavigator? navigator, RocketRepository? rocketRepository, CrewMemberRepository? crewMemberRepository, + LaunchRepository? launchRepository, }) { final innerChild = Scaffold( body: widget, @@ -32,6 +36,9 @@ extension PumpApp on WidgetTester { ), RepositoryProvider.value( value: crewMemberRepository ?? MockCrewMemberRepository(), + ), + RepositoryProvider.value( + value: launchRepository ?? MockLaunchRepository(), ) ], child: MaterialApp( diff --git a/test/launches/cubit/launches_cubit_test.dart b/test/launches/cubit/launches_cubit_test.dart new file mode 100644 index 0000000..b3856b3 --- /dev/null +++ b/test/launches/cubit/launches_cubit_test.dart @@ -0,0 +1,70 @@ +// ignore_for_file: prefer_const_constructors +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:launch_repository/launch_repository.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:spacex_api/spacex_api.dart'; +import 'package:spacex_demo/launches/cubit/launches_cubit.dart'; + +class MockLaunchRepository extends Mock implements LaunchRepository {} + +void main() { + group('CrewCubit', () { + late LaunchRepository launchRepository; + + final latestLaunch = Launch( + id: '0', + name: 'mock-launch-name', + links: const Links( + patch: Patch( + small: 'https://avatars.githubusercontent.com/u/2918581?v=4', + large: 'https://avatars.githubusercontent.com/u/2918581?v=4', + ), + webcast: 'https://www.youtube.com', + wikipedia: 'https://www.wikipedia.org/', + ), + ); + + setUp(() { + launchRepository = MockLaunchRepository(); + when(() => launchRepository.fetchLatestLaunch()) + .thenAnswer((_) async => latestLaunch); + }); + + test( + 'initial state is correct', + () => { + expect( + LaunchesCubit(launchRepository: launchRepository).state, + equals(const LaunchesState()), + ) + }, + ); + + blocTest( + 'emits state with updated launch', + build: () => LaunchesCubit(launchRepository: launchRepository), + act: (cubit) => cubit.fetchLatestLaunch(), + expect: () => [ + const LaunchesState(status: LaunchesStatus.loading), + LaunchesState( + status: LaunchesStatus.success, + latestLaunch: latestLaunch, + ), + ], + ); + + blocTest( + 'emits failure state when repository throws exception', + build: () { + when(() => launchRepository.fetchLatestLaunch()).thenThrow(Exception()); + return LaunchesCubit(launchRepository: launchRepository); + }, + act: (cubit) => cubit.fetchLatestLaunch(), + expect: () => [ + const LaunchesState(status: LaunchesStatus.loading), + const LaunchesState(status: LaunchesStatus.failure), + ], + ); + }); +} diff --git a/test/launches/cubit/launches_state_test.dart b/test/launches/cubit/launches_state_test.dart new file mode 100644 index 0000000..b482e38 --- /dev/null +++ b/test/launches/cubit/launches_state_test.dart @@ -0,0 +1,42 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:spacex_api/spacex_api.dart'; +import 'package:spacex_demo/launches/cubit/launches_cubit.dart'; + +void main() { + group('CrewState', () { + test('supports value comparison', () { + expect( + const LaunchesState( + status: LaunchesStatus.success, + latestLaunch: Launch( + id: '0', + name: 'mock-launch-name', + links: Links( + patch: Patch( + small: 'https://avatars.githubusercontent.com/u/2918581?v=4', + large: 'https://avatars.githubusercontent.com/u/2918581?v=4', + ), + webcast: 'https://www.youtube.com', + wikipedia: 'https://www.wikipedia.org/', + ), + ), + ), + const LaunchesState( + status: LaunchesStatus.success, + latestLaunch: Launch( + id: '0', + name: 'mock-launch-name', + links: Links( + patch: Patch( + small: 'https://avatars.githubusercontent.com/u/2918581?v=4', + large: 'https://avatars.githubusercontent.com/u/2918581?v=4', + ), + webcast: 'https://www.youtube.com', + wikipedia: 'https://www.wikipedia.org/', + ), + ), + ), + ); + }); + }); +} diff --git a/test/launches/view/launches_page_test.dart b/test/launches/view/launches_page_test.dart new file mode 100644 index 0000000..47bbccf --- /dev/null +++ b/test/launches/view/launches_page_test.dart @@ -0,0 +1,165 @@ +// ignore_for_file: prefer_const_constructors +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:launch_repository/launch_repository.dart'; +import 'package:mockingjay/mockingjay.dart'; +import 'package:mocktail_image_network/mocktail_image_network.dart'; +import 'package:spacex_api/spacex_api.dart'; +import 'package:spacex_demo/launches/launches.dart'; + +import '../../helpers/helpers.dart'; + +class MockLaunchRepository extends Mock implements LaunchRepository {} + +class MockLaunchesCubit extends MockCubit + implements LaunchesCubit {} + +void main() { + final latestLaunch = Launch( + id: '0', + name: 'mock-launch-name', + links: const Links( + patch: Patch( + small: 'https://avatars.githubusercontent.com/u/2918581?v=4', + large: 'https://avatars.githubusercontent.com/u/2918581?v=4', + ), + webcast: 'https://www.youtube.com', + wikipedia: 'https://www.wikipedia.org/', + ), + ); + + group('LaunchesPage', () { + late LaunchRepository launchRepository; + + setUp(() { + launchRepository = MockLaunchRepository(); + when( + () => launchRepository.fetchLatestLaunch(), + ).thenAnswer((_) async => latestLaunch); + }); + + test('has route', () { + expect( + LaunchesPage.route(), + isA>(), + ); + }); + + testWidgets('renders LaunchesView', (tester) async { + await tester.pumpApp( + Navigator( + onGenerateRoute: (_) => LaunchesPage.route(), + ), + launchRepository: launchRepository, + ); + + expect(find.byType(LaunchesPage), findsOneWidget); + }); + }); + + group('LaunchesView', () { + late LaunchesCubit launchesCubit; + late MockNavigator navigator; + + setUp(() { + launchesCubit = MockLaunchesCubit(); + navigator = MockNavigator(); + + when(() => navigator.push(any(that: isRoute()))) + .thenAnswer((_) async {}); + }); + + setUpAll(() { + registerFallbackValue(const LaunchesState()); + registerFallbackValue(Uri()); + }); + + testWidgets('renders empty page when status is initial', (tester) async { + const key = Key('launchesView_initial_sizedBox'); + + when(() => launchesCubit.state).thenReturn( + const LaunchesState(), + ); + + await tester.pumpApp( + BlocProvider.value( + value: launchesCubit, + child: const LaunchesView(), + ), + ); + + expect(find.byKey(key), findsOneWidget); + }); + + testWidgets( + 'renders loading indicator when status is loading', + (tester) async { + const key = Key('launchesView_loading_indicator'); + + when(() => launchesCubit.state).thenReturn( + const LaunchesState( + status: LaunchesStatus.loading, + ), + ); + + await tester.pumpApp( + BlocProvider.value( + value: launchesCubit, + child: const LaunchesView(), + ), + ); + + expect(find.byKey(key), findsOneWidget); + }, + ); + + testWidgets( + 'renders error text when status is failure', + (tester) async { + const key = Key('launchesView_failure_text'); + + when(() => launchesCubit.state).thenReturn( + const LaunchesState( + status: LaunchesStatus.failure, + ), + ); + + await tester.pumpApp( + BlocProvider.value( + value: launchesCubit, + child: const LaunchesView(), + ), + ); + + expect(find.byKey(key), findsOneWidget); + }, + ); + + testWidgets( + 'renders the latest launch when status is success', + (tester) async { + const key = Key('launchesView_success_latestLaunch'); + + when(() => launchesCubit.state).thenReturn( + LaunchesState( + status: LaunchesStatus.success, + latestLaunch: latestLaunch, + ), + ); + + await mockNetworkImages(() async { + await tester.pumpApp( + BlocProvider.value( + value: launchesCubit, + child: const LaunchesView(), + ), + ); + }); + + expect(find.byKey(key), findsOneWidget); + }, + ); + }); +} From 483fde1f3cbc8b7a99d13e7410b8708c57b154cc Mon Sep 17 00:00:00 2001 From: Nino Gjoni <49320058+ngjoni@users.noreply.github.com> Date: Tue, 16 Nov 2021 22:10:36 +0100 Subject: [PATCH 15/26] fix: replace positioned with container --- lib/launches/view/launches_page.dart | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/launches/view/launches_page.dart b/lib/launches/view/launches_page.dart index d5a7dd8..fb95062 100644 --- a/lib/launches/view/launches_page.dart +++ b/lib/launches/view/launches_page.dart @@ -134,10 +134,8 @@ class _LatestLaunch extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - Positioned( - left: 16, - bottom: 16, - right: 16, + Container( + alignment: Alignment.bottomCenter, child: SizedBox( height: 64, child: ElevatedButton( @@ -155,10 +153,8 @@ class _LatestLaunch extends StatelessWidget { ), ), ), - Positioned( - left: 16, - bottom: 16, - right: 16, + Container( + alignment: Alignment.bottomCenter, child: SizedBox( height: 64, child: ElevatedButton( From 4a2063cf651347427b255cd832aaba4f9067bf66 Mon Sep 17 00:00:00 2001 From: Nino Gjoni <49320058+ngjoni@users.noreply.github.com> Date: Wed, 1 Dec 2021 20:42:26 +0100 Subject: [PATCH 16/26] chore: implement suggestions --- lib/launches/view/launches_page.dart | 9 +++------ packages/spacex_api/build.yaml | 6 ++++++ packages/spacex_api/lib/src/models/crew_member.dart | 5 +---- packages/spacex_api/lib/src/models/launch.dart | 5 +---- packages/spacex_api/lib/src/models/rocket.dart | 4 +--- packages/spacex_api/lib/src/spacex_api_client.dart | 8 ++++---- test/helpers/pump_app.dart | 4 ++-- test/launches/cubit/launches_cubit_test.dart | 6 +++--- test/launches/cubit/launches_state_test.dart | 2 +- 9 files changed, 22 insertions(+), 27 deletions(-) create mode 100644 packages/spacex_api/build.yaml diff --git a/lib/launches/view/launches_page.dart b/lib/launches/view/launches_page.dart index fb95062..25515ce 100644 --- a/lib/launches/view/launches_page.dart +++ b/lib/launches/view/launches_page.dart @@ -38,14 +38,14 @@ class LaunchesView extends StatelessWidget { title: Text(l10n.latestLaunchAppBarTitle), ), body: const Center( - child: _Content(), + child: _LaunchesContent(), ), ); } } -class _Content extends StatelessWidget { - const _Content({Key? key}) : super(key: key); +class _LaunchesContent extends StatelessWidget { + const _LaunchesContent({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -89,7 +89,6 @@ class _LatestLaunch extends StatelessWidget { padding: const EdgeInsets.only(top: 10), child: Column( children: [ - //Latest Launch Row, following widget could be a list of launches ListTile( title: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, @@ -104,13 +103,11 @@ class _LatestLaunch extends StatelessWidget { children: [ Row( children: [ - // const Text('Name: '), Text(latestLaunch.name), ], ), Row( children: [ - // const Text('Flight Number:'), Text('${latestLaunch.flightNumber}'), ], ), diff --git a/packages/spacex_api/build.yaml b/packages/spacex_api/build.yaml new file mode 100644 index 0000000..5d6aeda --- /dev/null +++ b/packages/spacex_api/build.yaml @@ -0,0 +1,6 @@ +targets: + $default: + builders: + json_serializable: + options: + field_rename: snake diff --git a/packages/spacex_api/lib/src/models/crew_member.dart b/packages/spacex_api/lib/src/models/crew_member.dart index b24430f..b9b778c 100644 --- a/packages/spacex_api/lib/src/models/crew_member.dart +++ b/packages/spacex_api/lib/src/models/crew_member.dart @@ -6,7 +6,7 @@ part 'crew_member.g.dart'; /// {@template crew_member} /// A model containing data about a SpaceX crew member /// {@endtemplate} -@JsonSerializable(fieldRename: FieldRename.snake) +@JsonSerializable() class CrewMember extends Equatable { /// {@macro crew_member} const CrewMember({ @@ -60,9 +60,6 @@ class CrewMember extends Equatable { /// Converts this [CrewMember] instance into a JSON [Map] Map toJson() => _$CrewMemberToJson(this); - @override - bool get stringify => true; - @override String toString() => 'Crew Member($id, $name)'; } diff --git a/packages/spacex_api/lib/src/models/launch.dart b/packages/spacex_api/lib/src/models/launch.dart index 3c48210..3887763 100644 --- a/packages/spacex_api/lib/src/models/launch.dart +++ b/packages/spacex_api/lib/src/models/launch.dart @@ -7,7 +7,7 @@ part 'launch.g.dart'; /// {@template launch} /// A model containing data about a launch /// {@endtemplate} -@JsonSerializable(fieldRename: FieldRename.snake) +@JsonSerializable() class Launch extends Equatable { /// {@macro launch} const Launch({ @@ -76,9 +76,6 @@ class Launch extends Equatable { /// Converts this [Launch] instance into a JSON [Map] Map toJson() => _$LaunchToJson(this); - @override - bool get stringify => true; - @override String toString() => 'Latest Launch($id, $name)'; } diff --git a/packages/spacex_api/lib/src/models/rocket.dart b/packages/spacex_api/lib/src/models/rocket.dart index 2890784..8cfd62a 100644 --- a/packages/spacex_api/lib/src/models/rocket.dart +++ b/packages/spacex_api/lib/src/models/rocket.dart @@ -6,9 +6,7 @@ part 'rocket.g.dart'; /// {@template rocket} /// A model containing data about a SpaceX rocket. /// {@endtemplate} -@JsonSerializable( - fieldRename: FieldRename.snake, -) +@JsonSerializable() class Rocket extends Equatable { /// {@macro rocket} const Rocket({ diff --git a/packages/spacex_api/lib/src/spacex_api_client.dart b/packages/spacex_api/lib/src/spacex_api_client.dart index a3f5fd4..6de79d9 100644 --- a/packages/spacex_api/lib/src/spacex_api_client.dart +++ b/packages/spacex_api/lib/src/spacex_api_client.dart @@ -79,10 +79,10 @@ class SpaceXApiClient { /// REST call: `GET /launches/latest` Future fetchLatestLaunch() async { final uri = Uri.https(authority, '/v4/launches/latest/'); - final dynamic responseBody = await _getOne(uri); + final responseBody = await _getOne(uri); try { - return Launch.fromJson(responseBody as Map); + return Launch.fromJson(responseBody); } catch (_) { throw JsonDeserializationException(); } @@ -108,7 +108,7 @@ class SpaceXApiClient { } } - Future _getOne(Uri uri) async { + Future> _getOne(Uri uri) async { http.Response response; try { @@ -122,7 +122,7 @@ class SpaceXApiClient { } try { - return json.decode(response.body); + return json.decode(response.body) as Map; } catch (_) { throw JsonDecodeException(); } diff --git a/test/helpers/pump_app.dart b/test/helpers/pump_app.dart index 9f6dfe2..c3a8209 100644 --- a/test/helpers/pump_app.dart +++ b/test/helpers/pump_app.dart @@ -10,12 +10,12 @@ import 'package:mocktail/mocktail.dart'; import 'package:rocket_repository/rocket_repository.dart'; import 'package:spacex_demo/l10n/l10n.dart'; -import '../launches/cubit/launches_cubit_test.dart'; - class MockRocketRepository extends Mock implements RocketRepository {} class MockCrewMemberRepository extends Mock implements CrewMemberRepository {} +class MockLaunchRepository extends Mock implements LaunchRepository {} + extension PumpApp on WidgetTester { Future pumpApp( Widget widget, { diff --git a/test/launches/cubit/launches_cubit_test.dart b/test/launches/cubit/launches_cubit_test.dart index b3856b3..6ad0caf 100644 --- a/test/launches/cubit/launches_cubit_test.dart +++ b/test/launches/cubit/launches_cubit_test.dart @@ -4,7 +4,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:launch_repository/launch_repository.dart'; import 'package:mocktail/mocktail.dart'; import 'package:spacex_api/spacex_api.dart'; -import 'package:spacex_demo/launches/cubit/launches_cubit.dart'; +import 'package:spacex_demo/launches/launches.dart'; class MockLaunchRepository extends Mock implements LaunchRepository {} @@ -56,10 +56,10 @@ void main() { blocTest( 'emits failure state when repository throws exception', - build: () { + setUp: () { when(() => launchRepository.fetchLatestLaunch()).thenThrow(Exception()); - return LaunchesCubit(launchRepository: launchRepository); }, + build: () => LaunchesCubit(launchRepository: launchRepository), act: (cubit) => cubit.fetchLatestLaunch(), expect: () => [ const LaunchesState(status: LaunchesStatus.loading), diff --git a/test/launches/cubit/launches_state_test.dart b/test/launches/cubit/launches_state_test.dart index b482e38..85eb57d 100644 --- a/test/launches/cubit/launches_state_test.dart +++ b/test/launches/cubit/launches_state_test.dart @@ -1,6 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:spacex_api/spacex_api.dart'; -import 'package:spacex_demo/launches/cubit/launches_cubit.dart'; +import 'package:spacex_demo/launches/launches.dart'; void main() { group('CrewState', () { From a08982625e6cd735abdcb8057e30994c612204d8 Mon Sep 17 00:00:00 2001 From: Nino Gjoni <49320058+ngjoni@users.noreply.github.com> Date: Fri, 28 Jan 2022 04:12:47 +0100 Subject: [PATCH 17/26] chore: implement minor suggestions --- lib/home/widgets/home_page_content.dart | 2 +- lib/launches/cubit/launches_cubit.dart | 8 ++--- lib/launches/cubit/launches_state.dart | 10 ++++++ lib/launches/view/launches_page.dart | 33 ++++++++----------- .../launch_repository/example/lib/main.dart | 5 --- pubspec.lock | 14 ++++---- 6 files changed, 34 insertions(+), 38 deletions(-) diff --git a/lib/home/widgets/home_page_content.dart b/lib/home/widgets/home_page_content.dart index 0f76082..b2b7bbd 100644 --- a/lib/home/widgets/home_page_content.dart +++ b/lib/home/widgets/home_page_content.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:spacex_demo/crew/crew.dart'; import 'package:spacex_demo/home/home.dart'; import 'package:spacex_demo/l10n/l10n.dart'; -import 'package:spacex_demo/launches/view/launches_page.dart'; +import 'package:spacex_demo/launches/launches.dart'; import 'package:spacex_demo/rockets/rockets.dart'; class HomePageContent extends StatelessWidget { diff --git a/lib/launches/cubit/launches_cubit.dart b/lib/launches/cubit/launches_cubit.dart index f698af9..b295078 100644 --- a/lib/launches/cubit/launches_cubit.dart +++ b/lib/launches/cubit/launches_cubit.dart @@ -15,25 +15,23 @@ class LaunchesCubit extends Cubit { Future fetchLatestLaunch() async { emit( - LaunchesState( + state.copyWith( status: LaunchesStatus.loading, - latestLaunch: state.latestLaunch, ), ); try { final latestLaunch = await _launchRepository.fetchLatestLaunch(); emit( - LaunchesState( + state.copyWith( status: LaunchesStatus.success, latestLaunch: latestLaunch, ), ); } on Exception { emit( - LaunchesState( + state.copyWith( status: LaunchesStatus.failure, - latestLaunch: state.latestLaunch, ), ); } diff --git a/lib/launches/cubit/launches_state.dart b/lib/launches/cubit/launches_state.dart index 8dddc84..f96af40 100644 --- a/lib/launches/cubit/launches_state.dart +++ b/lib/launches/cubit/launches_state.dart @@ -11,6 +11,16 @@ class LaunchesState extends Equatable { final LaunchesStatus status; final Launch? latestLaunch; + LaunchesState copyWith({ + LaunchesStatus? status, + Launch? latestLaunch, + }) { + return LaunchesState( + status: status ?? this.status, + latestLaunch: latestLaunch ?? this.latestLaunch, + ); + } + @override List get props => [status, latestLaunch]; } diff --git a/lib/launches/view/launches_page.dart b/lib/launches/view/launches_page.dart index 25515ce..f9f51cb 100644 --- a/lib/launches/view/launches_page.dart +++ b/lib/launches/view/launches_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:intl/intl.dart'; import 'package:launch_repository/launch_repository.dart'; +import 'package:spacex_api/spacex_api.dart'; import 'package:spacex_demo/l10n/l10n.dart'; import 'package:spacex_demo/launches/cubit/launches_cubit.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -50,9 +51,9 @@ class _LaunchesContent extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = context.l10n; - final status = context.select((LaunchesCubit cubit) => cubit.state.status); + final state = context.select((LaunchesCubit cubit) => cubit.state); - switch (status) { + switch (state.status) { case LaunchesStatus.initial: return const SizedBox( key: Key('launchesView_initial_sizedBox'), @@ -68,24 +69,24 @@ class _LaunchesContent extends StatelessWidget { child: Text(l10n.rocketsFetchErrorMessage), ); case LaunchesStatus.success: - return const _LatestLaunch( - key: Key('launchesView_success_rocketList'), + return _LatestLaunch( + key: const Key('launchesView_success_rocketList'), + latestLaunch: state.latestLaunch!, ); } } } class _LatestLaunch extends StatelessWidget { - const _LatestLaunch({Key? key}) : super(key: key); + const _LatestLaunch({Key? key, required this.latestLaunch}) : super(key: key); + + final Launch latestLaunch; @override Widget build(BuildContext context) { final l10n = context.l10n; - final latestLaunch = - context.select((LaunchesCubit cubit) => cubit.state.latestLaunch!); - - return Container( + return Padding( padding: const EdgeInsets.only(top: 10), child: Column( children: [ @@ -101,16 +102,8 @@ class _LatestLaunch extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - Row( - children: [ - Text(latestLaunch.name), - ], - ), - Row( - children: [ - Text('${latestLaunch.flightNumber}'), - ], - ), + Text(latestLaunch.name), + Text('${latestLaunch.flightNumber}'), ], ), ) @@ -150,7 +143,7 @@ class _LatestLaunch extends StatelessWidget { ), ), ), - Container( + Align( alignment: Alignment.bottomCenter, child: SizedBox( height: 64, diff --git a/packages/launch_repository/example/lib/main.dart b/packages/launch_repository/example/lib/main.dart index 1af8d75..80458e6 100644 --- a/packages/launch_repository/example/lib/main.dart +++ b/packages/launch_repository/example/lib/main.dart @@ -1,16 +1,11 @@ -import 'dart:io'; - import 'package:launch_repository/launch_repository.dart'; Future main() async { final launchRepository = LaunchRepository(); - try { final latestLaunch = await launchRepository.fetchLatestLaunch(); print(latestLaunch); } on Exception catch (e) { print(e); } - - exit(0); } diff --git a/pubspec.lock b/pubspec.lock index c829f55..1827456 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -28,7 +28,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.8.1" + version: "2.8.2" bloc: dependency: "direct main" description: @@ -56,7 +56,7 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" charcode: dependency: transitive description: @@ -244,7 +244,7 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10" + version: "0.12.11" meta: dependency: transitive description: @@ -445,21 +445,21 @@ packages: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.17.10" + version: "1.17.12" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.2" + version: "0.4.3" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.4.0" + version: "0.4.2" typed_data: dependency: transitive description: @@ -515,7 +515,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" very_good_analysis: dependency: "direct dev" description: From f9c8361edbf2eef6d12d7b67921be0d905af0f13 Mon Sep 17 00:00:00 2001 From: Nino Gjoni <49320058+ngjoni@users.noreply.github.com> Date: Fri, 28 Jan 2022 15:33:05 +0100 Subject: [PATCH 18/26] chore: remove fieldRenames --- packages/spacex_api/lib/src/models/launch.dart | 8 ++------ packages/spacex_api/lib/src/models/rocket.dart | 8 ++------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/packages/spacex_api/lib/src/models/launch.dart b/packages/spacex_api/lib/src/models/launch.dart index 3887763..7dcc4b2 100644 --- a/packages/spacex_api/lib/src/models/launch.dart +++ b/packages/spacex_api/lib/src/models/launch.dart @@ -83,9 +83,7 @@ class Launch extends Equatable { /// {@template links} /// A model that represents available links to images, videos and articles. /// {@endtemplate} -@JsonSerializable( - fieldRename: FieldRename.snake, -) +@JsonSerializable() class Links extends Equatable { /// {@macro length} const Links({ @@ -120,9 +118,7 @@ class Links extends Equatable { /// {@template Patch} /// A model that represents small and large images of the mission patch. /// {@endtemplate} -@JsonSerializable( - fieldRename: FieldRename.snake, -) +@JsonSerializable() class Patch extends Equatable { /// {@macro patch} const Patch({ diff --git a/packages/spacex_api/lib/src/models/rocket.dart b/packages/spacex_api/lib/src/models/rocket.dart index 8cfd62a..5d5a774 100644 --- a/packages/spacex_api/lib/src/models/rocket.dart +++ b/packages/spacex_api/lib/src/models/rocket.dart @@ -113,9 +113,7 @@ class Rocket extends Equatable { /// {@template length} /// A model that represents a certain length in both meters and feet. /// {@endtemplate} -@JsonSerializable( - fieldRename: FieldRename.snake, -) +@JsonSerializable() class Length extends Equatable { /// {@macro length} const Length({ @@ -145,9 +143,7 @@ class Length extends Equatable { /// {@template mass} /// A model that represents a certain length in both meters and feet. /// {@endtemplate} -@JsonSerializable( - fieldRename: FieldRename.snake, -) +@JsonSerializable() class Mass extends Equatable { /// {@macro mass} const Mass({ From 00ec01c330b4a501b8ba7e06bf7a9094ceb1d6ef Mon Sep 17 00:00:00 2001 From: Nino Gjoni <49320058+ngjoni@users.noreply.github.com> Date: Fri, 28 Jan 2022 15:41:35 +0100 Subject: [PATCH 19/26] chore: comments and docs --- .../launch_repository/lib/src/launch_repository.dart | 6 +++--- packages/spacex_api/lib/src/models/launch.dart | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/launch_repository/lib/src/launch_repository.dart b/packages/launch_repository/lib/src/launch_repository.dart index 74c3202..c84a681 100644 --- a/packages/launch_repository/lib/src/launch_repository.dart +++ b/packages/launch_repository/lib/src/launch_repository.dart @@ -3,7 +3,7 @@ import 'package:spacex_api/spacex_api.dart'; ///Thrown when an error occurs while looking up for launches class LaunchException implements Exception {} -/// {@template launches_repository} +/// {@template launch_repository} /// A Dart package to manage the launches /// {@endtemplate} class LaunchRepository { @@ -13,9 +13,9 @@ class LaunchRepository { final SpaceXApiClient _spaceXApiClient; - ///Returns the latest launch + /// Returns the latest launch. /// - ///Throws a [LaunchException] if an error occurs. + /// Throws a [LaunchException] if an error occurs. Future fetchLatestLaunch() { try { return _spaceXApiClient.fetchLatestLaunch(); diff --git a/packages/spacex_api/lib/src/models/launch.dart b/packages/spacex_api/lib/src/models/launch.dart index 7dcc4b2..43cd771 100644 --- a/packages/spacex_api/lib/src/models/launch.dart +++ b/packages/spacex_api/lib/src/models/launch.dart @@ -5,7 +5,7 @@ import 'package:spacex_api/spacex_api.dart'; part 'launch.g.dart'; /// {@template launch} -/// A model containing data about a launch +/// A model containing data about a scheduled SpaceX rocket launch. /// {@endtemplate} @JsonSerializable() class Launch extends Equatable { @@ -34,9 +34,9 @@ class Launch extends Equatable { /// May be null final String? details; - /// A List of crew members + /// A list of crew members involved in the launch. /// - /// May be empty. + /// May be `null` or empty. final List? crew; /// The flightNumber of the launch @@ -85,7 +85,7 @@ class Launch extends Equatable { /// {@endtemplate} @JsonSerializable() class Links extends Equatable { - /// {@macro length} + /// {@macro links} const Links({ required this.patch, required this.webcast, @@ -115,7 +115,7 @@ class Links extends Equatable { 'Links(Patch($patch), Webcast: $webcast, Wikipedia: $wikipedia)'; } -/// {@template Patch} +/// {@template patch} /// A model that represents small and large images of the mission patch. /// {@endtemplate} @JsonSerializable() From d9119b2dd3a327ce99061c0ccbc4b5d0896f1b89 Mon Sep 17 00:00:00 2001 From: Nino Gjoni <49320058+ngjoni@users.noreply.github.com> Date: Fri, 28 Jan 2022 15:46:12 +0100 Subject: [PATCH 20/26] chore: remove unnecessary imports --- test/helpers/pump_app.dart | 2 -- test/home/view/home_page_test.dart | 1 - test/home/widgets/home_page_content_test.dart | 1 - 3 files changed, 4 deletions(-) diff --git a/test/helpers/pump_app.dart b/test/helpers/pump_app.dart index c3a8209..c819d3e 100644 --- a/test/helpers/pump_app.dart +++ b/test/helpers/pump_app.dart @@ -1,12 +1,10 @@ import 'package:crew_member_repository/crew_member_repository.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:launch_repository/launch_repository.dart'; import 'package:mockingjay/mockingjay.dart'; -import 'package:mocktail/mocktail.dart'; import 'package:rocket_repository/rocket_repository.dart'; import 'package:spacex_demo/l10n/l10n.dart'; diff --git a/test/home/view/home_page_test.dart b/test/home/view/home_page_test.dart index f307540..462bc05 100644 --- a/test/home/view/home_page_test.dart +++ b/test/home/view/home_page_test.dart @@ -1,6 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:spacex_demo/home/home.dart'; -import 'package:spacex_demo/home/widgets/home_page_content.dart'; import '../../helpers/pump_app.dart'; diff --git a/test/home/widgets/home_page_content_test.dart b/test/home/widgets/home_page_content_test.dart index 2cf354b..8473b26 100644 --- a/test/home/widgets/home_page_content_test.dart +++ b/test/home/widgets/home_page_content_test.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockingjay/mockingjay.dart'; -import 'package:mocktail/mocktail.dart'; import 'package:spacex_demo/home/widgets/home_page_content.dart'; import 'package:spacex_demo/home/widgets/spacex_category_card.dart'; From 7345a2acf1c56a1d1e91fad78c8e393bf9d3c49c Mon Sep 17 00:00:00 2001 From: Nino Gjoni <49320058+ngjoni@users.noreply.github.com> Date: Fri, 28 Jan 2022 16:28:20 +0100 Subject: [PATCH 21/26] fix: test crew member stringify --- packages/spacex_api/test/models/crew_member_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/spacex_api/test/models/crew_member_test.dart b/packages/spacex_api/test/models/crew_member_test.dart index bfa6211..d260b1d 100644 --- a/packages/spacex_api/test/models/crew_member_test.dart +++ b/packages/spacex_api/test/models/crew_member_test.dart @@ -57,7 +57,7 @@ void main() { wikipedia: 'https://www.wikipedia.org/', launches: const ['Launch 1', 'Launch 2'], ).stringify, - isTrue, + isNull, ); }); }); From eda3bf34de92019c6bcecf2f510c2b544bd1f943 Mon Sep 17 00:00:00 2001 From: Nino G Date: Fri, 28 Jan 2022 19:20:19 +0100 Subject: [PATCH 22/26] chore: correct test and rename right key --- lib/launches/view/launches_page.dart | 2 +- test/home/widgets/home_page_content_test.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/launches/view/launches_page.dart b/lib/launches/view/launches_page.dart index f9f51cb..0b3ff62 100644 --- a/lib/launches/view/launches_page.dart +++ b/lib/launches/view/launches_page.dart @@ -70,7 +70,7 @@ class _LaunchesContent extends StatelessWidget { ); case LaunchesStatus.success: return _LatestLaunch( - key: const Key('launchesView_success_rocketList'), + key: const Key('launchesView_success_latestLaunch'), latestLaunch: state.latestLaunch!, ); } diff --git a/test/home/widgets/home_page_content_test.dart b/test/home/widgets/home_page_content_test.dart index 8473b26..690a5b5 100644 --- a/test/home/widgets/home_page_content_test.dart +++ b/test/home/widgets/home_page_content_test.dart @@ -24,7 +24,7 @@ void main() { 'renders correct amount of ' 'SpaceXCategoryCards', (tester) async { await tester.pumpApp(const HomePageContent()); - expect(find.byType(SpaceXCategoryCard), findsNWidgets(2)); + expect(find.byType(SpaceXCategoryCard), findsNWidgets(3)); }, ); From d1ab80715049ac281c1ee257f8589310da0b27bb Mon Sep 17 00:00:00 2001 From: Nino Gjoni <49320058+ngjoni@users.noreply.github.com> Date: Sat, 29 Jan 2022 04:19:07 +0100 Subject: [PATCH 23/26] feat: add launch tests --- lib/launches/view/launches_page.dart | 1 + .../spacex_api/lib/src/models/launch.dart | 5 +- .../spacex_api/test/models/launches_test.dart | 126 ++++++++++++++++++ .../cubit/crew_member_details_state_test.dart | 2 +- test/home/widgets/home_page_content_test.dart | 19 +++ test/launches/cubit/launches_state_test.dart | 47 +++---- test/launches/view/launches_page_test.dart | 124 +++++++++++++++++ 7 files changed, 295 insertions(+), 29 deletions(-) create mode 100644 packages/spacex_api/test/models/launches_test.dart diff --git a/lib/launches/view/launches_page.dart b/lib/launches/view/launches_page.dart index 0b3ff62..d61cc61 100644 --- a/lib/launches/view/launches_page.dart +++ b/lib/launches/view/launches_page.dart @@ -122,6 +122,7 @@ class _LatestLaunch extends StatelessWidget { height: 35, ), Row( + key: const Key('launchesPage_link_buttons'), mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Container( diff --git a/packages/spacex_api/lib/src/models/launch.dart b/packages/spacex_api/lib/src/models/launch.dart index 43cd771..f8705c6 100644 --- a/packages/spacex_api/lib/src/models/launch.dart +++ b/packages/spacex_api/lib/src/models/launch.dart @@ -111,8 +111,7 @@ class Links extends Equatable { Map toJson() => _$LinksToJson(this); @override - String toString() => - 'Links(Patch($patch), Webcast: $webcast, Wikipedia: $wikipedia)'; + String toString() => 'Links(Webcast: $webcast, Wikipedia: $wikipedia)'; } /// {@template patch} @@ -142,5 +141,5 @@ class Patch extends Equatable { Map toJson() => _$PatchToJson(this); @override - String toString() => 'Patch(Small: $small , Large: $large)'; + String toString() => 'Patch(Small: $small)'; } diff --git a/packages/spacex_api/test/models/launches_test.dart b/packages/spacex_api/test/models/launches_test.dart new file mode 100644 index 0000000..90f4626 --- /dev/null +++ b/packages/spacex_api/test/models/launches_test.dart @@ -0,0 +1,126 @@ +// ignore_for_file: prefer_const_constructors +import 'package:spacex_api/spacex_api.dart'; +import 'package:test/test.dart'; + +void main() { + group('Launch', () { + test('supports value comparison', () { + expect( + Launch( + id: '1', + name: 'Starlink Mission 1337', + links: Links( + patch: Patch( + small: + 'https://avatars.githubusercontent.com/u/2918581?v=4'), + webcast: 'https://www.youtube.com', + wikipedia: 'https://www.wikipedia.org/')), + Launch( + id: '1', + name: 'Starlink Mission 1337', + links: Links( + patch: Patch( + small: + ' https://avatars.githubusercontent.com/u/2918581?v=4'), + webcast: 'https://www.youtube.com', + wikipedia: 'https://www.wikipedia.org/'))); + }); + + test('has concise toString', () { + expect( + Launch( + id: '1', + name: 'Starlink Mission 1337', + links: Links( + patch: Patch( + small: + 'https://avatars.githubusercontent.com/u/2918581?v=4'), + webcast: 'https://www.youtube.com', + wikipedia: 'https://www.wikipedia.org/')) + .toString(), + equals('Latest Launch(1, Starlink Mission 1337)')); + }); + + test('overrides stringify', () { + expect( + Launch( + id: '1', + name: 'Starlink Mission 1337', + links: Links( + patch: Patch( + small: + 'https://avatars.githubusercontent.com/u/2918581?v=4'), + webcast: 'https://www.youtube.com', + wikipedia: 'https://www.wikipedia.org/')) + .stringify, + isNull); + }); + group('Links', () { + test('supports value comparison', () { + expect( + Links( + patch: Patch( + small: 'https://avatars.githubusercontent.com/u/2918581?v=4'), + webcast: 'https://www.youtube.com', + wikipedia: 'https://www.wikipedia.org/'), + Links( + patch: Patch( + small: 'https://avatars.githubusercontent.com/u/2918581?v=4'), + webcast: 'https://www.youtube.com', + wikipedia: 'https://www.wikipedia.org/'), + ); + }); + + test('has concise toString', () { + expect( + Links( + patch: Patch( + small: + 'https://avatars.githubusercontent.com/u/2918581?v=4'), + webcast: 'https://www.youtube.com', + wikipedia: 'https://www.wikipedia.org/') + .toString(), + equals( + 'Links(Webcast: https://www.youtube.com, Wikipedia: https://www.wikipedia.org/)')); + }); + + test('overrides stringify', () { + expect( + Links( + patch: Patch( + small: + 'https://avatars.githubusercontent.com/u/2918581?v=4'), + webcast: 'https://www.youtube.com', + wikipedia: 'https://www.wikipedia.org/') + .stringify, + isNull); + }); + }); + + group('Patch', () { + test('supports value comparison', () { + expect( + const Patch( + small: 'https://avatars.githubusercontent.com/u/2918581?v=4'), + const Patch( + small: 'https://avatars.githubusercontent.com/u/2918581?v=4'), + ); + }); + + test('has concise toString', () { + expect( + Patch(small: 'https://avatars.githubusercontent.com/u/2918581?v=4') + .toString(), + equals( + 'Patch(Small: https://avatars.githubusercontent.com/u/2918581?v=4)')); + }); + + test('overrides stringify', () { + expect( + Patch(small: 'https://avatars.githubusercontent.com/u/2918581?v=4') + .stringify, + isNull); + }); + }); + }); +} diff --git a/test/crew_member_details/cubit/crew_member_details_state_test.dart b/test/crew_member_details/cubit/crew_member_details_state_test.dart index b7beea8..093148c 100644 --- a/test/crew_member_details/cubit/crew_member_details_state_test.dart +++ b/test/crew_member_details/cubit/crew_member_details_state_test.dart @@ -3,7 +3,7 @@ import 'package:spacex_api/spacex_api.dart'; import 'package:spacex_demo/crew_member_details/cubit/crew_member_details_cubit.dart'; void main() { - group('CrewMemberDewtailsState', () { + group('CrewMemberDetailsState', () { const crewMember = CrewMember( id: '0', name: 'Alejandro Ferrero', diff --git a/test/home/widgets/home_page_content_test.dart b/test/home/widgets/home_page_content_test.dart index 690a5b5..2a82e59 100644 --- a/test/home/widgets/home_page_content_test.dart +++ b/test/home/widgets/home_page_content_test.dart @@ -84,5 +84,24 @@ void main() { verify(() => navigator.push(any(that: isRoute()))).called(1); }, ); + + testWidgets( + 'navigates to LaunchesPage ' + 'when launch category card is tapped', + (tester) async { + await tester.pumpApp( + const HomePageContent(), + navigator: navigator, + ); + + await tester.tap( + find.byKey( + const Key('homePageContent_latestLaunch_spaceXCategoryCard'), + ), + ); + + verify(() => navigator.push(any(that: isRoute()))).called(1); + }, + ); }); } diff --git a/test/launches/cubit/launches_state_test.dart b/test/launches/cubit/launches_state_test.dart index 85eb57d..3e23c47 100644 --- a/test/launches/cubit/launches_state_test.dart +++ b/test/launches/cubit/launches_state_test.dart @@ -1,42 +1,39 @@ +// ignore_for_file: cascade_invocations import 'package:flutter_test/flutter_test.dart'; import 'package:spacex_api/spacex_api.dart'; import 'package:spacex_demo/launches/launches.dart'; void main() { - group('CrewState', () { + group('LaunchesState', () { + const launch = Launch( + id: '0', + name: 'mock-launch-name', + links: Links( + patch: Patch( + small: 'https://avatars.githubusercontent.com/u/2918581?v=4', + large: 'https://avatars.githubusercontent.com/u/2918581?v=4', + ), + webcast: 'https://www.youtube.com', + wikipedia: 'https://www.wikipedia.org/', + ), + ); test('supports value comparison', () { expect( const LaunchesState( status: LaunchesStatus.success, - latestLaunch: Launch( - id: '0', - name: 'mock-launch-name', - links: Links( - patch: Patch( - small: 'https://avatars.githubusercontent.com/u/2918581?v=4', - large: 'https://avatars.githubusercontent.com/u/2918581?v=4', - ), - webcast: 'https://www.youtube.com', - wikipedia: 'https://www.wikipedia.org/', - ), - ), + latestLaunch: launch, ), const LaunchesState( status: LaunchesStatus.success, - latestLaunch: Launch( - id: '0', - name: 'mock-launch-name', - links: Links( - patch: Patch( - small: 'https://avatars.githubusercontent.com/u/2918581?v=4', - large: 'https://avatars.githubusercontent.com/u/2918581?v=4', - ), - webcast: 'https://www.youtube.com', - wikipedia: 'https://www.wikipedia.org/', - ), - ), + latestLaunch: launch, ), ); }); + test('copyWith', () { + const state = LaunchesState(status: LaunchesStatus.loading); + state.copyWith(status: LaunchesStatus.success); + + expect(state.status, state.status); + }); }); } diff --git a/test/launches/view/launches_page_test.dart b/test/launches/view/launches_page_test.dart index 47bbccf..526d3b0 100644 --- a/test/launches/view/launches_page_test.dart +++ b/test/launches/view/launches_page_test.dart @@ -6,8 +6,10 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:launch_repository/launch_repository.dart'; import 'package:mockingjay/mockingjay.dart'; import 'package:mocktail_image_network/mocktail_image_network.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'package:spacex_api/spacex_api.dart'; import 'package:spacex_demo/launches/launches.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; import '../../helpers/helpers.dart'; @@ -16,7 +18,16 @@ class MockLaunchRepository extends Mock implements LaunchRepository {} class MockLaunchesCubit extends MockCubit implements LaunchesCubit {} +class MockUrlLauncherPlatorm extends Mock + with MockPlatformInterfaceMixin + implements UrlLauncherPlatform {} + void main() { + late LaunchesCubit launchesCubit; + late UrlLauncherPlatform urlLauncherPlatform; + + const status = LaunchesStatus.success; + final latestLaunch = Launch( id: '0', name: 'mock-launch-name', @@ -30,6 +41,35 @@ void main() { ), ); + setUp(() { + launchesCubit = MockLaunchesCubit(); + when(() => launchesCubit.state) + .thenReturn(LaunchesState(latestLaunch: latestLaunch, status: status)); + + urlLauncherPlatform = MockUrlLauncherPlatorm(); + UrlLauncherPlatform.instance = urlLauncherPlatform; + when(() => urlLauncherPlatform.canLaunch(any())) + .thenAnswer((_) async => true); + when( + () => urlLauncherPlatform.launch( + any(), + useSafariVC: any(named: 'useSafariVC'), + useWebView: any(named: 'useWebView'), + enableJavaScript: any(named: 'enableJavaScript'), + enableDomStorage: any(named: 'enableDomStorage'), + universalLinksOnly: any(named: 'universalLinksOnly'), + headers: any(named: 'headers'), + webOnlyWindowName: any(named: 'webOnlyWindowName'), + ), + ).thenAnswer((_) async => true); + }); + + setUpAll(() { + registerFallbackValue( + LaunchesState(latestLaunch: latestLaunch, status: LaunchesStatus.success), + ); + }); + group('LaunchesPage', () { late LaunchRepository launchRepository; @@ -161,5 +201,89 @@ void main() { expect(find.byKey(key), findsOneWidget); }, ); + + // group('open link buttons', () { + // const key = Key('launchesPage_link_buttons'); + // const webcastKey = Key('launchesPage_openWebcast_elevatedButton'); + // const wikipediaKey = Key('launchesPage_openWikipedia_elevatedButton'); + + // testWidgets( + // 'is rendered', + // (tester) async { + // await mockNetworkImages(() async { + // await tester.pumpApp( + // BlocProvider.value( + // value: launchesCubit, + // child: const LaunchesView(), + // ), + // ); + // }); + + // expect(find.byKey(key), findsOneWidget); + // }, + // ); + + // testWidgets( + // 'attempts to open webcast url when pressed', + // (tester) async { + // await mockNetworkImages(() async { + // await tester.pumpApp( + // BlocProvider.value( + // value: launchesCubit, + // child: const LaunchesView(), + // ), + // ); + // }); + + // await tester.tap(find.byKey(webcastKey)); + + // verify( + // () => urlLauncherPlatform.canLaunch(latestLaunch.links.webcast), + // ).called(1); + // verify( + // () => urlLauncherPlatform.launch( + // latestLaunch.links.webcast, + // useSafariVC: true, + // useWebView: false, + // enableJavaScript: false, + // enableDomStorage: false, + // universalLinksOnly: false, + // headers: const {}, + // ), + // ).called(1); + // }, + // ); + + // testWidgets( + // 'attempts to open wikipedia url when pressed', + // (tester) async { + // await mockNetworkImages(() async { + // await tester.pumpApp( + // BlocProvider.value( + // value: launchesCubit, + // child: const LaunchesView(), + // ), + // ); + // }); + + // await tester.tap(find.byKey(wikipediaKey)); + + // verify( + // () => urlLauncherPlatform.canLaunch(latestLaunch.links.wikipedia), + // ).called(1); + // verify( + // () => urlLauncherPlatform.launch( + // latestLaunch.links.wikipedia, + // useSafariVC: true, + // useWebView: false, + // enableJavaScript: false, + // enableDomStorage: false, + // universalLinksOnly: false, + // headers: const {}, + // ), + // ).called(1); + // }, + // ); + // }); }); } From 3897c05194e0cc4f13a397f2ca0bfac139c6c96f Mon Sep 17 00:00:00 2001 From: Nino Gjoni <49320058+ngjoni@users.noreply.github.com> Date: Wed, 16 Mar 2022 22:50:26 +0100 Subject: [PATCH 24/26] add: latest launch test --- .../test/spacex_api_client_test.dart | 97 +++++++++++++++++++ pubspec.lock | 17 +++- 2 files changed, 109 insertions(+), 5 deletions(-) diff --git a/packages/spacex_api/test/spacex_api_client_test.dart b/packages/spacex_api/test/spacex_api_client_test.dart index aa35b99..c58f3c8 100644 --- a/packages/spacex_api/test/spacex_api_client_test.dart +++ b/packages/spacex_api/test/spacex_api_client_test.dart @@ -10,6 +10,7 @@ class MockHttpClient extends Mock implements http.Client {} void main() { late Uri rocketUri; late Uri crewUri; + late Uri latestLaunchUri; group('SpaceXApiClient', () { late http.Client httpClient; @@ -41,11 +42,26 @@ void main() { ), ); + final latestLaunch = List.generate( + 3, + (i) => Launch( + id: '$i', + name: 'mock-launch-name-$i', + links: const Links( + patch: Patch( + small: 'https://avatars.githubusercontent.com/u/2918581?v=4'), + webcast: 'https://www.youtube.com', + wikipedia: 'https://www.wikipedia.org/'), + ), + ); + setUp(() { httpClient = MockHttpClient(); subject = SpaceXApiClient(httpClient: httpClient); rocketUri = Uri.https(SpaceXApiClient.authority, '/v4/rockets'); crewUri = Uri.https(SpaceXApiClient.authority, '/v4/crew'); + latestLaunchUri = + Uri.https(SpaceXApiClient.authority, '/v4/launches/latest/'); }); test('constructor returns normally', () { @@ -216,5 +232,86 @@ void main() { ); }); }); + +////////////////////////////////////// + group('.fetchLatestLaunch', () { + setUp(() { + when(() => httpClient.get(latestLaunchUri)).thenAnswer( + (_) async => http.Response(json.encode(latestLaunch), 200), + ); + }); + + test('throws HttpException when http client throws exception', () { + when(() => httpClient.get(latestLaunchUri)).thenThrow(Exception()); + + expect( + () => subject.fetchLatestLaunch(), + throwsA(isA()), + ); + }); + + test( + 'throws HttpRequestFailure when response status code is not 200', + () { + when(() => httpClient.get(latestLaunchUri)).thenAnswer( + (_) async => http.Response('', 400), + ); + + expect( + () => subject.fetchLatestLaunch(), + throwsA( + isA() + .having((error) => error.statusCode, 'statusCode', 400), + ), + ); + }, + ); + + test( + 'throws JsonDecodeException when decoding response fails', + () { + when(() => httpClient.get(latestLaunchUri)).thenAnswer( + (_) async => http.Response('definitely not json!', 200), + ); + + expect( + () => subject.fetchLatestLaunch(), + throwsA(isA()), + ); + }, + ); + + test( + 'throws JsonDeserializationException ' + 'when deserializing json body fails', + () { + when(() => httpClient.get(latestLaunchUri)).thenAnswer( + (_) async => http.Response( + '[{"this_is_not_a_latest_launch_doc": true}]', + 200, + ), + ); + + expect( + () => subject.fetchLatestLaunch(), + throwsA(isA()), + ); + }, + ); + + test('makes correct request', () async { + await subject.fetchLatestLaunch(); + verify( + () => httpClient.get(latestLaunchUri), + ).called(1); + }); + + test('returns correct list of the latest launch', () { + expect( + subject.fetchLatestLaunch(), + completion(equals(latestLaunch)), + ); + }); + }); }); } diff --git a/pubspec.lock b/pubspec.lock index 1827456..c0b0995 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,14 +7,14 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "26.0.0" + version: "31.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "2.3.0" + version: "2.8.0" args: dependency: transitive description: @@ -245,6 +245,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.3" meta: dependency: transitive description: @@ -445,21 +452,21 @@ packages: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.17.12" + version: "1.19.5" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.3" + version: "0.4.8" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.4.2" + version: "0.4.9" typed_data: dependency: transitive description: From 20b55f0fb0dfa7cf222462aca0818bf1da3a6787 Mon Sep 17 00:00:00 2001 From: Nino Gjoni <49320058+ngjoni@users.noreply.github.com> Date: Thu, 7 Apr 2022 21:19:49 +0200 Subject: [PATCH 25/26] chore: 100% test coverage --- ios/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- lib/launches/cubit/launches_cubit.dart | 8 +- lib/launches/cubit/launches_state.dart | 10 - lib/launches/view/launches_page.dart | 9 +- .../launch_repository/example/lib/main.dart | 1 + .../test/src/launch_repository_test.dart | 4 + .../test/rocket_repository_test.dart | 14 +- .../spacex_api/lib/src/models/launch.dart | 23 +-- .../spacex_api/lib/src/models/launch.g.dart | 6 +- .../spacex_api/lib/src/models/rocket.dart | 8 +- .../spacex_api/lib/src/models/rocket.g.dart | 8 +- .../spacex_api/test/models/launches_test.dart | 62 ++++-- .../spacex_api/test/models/rocket_test.dart | 18 ++ .../test/spacex_api_client_test.dart | 45 ++--- test/crew/view/crew_page_test.dart | 4 +- test/home/widgets/home_page_content_test.dart | 8 +- test/launches/cubit/launches_cubit_test.dart | 4 + test/launches/cubit/launches_state_test.dart | 17 +- test/launches/view/launches_page_test.dart | 187 ++++++++++-------- .../cubit/rocket_details_cubit_test.dart | 3 +- .../cubit/rocket_details_state_test.dart | 3 +- .../view/rocket_details_page_test.dart | 18 +- test/rockets/cubit/rockets_cubit_test.dart | 1 + test/rockets/view/rockets_page_test.dart | 5 +- 25 files changed, 275 insertions(+), 195 deletions(-) diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index febcef1..3644e31 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -168,7 +168,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1210; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 8605956..c87d15a 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ { Future fetchLatestLaunch() async { emit( - state.copyWith( + LaunchesState( status: LaunchesStatus.loading, + latestLaunch: state.latestLaunch, ), ); try { final latestLaunch = await _launchRepository.fetchLatestLaunch(); emit( - state.copyWith( + LaunchesState( status: LaunchesStatus.success, latestLaunch: latestLaunch, ), ); } on Exception { emit( - state.copyWith( + LaunchesState( status: LaunchesStatus.failure, + latestLaunch: state.latestLaunch, ), ); } diff --git a/lib/launches/cubit/launches_state.dart b/lib/launches/cubit/launches_state.dart index f96af40..8dddc84 100644 --- a/lib/launches/cubit/launches_state.dart +++ b/lib/launches/cubit/launches_state.dart @@ -11,16 +11,6 @@ class LaunchesState extends Equatable { final LaunchesStatus status; final Launch? latestLaunch; - LaunchesState copyWith({ - LaunchesStatus? status, - Launch? latestLaunch, - }) { - return LaunchesState( - status: status ?? this.status, - latestLaunch: latestLaunch ?? this.latestLaunch, - ); - } - @override List get props => [status, latestLaunch]; } diff --git a/lib/launches/view/launches_page.dart b/lib/launches/view/launches_page.dart index d61cc61..435d0f7 100644 --- a/lib/launches/view/launches_page.dart +++ b/lib/launches/view/launches_page.dart @@ -95,7 +95,10 @@ class _LatestLaunch extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ CircleAvatar( - backgroundImage: NetworkImage(latestLaunch.links.patch.small), + backgroundImage: NetworkImage( + latestLaunch.links.patch.small ?? + 'assets/images/img_spacex_launch.jpeg', + ), radius: 50, ), Expanded( @@ -136,7 +139,7 @@ class _LatestLaunch extends StatelessWidget { onPressed: () async { final url = latestLaunch.links.webcast; - if (await canLaunch(url)) { + if (await canLaunch(url!)) { await launch(url); } }, @@ -155,7 +158,7 @@ class _LatestLaunch extends StatelessWidget { onPressed: () async { final url = latestLaunch.links.wikipedia; - if (await canLaunch(url)) { + if (await canLaunch(url!)) { await launch(url); } }, diff --git a/packages/launch_repository/example/lib/main.dart b/packages/launch_repository/example/lib/main.dart index 80458e6..6255ef6 100644 --- a/packages/launch_repository/example/lib/main.dart +++ b/packages/launch_repository/example/lib/main.dart @@ -4,6 +4,7 @@ Future main() async { final launchRepository = LaunchRepository(); try { final latestLaunch = await launchRepository.fetchLatestLaunch(); + print(latestLaunch); } on Exception catch (e) { print(e); diff --git a/packages/launch_repository/test/src/launch_repository_test.dart b/packages/launch_repository/test/src/launch_repository_test.dart index a1b0b78..a72b8ba 100644 --- a/packages/launch_repository/test/src/launch_repository_test.dart +++ b/packages/launch_repository/test/src/launch_repository_test.dart @@ -11,9 +11,13 @@ void main() { late SpaceXApiClient spaceXApiClient; late LaunchRepository subject; + final date = DateTime.now(); + final latestLaunch = Launch( id: '0', name: 'mock-launch-name', + dateLocal: date, + dateUtc: date, links: const Links( patch: Patch( small: 'https://avatars.githubusercontent.com/u/2918581?v=4', diff --git a/packages/rocket_repository/test/rocket_repository_test.dart b/packages/rocket_repository/test/rocket_repository_test.dart index 4c946b1..a27724e 100644 --- a/packages/rocket_repository/test/rocket_repository_test.dart +++ b/packages/rocket_repository/test/rocket_repository_test.dart @@ -13,13 +13,13 @@ void main() { final rockets = List.generate( 3, (i) => Rocket( - id: '$i', - name: 'mock-rocket-name-$i', - description: 'mock-rocket-description-$i', - height: const Length(meters: 1, feet: 1), - diameter: const Length(meters: 1, feet: 1), - mass: const Mass(kg: 1, lb: 1), - ), + id: '$i', + name: 'mock-rocket-name-$i', + description: 'mock-rocket-description-$i', + height: const Length(meters: 1, feet: 1), + diameter: const Length(meters: 1, feet: 1), + mass: const Mass(kg: 1, lb: 1), + firstFlight: DateTime.now()), ); setUp(() { diff --git a/packages/spacex_api/lib/src/models/launch.dart b/packages/spacex_api/lib/src/models/launch.dart index f8705c6..a36d17f 100644 --- a/packages/spacex_api/lib/src/models/launch.dart +++ b/packages/spacex_api/lib/src/models/launch.dart @@ -15,12 +15,12 @@ class Launch extends Equatable { required this.name, required this.links, this.details, - this.crew, + this.crew = const [], this.flightNumber, this.rocket, this.success, - this.dateUtc, - this.dateLocal, + required this.dateUtc, + required this.dateLocal, }); /// The ID of the launch. @@ -67,7 +67,8 @@ class Launch extends Equatable { rocket, success, dateUtc, - dateLocal + dateLocal, + links ]; /// Converts a JSON [Map] into a [Launch] instance @@ -88,21 +89,21 @@ class Links extends Equatable { /// {@macro links} const Links({ required this.patch, - required this.webcast, - required this.wikipedia, + this.webcast, + this.wikipedia, }); /// The Patch for the launch mission final Patch patch; /// The launch video link - final String webcast; + final String? webcast; /// The latest launch information on wikipedia - final String wikipedia; + final String? wikipedia; @override - List get props => [patch, webcast, wikipedia]; + List get props => [patch, webcast, wikipedia]; /// Converts a JSON [Map] into a [Links] instance. static Links fromJson(Map json) => _$LinksFromJson(json); @@ -121,12 +122,12 @@ class Links extends Equatable { class Patch extends Equatable { /// {@macro patch} const Patch({ - required this.small, + this.small, this.large, }); /// A small patch image link - final String small; + final String? small; /// A large patch image link final String? large; diff --git a/packages/spacex_api/lib/src/models/launch.g.dart b/packages/spacex_api/lib/src/models/launch.g.dart index dded7cd..b691ee7 100644 --- a/packages/spacex_api/lib/src/models/launch.g.dart +++ b/packages/spacex_api/lib/src/models/launch.g.dart @@ -43,8 +43,8 @@ Map _$LaunchToJson(Launch instance) => { Links _$LinksFromJson(Map json) { return Links( patch: Patch.fromJson(json['patch'] as Map), - webcast: json['webcast'] as String, - wikipedia: json['wikipedia'] as String, + webcast: json['webcast'] as String?, + wikipedia: json['wikipedia'] as String?, ); } @@ -56,7 +56,7 @@ Map _$LinksToJson(Links instance) => { Patch _$PatchFromJson(Map json) { return Patch( - small: json['small'] as String, + small: json['small'] as String?, large: json['large'] as String?, ); } diff --git a/packages/spacex_api/lib/src/models/rocket.dart b/packages/spacex_api/lib/src/models/rocket.dart index 5d5a774..8f0767d 100644 --- a/packages/spacex_api/lib/src/models/rocket.dart +++ b/packages/spacex_api/lib/src/models/rocket.dart @@ -16,13 +16,13 @@ class Rocket extends Equatable { required this.height, required this.diameter, required this.mass, + required this.firstFlight, this.flickrImages = const [], this.active, this.stages, this.boosters, this.costPerLaunch, this.successRatePct, - this.firstFlight, this.country, this.company, this.wikipedia, @@ -46,6 +46,9 @@ class Rocket extends Equatable { /// The mass of the rocket. final Mass mass; + /// The date this rocket was first launched. + final DateTime? firstFlight; + /// A collection of images if this rocket hosted on https://flickr.com /// /// May be empty. @@ -68,9 +71,6 @@ class Rocket extends Equatable { /// This value must be in between `0` and `100`. final int? successRatePct; - /// The date this rocket was first launched. - final DateTime? firstFlight; - /// The country in which this rocket was built. final String? country; diff --git a/packages/spacex_api/lib/src/models/rocket.g.dart b/packages/spacex_api/lib/src/models/rocket.g.dart index 866d647..190f0a0 100644 --- a/packages/spacex_api/lib/src/models/rocket.g.dart +++ b/packages/spacex_api/lib/src/models/rocket.g.dart @@ -14,6 +14,9 @@ Rocket _$RocketFromJson(Map json) { height: Length.fromJson(json['height'] as Map), diameter: Length.fromJson(json['diameter'] as Map), mass: Mass.fromJson(json['mass'] as Map), + firstFlight: json['first_flight'] == null + ? null + : DateTime.parse(json['first_flight'] as String), flickrImages: (json['flickr_images'] as List) .map((e) => e as String) .toList(), @@ -22,9 +25,6 @@ Rocket _$RocketFromJson(Map json) { boosters: json['boosters'] as int?, costPerLaunch: json['cost_per_launch'] as int?, successRatePct: json['success_rate_pct'] as int?, - firstFlight: json['first_flight'] == null - ? null - : DateTime.parse(json['first_flight'] as String), country: json['country'] as String?, company: json['company'] as String?, wikipedia: json['wikipedia'] as String?, @@ -38,13 +38,13 @@ Map _$RocketToJson(Rocket instance) => { 'height': instance.height, 'diameter': instance.diameter, 'mass': instance.mass, + 'first_flight': instance.firstFlight?.toIso8601String(), 'flickr_images': instance.flickrImages, 'active': instance.active, 'stages': instance.stages, 'boosters': instance.boosters, 'cost_per_launch': instance.costPerLaunch, 'success_rate_pct': instance.successRatePct, - 'first_flight': instance.firstFlight?.toIso8601String(), 'country': instance.country, 'company': instance.company, 'wikipedia': instance.wikipedia, diff --git a/packages/spacex_api/test/models/launches_test.dart b/packages/spacex_api/test/models/launches_test.dart index 90f4626..21fba7a 100644 --- a/packages/spacex_api/test/models/launches_test.dart +++ b/packages/spacex_api/test/models/launches_test.dart @@ -4,26 +4,48 @@ import 'package:test/test.dart'; void main() { group('Launch', () { + final date = DateTime.now(); + final crewMembers = List.generate( + 3, + (i) => CrewMember( + id: '$i', + name: 'Alejandro Ferrero', + status: 'active', + agency: 'Very Good Aliens', + image: + 'https://media-exp1.licdn.com/dms/image/C4D03AQHVNIVOMkwQaA/profile-displayphoto-shrink_200_200/0/1631637257882?e=1637193600&v=beta&t=jFm-Ckb0KS0Z5hJDbo3ZBSEZSYLHfllUf4N-IV2NDTc', + wikipedia: 'https://www.wikipedia.org/', + launches: ['Launch $i'], + ), + ); + final launch = Launch( + id: '1', + name: 'Starlink Mission 1337', + dateLocal: date, + dateUtc: date, + crew: crewMembers, + links: Links( + patch: Patch( + small: 'https://avatars.githubusercontent.com/u/2918581?v=4'), + webcast: 'https://www.youtube.com', + wikipedia: 'https://www.wikipedia.org/')); + test('supports value comparison', () { expect( - Launch( - id: '1', - name: 'Starlink Mission 1337', - links: Links( - patch: Patch( - small: - 'https://avatars.githubusercontent.com/u/2918581?v=4'), - webcast: 'https://www.youtube.com', - wikipedia: 'https://www.wikipedia.org/')), - Launch( - id: '1', - name: 'Starlink Mission 1337', - links: Links( - patch: Patch( - small: - ' https://avatars.githubusercontent.com/u/2918581?v=4'), - webcast: 'https://www.youtube.com', - wikipedia: 'https://www.wikipedia.org/'))); + launch, + Launch( + id: '1', + name: 'Starlink Mission 1337', + crew: crewMembers, + dateLocal: date, + dateUtc: date, + links: Links( + patch: Patch( + small: + 'https://avatars.githubusercontent.com/u/2918581?v=4'), + webcast: 'https://www.youtube.com', + wikipedia: 'https://www.wikipedia.org/')), + ); }); test('has concise toString', () { @@ -31,6 +53,8 @@ void main() { Launch( id: '1', name: 'Starlink Mission 1337', + dateLocal: date, + dateUtc: date, links: Links( patch: Patch( small: @@ -46,6 +70,8 @@ void main() { Launch( id: '1', name: 'Starlink Mission 1337', + dateLocal: date, + dateUtc: date, links: Links( patch: Patch( small: diff --git a/packages/spacex_api/test/models/rocket_test.dart b/packages/spacex_api/test/models/rocket_test.dart index dd26a2c..e06bbd9 100644 --- a/packages/spacex_api/test/models/rocket_test.dart +++ b/packages/spacex_api/test/models/rocket_test.dart @@ -4,6 +4,21 @@ import 'package:test/test.dart'; void main() { group('Rocket', () { + test('checks if the first flight date is not null', () { + const DateTime? date = null; + final rocket = Rocket( + id: '0', + name: 'no first flight', + description: 'never in the air', + height: const Length(meters: 1, feet: 1), + diameter: const Length(meters: 1, feet: 1), + mass: const Mass(kg: 1, lb: 1), + firstFlight: date ?? DateTime.now(), + ); + + expect(rocket.firstFlight, isNotNull); + }); + test('supports value comparisons', () { expect( Rocket( @@ -13,6 +28,7 @@ void main() { height: const Length(meters: 1, feet: 1), diameter: const Length(meters: 1, feet: 1), mass: const Mass(kg: 1, lb: 1), + firstFlight: DateTime(2021), ), Rocket( id: '1', @@ -21,6 +37,7 @@ void main() { height: const Length(meters: 1, feet: 1), diameter: const Length(meters: 1, feet: 1), mass: const Mass(kg: 1, lb: 1), + firstFlight: DateTime(2021), ), ); }); @@ -34,6 +51,7 @@ void main() { height: const Length(meters: 1, feet: 1), diameter: const Length(meters: 1, feet: 1), mass: const Mass(kg: 1, lb: 1), + firstFlight: DateTime(2021), ).toString(), equals('Rocket(1, mock-rocket-name-1)'), ); diff --git a/packages/spacex_api/test/spacex_api_client_test.dart b/packages/spacex_api/test/spacex_api_client_test.dart index c58f3c8..e02f383 100644 --- a/packages/spacex_api/test/spacex_api_client_test.dart +++ b/packages/spacex_api/test/spacex_api_client_test.dart @@ -16,16 +16,18 @@ void main() { late http.Client httpClient; late SpaceXApiClient subject; + final date = DateTime.now(); + final rockets = List.generate( 3, (i) => Rocket( - id: '$i', - name: 'mock-rocket-name-$i', - description: 'mock-rocket-description-$i', - height: const Length(meters: 1, feet: 1), - diameter: const Length(meters: 1, feet: 1), - mass: const Mass(kg: 1, lb: 1), - ), + id: '$i', + name: 'mock-rocket-name-$i', + description: 'mock-rocket-description-$i', + height: const Length(meters: 1, feet: 1), + diameter: const Length(meters: 1, feet: 1), + mass: const Mass(kg: 1, lb: 1), + firstFlight: DateTime.now()), ); final crewMembers = List.generate( @@ -42,17 +44,16 @@ void main() { ), ); - final latestLaunch = List.generate( - 3, - (i) => Launch( - id: '$i', - name: 'mock-launch-name-$i', - links: const Links( - patch: Patch( - small: 'https://avatars.githubusercontent.com/u/2918581?v=4'), - webcast: 'https://www.youtube.com', - wikipedia: 'https://www.wikipedia.org/'), - ), + final latestLaunch = Launch( + id: '1337', + name: 'mock-launch-name-1337', + dateLocal: date, + dateUtc: date, + links: const Links( + patch: Patch( + small: 'https://avatars.githubusercontent.com/u/2918581?v=4'), + webcast: 'https://www.youtube.com', + wikipedia: 'https://www.wikipedia.org/'), ); setUp(() { @@ -121,7 +122,7 @@ void main() { test( 'throws JsonDeserializationException ' 'when deserializing json body fails', - () { + () async { when(() => httpClient.get(rocketUri)).thenAnswer( (_) async => http.Response( '[{"this_is_not_a_rocket_doc": true}]', @@ -233,7 +234,6 @@ void main() { }); }); -////////////////////////////////////// group('.fetchLatestLaunch', () { setUp(() { when(() => httpClient.get(latestLaunchUri)).thenAnswer( @@ -287,7 +287,7 @@ void main() { () { when(() => httpClient.get(latestLaunchUri)).thenAnswer( (_) async => http.Response( - '[{"this_is_not_a_latest_launch_doc": true}]', + '{"this_is_not_the_latest_launch": true}', 200, ), ); @@ -301,12 +301,13 @@ void main() { test('makes correct request', () async { await subject.fetchLatestLaunch(); + verify( () => httpClient.get(latestLaunchUri), ).called(1); }); - test('returns correct list of the latest launch', () { + test('returns correct element of the latest launch', () { expect( subject.fetchLatestLaunch(), completion(equals(latestLaunch)), diff --git a/test/crew/view/crew_page_test.dart b/test/crew/view/crew_page_test.dart index 79cb09a..be1d466 100644 --- a/test/crew/view/crew_page_test.dart +++ b/test/crew/view/crew_page_test.dart @@ -67,7 +67,9 @@ void main() { navigator = MockNavigator(); when(() => navigator.push(any(that: isRoute()))) - .thenAnswer((_) async {}); + .thenAnswer((_) async { + return null; + }); }); setUpAll(() { diff --git a/test/home/widgets/home_page_content_test.dart b/test/home/widgets/home_page_content_test.dart index 2a82e59..94a10a0 100644 --- a/test/home/widgets/home_page_content_test.dart +++ b/test/home/widgets/home_page_content_test.dart @@ -14,10 +14,14 @@ void main() { navigator = MockNavigator(); when(() => navigator.push(any(that: isRoute()))) - .thenAnswer((_) async {}); + .thenAnswer((_) async { + return null; + }); when(() => navigator.push(any(that: isRoute()))) - .thenAnswer((_) async {}); + .thenAnswer((_) async { + return null; + }); }); testWidgets( diff --git a/test/launches/cubit/launches_cubit_test.dart b/test/launches/cubit/launches_cubit_test.dart index 6ad0caf..4738e0e 100644 --- a/test/launches/cubit/launches_cubit_test.dart +++ b/test/launches/cubit/launches_cubit_test.dart @@ -12,9 +12,13 @@ void main() { group('CrewCubit', () { late LaunchRepository launchRepository; + final date = DateTime.now(); + final latestLaunch = Launch( id: '0', name: 'mock-launch-name', + dateLocal: date, + dateUtc: date, links: const Links( patch: Patch( small: 'https://avatars.githubusercontent.com/u/2918581?v=4', diff --git a/test/launches/cubit/launches_state_test.dart b/test/launches/cubit/launches_state_test.dart index 3e23c47..d46f0ba 100644 --- a/test/launches/cubit/launches_state_test.dart +++ b/test/launches/cubit/launches_state_test.dart @@ -5,10 +5,13 @@ import 'package:spacex_demo/launches/launches.dart'; void main() { group('LaunchesState', () { - const launch = Launch( + final date = DateTime.now(); + final launch = Launch( id: '0', name: 'mock-launch-name', - links: Links( + dateLocal: date, + dateUtc: date, + links: const Links( patch: Patch( small: 'https://avatars.githubusercontent.com/u/2918581?v=4', large: 'https://avatars.githubusercontent.com/u/2918581?v=4', @@ -19,21 +22,15 @@ void main() { ); test('supports value comparison', () { expect( - const LaunchesState( + LaunchesState( status: LaunchesStatus.success, latestLaunch: launch, ), - const LaunchesState( + LaunchesState( status: LaunchesStatus.success, latestLaunch: launch, ), ); }); - test('copyWith', () { - const state = LaunchesState(status: LaunchesStatus.loading); - state.copyWith(status: LaunchesStatus.success); - - expect(state.status, state.status); - }); }); } diff --git a/test/launches/view/launches_page_test.dart b/test/launches/view/launches_page_test.dart index 526d3b0..321c00c 100644 --- a/test/launches/view/launches_page_test.dart +++ b/test/launches/view/launches_page_test.dart @@ -27,10 +27,13 @@ void main() { late UrlLauncherPlatform urlLauncherPlatform; const status = LaunchesStatus.success; + final date = DateTime.now(); final latestLaunch = Launch( id: '0', name: 'mock-launch-name', + dateLocal: date, + dateUtc: date, links: const Links( patch: Patch( small: 'https://avatars.githubusercontent.com/u/2918581?v=4', @@ -108,7 +111,9 @@ void main() { navigator = MockNavigator(); when(() => navigator.push(any(that: isRoute()))) - .thenAnswer((_) async {}); + .thenAnswer((_) async { + return null; + }); }); setUpAll(() { @@ -202,88 +207,102 @@ void main() { }, ); - // group('open link buttons', () { - // const key = Key('launchesPage_link_buttons'); - // const webcastKey = Key('launchesPage_openWebcast_elevatedButton'); - // const wikipediaKey = Key('launchesPage_openWikipedia_elevatedButton'); - - // testWidgets( - // 'is rendered', - // (tester) async { - // await mockNetworkImages(() async { - // await tester.pumpApp( - // BlocProvider.value( - // value: launchesCubit, - // child: const LaunchesView(), - // ), - // ); - // }); - - // expect(find.byKey(key), findsOneWidget); - // }, - // ); - - // testWidgets( - // 'attempts to open webcast url when pressed', - // (tester) async { - // await mockNetworkImages(() async { - // await tester.pumpApp( - // BlocProvider.value( - // value: launchesCubit, - // child: const LaunchesView(), - // ), - // ); - // }); - - // await tester.tap(find.byKey(webcastKey)); - - // verify( - // () => urlLauncherPlatform.canLaunch(latestLaunch.links.webcast), - // ).called(1); - // verify( - // () => urlLauncherPlatform.launch( - // latestLaunch.links.webcast, - // useSafariVC: true, - // useWebView: false, - // enableJavaScript: false, - // enableDomStorage: false, - // universalLinksOnly: false, - // headers: const {}, - // ), - // ).called(1); - // }, - // ); - - // testWidgets( - // 'attempts to open wikipedia url when pressed', - // (tester) async { - // await mockNetworkImages(() async { - // await tester.pumpApp( - // BlocProvider.value( - // value: launchesCubit, - // child: const LaunchesView(), - // ), - // ); - // }); - - // await tester.tap(find.byKey(wikipediaKey)); - - // verify( - // () => urlLauncherPlatform.canLaunch(latestLaunch.links.wikipedia), - // ).called(1); - // verify( - // () => urlLauncherPlatform.launch( - // latestLaunch.links.wikipedia, - // useSafariVC: true, - // useWebView: false, - // enableJavaScript: false, - // enableDomStorage: false, - // universalLinksOnly: false, - // headers: const {}, - // ), - // ).called(1); - // }, - // ); - // }); + group('open link buttons', () { + const key = Key('launchesPage_link_buttons'); + const webcastKey = Key('launchesPage_openWebcast_elevatedButton'); + const wikipediaKey = Key('launchesPage_openWikipedia_elevatedButton'); + + testWidgets( + 'is rendered when the latest launch contain a wikipedia url', + (tester) async { + when(() => launchesCubit.state).thenReturn( + LaunchesState( + status: LaunchesStatus.success, + latestLaunch: latestLaunch, + ), + ); + await mockNetworkImages(() async { + await tester.pumpApp( + BlocProvider.value( + value: launchesCubit, + child: const LaunchesView(), + ), + ); + }); + + expect(find.byKey(key), findsOneWidget); + }, + ); + + testWidgets('attemps to open wikipedia url when pressed', (tester) async { + await mockNetworkImages(() async { + when(() => launchesCubit.state).thenReturn( + LaunchesState( + status: LaunchesStatus.success, + latestLaunch: latestLaunch, + ), + ); + await tester.pumpApp( + BlocProvider.value( + value: launchesCubit, + child: const LaunchesView(), + ), + ); + }); + await tester.tap(find.byKey(wikipediaKey)); + + verify( + () => urlLauncherPlatform.canLaunch(latestLaunch.links.wikipedia!), + ).called(1); + verify( + () => urlLauncherPlatform.launch( + latestLaunch.links.wikipedia!, + useSafariVC: true, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: const {}, + ), + ).called(1); + }); + + testWidgets( + 'attempts to open webcast url when pressed', + (tester) async { + when(() => launchesCubit.state).thenReturn( + LaunchesState( + status: LaunchesStatus.success, + latestLaunch: latestLaunch, + ), + ); + await mockNetworkImages(() async { + await tester.pumpApp( + BlocProvider.value( + value: launchesCubit, + child: const LaunchesView(), + ), + ); + }); + + await tester.tap(find.byKey(webcastKey)); + + verify( + () => urlLauncherPlatform.canLaunch(latestLaunch.links.webcast!), + ).called(1); + verify( + () => urlLauncherPlatform.launch( + latestLaunch.links.webcast!, + useSafariVC: true, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: const {}, + ), + ).called(1); + }, + ); + }); }); } diff --git a/test/rocket_details/cubit/rocket_details_cubit_test.dart b/test/rocket_details/cubit/rocket_details_cubit_test.dart index dfe46af..11449da 100644 --- a/test/rocket_details/cubit/rocket_details_cubit_test.dart +++ b/test/rocket_details/cubit/rocket_details_cubit_test.dart @@ -9,13 +9,14 @@ class MockRocketRepository extends Mock implements RocketRepository {} void main() { group('RocketDetailsCubit', () { - const rocket = Rocket( + final rocket = Rocket( id: '0', name: 'mock-rocket-name', description: 'mock-rocket-description', height: Length(meters: 1, feet: 1), diameter: Length(meters: 1, feet: 1), mass: Mass(kg: 1, lb: 1), + firstFlight: DateTime.now(), ); test('initial state is correct', () { diff --git a/test/rocket_details/cubit/rocket_details_state_test.dart b/test/rocket_details/cubit/rocket_details_state_test.dart index d3171db..dbcd69f 100644 --- a/test/rocket_details/cubit/rocket_details_state_test.dart +++ b/test/rocket_details/cubit/rocket_details_state_test.dart @@ -5,13 +5,14 @@ import 'package:spacex_demo/rocket_details/rocket_details.dart'; void main() { group('RocketDetailsState', () { - const rocket = Rocket( + final rocket = Rocket( id: '0', name: 'mock-rocket-name', description: 'mock-rocket-description', height: Length(meters: 1, feet: 1), diameter: Length(meters: 1, feet: 1), mass: Mass(kg: 1, lb: 1), + firstFlight: DateTime.now(), ); test('supports value comparison', () { diff --git a/test/rocket_details/view/rocket_details_page_test.dart b/test/rocket_details/view/rocket_details_page_test.dart index 1324246..33a5121 100644 --- a/test/rocket_details/view/rocket_details_page_test.dart +++ b/test/rocket_details/view/rocket_details_page_test.dart @@ -139,15 +139,16 @@ void main() { testWidgets('renders cross icon when rocket is inactive', (tester) async { when(() => rocketDetailsCubit.state).thenReturn( - const RocketDetailsState( + RocketDetailsState( rocket: Rocket( id: '0', name: 'mock-rocket-name', description: 'mock-rocket-description', - height: Length(meters: 1, feet: 1), - diameter: Length(meters: 1, feet: 1), - mass: Mass(kg: 1, lb: 1), + height: const Length(meters: 1, feet: 1), + diameter: const Length(meters: 1, feet: 1), + mass: const Mass(kg: 1, lb: 1), active: false, + firstFlight: DateTime.now(), ), ), ); @@ -201,14 +202,15 @@ void main() { 'is not rendered when the rocket does not contain a wikipedia url', (tester) async { when(() => rocketDetailsCubit.state).thenReturn( - const RocketDetailsState( + RocketDetailsState( rocket: Rocket( id: '0', name: 'mock-rocket-name', description: 'mock-rocket-description', - height: Length(meters: 1, feet: 1), - diameter: Length(meters: 1, feet: 1), - mass: Mass(kg: 1, lb: 1), + height: const Length(meters: 1, feet: 1), + diameter: const Length(meters: 1, feet: 1), + mass: const Mass(kg: 1, lb: 1), + firstFlight: DateTime.now(), ), ), ); diff --git a/test/rockets/cubit/rockets_cubit_test.dart b/test/rockets/cubit/rockets_cubit_test.dart index 4f9be46..dc63b84 100644 --- a/test/rockets/cubit/rockets_cubit_test.dart +++ b/test/rockets/cubit/rockets_cubit_test.dart @@ -21,6 +21,7 @@ void main() { height: const Length(meters: 1, feet: 1), diameter: const Length(meters: 1, feet: 1), mass: const Mass(kg: 1, lb: 1), + firstFlight: DateTime.now(), ), ); diff --git a/test/rockets/view/rockets_page_test.dart b/test/rockets/view/rockets_page_test.dart index c5cb415..d77e53d 100644 --- a/test/rockets/view/rockets_page_test.dart +++ b/test/rockets/view/rockets_page_test.dart @@ -24,6 +24,7 @@ void main() { height: const Length(meters: 1, feet: 1), diameter: const Length(meters: 1, feet: 1), mass: const Mass(kg: 1, lb: 1), + firstFlight: DateTime.now(), ), ); @@ -64,7 +65,9 @@ void main() { navigator = MockNavigator(); when(() => navigator.push(any(that: isRoute()))) - .thenAnswer((_) async {}); + .thenAnswer((_) async { + return null; + }); }); setUpAll(() { From 7768873f9b965517bd5794a9298e40f54711fac8 Mon Sep 17 00:00:00 2001 From: Nino Gjoni <49320058+ngjoni@users.noreply.github.com> Date: Tue, 12 Apr 2022 22:24:49 +0200 Subject: [PATCH 26/26] chore: implement suggested changes --- lib/launches/view/launches_page.dart | 4 ++-- .../test/rocket_repository_test.dart | 15 ++++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/launches/view/launches_page.dart b/lib/launches/view/launches_page.dart index 435d0f7..4388efb 100644 --- a/lib/launches/view/launches_page.dart +++ b/lib/launches/view/launches_page.dart @@ -4,7 +4,7 @@ import 'package:intl/intl.dart'; import 'package:launch_repository/launch_repository.dart'; import 'package:spacex_api/spacex_api.dart'; import 'package:spacex_demo/l10n/l10n.dart'; -import 'package:spacex_demo/launches/cubit/launches_cubit.dart'; +import 'package:spacex_demo/launches/launches.dart'; import 'package:url_launcher/url_launcher.dart'; class LaunchesPage extends StatelessWidget { @@ -51,7 +51,7 @@ class _LaunchesContent extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = context.l10n; - final state = context.select((LaunchesCubit cubit) => cubit.state); + final state = context.watch().state; switch (state.status) { case LaunchesStatus.initial: diff --git a/packages/rocket_repository/test/rocket_repository_test.dart b/packages/rocket_repository/test/rocket_repository_test.dart index a27724e..184313d 100644 --- a/packages/rocket_repository/test/rocket_repository_test.dart +++ b/packages/rocket_repository/test/rocket_repository_test.dart @@ -13,13 +13,14 @@ void main() { final rockets = List.generate( 3, (i) => Rocket( - id: '$i', - name: 'mock-rocket-name-$i', - description: 'mock-rocket-description-$i', - height: const Length(meters: 1, feet: 1), - diameter: const Length(meters: 1, feet: 1), - mass: const Mass(kg: 1, lb: 1), - firstFlight: DateTime.now()), + id: '$i', + name: 'mock-rocket-name-$i', + description: 'mock-rocket-description-$i', + height: const Length(meters: 1, feet: 1), + diameter: const Length(meters: 1, feet: 1), + mass: const Mass(kg: 1, lb: 1), + firstFlight: DateTime.now(), + ), ); setUp(() {