Phase 09

Family Providers & Dynamic Filter Architecture (shipment_providers.dart)

Calculating reading time... Module 2: Transwave Deep Dives

🎯 Senior Engineering Objective

Master Riverpod Family providers, multi-parameter value object immutability, mandatory operator == and hashCode overrides, integer currency handling in EUR Cents, and dynamic pagination pipelines in shipment_providers.dart.

1. Why Does This File Exist?

In Transwave, travelers browse available package shipments based on complex criteria: departure/destination country, city, price range, weight limit, urgency, and geographic bounding box coordinates.

🌊 Core Problem Solved

shipment_providers.dart encapsulates ShipmentListFilters (a 25-field value object) and exposes shipmentListProvider. It enables dynamic marketplace filtering while enforcing Riverpod cache key deduplication so identical search parameters never trigger duplicate REST API calls.

2. Who Uses This File? (Codebase Search)

Consumer File Consumption Pattern Purpose
lib/features/shipments/screens/shipment_list_screen.dart ref.watch(shipmentListProvider(currentFilters)) Renders paginated shipment cards for current filter selection.
lib/features/shipments/widgets/shipment_filter_sheet.dart ref.read(mapFiltersProvider.notifier).updateFilters(...) Mutates filter criteria and pushes updated parameter object to family provider.
lib/features/home/providers/map_items_provider.dart ref.watch(shipmentListProvider(...)) Integrates shipments into map spatial viewport caching (Phase 10).

3. Dependency Graph

ShipmentListFilters
shipmentListProvider (Family)
ShipmentRepository
ShipmentListScreen

4. Execution Timeline

Family Parameter Lookup Lifecycle
1
User Selects Filter Options
User selects France to Tunisia route in filter modal sheet.
2
Filter State Emits Immutable Value Object
ShipmentListFilters instance created with country parameters.
3
Riverpod Key Hash Lookup
Riverpod evaluates operator == and hashCode against existing cached family entries.
4
Cache Hit or API Fetch
If previously fetched, returns cached result immediately. If new search, invokes ShipmentRepository.fetchShipments().

5. Provider Lifecycle

Because shipmentListProvider is annotated with @riverpod, Riverpod creates an AutoDisposeFutureProviderFamily. When all widgets watching a specific filter selection unmount (e.g. user leaves the list screen), Riverpod schedules auto-disposal to free up device memory.

Special Section A: Family Parameter Equality Breakdown (operator == & hashCode)

This is the most critical Riverpod rule when working with Family providers accepting custom objects:

🚨 The Un-Overridden Family Parameter Disaster

When a widget calls ref.watch(shipmentListProvider(filters)), Riverpod uses filters as a Map cache key.
• If operator == is omitted, Dart compares memory addresses (identical). Every widget rebuild creates a new instance address, causing an infinite fetch loop that spams the backend API!
• When operator == and hashCode are properly overridden, Riverpod checks field values. Identical filter selections reuse the cached result instantly.

Dart lib/features/shipments/providers/shipment_providers.dart
class ShipmentListFilters {
  const ShipmentListFilters({
    this.status,
    this.category,
    this.departureCountry,
    this.destinationCountry,
    this.minPriceCents,
    this.maxPriceCents,
    this.page = 1,
    this.limit = 20,
  });

  final ShipmentStatus? status;
  final String? category;
  final String? departureCountry;
  final String? destinationCountry;
  final int? minPriceCents;
  final int? maxPriceCents;
  final int page;
  final int limit;

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is ShipmentListFilters &&
          runtimeType == other.runtimeType &&
          status == other.status &&
          category == other.category &&
          departureCountry == other.departureCountry &&
          destinationCountry == other.destinationCountry &&
          minPriceCents == other.minPriceCents &&
          maxPriceCents == other.maxPriceCents &&
          page == other.page &&
          limit == other.limit;

  @override
  int get hashCode => Object.hash(
        status,
        category,
        departureCountry,
        destinationCountry,
        minPriceCents,
        maxPriceCents,
        page,
        limit,
      );
}

Special Section B: Integer Currency Protocol (EUR Cents)

In compliance with Transwave Architectural Commandment #9, all API monetary values are transmitted and stored as integer EUR cents (e.g., €15.50 is stored as 1550).

✨ Why EUR Cents Integers?

Floating-point arithmetic in Dart (e.g. 0.1 + 0.2 = 0.30000000000000004) introduces rounding errors in monetary calculations. Using integer cents guarantees exact arithmetic across backend, database, Stripe Connect escrow payments, and Flutter UI formatting:
String displayPrice = (cents / 100).toStringAsFixed(2); // "15.50 €"

6. Alternative Designs Comparison

Design Pattern Pros Cons Why Not Chosen
Family FutureProvider (Current) Automatic cache key deduplication; clean AsyncValue UI integration. Requires manual operator == on custom parameter class. Chosen: Gold standard for parametric dynamic REST endpoints.
Global Single Filter State Notifier No family parameters needed. Couples filter state globally; cannot open two screens with different search filters. Violates screen isolation.
Stateful Widget Local Fetching Zero Riverpod setup required. No caching; loses search results when navigating away. Poor user experience.

7. Performance & Memory Analysis

Family providers create a distinct sub-provider instance for each unique parameter hash. Using autoDispose ensures that as soon as the user exits the marketplace screen, all cached shipment arrays and provider instances are evicted from device RAM.

8. Testing Strategy

Unit testing family providers is straightforward. Create a ProviderContainer, override shipmentRepositoryProvider with a mock implementation, and call container.read(shipmentListProvider(myFilters).future). Verify that passing equal filter objects returns the same cached future!

Special Section J: 15 Senior Flutter Interview Questions & Answers

Answer: Dart defaults to reference equality. Every widget rebuild creates a new parameter instance in memory, causing Riverpod to treat it as a new cache key. This results in infinite fetch loops and memory leaks.

Answer: Floating-point numbers suffer from binary representation imprecision (e.g. `0.1 + 0.2`). Storing currency as integer cents guarantees exact precision across API endpoints, database queries, and Stripe escrow transfers.

Answer: Any parameter added after Ref ref in an @riverpod function or build() method is automatically recognized by riverpod_generator and compiled into a Family provider.

📊 Senior Engineering Scorecard & Review

Value Object Immutability
10 / 10: Impeccable operator == and hashCode implementation.
Currency Precision
10 / 10: Strictly integer EUR Cents (Commandment #9).
Cache Deduplication
10 / 10: Prevents duplicate REST calls across identical searches.