Skip to content

Implement launch details screen#38

Merged
ashtanko merged 20 commits intomainfrom
feature/launch_details_screen
Sep 6, 2025
Merged

Implement launch details screen#38
ashtanko merged 20 commits intomainfrom
feature/launch_details_screen

Conversation

@ashtanko
Copy link
Owner

@ashtanko ashtanko commented Sep 6, 2025

Summary by CodeRabbit

  • New Features

    • Rich, animated Launch detail screen plus many new UI components (cards, chips, info grid, timeline, carousel placeholder, animated link buttons).
  • Localization

    • Added Ukrainian locale and expanded English, German, and Portuguese translations for launch UI text.
  • Models & API

    • Expanded and more strongly-typed launch/network models with nested stage/payload/site data, new link fields, and resource mapping helpers.
  • Tests

    • Added many unit, widget, and integration tests (including a new navigation integration test) and adjusted an assertion to be less strict.
  • Chores

    • CI concurrency control enabled and url_launcher dependency added.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 6, 2025

Walkthrough

Adds Ukrainian localization and many locale keys; introduces multiple Freezed network models (core, first/second stage, payload) with generated serializers; extends launch/rocket/links models and resource mappings; refactors Launch screen into Bloc + stateful content and many UI widgets; adds url_launcher, tests, and CI concurrency plus a new integration test.

Changes

