A clean, Material 3 shipment and delivery tracking app built with Flutter. Search a parcel by tracking number, browse recent shipments, and follow every checkpoint on a vertical timeline, in light or dark mode.
The app is fully self-contained. It runs on bundled mock data behind a repository interface, so there is no backend or API key to set up, and a real API can be plugged in without touching the UI.
- Track by number: look up shipments such as
SPX-1001. Matching is case-insensitive and ignores extra spaces, and you get clear feedback when nothing matches. - Recent shipments: cards show the route, service type, weight, latest update and a colour-coded status chip (Pending, In Transit, Out for Delivery, Delivered).
- Status filters: filter chips with live counts for each status.
- Shipment details:
- origin → destination route summary
- estimated delivery date (or the actual delivery time once delivered)
- animated delivery progress bar with a step indicator
- vertical tracking timeline with location, time and description for every event
- sender and receiver contact cards
- package details (service, weight, last update)
- Light and dark themes: follows the system setting, with a one-tap toggle.
- Responsive layout: content is width-constrained so it also looks good on tablets and the web.
- Pull to refresh and loading, empty and error states.
- Tested: unit tests for the data layer, models and formatters, plus widget tests for the main user flows.
Demo tracking numbers: SPX-1001 to SPX-1006, covering routes between
Lagos, Abuja, Port Harcourt, Kano and Ibadan.
| Area | Choice |
|---|---|
| Framework | Flutter (Android, iOS, Web) |
| Language | Dart 3 (records, switch expressions, enhanced enums) |
| UI | Material 3, ColorScheme.fromSeed, custom widgets |
| Architecture | Repository pattern + constructor dependency injection |
| Data | In-memory mock data (JSON-serialisable models) |
| Testing | flutter_test (unit + widget tests) |
| Linting | flutter_lints |
No third-party packages are required beyond the Flutter SDK.
lib/
├── main.dart # Entry point
├── app.dart # MaterialApp, theme mode, repository wiring
├── data/
│ ├── shipment_repository.dart # Abstract ShipmentRepository contract
│ ├── mock_shipment_repository.dart # In-memory implementation
│ └── mock_shipments.dart # Demo shipments (Nigerian routes)
├── models/
│ ├── shipment.dart # Shipment entity (+ fromJson/toJson)
│ ├── shipment_status.dart # Status enum with label & progress
│ ├── tracking_event.dart # Timeline checkpoint
│ └── contact.dart # Sender / receiver
├── screens/
│ ├── home_screen.dart # Search, filters, recent shipments
│ └── shipment_detail_screen.dart # Progress, timeline, parties, details
├── theme/
│ └── app_theme.dart # Light/dark Material 3 themes, status colours
├── utils/
│ └── formatters.dart # Date/time helpers (no intl dependency)
└── widgets/
├── shipment_card.dart
├── status_chip.dart
├── shipment_progress.dart
├── tracking_timeline.dart
├── route_summary.dart
└── section_card.dart
test/
├── data/ # Repository tests
├── models/ # Model & formatter tests
└── widgets/ # Widget / flow tests
- Flutter SDK (stable channel, Dart 3.x)
- An emulator, a physical device, or Chrome for the web build
git clone https://github.com/Josephobaje/shipment-tracker-flutter.git
cd shipment-tracker-flutter
flutter pub get
flutter run # pick a device, or: flutter run -d chromeflutter analyze
flutter testflutter build apk --release # Android
flutter build web # WebThe UI only depends on the ShipmentRepository abstraction:
abstract class ShipmentRepository {
Future<List<Shipment>> getRecentShipments();
Future<Shipment?> findByTrackingNumber(String trackingNumber);
}To connect a real backend:
-
Add an HTTP client, e.g.
flutter pub add http. -
Create an implementation. The models already include
fromJson/toJson:import 'dart:convert'; import 'package:http/http.dart' as http; class ApiShipmentRepository implements ShipmentRepository { ApiShipmentRepository({required this.baseUrl, http.Client? client}) : _client = client ?? http.Client(); final String baseUrl; final http.Client _client; @override Future<List<Shipment>> getRecentShipments() async { final res = await _client.get(Uri.parse('$baseUrl/shipments')); final list = jsonDecode(res.body) as List<dynamic>; return list .map((e) => Shipment.fromJson(e as Map<String, dynamic>)) .toList(); } @override Future<Shipment?> findByTrackingNumber(String trackingNumber) async { final id = normalizeTrackingNumber(trackingNumber); final res = await _client.get(Uri.parse('$baseUrl/shipments/$id')); if (res.statusCode == 404) return null; return Shipment.fromJson(jsonDecode(res.body) as Map<String, dynamic>); } }
-
Inject it in
main.dart:runApp(ShipmentTrackerApp( repository: ApiShipmentRepository(baseUrl: 'https://api.example.com'), ));
No screen or widget changes are needed. The same injection point makes it easy to pass fakes in tests.
- REST/GraphQL repository implementation with error handling and retries
- State management with Riverpod or Bloc as the app grows
- Map view of the route with live courier location
- Push notifications for status changes
- Barcode / QR scanning of tracking labels
- Offline cache of tracked shipments
- Saved/favourite shipments and search history
- Localisation (English, Yoruba, Hausa, Igbo, Pidgin)
- CI pipeline (analyze, test, build) with GitHub Actions
Joseph Obaje: Full Stack Developer (Flutter · PHP · Python), Nigeria
- GitHub: @Josephobaje
- LinkedIn: joseph-obaje
If you find this project useful, consider giving it a ⭐.
This project is licensed under the MIT License.