Parcel tracking timeline
Vertical timeline of shipment events with completed, current and pending states drawn with a connector line.
FlutterCardsfluttertimelinetrackingshipment
Code
import 'package:flutter/material.dart';
class TrackEvent {
const TrackEvent(this.title, {this.detail, this.at});
final String title;
final String? detail;
final DateTime? at; // null means it has not happened yet
}
class TrackingTimeline extends StatelessWidget {
const TrackingTimeline({super.key, required this.events});
final List<TrackEvent> events; // in delivery order
@override
Widget build(BuildContext context) {
// The current step is the last event that already has a timestamp.
final current = events.lastIndexWhere((e) => e.at != null);
return Column(children: [
for (var i = 0; i < events.length; i++)
_Row(
event: events[i],
isFirst: i == 0,
isLast: i == events.length - 1,
state: i < current ? _S.done : (i == current ? _S.current : _S.pending),
),
]);
}
}
enum _S { done, current, pending }
class _Row extends StatelessWidget {
const _Row({required this.event, required this.isFirst, required this.isLast, required this.state});
final TrackEvent event;
final bool isFirst, isLast;
final _S state;
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
final active = state != _S.pending;
final line = active ? scheme.primary : scheme.outlineVariant;
final loc = MaterialLocalizations.of(context);
return IntrinsicHeight(
child: Row(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
SizedBox(
width: 32,
child: Column(children: [
Expanded(child: Container(width: 2, color: isFirst ? Colors.transparent : line)),
Container(
width: state == _S.current ? 18 : 12,
height: state == _S.current ? 18 : 12,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: state == _S.pending ? scheme.surface : scheme.primary,
border: Border.all(color: line, width: 2),
),
child: state == _S.done ? Icon(Icons.check, size: 8, color: scheme.onPrimary) : null,
),
Expanded(child: Container(width: 2, color: isLast ? Colors.transparent : (state == _S.done ? scheme.primary : scheme.outlineVariant))),
]),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(event.title, style: TextStyle(fontWeight: state == _S.current ? FontWeight.bold : FontWeight.normal,
color: state == _S.pending ? scheme.outline : null)),
if (event.detail != null) Text(event.detail!, style: Theme.of(context).textTheme.bodySmall),
if (event.at != null)
Text('${loc.formatMediumDate(event.at!)}, ${loc.formatTimeOfDay(TimeOfDay.fromDateTime(event.at!))}',
style: Theme.of(context).textTheme.labelSmall),
]),
),
),
]),
);
}
}
How it works
What it solves
Customers check a parcel's status over and over. A vertical timeline shows what has happened, where it is now and what is still to come, at a glance.
How it works
Events are listed in delivery order. A pending event has no timestamp, so the current step is simply the last event that has one. Rows before it are done, the current row gets a larger dot and bold title, and rows after it are muted.
Each row is an IntrinsicHeight row with a narrow rail column: an expanding line segment above the dot and another below. The first and last rows hide their outer segments, which makes the line appear continuous across rows without a CustomPainter. The lower segment is highlighted only for completed steps, so progress colouring ends exactly at the current dot.
Adapting it
Map your carrier statuses to TrackEvents, appending the expected remaining steps with null timestamps. Replace the check icon with status icons per event type. For failed deliveries add a fourth state with the error colour.
Pitfalls
IntrinsicHeightis fine for dozens of rows but costly in very long lists.- If timestamps arrive out of order, sort them first or
lastIndexWhereselects the wrong step. - Show times in the recipient's time zone, not the depot's.
How to use it
Copy the file to lib/tracking/. No packages.
TrackingTimeline(events: [
TrackEvent('Picked up', at: DateTime.now()),
TrackEvent('Out for delivery'),
TrackEvent('Delivered'),
])
Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.