Cohort / File(s) Summary of changes
CI workflow
**/.github/workflows/ci.yml
Add concurrency group keyed by workflow+PR/ref and cancel-in-progress: true; include new integration test in the integration-tests script.
Integration tests
integration_test/app_test.dart, integration_test/launch_navigation_test.dart, integration_test/launch_test.dart
Loosen app_test assertion; add launch_navigation_test.dart (navigation interaction); refactor launch_test.dart to use a mocked Bloc and locale-parameterized pump helpers.
Network models (new & updated)
lib/data/network/model/core/..., lib/data/network/model/launch/..., lib/data/network/model/payload/...
Add NetworkCoreModel, NetworkFirstStageModel, NetworkSecondStageModel, NetworkPayloadModel (with generated .freezed/.g.dart); change NetworkLaunchFullModel ships → List<String>?, rename staticFireDateUtcstaticFireDate; extend NetworkRocketModel and NetworkLaunchLinksModel with stage and extra link/flickr fields.
Generated model code
lib/data/network/model/**/*.freezed.dart, **.g.dart
Add/update Freezed and json_serializable generated files to reflect new models/fields, copyWith, when/map helpers, and (de)serializers.
Resource models & mapping extensions
lib/models/core/*, lib/models/launch/*, lib/models/payload/*, lib/models/launch/extension/*
Add CoreResource, PayloadResource, FirstStage/SecondStage/LaunchSite/other resources; add extensions to convert network models → resources; expand LaunchFullResource, RocketResource, LinksResource; update date-format helpers and toResource mappings.
Launch screen & widgets
lib/features/launch/launch_screen.dart, lib/features/launch/widget/*
Split LaunchScreen into LaunchScreenBlocContent + stateful LaunchScreenContent; add many UI widgets (AnimatedLinkButtonWidget, FeatureChipWidget, InfoGridWidget, LaunchSiteCardWidget, LinksCardWidget, MissionOverviewCardWidget, PayloadCardWidget, RecoveryShipsCardWidget, RocketCardWidget, StatCard/QuickStats, StatusSectionWidget, Timeline widgets) and update imports/localization usage.
Localization & ARB files
lib/app/localization.dart, lib/l10n/*.dart, lib/l10n/intl_*.arb
Add Ukrainian locale and many new localization keys; add/enrich en/de/pt/uk localization implementations and update ARB entries (flightNumber, mission/timeline, rocket/payload, links, etc.).
Dependency
pubspec.yaml
Add url_launcher: ^6.3.2.
Tests
test/**
Add extensive unit/widget tests for models, resources, mapping extensions, widgets and update integration tests and fixtures to match LaunchFullResource shape.
Build tooling
Makefile
Switch build_runner calls from flutter pub run to dart run in several targets.
Feature helpers
lib/features/launch/info_item.dart
Add InfoItem Equatable value class used by InfoGrid and tests.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant UI as LaunchScreenBlocContent
  participant Bloc as LaunchBloc
  participant Repo as LaunchesRepository
  participant API as Network API
  participant Content as LaunchScreenContent

  User->>UI: open(flightNumber)
  UI->>Bloc: dispatch LaunchLoadEvent(flightNumber)
  Bloc->>Repo: loadLaunchFull(flightNumber)
  Repo->>API: GET /launches/{id}
  API-->>Repo: NetworkLaunchFullModel JSON
  Repo->>Bloc: mapped LaunchFullResource
  Bloc-->>UI: LaunchSuccessState(resource)
  UI->>Content: build(resource)
  Content->>Content: render status, rocket, payload, timeline, links, site, ships

  alt API error
    API-->>Repo: error
    Repo->>Bloc: LaunchFailure
    Bloc-->>UI: LaunchErrorState
    UI->>User: show ErrorContent (retry -> LaunchLoadEvent)
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

Hop-hop, I test and cheer,
Models sprout and widgets appear.
Links that open, chips that shine,
Timelines tick and locales align.
A Ukrainian hop for this code‑bunny's flight—🐇🚀

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/launch_details_screen

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codacy-production
Copy link

codacy-production bot commented Sep 6, 2025

Coverage summary from Codacy

See diff coverage on Codacy

Coverage variation Diff coverage
+10.45% (target: -1.00%) 84.75%
Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (05c9951) 1424 930 65.31%
Head commit (8ea9176) 2306 (+882) 1747 (+817) 75.76% (+10.45%)

Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>

Diff coverage details
Coverable lines Covered lines Diff coverage
Pull request (#38) 905 767 84.75%

Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%

See your quality gate settings    Change summary preferences

@codecov
Copy link

codecov bot commented Sep 6, 2025

Codecov Report

❌ Patch coverage is 84.66258% with 125 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.83%. Comparing base (05c9951) to head (8ea9176).
⚠️ Report is 27 commits behind head on main.

Files with missing lines Patch % Lines
lib/features/launch/launch_screen.dart 72.72% 45 Missing ⚠️
lib/features/launch/widget/stat_card_widget.dart 0.00% 40 Missing ⚠️
lib/features/launch/widget/rocket_card_widget.dart 74.66% 19 Missing ⚠️
lib/features/launch/widget/links_card_widget.dart 77.77% 12 Missing ⚠️
...res/launch/widget/animated_link_button_widget.dart 80.76% 5 Missing ⚠️
lib/features/launch/widget/timeline_widgets.dart 97.18% 2 Missing ⚠️
...ib/features/launch/widget/feature_chip_widget.dart 96.15% 1 Missing ⚠️
.../features/launch/widget/status_section_widget.dart 96.66% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #38      +/-   ##
==========================================
+ Coverage   72.03%   80.83%   +8.80%     
==========================================
  Files          69       96      +27     
  Lines        1137     1931     +794     
==========================================
+ Hits          819     1561     +742     
- Misses        318      370      +52     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 23

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
lib/data/network/model/launch/full/network_launch_full_model.dart (2)

49-50: Don’t mark a nullable field as required

required List? cores is misleading; remove required or make the field non-nullable with a default.

-    required List<NetworkCore>? cores,
+    List<NetworkCore>? cores,

Alternative (if you prefer non-nullable):

-    required List<NetworkCore>? cores,
+    @Default(<NetworkCore>[]) List<NetworkCore> cores,

59-63: Fix type: block should be int?, not String?

As-is, JSON deserialization will fail when the API returns an integer for block.

-    @JsonKey(name: 'block') String? block,
+    @JsonKey(name: 'block') int? block,
test/data/network/model/network_launch_full_model_test.dart (1)

141-145: Fix toJson expectations: don't cast JSON back to model types

toJson should yield JSON-serializable maps. Casting json['rocket'] to NetworkRocketModel will fail. Assert on the nested map instead.

Apply:

-      expect((json['rocket'] as NetworkRocketModel).name, 'Falcon 1');
+      expect(json['rocket'], isA<Map<String, dynamic>>());
+      expect((json['rocket'] as Map<String, dynamic>)['rocket_name'], 'Falcon 1');
lib/models/launch/launch_full_resource.dart (1)

9-25: Enforce immutability for ships (mutable List breaks @immutable intent).

ships is mutable and externally modifiable, which can cause subtle state/equality bugs. Wrap it as unmodifiable at construction and in copyWith. This removes the const constructor (trade-off is worth the safety).

Apply:

-class LaunchFullResource extends Equatable {
-  const LaunchFullResource({
+class LaunchFullResource extends Equatable {
+  LaunchFullResource({
     required this.id,
     required this.flightNumber,
     this.missionName,
     this.launchDate,
     this.staticFireDate,
     this.isTentative,
     this.tentativeMaxPrecision,
     this.details,
     this.tbd,
     this.launchWindow,
     this.rocket,
     this.launchSuccess,
     this.links,
     this.launchSite,
-    required this.ships,
-  });
+    required List<String> ships,
+  }) : ships = List.unmodifiable(ships);
@@
-  final List<String> ships;
+  final List<String> ships;
@@
-      ships: ships ?? this.ships,
+      ships: ships != null ? List.unmodifiable(ships) : this.ships,

If you rely on const constructions elsewhere, I can suggest a factory that preserves const for already-unmodifiable inputs. Want that?

Also applies to: 41-41, 74-76

lib/data/network/model/launch/full/network_launch_full_model.freezed.dart (1)

1569-1572: Critical: NetworkCore.block typed as String? — likely runtime cast error.

SpaceX v3 returns numeric block. json_serializable will generate json['block'] as String?, failing when it’s an int. Change the source model (network_launch_full_model.dart) so nested NetworkCore.block is int?, then re-run codegen.

Apply equivalent changes in the source (illustrative diff against the generated file; do not hand-edit generated code):

-  @JsonKey(name: 'block')
-  String? get block;
+  @JsonKey(name: 'block')
+  int? get block;
...
-      @JsonKey(name: 'block') String? block,
+      @JsonKey(name: 'block') int? block,

Also applies to: 1647-1656, 1969-1972, 2108-2112

🧹 Nitpick comments (116)
pubspec.yaml (1)

49-49: url_launcher schemes are http(s) only; add LinkLauncher abstraction
grep search confirmed no Uri.parse calls with tel:, sms:, mailto:, geo:, maps:, market:, itms: or itms-apps schemes—only http/https are used. Recommend introducing a small LinkLauncher wrapper around launchUrl to enable mocking in tests.

integration_test/app_test.dart (1)

17-17: Assertion relax is fine; prefer locale-agnostic finder

Using findsAtLeast(1) is OK, but relying on English text can be brittle with multi-locale runs. Prefer keys/semantics or pulling the localized string from AppLocalizations in the test.

integration_test/launch_navigation_test.dart (2)

10-10: Remove no-op setUp

setUp(() {}) is redundant here.

-  setUp(() {});
+

18-21: Avoid data-dependent hard-coded Key

Key('FalconSat1') ties the test to specific seed data. Prefer a stable key (e.g., ValueKey('launch_$id')) or find by semantics/label built from domain data.

Would you like a patch adding stable ValueKey wiring to LaunchItem (e.g., ValueKey('launch_${model.flightNumber}')) and updating the test accordingly?

lib/l10n/intl_uk.arb (2)

145-148: Polish obvious UA wording issues

A few strings read awkwardly or contain grammatical slips in Ukrainian.

-"borderRadiusButtonTitle": "Радіус кордону",
+"borderRadiusButtonTitle": "Радіус заокруглення",
-"borderSideButtonTitle": "Кордона сторона",
+"borderSideButtonTitle": "Лінія межі",

-"gridFins": "Сітчасті кермові",
+"gridFins": "Решітчасті керма",
-"landingLegs": "Ноги приземлення",
+"landingLegs": "Посадкові опори",

-"firstStage": "🚀 Перша ступінь",
+"firstStage": "🚀 Перший ступінь",

-"recoveryShips": "Судна порятунку",
+"recoveryShips": "Рятувальні судна",

If you prefer, I can review the full UA set for consistency (cases, gender, and aerospace terminology).

Also applies to: 239-244, 219-223, 255-258


97-104: Consider ICU plurals for days

Ukrainian has complex plurals; ICU messages will render correctly for 1/2–4/5+.

-"daysSinceTodayTitle": "{days} днів тому",
+"daysSinceTodayTitle": "{days, plural, one {# день тому} few {# дні тому} many {# днів тому} other {# днів тому}}",
-"daysFromTodayTitle": "Через {days} днів",
+"daysFromTodayTitle": "{days, plural, one {Через # день} few {Через # дні} many {Через # днів} other {Через # днів}}",

Note: Adjust other locales accordingly to keep key parity.

lib/features/launch/widget/feature_chip_widget.dart (2)

17-25: Confirm Color.withValues() SDK support or use withOpacity() for broader compatibility

withValues(alpha: ...) requires a fairly recent Flutter SDK. If your min SDK might be older, switch to withOpacity().

Apply:

-            ? theme.colorScheme.primary.withValues(alpha: 0.1)
+            ? theme.colorScheme.primary.withOpacity(0.1)
...
-              ? theme.colorScheme.primary.withValues(alpha: 0.3)
+              ? theme.colorScheme.primary.withOpacity(0.3)
-              : theme.colorScheme.outline.withValues(alpha: 0.2),
+              : theme.colorScheme.outline.withOpacity(0.2),

Also applies to: 33-44


27-49: Add Semantics for accessibility

Expose selection state to screen readers.

-      child: Row(
+      child: Semantics(
+        label: label,
+        selected: active,
+        child: Row(
           mainAxisSize: MainAxisSize.min,
           children: [
             Icon(
               active ? Icons.check_circle : Icons.cancel,
               size: 14,
               color: active
                   ? theme.colorScheme.primary
                   : theme.colorScheme.onSurfaceVariant,
             ),
             const SizedBox(width: 4),
             Text(
               label,
               style: TextStyle(
                 fontSize: 12,
                 color: active
                     ? theme.colorScheme.primary
                     : theme.colorScheme.onSurfaceVariant,
                 fontWeight: FontWeight.w500,
               ),
             ),
           ],
-      ),
+        ),
+      ),
lib/features/launch/widget/animated_link_button_widget.dart (2)

28-41: Optional: AnimatedContainer is static here

No properties change over time; a plain Container suffices unless you plan to animate.


29-33: SDK check for Color.withValues()

Same note as other file; consider withOpacity() for backward compatibility.

lib/l10n/intl_de.arb (1)

196-197: Add placeholders metadata for flightNumber

Improves generated API clarity and tooling.

-  "flightNumber": "Flug #{number}"
+  "flightNumber": "Flug #{number}",
+  "@flightNumber": {
+    "placeholders": { "number": {} }
+  }
lib/l10n/intl_pt.arb (2)

196-197: Add placeholders metadata for flightNumber

Match other locales and aid codegen.

-  "flightNumber": "Voo #{number}"
+  "flightNumber": "Voo #{number}",
+  "@flightNumber": {
+    "placeholders": { "number": {} }
+  }

177-177: Clarify “not available” wording

“N/D” can be unclear; consider a full phrase.

-  "notAvailable": "N/D",
+  "notAvailable": "Não disponível",
lib/l10n/intl_en.arb (1)

154-158: Add a brief description for @transparentButtonTitle.

Metadata helps translators; even a one-liner keeps consistency with nearby entries.

-  "@transparentButtonTitle": {},
+  "@transparentButtonTitle": {
+    "description": "Label for transparent button style"
+  },
lib/l10n/app_localizations_uk.dart (1)

12-12: Polish several Ukrainian UI strings (more idiomatic).

Improves clarity and correctness; safe, user-facing text quality.

-  String get appTitle => 'шаблон_flutter_bloc_app';
+  String get appTitle => 'Шаблон Flutter BLoC';
-  String get borderRadiusButtonTitle => 'Радіус кордону';
+  String get borderRadiusButtonTitle => 'Радіус кутів';
-  String get borderSideButtonTitle => 'Кордона сторона';
+  String get borderSideButtonTitle => 'Товщина обвідки';
-  String get gridFins => 'Сітчасті кермові';
+  String get gridFins => 'Решітчасті керма';
-  String get landingLegs => 'Ноги приземлення';
+  String get landingLegs => 'Посадкові опори';
-  String get recoveryShips => 'Судна порятунку';
+  String get recoveryShips => 'Рятувальні судна';
-  String get siteIdLabel => 'ID сайту:';
+  String get siteIdLabel => 'ID майданчика:';

Also applies to: 154-158, 232-236, 244-246, 295-295

.github/workflows/ci.yml (1)

101-116: Speed up integration job with pub cache.

Mirror the unit-test job caching to shave minutes off CI.

     steps:
       - name: Checkout
         uses: actions/checkout@v5

       - name: Setup Flutter
         uses: subosito/flutter-action@v2
         with:
           channel: stable
+
+      - name: Cache pub dependencies
+        uses: actions/cache@v4
+        with:
+          path: ${{ env.HOME }}/.pub-cache
+          key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.lock') }}
+          restore-keys: ${{ runner.os }}-pub-
test/repository/launches_repository_test.dart (2)

33-39: Prefer Exception over Error in tests.

Throwing Error is for VM/runtime failures; app/repo code should surface Exceptions.

-        when(repository.getLaunches()).thenAnswer((_) => Future.error(Error()));
+        when(repository.getLaunches()).thenAnswer((_) => Future.error(Exception('failed')));
 ...
-        when(repository.getLaunch(flightNumber))
-            .thenAnswer((_) => Future.error(Error()));
+        when(repository.getLaunch(flightNumber))
+            .thenAnswer((_) => Future.error(Exception('failed')));

Also applies to: 53-61


97-107: Validate the new ships field explicitly.

Add an assertion to document the contract (non-null, empty by default).

       test('returns full launch', () {
         when(repository.getLaunch(flightNumber))
             .thenAnswer((_) => Future.value(mockFullLaunch));
         expect(
           repository.getLaunch(flightNumber),
           completion(equals(mockFullLaunch)),
         );
+        expect(mockFullLaunch.ships, isEmpty);
       });
test/models/payload/payload_resource_test.dart (1)

45-48: Strengthen equality test with non-const lists.

Current equality may pass due to constant canonicalization of list literals. Add a case where two instances use non-const lists to ensure deep equality behaves as intended at runtime.

Example add-on test:

test('value equality with non-const lists', () {
  final r1 = const PayloadResource(
    payloadId: 'X',
    noradId: null,
    customers: null,
  ).copyWith(
    noradId: [1, 2],
    customers: ['A'],
  );
  final r2 = const PayloadResource(
    payloadId: 'X',
    noradId: null,
    customers: null,
  ).copyWith(
    noradId: [1, 2],
    customers: ['A'],
  );
  expect(r1, equals(r2));
});

If equality fails, you may need Equatable’s deep collection support or to normalize lists.

Also applies to: 50-52

lib/features/launch/info_item.dart (2)

2-2: Make constructor const (cheap win for immutability/perf).

-  InfoItem(this.label, this.value);
+  const InfoItem(this.label, this.value);

2-2: Consider named parameters to prevent argument swaps.

Low-risk API hardening if usage is internal. If public, defer to a follow-up.

-  const InfoItem(this.label, this.value);
+  const InfoItem({required this.label, required this.value});
test/models/first_stage_ext_test.dart (3)

68-68: Test name mismatch: uses empty list, not null.

-    test('should handle null cores correctly', () {
+    test('should handle empty cores correctly', () {

72-72: Prefer isEmpty matcher over raw list equality.

-      expect(resource.cores, <dynamic>[]);
+      expect(resource.cores, isEmpty);

69-73: Add a case for truly null cores (if the model allows it).

Covers cores == null vs [] semantics in toResource().

Example:

test('should handle null cores correctly', () {
  const nullModel = NetworkFirstStageModel(cores: null);
  final resource = nullModel.toResource();
  expect(resource.cores, isEmpty); // or isNull, per design
});
test/data/network/model/core/network_core_model_test.dart (1)

34-39: Ensure toJson always includes nulls or use round-trip equivalence

  • Default includeIfNull is true, so expect(result, json) currently passes (fuchsia.googlesource.com)
  • To avoid brittleness if null-omission is ever enabled, either add @JsonSerializable(includeIfNull: true) to NetworkCoreModel or refactor the test to round-trip via fromJson(toJson()) and compare models
test/models/launch/launch_site_ext_test.dart (1)

28-30: Use isNull matcher for clarity.

-      expect(resource.siteId, null);
-      expect(resource.siteName, null);
-      expect(resource.siteNameLong, null);
+      expect(resource.siteId, isNull);
+      expect(resource.siteName, isNull);
+      expect(resource.siteNameLong, isNull);
test/models/launch/rocket_resource_test.dart (1)

61-64: Consider immutability checks

Optionally assert that nested lists are unmodifiable to protect state objects from accidental mutation.

test/models/payload/payload_ext_test.dart (1)

44-76: Add immutability assertions for list fields

Ensure toResource returns unmodifiable lists for noradId/customers to prevent downstream mutation.

Apply this diff to add a test:

   test('should handle null fields correctly', () {
     const model = NetworkPayloadModel(
       payloadId: null,
       noradId: null,
       reused: null,
       customers: null,
       nationality: null,
       manufacturer: null,
       payloadType: null,
       payloadMassKg: null,
       payloadMassLbs: null,
       orbit: null,
     );
     final resource = model.toResource();
 
     expect(
       resource,
       equals(
         const PayloadResource(
           payloadId: null,
           noradId: null,
           reused: null,
           customers: null,
           nationality: null,
           manufacturer: null,
           payloadType: null,
           payloadMassKg: null,
           payloadMassLbs: null,
           orbit: null,
         ),
       ),
     );
   });
+
+  test('toResource should return unmodifiable lists', () {
+    final model = NetworkPayloadModel.fromJson(json);
+    final resource = model.toResource();
+    expect(() => resource.customers?.add('NASA'), throwsUnsupportedError);
+    expect(() => resource.noradId?.add(99999), throwsUnsupportedError);
+  });

If this fails, update the mapping (models/payload/payload_ext.dart) or the resource to wrap lists with List.unmodifiable or EquatableListView.

test/models/core/core_ext_test.dart (2)

8-19: Type the JSON literal to Map<String, dynamic> (and make it const)

Prevents implicit dynamic map issues and enables compile-time const.

-    final json = {
+    const Map<String, dynamic> json = {

44-76: Add a mixed/nulls test case

You have “all set” and “all null” cases. Consider a mixed case (some nulls, some set) to catch partial mapping regressions.

test/models/launch/rocket_resource_ext_test.dart (1)

52-65: Consider adding an “empty lists” case

A rocket with non-null stages but empty cores/payloads would harden behavior around empty collections vs nulls.

test/models/second_stage_resource_test.dart (1)

55-62: Avoid asserting Equatable.props directly

Asserting props ties tests to implementation details. Prefer asserting public fields or equality semantics only.

Example:

expect(resource1.block, 1);
expect(resource1.payloads, [payload1, payload2]); // deep equals
lib/models/payload/payload_ext.dart (1)

4-19: Correct field mapping; consider defensive copies for list fields

To prevent downstream mutation affecting equality, defensively copy list fields when present.

   return PayloadResource(
     payloadId: payloadId,
-    noradId: noradId,
+    noradId: noradId == null ? null : List.unmodifiable(noradId),
     reused: reused,
-    customers: customers,
+    customers: customers == null ? null : List.unmodifiable(customers),
     nationality: nationality,
     manufacturer: manufacturer,
     payloadType: payloadType,
     payloadMassKg: payloadMassKg,
     payloadMassLbs: payloadMassLbs,
     orbit: orbit,
   );
test/features/launch/bloc/launch_screen_test.dart (2)

77-79: Avoid tautological byType checks

You pump LaunchScreenBlocContent directly, so asserting its presence adds little. Prefer asserting specific child widgets/text relevant to the state.

Also applies to: 97-99


81-99: Consider adding a Ukrainian locale test

PR mentions UA localization; mirror the PT test with Locale('uk') and assert the correct translation.

test/models/first_stage_resource_test.dart (1)

54-57: Add an explicit “empty list” case

You cover null; add a cores: [] case to ensure props is [[]] and equality behaves as expected.

lib/data/network/model/launch/full/network_launch_full_model.dart (3)

89-91: Tighten norad_id to List?

Improves type safety and matches resource/tests; add a custom converter if the API mixes types.

-    @JsonKey(name: 'norad_id') List<dynamic>? noradId,
+    @JsonKey(name: 'norad_id') List<int>? noradId,

111-117: Use double? for orbital numeric fields that can be fractional

longitude and inclination_deg are often non-integers; current ints risk cast errors or precision loss.

-    @JsonKey(name: 'longitude') int? longitude,
+    @JsonKey(name: 'longitude') double? longitude,
...
-    @JsonKey(name: 'inclination_deg') int? inclinationDeg,
+    @JsonKey(name: 'inclination_deg') double? inclinationDeg,

46-84: Reduce model duplication across network layer

You now have standalone models (e.g., NetworkFirstStageModel/NetworkCoreModel). Consider reusing them here to avoid drift (types like block) and regenerate only one source of truth.

lib/models/core/core_ext.dart (1)

4-4: Minor naming clarity for the extension.

Consider a more descriptive name like NetworkCoreModelExt or a short doc comment for discoverability.

lib/models/launch/second_stage_resource.dart (1)

7-13: Make payloads truly immutable to prevent hash/equality drift.

Lists are mutable; mutating payloads after construction can change equality/hash behavior. Copy to an unmodifiable list in the constructor.

Apply:

-  const SecondStageResource({this.block, this.payloads});
+  const SecondStageResource({this.block, List<PayloadResource>? payloads})
+      : payloads = payloads == null ? null : List.unmodifiable(payloads);

If you intend null and empty to be equivalent in equality, also adjust props accordingly:

-  List<Object?> get props => [block, payloads];
+  List<Object?> get props => [block, payloads ?? const <PayloadResource>[]];
test/models/core/core_resource_test.dart (2)

45-48: Nit: avoid asserting on hashCode.

hashCode is an implementation detail; equality is sufficient. Consider dropping the hashCode assertion to reduce brittleness.

-      expect(core1.hashCode, equals(core2.hashCode));
+      // Equality is sufficient; hashCode is an implementation detail.

54-70: Props-order test is tightly coupled to implementation details.

This will fail on harmless props reordering. Prefer checking individual fields or using contains/hasLength matchers for stability.

test/models/launch/launch_full_resource_test.dart (2)

19-21: Stringly-typed dates risk parsing/locale issues.

If possible, model launchDate/staticFireDate as DateTime in the resource and format at the UI layer.


63-84: Reduce coupling to props order.

Asserting the entire props list (including nulls) makes tests brittle. Prefer asserting fields directly or using matchers against a subset.

test/models/launch/launch_site_resource_test.dart (1)

33-47: Avoid hard-coupling tests to Equatable.props

Asserting the exact contents/order of props can be brittle if implementation details change. Consider relying on equality/inequality and adding behavior-focused tests instead (e.g., mapping usage), or keep one minimal props check and drop the null-props assertion.

lib/models/launch/first_stage_ext.dart (1)

7-9: Return an immutable list

Defensively copy to an unmodifiable list to preserve immutability guarantees of resources.

Apply:

   FirstStageResource toResource() {
     return FirstStageResource(
-      cores: cores?.map((e) => e.toResource()).toList(),
+      cores: cores == null
+          ? null
+          : List.unmodifiable(cores!.map((e) => e.toResource())),
     );
   }
test/data/network/model/network_launch_full_model_test.dart (1)

136-139: Optional: assert static_fire_date_utc in serialized JSON

This guards the rename by verifying JSON key mapping.

You can add:

expect(json['static_fire_date_utc'], '2007-03-10T00:00:00.000Z');
lib/models/launch/rocket_resource.dart (1)

3-6: Unify import style

Mixing package and relative imports for sibling models can cause duplicate-type issues in some tooling. Use package imports consistently.

Apply:

-import 'package:flutter_bloc_app_template/models/launch/second_stage_resource.dart';
-
-import 'first_stage_resource.dart';
+import 'package:flutter_bloc_app_template/models/launch/second_stage_resource.dart';
+import 'package:flutter_bloc_app_template/models/launch/first_stage_resource.dart';
test/data/network/model/network_first_stage_model_test.dart (1)

5-34: Optional: add null/empty cores cases

Consider tests for cores: null and cores: [] to cover edge cases in parsing and serialization.

lib/features/launch/widget/mission_overview_card_widget.dart (3)

20-22: Prefer withOpacity over withValues for broader SDK compatibility

Unless you’ve added a custom Color extension, withValues isn’t guaranteed across Flutter SDKs. Use withOpacity to avoid build/runtime surprises.

-              theme.colorScheme.primaryContainer.withValues(alpha: 0.5),
-              theme.colorScheme.primaryContainer.withValues(alpha: 0.1),
+              theme.colorScheme.primaryContainer.withOpacity(0.5),
+              theme.colorScheme.primaryContainer.withOpacity(0.1),
...
-                      color: theme.colorScheme.primary.withValues(alpha: 0.1),
+                      color: theme.colorScheme.primary.withOpacity(0.1),
...
-                  color: theme.colorScheme.surface.withValues(alpha: 0.7),
+                  color: theme.colorScheme.surface.withOpacity(0.7),

Also applies to: 37-38, 58-58


62-62: Treat empty strings as “no details”

Current null-check lets empty/whitespace through. Normalize to show the fallback in that case.

-                  details ?? loc.noDetails,
+                  (details?.trim().isNotEmpty ?? false) ? details! : loc.noDetails,

34-44: Add semantics for accessibility

Expose a semantic label/role for the info icon to improve TalkBack/VoiceOver.

-                  Container(
+                  Semantics(
+                    label: loc.missionOverview,
+                    button: false,
+                    child: Container(
                       padding: const EdgeInsets.all(8),
                       decoration: BoxDecoration(
                         color: theme.colorScheme.primary.withOpacity(0.1),
                         borderRadius: BorderRadius.circular(12),
                       ),
                       child: Icon(
                         Icons.info_outline,
                         color: theme.colorScheme.primary,
                       ),
-                  ),
+                    ),
+                  ),
lib/features/launch/widget/info_grid_widget.dart (3)

21-27: Use theme-aware color for labels

Avoid hard-coded greys; respect theming (light/dark/high-contrast).

-                  Text(
-                    item.label,
-                    style: TextStyle(
-                      fontSize: 12,
-                      color: Colors.grey[600],
-                    ),
-                  ),
+                  Text(
+                    item.label,
+                    style: Theme.of(context).textTheme.labelSmall?.copyWith(
+                      color: Theme.of(context).colorScheme.onSurfaceVariant,
+                    ),
+                  ),

16-18: Consider making tile width configurable

A fixed 150px can clip localized/long values on narrow screens. Expose a tileWidth parameter with a sensible default.

-class InfoGridWidget extends StatelessWidget {
-  const InfoGridWidget({super.key, required this.items});
+class InfoGridWidget extends StatelessWidget {
+  const InfoGridWidget({super.key, required this.items, this.tileWidth = 160});
 
   final List<InfoItem> items;
+  final double tileWidth;
...
-            (item) => SizedBox(
-              width: 150,
+            (item) => SizedBox(
+              width: tileWidth,

29-35: Guard against overflow in values

Long strings (e.g., orbits, IDs) can overflow. Clamp lines and add ellipsis.

-                  Text(
-                    item.value,
-                    style: const TextStyle(
-                      fontSize: 14,
-                      fontWeight: FontWeight.w600,
-                    ),
-                  ),
+                  Text(
+                    item.value,
+                    maxLines: 2,
+                    overflow: TextOverflow.ellipsis,
+                    style: const TextStyle(
+                      fontSize: 14,
+                      fontWeight: FontWeight.w600,
+                    ),
+                  ),
test/data/network/model/payload/network_payload_model_test.dart (1)

6-17: Minor: make the input JSON const for safety

Immutability reduces accidental mutations in tests.

-    final json = {
+    const json = {
       'payload_id': 'FalconSAT-2',
       'norad_id': [12345, 67890],
       'reused': false,
       'customers': ['DARPA'],
       'nationality': 'United States',
       'manufacturer': 'SSTL',
       'payload_type': 'Satellite',
       'payload_mass_kg': 20,
       'payload_mass_lbs': 44,
       'orbit': 'LEO',
     };
lib/features/launch/widget/links_card_widget.dart (3)

53-55: Hide the card when there are no links

Avoid rendering an empty card when all links are null.

     final linkItems = [
       ...
     ];
+
+    if (linkItems.isEmpty) return const SizedBox.shrink();
 
     return Card(

54-66: Match Card shape with inner Container and clip the gradient

Prevents subtle corner mismatches and ensures proper clipping.

-    return Card(
-      child: Container(
+    return Card(
+      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
+      clipBehavior: Clip.antiAlias,
+      child: Container(
         decoration: BoxDecoration(
           gradient: LinearGradient(

59-61: Confirm Flutter version for Color.withValues or switch to withOpacity

Color.withValues is relatively new; use withOpacity for broader compatibility if your min SDK doesn’t support it.

-              theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.5),
-              theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.2),
+              theme.colorScheme.surfaceContainerHighest.withOpacity(0.5),
+              theme.colorScheme.surfaceContainerHighest.withOpacity(0.2),
@@
-                      color: theme.colorScheme.primary.withValues(alpha: 0.1),
+                      color: theme.colorScheme.primary.withOpacity(0.1),

Also applies to: 77-77

lib/data/network/model/launch/network_second_stage_model.dart (1)

8-18: Freezed class style and JSON config

Two tweaks for robustness:

  • Prefer class over abstract class with Freezed (common style and avoids older patterns).
  • Add @JsonSerializable(explicitToJson: true) so nested lists call toJson() without manual mapping.
 @freezed
-abstract class NetworkSecondStageModel with _$NetworkSecondStageModel {
+@JsonSerializable(explicitToJson: true)
+class NetworkSecondStageModel with _$NetworkSecondStageModel {
   const factory NetworkSecondStageModel({
     int? block,
     List<NetworkPayloadModel>? payloads,
   }) = _NetworkSecondStageModel;
 
   const NetworkSecondStageModel._();
test/data/network/model/network_second_stage_model_test.dart (2)

45-53: Use the generated toJson for round-trip testing

This validates the generator output directly and reduces boilerplate.

-    test('toJson should convert correctly', () {
-      final model = NetworkSecondStageModel.fromJson(json);
-      final result = {
-        'block': model.block,
-        'payloads': model.payloads?.map((p) => p.toJson()).toList(),
-      };
-
-      expect(result, json);
-    });
+    test('toJson should convert correctly', () {
+      final model = NetworkSecondStageModel.fromJson(json);
+      expect(model.toJson(), equals(json));
+    });

63-70: Avoid dynamic list typing in expectation

Use isEmpty for clarity and to avoid type inference surprises.

-      final updated = model.copyWith(block: 2, payloads: []);
-
-      expect(updated.block, 2);
-      expect(updated.payloads, <dynamic>[]);
+      final updated = model.copyWith(block: 2, payloads: []);
+      expect(updated.block, 2);
+      expect(updated.payloads, isEmpty);
lib/features/launch/widget/status_section_widget.dart (2)

13-71: Add Semantics for screen readers

Announce the mission status for accessibility.

-    return Container(
+    return Semantics(
+      container: true,
+      label: success ? loc.missionSuccessful : loc.missionFailed,
+      child: Container(
       padding: const EdgeInsets.all(16),
       decoration: BoxDecoration(
@@
-      ),
-      child: Row(
+      ),
+      child: Row(
         children: [
@@
-      ),
-    );
+      ),
+    );

26-26: Confirm Color.withValues availability or use withOpacity

Switch to withOpacity if your minimum Flutter SDK doesn’t include withValues.

-            color: (success ? Colors.green : Colors.red).withValues(alpha: 0.3),
+            color: (success ? Colors.green : Colors.red).withOpacity(0.3),
@@
-              color: Colors.white.withValues(alpha: 0.2),
+              color: Colors.white.withOpacity(0.2),
@@
-                    color: Colors.white.withValues(alpha: 0.9),
+                    color: Colors.white.withOpacity(0.9),

Also applies to: 37-37, 62-62

lib/features/launch/widget/recovery_ships_card_widget.dart (2)

15-17: Match Card shape with inner Container and clip

Keeps the gradient corners perfectly aligned.

-    return Card(
-      child: Container(
+    return Card(
+      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
+      clipBehavior: Clip.antiAlias,
+      child: Container(

20-22: Confirm Color.withValues availability or use withOpacity

Use withOpacity for wider SDK compatibility.

-              Colors.cyan.withValues(alpha: 0.1),
-              Colors.cyan.withValues(alpha: 0.05),
+              Colors.cyan.withOpacity(0.1),
+              Colors.cyan.withOpacity(0.05),
@@
-                      color: Colors.cyan.withValues(alpha: 0.1),
+                      color: Colors.cyan.withOpacity(0.1),
@@
-                            color: Colors.cyan.withValues(alpha: 0.3),
+                            color: Colors.cyan.withOpacity(0.3),

Also applies to: 38-38, 66-66

lib/data/network/model/launch/network_launch_model.dart (2)

36-37: Ensure nested models serialize correctly (toJson).

If these models are ever serialized, add explicitToJson to avoid nested objects being left as dynamic Maps.

Apply this diff on the factory to enable nested serialization:

 abstract class NetworkRocketModel with _$NetworkRocketModel {
-  const factory NetworkRocketModel({
+  @JsonSerializable(explicitToJson: true)
+  const factory NetworkRocketModel({
     @JsonKey(name: 'rocket_id') required String id,
     @JsonKey(name: 'rocket_type') String? type,
     @JsonKey(name: 'rocket_name') String? name,
     @JsonKey(name: 'first_stage') NetworkFirstStageModel? firstStage,
     @JsonKey(name: 'second_stage') NetworkSecondStageModel? secondStage,
   }) = _NetworkRocketModel;

Please confirm whether toJson is used for NetworkRocketModel anywhere; if yes, re-run build_runner.


54-56: Consider stronger types for URLs and add mapping tests.

For articleLink/wikipedia/videoLink/presskit, using Uri? instead of String? can prevent invalid URLs at the edge. Also add JSON (de)serialization tests for these new keys.

Would you like me to add a small test asserting reddit_launch/video_link/presskit mapping?

integration_test/launch_test.dart (3)

21-37: Stub the bloc stream, not only the state.

Some widgets read bloc.stream (e.g., BlocBuilder) and won’t rebuild from a static state. Stub the stream to a matching success state.

Apply:

   when(() => launchBloc.state).thenReturn(
     const LaunchSuccessState(
       launch: mockLaunchResource,
     ),
   );
+  when(() => launchBloc.stream).thenAnswer(
+    (_) => Stream.value(
+      const LaunchSuccessState(launch: mockLaunchResource),
+    ),
+  );

If LaunchScreenBlocContent reads only context.read<LaunchBloc>().state, this is harmless but safer for future changes.


17-19: Close the mocked bloc in tearDown.

Avoid leaking subscriptions/resources even for mocks.

Add:

 setUp(() {
   launchBloc = MockLaunchBloc();
 });
+tearDown(() {
+  launchBloc.close();
+});

50-77: Reduce repetition by parameterizing locale assertions.

Loop over locale/expectations to shorten and ease maintenance.

I can provide a parameterized helper if you’d like.

lib/features/launch/widget/launch_site_card_widget.dart (1)

14-16: Clip the gradient to the card’s rounded shape.

Without clipBehavior/shape on Card, the gradient container’s radius can visually mismatch ink/press states.

Apply:

-    return Card(
-      child: Container(
+    return Card(
+      clipBehavior: Clip.antiAlias,
+      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
+      child: Container(
         decoration: BoxDecoration(
           gradient: LinearGradient(
...
-          borderRadius: BorderRadius.circular(20),
+          borderRadius: BorderRadius.circular(20),

Also applies to: 25-26

lib/features/launch/widget/payload_card_widget.dart (1)

82-114: Render customers only when non-empty.

Skip the Wrap when the list is empty.

Apply:

-                    if (payload.customers != null)
+                    if ((payload.customers?.isNotEmpty ?? false))
                       Wrap(
                         spacing: 8,
                         runSpacing: 8,
                         children: payload.customers!
lib/models/payload/payload_resource.dart (2)

26-27: Confirm numeric types for payload mass.

SpaceX data often exposes mass as non-integer. Consider double? (or num?) for payloadMassKg/payloadMassLbs to avoid truncation.

If upstream network model uses double?, align the resource type and UI formatting accordingly.


20-23: List equality semantics.

Equatable compares lists by == (identity). If deep equality of noradId/customers matters, wrap with UnmodifiableListView and compare using collection equality, or switch to Freezed data classes.

Do current tests rely on deep list equality? If yes, I can provide a small refactor.

lib/data/network/model/launch/full/network_launch_full_model.g.dart (1)

28-29: Casting ships to List is correct; consider aligning missionId too.

The change to cast ships as List tightens types. For consistency/type-safety against the API, consider also mapping missionId to List (currently List).

lib/data/network/model/payload/network_payload_model.dart (1)

16-17: Consider double for payload mass fields (API often returns fractional values).

If the source API supplies fractional masses, switch to double to avoid truncation; regenerate after change.

Apply:

-    @JsonKey(name: 'payload_mass_kg') int? payloadMassKg,
-    @JsonKey(name: 'payload_mass_lbs') int? payloadMassLbs,
+    @JsonKey(name: 'payload_mass_kg') double? payloadMassKg,
+    @JsonKey(name: 'payload_mass_lbs') double? payloadMassLbs,
lib/features/launch/widget/rocket_card_widget.dart (4)

16-16: firstOrNull may not be available without SDK support/import.

If targeting a Dart SDK without Iterable.firstOrNull, replace with a safe fallback.

-    final core = rocket.firstStage?.cores?.firstOrNull;
+    final core = (rocket.firstStage?.cores?.isNotEmpty ?? false)
+        ? rocket.firstStage!.cores!.first
+        : null;

25-27: Color.withValues requires recent Flutter; use withOpacity if needed.

To support older stable channels, you can switch to withOpacity.

-              theme.colorScheme.tertiaryContainer.withValues(alpha: 0.5),
-              theme.colorScheme.tertiaryContainer.withValues(alpha: 0.1),
+              theme.colorScheme.tertiaryContainer.withOpacity(0.5),
+              theme.colorScheme.tertiaryContainer.withOpacity(0.1),
...
-                color: theme.colorScheme.tertiary.withValues(alpha: 0.1),
+                color: theme.colorScheme.tertiary.withOpacity(0.1),

Also applies to: 40-41


67-69: Duplicate label in value (“Block … Block N”).

The value should only contain the number (or a simple formatted token), not the label again.

-                        InfoItem(loc.rocketBlock,
-                            '${loc.rocketBlock} ${core?.block!}'),
+                        InfoItem(loc.rocketBlock, '${core!.block!}'),

100-121: Spacer gaps appear when preceding chips are absent; prefer Wrap with spacing.

Prevents leading/trailing 8px gaps and simplifies layout.

-                          SingleChildScrollView(
-                            scrollDirection: Axis.horizontal,
-                            child: Row(
-                              children: [
-                                if (core?.gridfins != null)
-                                  FeatureChipWidget(
-                                    label: loc.gridFins,
-                                    active: core!.gridfins!,
-                                  ),
-                                const SizedBox(width: 8),
-                                if (core?.legs != null)
-                                  FeatureChipWidget(
-                                    label: loc.landingLegs,
-                                    active: core!.legs!,
-                                  ),
-                                const SizedBox(width: 8),
-                                if (core?.reused != null)
-                                  FeatureChipWidget(
-                                    label: loc.reused,
-                                    active: core!.reused!,
-                                  ),
-                              ],
-                            ),
-                          ),
+                          SingleChildScrollView(
+                            scrollDirection: Axis.horizontal,
+                            child: Wrap(
+                              spacing: 8,
+                              children: [
+                                if (core?.gridfins != null)
+                                  FeatureChipWidget(
+                                    label: loc.gridFins,
+                                    active: core!.gridfins!,
+                                  ),
+                                if (core?.legs != null)
+                                  FeatureChipWidget(
+                                    label: loc.landingLegs,
+                                    active: core!.legs!,
+                                  ),
+                                if (core?.reused != null)
+                                  FeatureChipWidget(
+                                    label: loc.reused,
+                                    active: core!.reused!,
+                                  ),
+                              ],
+                            ),
+                          ),
lib/data/network/model/launch/network_launch_model.g.dart (3)

42-49: Nested stage parsing looks correct; ensure serialization doesn't leak raw objects

Deserialization is fine. However, in ToJson you currently emit the object refs (see Lines 57–58). If any caller uses toJson() directly (e.g., caching) instead of jsonEncode, nested objects will remain non-encodable. Prefer @JsonSerializable(explicitToJson: true) on the parent models so generated toJson calls .toJson() on nested types.

Apply at the source (not in this generated file), e.g.:

-@JsonSerializable()
+@JsonSerializable(explicitToJson: true)
 class NetworkRocketModel { ... }

-@JsonSerializable()
+@JsonSerializable(explicitToJson: true)
 class NetworkLaunchModel { ... }

-@JsonSerializable()
+@JsonSerializable(explicitToJson: true)
 class NetworkLaunchLinksModel { ... }

57-58: Prefer emitting JSON maps for nested stages

Same note as above: writing instance.firstStage/secondStage directly can leave raw objects in the returned map. With explicitToJson: true, this becomes instance.firstStage?.toJson() and instance.secondStage?.toJson().


69-71: New link fields parsed correctly; consider end-to-end coverage

reddit_launch, video_link, and presskit are parsed. Verify downstream resource mapping (LinksResource) includes these if product needs them; otherwise they’ll be silently dropped.

lib/models/core/core_resource.dart (1)

6-17: Optional: add ergonomics (copyWith, toString) and an empty factory

These help call sites and testing without adding runtime cost.

Example:

 class CoreResource extends Equatable {
   const CoreResource({
@@
   });
+
+  CoreResource copyWith({
+    String? coreSerial,
+    int? flight,
+    int? block,
+    bool? gridfins,
+    bool? legs,
+    bool? reused,
+    bool? landSuccess,
+    bool? landingIntent,
+    String? landingType,
+    String? landingVehicle,
+  }) {
+    return CoreResource(
+      coreSerial: coreSerial ?? this.coreSerial,
+      flight: flight ?? this.flight,
+      block: block ?? this.block,
+      gridfins: gridfins ?? this.gridfins,
+      legs: legs ?? this.legs,
+      reused: reused ?? this.reused,
+      landSuccess: landSuccess ?? this.landSuccess,
+      landingIntent: landingIntent ?? this.landingIntent,
+      landingType: landingType ?? this.landingType,
+      landingVehicle: landingVehicle ?? this.landingVehicle,
+    );
+  }
+
+  static const empty = CoreResource();
test/models/launch_ext_model_tests.dart (5)

103-113: Consider asserting new link fields if surfaced in LinksResource

If LinksResource exposes redditLaunch, videoLink, or presskit, add expectations to prevent regressions. If intentionally omitted from domain, ignore.


67-72: You include ships in the input but don’t assert mapping

If LaunchFullResource exposes ships, add an expectation to cover it.

Example:

-        'ships': ['ELSBETH3', 'GOQUEST', 'GOSEARCHER'],
+        'ships': ['ELSBETH3', 'GOQUEST', 'GOSEARCHER'],
@@
-      final launchResource = networkLaunchModel.toResource();
+      final launchResource = networkLaunchModel.toResource();
+      expect(
+        launchResource.ships,
+        ['ELSBETH3', 'GOQUEST', 'GOSEARCHER'],
+      );

185-191: Time-delta test is flaky; freeze “now”

Using DateTime.now() makes this off-by-one across day boundaries. Freeze the clock for determinism.

Apply:

+import 'package:clock/clock.dart';
@@
-      final date = DateTime.now().add(const Duration(days: 5));
-      final launchDays = date.formatDateWithDays();
+      withClock(Clock.fixed(DateTime.utc(2030, 01, 01, 12)), () {
+        final date = clock.now().add(const Duration(days: 5));
+        final launchDays = date.formatDateWithDays();
 
-      expect(launchDays, isA<From>());
-      expect((launchDays as From).formattedDate, '4');
+        expect(launchDays, isA<From>());
+        expect((launchDays as From).formattedDate, '5');
+      });

193-199: Same flakiness for “Since”

Freeze the clock to avoid boundary drift.

-      final date = DateTime.now().subtract(const Duration(days: 5));
-      final launchDays = date.formatDateWithDays();
+      withClock(Clock.fixed(DateTime.utc(2030, 01, 06, 12)), () {
+        final date = clock.now().subtract(const Duration(days: 5));
+        final launchDays = date.formatDateWithDays();
 
-      expect(launchDays, isA<Since>());
-      expect((launchDays as Since).formattedDate, '5');
+        expect(launchDays, isA<Since>());
+        expect((launchDays as Since).formattedDate, '5');
+      });

222-228: Make the fixture UTC to match “UTC” label

Use a UTC DateTime to avoid timezone surprises.

-      final date = DateTime(2025, 12, 25, 14, 30);
+      final date = DateTime.utc(2025, 12, 25, 14, 30);
       final formattedTime =
           date.formatDate(format: 'MMMM dd, yyyy - HH:mm UTC');
lib/models/launch/launch_full_resource.dart (1)

30-41: Consider carrying raw DateTime alongside formatted strings.

Storing launchDate/staticFireDate as String? limits sorting, timezone transforms, and reformatting. Consider adding DateTime? raw fields (or a value object) and derive presentation in the UI layer.

I can draft a minimal change that adds raw fields while keeping these Strings for backward compatibility.

lib/features/launch/widget/timeline_widgets.dart (3)

21-33: Align Card shape with gradient clipping.

The child Container has its own borderRadius while Card uses its default shape (can cause visual mismatch). Let Card define the radius and clip; drop the child’s borderRadius.

-    return Card(
-      child: Container(
+    return Card(
+      clipBehavior: Clip.antiAlias,
+      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
+      child: Container(
         decoration: BoxDecoration(
           gradient: LinearGradient(
@@
-          ),
-          borderRadius: BorderRadius.circular(20),
+          ),
         ),

134-146: Guard against long text overflow.

Titles/dates can be long (localized). Add maxLines/ellipsis.

-              Text(
-                title,
+              Text(
+                title,
                 style: theme.textTheme.titleMedium?.copyWith(
                   fontWeight: FontWeight.bold,
                 ),
+                maxLines: 1,
+                overflow: TextOverflow.ellipsis,
               ),
-              Text(
-                subtitle,
+              Text(
+                subtitle,
                 style: theme.textTheme.bodySmall?.copyWith(
                   color: theme.colorScheme.onSurfaceVariant,
                 ),
+                maxLines: 1,
+                overflow: TextOverflow.ellipsis,
               ),

62-75: Hard-coded completed: true — consider data-driven states.

Both “Static fire” and “Launch” are always rendered as completed. If data is missing or tentative, reflect that in UI (or hide the item).

I can add optional booleans (e.g., staticFireCompleted, launchCompleted) with sensible defaults and update call sites.

lib/models/launch/extension/launch_ext.dart (1)

65-67: Optional: localizable, consistent time pattern.

launchTime uses a fixed pattern. Consider DateFormat.yMMMEd().add_jm() to respect locale or reuse a shared date formatter.

I can extract a small DateFormats helper to centralize patterns.

lib/features/launch/widget/stat_card_widget.dart (3)

73-85: Color.withValues may require newer Flutter; prefer withOpacity for broad SDK compatibility

Reduce risk of build breaks on older channels.

-        gradient: LinearGradient(
+        gradient: LinearGradient(
           colors: [
-            color.withValues(alpha: 0.1),
-            color.withValues(alpha: 0.05),
+            color.withOpacity(0.10),
+            color.withOpacity(0.05),
           ],
@@
-        border: Border.all(
-          color: color.withValues(alpha: 0.3),
+        border: Border.all(
+          color: color.withOpacity(0.30),
           width: 1,
         ),

If the project guarantees a Flutter version that supports withValues, feel free to keep it.


100-106: Improve value text contrast in dark/light themes

Using the raw accent color for text may fail contrast in some themes. Consider deriving from the theme or clamping luminance.

Example:

-            style: TextStyle(
+            style: TextStyle(
               fontSize: 14,
               fontWeight: FontWeight.bold,
-              color: color,
+              color: Theme.of(context).brightness == Brightness.dark
+                  ? Colors.white
+                  : color,
             ),

87-111: Add semantics for accessibility

Expose label/value to screen readers for each card.

Wrap the Column with:

Semantics(
  label: '$label',
  value: value,
  child: Column(
    // ...
  ),
)
lib/features/launch/launch_screen.dart (3)

124-131: System UI overlay style is hardcoded to light icons

On light backgrounds this can reduce visibility. Make it adaptive or set it on the SliverAppBar.

Example:

final overlay = Theme.of(context).brightness == Brightness.dark
    ? SystemUiOverlayStyle.light
    : SystemUiOverlayStyle.dark;

Then either drop the global AnnotatedRegion or set systemOverlayStyle: overlay on SliverAppBar.


120-123: Deduplicate deep payload chaining

Store payload once and reuse. Reduces risk of NPEs and improves readability.

@@
-    final images = <String>[];
+    final images = <String>[];
+    final payload =
+        widget.resource.rocket?.secondStage?.payloads?.firstOrNull;
@@
-                      if (widget.resource.rocket?.secondStage?.payloads
-                              ?.firstOrNull !=
-                          null) ...[
+                      if (payload != null) ...[
                         const SizedBox(height: 20),
-                        QuickStatsSectionWidget(
-                          payload: widget
-                              .resource.rocket!.secondStage!.payloads!.first,
-                          rocketName: widget.resource.rocket?.rocketName,
-                        ),
+                        QuickStatsSectionWidget(
+                          payload: payload,
+                          rocketName: widget.resource.rocket?.rocketName,
+                        ),
                         const SizedBox(height: 20),
-                        PayloadCardWidget(
-                          payload: widget
-                              .resource.rocket!.secondStage!.payloads!.first,
-                        ),
+                        PayloadCardWidget(payload: payload),
                       ],

Also applies to: 351-364


145-147: Color.withValues appears multiple times; confirm SDK support

If your min Flutter SDK doesn’t include Color.withValues, switch to withOpacity for compatibility.

Also applies to: 158-160, 215-216, 282-285, 299-300, 303-307

lib/data/network/model/launch/full/network_launch_full_model.freezed.dart (2)

24-25: Prefer strong typing for missionId.

If API returns strings, switch missionId from List<dynamic>? to List<String>? in the source and regenerate.

- @JsonKey(name: 'mission_id')
- List<dynamic>? get missionId;
+ @JsonKey(name: 'mission_id')
+ List<String>? get missionId;

Also applies to: 786-804, 256-261


2486-2494: Consider double? for payload mass fields.

If payload masses can be non-integer, use double? to avoid cast issues/loss.

- @JsonKey(name: 'payload_mass_kg') int? get payloadMassKg;
- @JsonKey(name: 'payload_mass_lbs') int? get payloadMassLbs;
+ @JsonKey(name: 'payload_mass_kg') double? get payloadMassKg;
+ @JsonKey(name: 'payload_mass_lbs') double? get payloadMassLbs;

Also applies to: 2900-2903

lib/data/network/model/payload/network_payload_model.freezed.dart (1)

28-31: Optional: allow non-integer masses.

Switch payloadMassKg/payloadMassLbs to double? in the source if API may return decimals; regenerate to avoid as int casts failing.

- @JsonKey(name: 'payload_mass_kg') int? payloadMassKg;
- @JsonKey(name: 'payload_mass_lbs') int? payloadMassLbs;
+ @JsonKey(name: 'payload_mass_kg') double? payloadMassKg;
+ @JsonKey(name: 'payload_mass_lbs') double? payloadMassLbs;

Also applies to: 450-456

lib/l10n/app_localizations.dart (1)

387-650: Add missing description metadata for ambiguous keys in translation ARBs

intl_de.arb and intl_pt.arb currently lack any @…description entries for these message IDs, which can lead to mistranslations:

  • payload
  • payloadTitle
  • type
  • rocketType
  • id
  • flight

Add succinct description fields in the ARB metadata for each of these keys (e.g. in lib/l10n/intl_en.arb), then regenerate or propagate to the other locale files so translators have clear context.

lib/l10n/app_localizations_en.dart (2)

215-216: Avoid emoji in localized copy; move to UI for accessibility.

Screen readers may read the emoji literally. Prefer keeping copy text-only and add the emoji in the widget tree with proper Semantics.

Apply:

-  String get firstStage => '🚀 First Stage';
+  String get firstStage => 'First Stage';

296-298: Format flight numbers with locale-aware grouping.

Improves readability for large numbers and uses the existing intl import.

Apply:

-  String flightNumber(Object number) {
-    return 'Flight #$number';
-  }
+  String flightNumber(Object number) {
+    final n = int.tryParse('$number');
+    final formatted = n == null
+        ? '$number'
+        : intl.NumberFormat.decimalPattern(localeName).format(n);
+    return 'Flight #$formatted';
+  }
lib/l10n/app_localizations_pt.dart (4)

238-241: Spell out “not available” instead of abbreviation.

“N/D” can be unclear to users. Recommend a full phrase.

-  String get notAvailable => 'N/D';
+  String get notAvailable => 'Não disponível';

274-276: More natural PT phrasing for “Watch Video”.

-  String get watchVideo => 'Assistir Vídeo';
+  String get watchVideo => 'Assistir ao vídeo';

295-298: Prefer PT idiom for flight numbering.

-  String flightNumber(Object number) {
-    return 'Voo #$number';
-  }
+  String flightNumber(Object number) {
+    return 'Voo nº $number';
+  }

166-170: Follow-up: older PT keys remain untranslated (“Appearance”, “About”).

Not part of this hunk, but worth aligning for consistency later.

lib/l10n/app_localizations_de.dart (4)

271-273: Use “und” instead of ampersand in running text.

-  String get linksResources => 'Links & Ressourcen';
+  String get linksResources => 'Links und Ressourcen';

247-249: UI convention: prefer “ID” over “Kennung”.

Common UI label and matches siteIdLabel.

-  String get id => 'Kennung';
+  String get id => 'ID';

295-298: More idiomatic DE numbering for flights.

-  String flightNumber(Object number) {
-    return 'Flug #$number';
-  }
+  String flightNumber(Object number) {
+    return 'Flug Nr. $number';
+  }

166-170: Follow-up: some older DE keys are still English (“Appearance”, “About”).

Not in this diff, but consider localizing them for consistency.

Comment on lines +27 to 30
String formatDate({String format = 'hh:mm a, MMM yyyy'}) {
if (this == null) return '';
return DateFormat('hh:mm a, MMM yyyy').format(this!);
return DateFormat(format).format(this!);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Timezone bug: label says “UTC” but time isn’t converted to UTC.

formatDate doesn’t convert to UTC, yet launchDate is formatted with “UTC” in the pattern. Convert when labeling UTC.

Apply:

-  String formatDate({String format = 'hh:mm a, MMM yyyy'}) {
+  String formatDate({String format = 'hh:mm a, MMM yyyy', bool useUtc = false}) {
     if (this == null) return '';
-    return DateFormat(format).format(this!);
+    final dt = useUtc ? this!.toUtc() : this!;
+    return DateFormat(format).format(dt);
   }

And update the UTC-labeled usage:

-      launchDate: launchDate.formatDate(format: 'MMMM dd, yyyy - HH:mm UTC'),
+      launchDate: launchDate.formatDate(
+        format: "MMMM dd, yyyy - HH:mm 'UTC'",
+        useUtc: true,
+      ),

Also applies to: 80-82

@@ -0,0 +1,115 @@
import 'package:bloc_test/bloc_test.dart';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Stabilize tests by stubbing the bloc stream

Some Bloc widgets subscribe to bloc.stream; stubbing avoids flakiness.

+import 'dart:async';
 import 'package:bloc_test/bloc_test.dart';
   setUp(() {
     launchBloc = MockLaunchBloc();
+    when(() => launchBloc.stream).thenAnswer((_) => Stream<LaunchState>.empty());
   });

Also applies to: 39-41

🤖 Prompt for AI Agents
In test/features/launch/bloc/launch_screen_test.dart around lines 1 and also
apply to lines 39-41, the test fails to stub the bloc.stream which causes
flakiness because widgets subscribe to the stream; fix by stubbing the bloc
stream using whenListen (from bloc_test) or equivalent before rendering the
widget—provide a Stream.of the desired sequence of states and set the
initialState argument to match the bloc's current state so the widget receives
predictable events; apply the same whenListen stub to the other test locations
(lines 39-41).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (23)
test/features/launch/widget/recovery_ships_card_widget_test.dart (4)

14-14: Use pumpAndSettle() to deflake widget stabilization.
Prevents race conditions in CI if there are implicit animations or async rebuilds.

-    await tester.pump();
+    await tester.pumpAndSettle();
@@
-    await tester.pump();
+    await tester.pumpAndSettle();
@@
-    await tester.pump();
+    await tester.pumpAndSettle();

Also applies to: 27-27, 49-49


9-18: Avoid brittle SizedBox assertion for “renders nothing”.
Asserting exactly one SizedBox is fragile; assert absence of header/icons instead while keeping the Card check.

-    expect(find.byType(SizedBox), findsOne);
-    expect(find.byType(Card), findsNothing);
+    expect(find.text(S.current.recoveryShips), findsNothing);
+    expect(find.byIcon(Icons.directions_boat), findsNothing);
+    expect(find.byIcon(Icons.anchor), findsNothing);
+    expect(find.byType(Card), findsNothing);

20-40: Assert Card presence and prefer const list literal.
Adds a stronger structural check and a small immutability win.

-    final ships = ['GO Ms. Tree', 'GO Ms. Chief', 'GO Ms. Independence'];
+    const ships = ['GO Ms. Tree', 'GO Ms. Chief', 'GO Ms. Independence'];
@@
-    await tester.pump();
+    await tester.pumpAndSettle();
@@
+    // Card should render when there are ships
+    expect(find.byType(Card), findsOneWidget);

42-55: Mirror the multi-ship assertions: const list and Card presence.
Keeps tests consistent and slightly more robust.

-    final ships = ['GO Ms. Tree'];
+    const ships = ['GO Ms. Tree'];
@@
-    await tester.pump();
+    await tester.pumpAndSettle();
@@
+    expect(find.byType(Card), findsOneWidget);
test/features/launch/widget/feature_chip_widget_test.dart (4)

23-25: Prefer deterministic icon lookup

Using find.byType(Icon) can return an unrelated Icon. Query the specific icon instead.

-    final icon = tester.widget<Icon>(find.byType(Icon));
-    expect(icon.icon, Icons.check_circle);
+    expect(find.byIcon(Icons.check_circle), findsOneWidget);

54-55: Same here: assert the specific inactive icon

-    final icon = tester.widget<Icon>(find.byType(Icon));
-    expect(icon.icon, Icons.cancel);
+    expect(find.byIcon(Icons.cancel), findsOneWidget);

17-17: Unnecessary pumpAndSettle

No async animations here. Removing saves test time.

-    await tester.pumpAndSettle();

48-48: Unnecessary pumpAndSettle

Same rationale as above.

-    await tester.pumpAndSettle();
lib/features/launch/info_item.dart (4)

5-9: Make constructor const and the Key optional/nullable.
Allows const instantiation where possible and avoids forcing a Key at call sites.

-  InfoItem(this.label, this.value, this.key);
+  const InfoItem(this.label, this.value, [this.key]);

   final String label;
   final String value;
-  final Key key;
+  final Key? key;

4-4: Annotate as immutable and enable Equatable stringify for better logs.
Minor DX improvements.

+@immutable
 class InfoItem extends Equatable {
+  @override
+  bool? get stringify => true;

7-9: Consider a clearer name than key (e.g., tileKey or uiKey).
Avoids confusion with Flutter widget key params when reading call sites.


11-16: Remove UI key from Equatable props
Including the UI key ties value equality to a rendering concern. Recommend removing it:

-  List<Object?> get props => [
-        label,
-        value,
-        key,
-      ];
+  List<Object?> get props => [label, value];
test/features/launch/widget/payload_card_widget_test.dart (5)

24-36: Stabilize i18n lookups and drop redundant pump

Use l10n from the widget context (safer than S.current across tests) and replace the extra pump with pumpAndSettle.

   testWidgets('renders title and subtitle correctly', (tester) async {
     await tester.pumpLocalizedWidget(
       child: PayloadCardWidget(
         payload: payload,
       ),
       locale: const Locale('en'),
     );
-
-    await tester.pump();
-    expect(find.text(S.current.payloadTitle), findsOneWidget);
+    await tester.pumpAndSettle();
+    final l10n =
+        S.of(tester.element(find.byType(PayloadCardWidget)));
+    expect(find.text(l10n.payloadTitle), findsOneWidget);
-    expect(find.text('${payload.payloadId} • ${payload.payloadType}'),
+    expect(find.text('${payload.payloadId} • ${payload.payloadType}'),
         findsOneWidget);
   });

Repeat the l10n pattern in other tests that use S.current.


38-58: Prefer keyed assertions over text to reduce brittleness

Text can change with copy edits or formatting; stable ValueKeys make tests resilient. If the widget can expose keys for all info rows, assert on them here.

     // Expand the tile
-    await tester.tap(find.text(S.current.payloadTitle));
+    await tester.tap(find.text(S.current.payloadTitle));
     await tester.pumpAndSettle();

-    expect(find.text(payload.payloadId!), findsOneWidget);
-    expect(find.text(payload.payloadType!), findsOneWidget);
-    expect(find.text('${payload.payloadMassKg} kg'), findsOneWidget);
-    expect(find.text(payload.orbit!), findsOneWidget);
-    expect(find.text(payload.manufacturer!), findsOneWidget);
-    expect(find.text(payload.nationality!), findsOneWidget);
+    expect(find.byKey(const Key('info_id')), findsOneWidget);
+    expect(find.byKey(const Key('info_type')), findsOneWidget);
+    expect(find.byKey(const Key('info_mass')), findsOneWidget);
+    expect(find.byKey(const Key('info_orbit')), findsOneWidget);
+    expect(find.byKey(const Key('info_manufacturer')), findsOneWidget);
+    expect(find.byKey(const Key('info_nationality')), findsOneWidget);

If these keys don’t exist yet, consider adding them in the widget as static consts.


60-77: Make customer checks resilient to different renderings

Avoid null-assert and allow for chips/comma-separated layouts by using textContaining.

-    for (final customer in payload.customers!) {
-      expect(find.text(customer), findsOneWidget);
-    }
+    final customers = payload.customers ?? const <String>[];
+    for (final c in customers) {
+      expect(find.textContaining(c), findsOneWidget);
+    }

79-99: Also assert absence of optional sections when missing

Strengthen this test by verifying that optional info rows aren’t rendered.

     await tester.pumpAndSettle();

     expect(find.text(emptyPayload.payloadId!), findsOneWidget);
     expect(find.text(emptyPayload.payloadType!), findsOneWidget);
     expect(find.byKey(const Key('info_id')), findsOneWidget);
     expect(find.byKey(const Key('info_type')), findsOneWidget);
+    expect(find.byKey(const Key('info_mass')), findsNothing);
+    expect(find.byKey(const Key('info_orbit')), findsNothing);
+    expect(find.byKey(const Key('info_manufacturer')), findsNothing);
+    expect(find.byKey(const Key('info_nationality')), findsNothing);
+    expect(find.byKey(const Key('info_customers')), findsNothing);

If keys aren’t available, add negative text assertions (e.g., expect(find.text('kg'), findsNothing)) as a fallback.


1-7: De-fragile test helper import

The deep relative path to pumpLocalizedWidget (../../../bloc/utils.dart) is brittle. Consider moving common test utilities under a top-level test_support/helpers.dart and importing from there.

test/features/launch/widget/launch_site_card_widget_test.dart (1)

10-11: Unnecessary binding initialization

TestWidgetsFlutterBinding.ensureInitialized() is redundant in widget tests when using tester.pumpWidget(...) helpers. Safe to remove.

-  TestWidgetsFlutterBinding.ensureInitialized();
test/features/launch/widget/stat_card_widget_test.dart (2)

1-59: File name/content mismatch and duplication

This file tests LaunchSiteCardWidget but is named stat_card_widget_test.dart. It also largely duplicates launch_site_card_widget_test.dart. Prefer to either:

  • Rename to launch_site_card_widget_test.dart and consolidate tests in a single file, or
  • Replace contents with actual StatCardWidget tests if that’s the intent.

Reduces confusion and keeps CI time lean.


1-2: Consistent imports for Locale

Both dart:ui and package:flutter/material.dart provide Locale. For consistency with the other test, import style can be unified, but it’s non-blocking.

test/features/launch/launch_screen_test.dart (3)

1-12: Use AppLocalizations instead of hard-coded strings to reduce brittleness

Asserting raw text literals will break on copy tweaks. Prefer l10n lookups so the tests track the source of truth.

Apply this diff:

@@
 import 'package:flutter_test/flutter_test.dart';
 import 'package:mocktail/mocktail.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@@
   testWidgets('render launch details UI on success state', (tester) async {
@@
       await tester.pumpAndSettle();
 
-      expect(find.byType(LaunchScreenBlocContent), findsNWidgets(1));
-      expect(find.text('Mission Successful'), findsOneWidget);
+      expect(find.byType(LaunchScreenBlocContent), findsOneWidget);
+      final l10nEn = AppLocalizations.of(
+        tester.element(find.byType(LaunchScreenBlocContent)),
+      )!;
+      expect(find.text(l10nEn.missionSuccessful), findsOneWidget);
     });
@@
   testWidgets('render launch details UI on success state localized',
       (tester) async {
@@
       await tester.pumpAndSettle();
 
-      expect(find.byType(LaunchScreenBlocContent), findsNWidgets(1));
-      expect(find.text('Missão bem-sucedida'), findsOneWidget);
+      expect(find.byType(LaunchScreenBlocContent), findsOneWidget);
+      final l10nPt = AppLocalizations.of(
+        tester.element(find.byType(LaunchScreenBlocContent)),
+      )!;
+      expect(find.text(l10nPt.missionSuccessful), findsOneWidget);
     });
@@
   testWidgets('renders error', (tester) async {
@@
       await tester.pumpAndSettle();
 
-      expect(find.text('Try Again'), findsOneWidget);
+      final l10nEn = AppLocalizations.of(
+        tester.element(find.byType(LaunchScreenBlocContent)),
+      )!;
+      expect(find.text(l10nEn.tryAgain), findsOneWidget);
     });

Note: adjust the l10n getter names if they differ.

Also applies to: 62-79, 81-99, 101-113


77-79: Prefer findsOneWidget over findsNWidgets(1)

Clearer intent and more idiomatic in Flutter tests.

-expect(find.byType(LaunchScreenBlocContent), findsNWidgets(1));
+expect(find.byType(LaunchScreenBlocContent), findsOneWidget);

Also applies to: 97-99


81-99: Add a quick Ukrainian locale assertion to cover the newly added l10n

Since this PR adds Ukrainian, add a minimal check to prevent regressions.

Example snippet:

testWidgets('render launch details UI on success state (uk)', (tester) async {
  when(() => launchBloc.state).thenReturn(const LaunchSuccessState(launch: mockLaunchResource));
  await tester.pumpLocalizedWidgetWithBloc<LaunchBloc>(
    bloc: launchBloc,
    child: const LaunchScreenBlocContent(flightNumber: 1),
    locale: const Locale('uk'),
  );
  await tester.pumpAndSettle();

  final l10nUk = AppLocalizations.of(
    tester.element(find.byType(LaunchScreenBlocContent)),
  )!;
  expect(find.text(l10nUk.missionSuccessful), findsOneWidget);
});

Want me to push this test?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ba90644 and b5a3f70.

📒 Files selected for processing (12)
  • integration_test/launch_test.dart (1 hunks)
  • lib/features/launch/info_item.dart (1 hunks)
  • lib/features/launch/widget/info_grid_widget.dart (1 hunks)
  • lib/features/launch/widget/launch_site_card_widget.dart (1 hunks)
  • lib/features/launch/widget/payload_card_widget.dart (1 hunks)
  • lib/features/launch/widget/rocket_card_widget.dart (1 hunks)
  • test/features/launch/launch_screen_test.dart (1 hunks)
  • test/features/launch/widget/feature_chip_widget_test.dart (1 hunks)
  • test/features/launch/widget/launch_site_card_widget_test.dart (1 hunks)
  • test/features/launch/widget/payload_card_widget_test.dart (1 hunks)
  • test/features/launch/widget/recovery_ships_card_widget_test.dart (1 hunks)
  • test/features/launch/widget/stat_card_widget_test.dart (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • lib/features/launch/widget/rocket_card_widget.dart
  • lib/features/launch/widget/info_grid_widget.dart
  • lib/features/launch/widget/launch_site_card_widget.dart
  • lib/features/launch/widget/payload_card_widget.dart
  • integration_test/launch_test.dart
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Codacy Static Code Analysis
  • GitHub Check: Flutter Integration Tests (31)
  • GitHub Check: App
🔇 Additional comments (10)
test/features/launch/widget/recovery_ships_card_widget_test.dart (1)

1-7: Imports and localization test harness look correct.
All required deps are present and used. Good setup for localized widget tests.

test/features/launch/widget/feature_chip_widget_test.dart (1)

5-16: Good test scaffolding

Clear arrange–act–assert structure and const usage; looks solid.

lib/features/launch/info_item.dart (1)

4-17: LGTM overall — simple, immutable value object fits the use case.
Fields are final and Equatable use is correct.

test/features/launch/widget/payload_card_widget_test.dart (1)

9-22: LGTM: Clear setup and representative payload fixture

Good coverage for expanded/empty states with realistic data.

test/features/launch/widget/launch_site_card_widget_test.dart (3)

22-33: LGTM: title renders with localization

Covers the localized title path and locale wiring via pumpLocalizedWidget.


35-49: LGTM: full site details render

Validates name, long name, and labeled siteId.


51-67: LGTM: minimal data behavior

Good assertion that raw siteId doesn’t render without the label.

test/features/launch/widget/stat_card_widget_test.dart (2)

21-29: LGTM: title renders with localization

Confirms locale wiring for the title.


31-42: LGTM: full site details render

Matches expectations for name, long name, and labeled id.

test/features/launch/launch_screen_test.dart (1)

43-61: LGTM: loading-state path covered well

The initial state test is concise and asserts the spinner correctly.

Comment on lines +27 to +35
final container = tester.widget<Container>(find.byType(Container).first);
final decoration = container.decoration as BoxDecoration;
final borderColor = (decoration.border as Border).top.color;

// Use the same theme primary color with alpha as in widget
final theme = ThemeData().colorScheme;
final expectedColor = theme.primary.withValues(alpha: 0.3);
expect(borderColor, equals(expectedColor));
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Scope Container lookup to the widget and use Theme.of(context); also avoid Color.withValues

Picking the first Container in the whole tree is brittle and may crash on cast. Scope the search to FeatureChipWidget. Also use withOpacity(0.3) for broader Flutter compatibility.

-    final container = tester.widget<Container>(find.byType(Container).first);
-    final decoration = container.decoration as BoxDecoration;
-    final borderColor = (decoration.border as Border).top.color;
-
-    // Use the same theme primary color with alpha as in widget
-    final theme = ThemeData().colorScheme;
-    final expectedColor = theme.primary.withValues(alpha: 0.3);
+    final featureChip = find.byType(FeatureChipWidget);
+    final container = tester.widget<Container>(
+      find.descendant(of: featureChip, matching: find.byType(Container)).first,
+    );
+    final decoration = container.decoration as BoxDecoration;
+    final borderColor = (decoration.border as Border).top.color;
+
+    // Use the same theme as the rendered tree
+    final theme = Theme.of(tester.element(featureChip)).colorScheme;
+    final expectedColor = theme.primary.withOpacity(0.3);
     expect(borderColor, equals(expectedColor));

Run to check if withValues is used elsewhere and replace with withOpacity if needed:


🏁 Script executed:

#!/bin/bash
rg -n "withValues\s*\(" -g '!**/build/**' -S

Length of output: 6352


Scope Container lookup in the test and replace .withValues usage across codebase

  • In test/features/launch/widget/feature_chip_widget_test.dart, scope the Container lookup to the FeatureChipWidget, use Theme.of(...) and withOpacity(0.3) instead of ThemeData().colorScheme and withValues(alpha: 0.3) (as shown in the diff).
  • Replace every .withValues(alpha: x) (and colorScheme.*.withValues(alpha: x)) in lib/features/launch/** with .withOpacity(x)` for broader Flutter compatibility; verify no remaining instances via
    rg -n "withValues\s*\(" -g '!**/build/**'
🤖 Prompt for AI Agents
In test/features/launch/widget/feature_chip_widget_test.dart around lines 27 to
35, the test is using a global Container lookup and
ThemeData().colorScheme.withValues(...) which is incorrect; scope the Container
lookup to the FeatureChipWidget instance (use a finder that targets the
FeatureChipWidget or its descendant Container), obtain the theme via
Theme.of(tester.element(find.byType(FeatureChipWidget))).colorScheme and replace
the .withValues(alpha: 0.3) usage with .withOpacity(0.3); then update every
occurrence of colorScheme.*.withValues(...) or .withValues(alpha: x) under
lib/features/launch/** to .withOpacity(x) and run a search (e.g., ripgrep) to
verify no remaining withValues calls outside build folders.

ashtanko and others added 14 commits September 6, 2025 14:24
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
…lutter_bloc_app_template into feature/launch_details_screen
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
test/features/launch/launch_screen_test.dart (1)

41-45: Nice: stream/close stubs added to avoid MissingStubError

This resolves the prior bloc stream subscription issue and ensures proper teardown.

🧹 Nitpick comments (3)
test/features/launch/launch_screen_test.dart (3)

49-51: Rename test description or stub the actual initial state

The description says “initial” but the stubbed state is LaunchLoadingState. Either rename to “loading state” or, if an Initial state exists, stub that instead.

-        'renders CircularProgressIndicator '
-        'when launch state is initial', (tester) async {
+        'renders CircularProgressIndicator when launch state is loading', (tester) async {

81-81: Use findsOneWidget for clarity

findsNWidgets(1) is equivalent but less idiomatic here.

-      expect(find.byType(LaunchScreenBlocContent), findsNWidgets(1));
+      expect(find.byType(LaunchScreenBlocContent), findsOneWidget);

82-82: Avoid brittle assertions on hard-coded localized strings

Asserting literal text couples tests to translation wording. Prefer asserting against the localization getter (e.g., AppLocalizations) or a widget Key.

Also applies to: 102-102, 116-116

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b5a3f70 and b449f78.

📒 Files selected for processing (8)
  • .github/workflows/ci.yml (2 hunks)
  • integration_test/launch_navigation_test.dart (1 hunks)
  • lib/features/launch/launch_screen.dart (3 hunks)
  • lib/models/launch/second_stage_ext.dart (1 hunks)
  • test/features/launch/launch_screen_test.dart (1 hunks)
  • test/features/launch/widget/stat_card_widget_test.dart (1 hunks)
  • test/models/launch/rocket_resource_test.dart (1 hunks)
  • test/models/second_stage_ext_test.dart (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
  • lib/models/launch/second_stage_ext.dart
  • .github/workflows/ci.yml
  • test/models/launch/rocket_resource_test.dart
  • integration_test/launch_navigation_test.dart
  • test/models/second_stage_ext_test.dart
  • test/features/launch/widget/stat_card_widget_test.dart
  • lib/features/launch/launch_screen.dart

rocket: mockRocket,
launchSuccess: true,
links: mockLinks,
ships: [],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix const construction: make the empty list const

mockLaunchResource is const, so all nested values must be const. ships: [] will fail to compile in a const context.

Apply this diff (adjust the generic to match the field’s element type if needed):

-  ships: [],
+  ships: const [],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ships: [],
ships: const [],
🤖 Prompt for AI Agents
In test/features/launch/launch_screen_test.dart around line 33, the const
mockLaunchResource contains a non-const empty list (ships: []), which breaks
const construction; change it to a const empty list (e.g., ships: const []) and,
if the field has a specific element type, use a typed const (e.g., ships: const
<Ship>[]), so all nested values are const and the file compiles.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
lib/models/launch/extension/launch_ext.dart (2)

27-30: Timezone bug: label says “UTC” but the formatter doesn’t convert to UTC

formatDate currently formats the given DateTime as-is. When labeling as UTC elsewhere, this yields incorrect times.

Apply:

-  String formatDate({String format = 'hh:mm a, MMM yyyy'}) {
+  String formatDate({
+    String format = 'hh:mm a, MMM yyyy',
+    bool useUtc = false,
+  }) {
     if (this == null) return '';
-    return DateFormat(format).format(this!);
+    final dt = useUtc ? this!.toUtc() : this!;
+    return DateFormat(format).format(dt);
   }

81-83: Quote literal “UTC” and actually format in UTC

Ensure the label matches the timezone used.

Apply:

-      launchDate: launchDate.formatDate(format: 'MMMM dd, yyyy - HH:mm UTC'),
+      launchDate: launchDate.formatDate(
+        format: "MMMM dd, yyyy - HH:mm 'UTC'",
+        useUtc: true,
+      ),
-      staticFireDate: staticFireDate.formatDate(format: 'MMMM dd, yyyy'),
+      staticFireDate: staticFireDate.formatDate(format: 'MMMM dd, yyyy'),

(Note: staticFireDate stays local by design; keep or switch to UTC based on UX intent.)

🧹 Nitpick comments (13)
test/features/launch/info_item_test.dart (4)

1-1: Prefer foundation import for Key-only usage.

Lighter dependency; Key and ValueKey live in flutter/foundation.dart.

-import 'package:flutter/widgets.dart';
+import 'package:flutter/foundation.dart';

8-9: Deduplicate constant keys to reduce noise and improve clarity.

Define shared consts once and reuse.

 group('InfoItem', () {
-  test('supports value equality', () {
-      final item1 = InfoItem('Label', 'Value', const Key('testKey'));
-      final item2 = InfoItem('Label', 'Value', const Key('testKey'));
+  // Shared keys for all tests
+  const testKey = Key('testKey');
+  const otherKey = Key('otherKey');
+
+  test('supports value equality', () {
+      final item1 = InfoItem('Label', 'Value', testKey);
+      final item2 = InfoItem('Label', 'Value', testKey);
 ...
-    final base = InfoItem('Label', 'Value', const Key('testKey'));
+    final base = InfoItem('Label', 'Value', testKey);
 ...
-    final differentLabel = InfoItem('OtherLabel', 'Value', const Key('testKey'));
-    final differentValue = InfoItem('Label', 'OtherValue', const Key('testKey'));
-    final differentKey = InfoItem('Label', 'Value', const Key('otherKey'));
+    final differentLabel = InfoItem('OtherLabel', 'Value', testKey);
+    final differentValue = InfoItem('Label', 'OtherValue', testKey);
+    final differentKey = InfoItem('Label', 'Value', otherKey);
 ...
-    final item = InfoItem('Label', 'Value', const Key('testKey'));
+    final item = InfoItem('Label', 'Value', testKey);
-    expect(item.props, equals(['Label', 'Value', const Key('testKey')]));
+    expect(item.props, equals(['Label', 'Value', testKey]));

Also applies to: 16-23, 30-32


29-33: Avoid brittle assertion on props shape.

Asserting exact props ties tests to an internal representation. Prefer order-checked containment to stay resilient to added fields.

-    expect(item.props, equals(['Label', 'Value', testKey]));
+    expect(item.props, containsAllInOrder(['Label', 'Value', testKey]));

7-13: Optional: Add a Set semantics test to validate hash/equality in hashed collections.

This guards against regressions where equals/hashCode diverge.

   test('supports value equality', () {
     final item1 = InfoItem('Label', 'Value', testKey);
     final item2 = InfoItem('Label', 'Value', testKey);

     expect(item1, equals(item2));
     expect(item1.hashCode, equals(item2.hashCode));
   });
+
+  test('works as a Set key (deduplication)', () {
+    final a = InfoItem('Label', 'Value', testKey);
+    final b = InfoItem('Label', 'Value', testKey);
+    final s = {a, b};
+    expect(s.length, 1);
+    expect(s.contains(b), isTrue);
+  });
test/models/launch/extension/launch_ext_test.dart (1)

35-39: Stabilize locale-dependent expectation

DateFormat('MMM') depends on the default locale. Pin it in tests to avoid CI flakiness.

Apply:

@@
-import 'package:flutter_test/flutter_test.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:intl/intl.dart';
@@
 void main() {
+  setUpAll(() {
+    Intl.defaultLocale = 'en_US';
+  });
lib/models/launch/links_resource.dart (1)

12-16: Consider deep-equality for flickrImages in Equatable

If UI state equality relies on LinksResource, list identity vs. content changes may cause unnecessary rebuilds or missed updates.

Option A (simple): hash the list contents in props

+import 'package:collection/collection.dart';
@@
   @override
   List<Object?> get props => [
@@
-        flickrImages,
+        const DeepCollectionEquality().hash(flickrImages),
       ];

Option B: store an unmodifiable copy and compare by contents where needed.

Also applies to: 23-26, 35-38

lib/features/launch/launch_screen.dart (5)

124-131: Prefer AppBar/SliverAppBar systemOverlayStyle over global AnnotatedRegion

Avoids white status bar icons on light surfaces when collapsed and scopes the effect to the app bar.

-    return AnnotatedRegion<SystemUiOverlayStyle>(
-      value: const SystemUiOverlayStyle(
-        statusBarColor: Colors.transparent,
-        statusBarIconBrightness: Brightness.light,
-      ),
-      child: Scaffold(
+    return Scaffold(
         extendBodyBehindAppBar: true,
         body: CustomScrollView(
@@
-            SliverAppBar(
+            SliverAppBar(
+              systemOverlayStyle: SystemUiOverlayStyle.light,

If you keep AnnotatedRegion, ensure the collapsed AppBar icon contrast is acceptable on colorScheme.surface.

Also applies to: 134-141


122-123: Make fallback list const to avoid new allocs

Minor, but keeps types crisp and avoids reallocation.

-    final images = widget.resource.links?.flickrImages ?? [];
+    final images = widget.resource.links?.flickrImages ?? const <String>[];

193-209: Disable paging when ≤1 image and reduce image flicker

Improves UX for single/no-image cases and prevents frame jumps on reloads.

-                      PageView.builder(
+                      PageView.builder(
                         controller: _pageController,
+                        physics: images.length <= 1
+                            ? const NeverScrollableScrollPhysics()
+                            : const BouncingScrollPhysics(),
                         onPageChanged: (index) {
                           setState(() => _currentImageIndex = index);
                         },
                         itemCount: images.length,
                         itemBuilder: (context, index) {
                           return Stack(
                             fit: StackFit.expand,
                             children: [
-                              Image.network(
+                              Image.network(
                                 images[index],
                                 fit: BoxFit.cover,
+                                gaplessPlayback: true,
                                 errorBuilder: (context, error, stackTrace) =>
                                     const SizedBox.shrink(),
                               ),

280-301: Add semantics for page indicators

Improve accessibility by exposing current position to screen readers.

-                      child: Row(
+                      child: Semantics(
+                        label: 'Image carousel position',
+                        value: '${_currentImageIndex + 1} of ${images.length}',
+                        child: Row(
                           mainAxisAlignment: MainAxisAlignment.center,
                           children: images.asMap().entries.map((entry) {
@@
-                        }).toList(),
-                      ),
+                        }).toList(),
+                        ),
+                      ),

382-391: Consider rendering Timeline when only one date is present

Currently requires both launchDate and staticFireDate. UX may benefit from showing whichever exists.

-                      if (widget.resource.launchDate != null &&
-                          widget.resource.staticFireDate != null) ...[
+                      if (widget.resource.launchDate != null ||
+                          widget.resource.staticFireDate != null) ...[
                         const SizedBox(height: 20),
                         TimelineCardWidget(
-                          launchDate: widget.resource.launchDate!,
-                          staticFireDate: widget.resource.staticFireDate!,
+                          launchDate: widget.resource.launchDate,
+                          staticFireDate: widget.resource.staticFireDate,
                           launchSuccess: widget.resource.launchSuccess ?? false,
                         ),

Ensure TimelineCardWidget handles nulls accordingly.

test/models/launch_ext_model_tests.dart (2)

218-223: Avoid time-based test flakiness by freezing "now"

Using DateTime.now() can flake around midnight/DST. Freeze time with package:clock.

-      final date = DateTime.now().add(const Duration(days: 5));
-      final launchDays = date.formatDateWithDays();
+      final fixedNow = DateTime.utc(2025, 01, 01, 12);
+      final date = fixedNow.add(const Duration(days: 5));
+      final launchDays = withClock(Clock.fixed(fixedNow), () => date.formatDateWithDays());
@@
-      final date = DateTime.now().subtract(const Duration(days: 5));
-      final launchDays = date.formatDateWithDays();
+      final fixedNow = DateTime.utc(2025, 01, 01, 12);
+      final date = fixedNow.subtract(const Duration(days: 5));
+      final launchDays = withClock(Clock.fixed(fixedNow), () => date.formatDateWithDays());

Add imports:

import 'package:clock/clock.dart';

Also applies to: 226-231


165-174: Prefer constructing real network models over Mockito for data classes

If NetworkRocketModel is a Freezed/data class, create it via factory/JSON instead of mocking, reducing reliance on dynamic stubbing.

Example:

final networkRocketModel = NetworkRocketModel(name: 'Falcon 9', type: 'Reusable');

If not feasible, keep the mock.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b449f78 and b0e8969.

📒 Files selected for processing (11)
  • Makefile (1 hunks)
  • lib/data/network/model/launch/network_launch_model.dart (3 hunks)
  • lib/data/network/model/launch/network_launch_model.freezed.dart (28 hunks)
  • lib/data/network/model/launch/network_launch_model.g.dart (3 hunks)
  • lib/features/launch/launch_screen.dart (3 hunks)
  • lib/models/launch/extension/launch_ext.dart (6 hunks)
  • lib/models/launch/links_resource.dart (2 hunks)
  • test/data/network/model/network_launch_full_model_test.dart (3 hunks)
  • test/features/launch/info_item_test.dart (1 hunks)
  • test/models/launch/extension/launch_ext_test.dart (1 hunks)
  • test/models/launch_ext_model_tests.dart (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • lib/data/network/model/launch/network_launch_model.g.dart
  • lib/data/network/model/launch/network_launch_model.freezed.dart
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-11T19:29:38.429Z
Learnt from: ashtanko
PR: ashtanko/flutter_bloc_app_template#30
File: lib/di/app_bloc_providers.dart:21-26
Timestamp: 2025-08-11T19:29:38.429Z
Learning: In the flutter_bloc_app_template project, immediate event dispatch using cascade notation (..add()) in BlocProvider creation is the preferred pattern. The blocs should dispatch initial events immediately upon creation rather than deferring them to widget initialization or post-frame callbacks.

Applied to files:

  • lib/features/launch/launch_screen.dart
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: App
  • GitHub Check: Flutter Integration Tests (31)
🔇 Additional comments (14)
Makefile (1)

8-8: Ensure DART and FLUTTER env-vars override correct executables
Unable to verify SDK resolution in this environment (both dart and flutter commands not found); please manually confirm that using DART/FLUTTER variables in the Makefile picks up the intended Dart/Flutter SDKs for all developer setups and CI pipelines.

test/features/launch/info_item_test.dart (2)

7-13: LGTM: Equality and hashCode assertions are solid.

Covers the core Equatable contract well.


15-27: Confirm: Should Key participate in value equality?

Including key in props means identical label/value with a different key are not equal. This affects diffing (e.g., Set/Map membership, list de-duping). Verify this is intentional for your UI model; otherwise consider excluding key from props.

test/models/launch/extension/launch_ext_test.dart (2)

8-14: LGTM: day-difference behavior matches current implementation

Asserting "4" for now + 5 days aligns with floor semantics in formatDateWithDays().


76-101: LGTM: network → resource mapping assertions are solid

Coverage for id/flightNumber/time/links/rocket looks good.

lib/data/network/model/launch/network_launch_model.dart (2)

36-38: LGTM: staged rocket fields are correctly added and keyed

first_stage and second_stage wiring looks correct for Freezed/JSON.


54-58: LGTM: extended links fields (reddit/video/presskit/flickr) match API keys

Types and JsonKey names align with SpaceX v3 schema patterns.

lib/models/launch/extension/launch_ext.dart (1)

66-68: LGTM: launchDays + launchTime mapping

Consistent use of the new formatter; once the UTC fix lands, this remains valid.

lib/features/launch/launch_screen.dart (4)

26-35: Good use of immediate event dispatch on Bloc creation

Matches the project preference to dispatch initial events via cascade (..add(...)) in the provider.


253-272: Good guard against empty mission patch URL

Avoids futile network calls when the URL is empty.


111-116: Properly disposing controllers

Prevents leaks; good lifecycle hygiene.


51-71: Dart SDK constraint already supports Dart 3 pattern matching pubspec.yaml specifies sdk: ">=3.2.0 <4.0.0", satisfying the requirement.

test/models/launch_ext_model_tests.dart (2)

254-260: If labeling output as UTC, ensure the time is actually converted to UTC

The test expects '... 14:30 UTC' from a local DateTime(2025, 12, 25, 14, 30). Verify that formatDate() converts to UTC before appending 'UTC'; otherwise results vary by environment.

Option A: pass a UTC DateTime.utc(...) in the test. Option B: adjust implementation to call .toUtc() before formatting when 'UTC' is part of the format.


48-128: Solid coverage of nested mappings

Good assertions for rocket, links, site, and date formatting in LaunchFullResource.toResource().

Comment on lines +145 to +146
color: Colors.black.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular(8),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Fix compile errors: Color.withValues(...) is not a Flutter API

Use withOpacity(...) (or withAlpha(...)) instead. Current code won’t compile on stable Flutter.

Apply this diff:

-                    color: Colors.black.withValues(alpha: 0.6),
+                    color: Colors.black.withOpacity(0.6),
@@
-                          color: Colors.black.withValues(alpha: 0.5),
+                          color: Colors.black.withOpacity(0.5),
@@
-                                      Colors.black.withValues(alpha: 0.20),
-                                      Colors.black.withValues(alpha: 0.70),
+                                      Colors.black.withOpacity(0.20),
+                                      Colors.black.withOpacity(0.70),
@@
-                                  color: Colors.white.withValues(alpha: 0.1),
+                                  color: Colors.white.withOpacity(0.1),
@@
-                                  : Colors.white.withValues(alpha: 0.4),
+                                  : Colors.white.withOpacity(0.4),
@@
-                          color: colorScheme.primaryContainer
-                              .withValues(alpha: 0.9),
+                          color:
+                              colorScheme.primaryContainer.withOpacity(0.9),
@@
-                            BoxShadow(
-                              color: Colors.black.withValues(alpha: 0.2),
+                            BoxShadow(
+                              color: Colors.black.withOpacity(0.2),

To catch any other occurrences:

Also applies to: 158-159, 216-218, 248-252, 295-297, 311-316


🏁 Script executed:

#!/bin/bash
rg -n "withValues\(" -S lib

Length of output: 6216


Replace all .withValues(alpha: …) calls with .withOpacity(…)

The non-existent withValues API causes compile errors on stable Flutter. A global search (rg -n "withValues\(" -S lib) uncovered occurrences in:

  • lib/features/launch/launch_screen.dart
  • lib/features/launch/widget/feature_chip_widget.dart
  • lib/features/launch/widget/launch_site_card_widget.dart
  • lib/features/launch/widget/payload_card_widget.dart
  • lib/features/launch/widget/stat_card_widget.dart
  • lib/features/launch/widget/mission_overview_card_widget.dart
  • lib/features/launch/widget/status_section_widget.dart
  • lib/features/launch/widget/recovery_ships_card_widget.dart
  • lib/features/launch/widget/timeline_widgets.dart
  • lib/features/launch/widget/links_card_widget.dart
  • lib/features/launch/widget/rocket_card_widget.dart
  • lib/features/launch/widget/animated_link_button_widget.dart

In each case, replace:

.someColor.withValues(alpha: 0.x)

with:

.someColor.withOpacity(0.x)
🤖 Prompt for AI Agents
In lib/features/launch/launch_screen.dart around lines 145 to 146, the code
calls a non-existent Colors.withValues(alpha: …) API which causes compile
errors; replace .withValues(alpha: x) with .withOpacity(x). Apply the same
replacement for all other occurrences in the launch feature files listed in the
review (search project for "withValues(") so every .withValues(alpha: 0.x)
becomes .withOpacity(0.x).

ashtanko and others added 2 commits September 6, 2025 18:16
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@ashtanko ashtanko merged commit 7e2f881 into main Sep 6, 2025
9 checks passed
@ashtanko ashtanko deleted the feature/launch_details_screen branch September 6, 2025 17:59
@coderabbitai coderabbitai bot mentioned this pull request Sep 7, 2025
This was referenced Sep 8, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant