Family Providers & Dynamic Filter Architecture (shipment_providers.dart)
🎯 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.
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
4. Execution Timeline
ShipmentListFilters instance created with country parameters.operator == and hashCode against existing cached family entries.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:
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.
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).
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
operator == and hashCode implementation.