Own product

Contrail

How can a travel app expose uncertainty without abandoning travellers to raw data?

Contrail displays a dark globe with the great-circle route from London to Singapore and a day-night boundary.
Documented Android device state. Route, sun position and boarding-pass view appear together; this is not a recreated mock-up.

Contrail connects flight data, journey timing and native Android notifications. Measurement, estimation and stale state remain visibly different.

Field
Flight and journey companion
Period
2026
Status
In development · v1.5
Platforms
Android
Services
Flutter app, Native Android integration, Backend scheduling, Release preparation

Context

Airline and ADS-B data do not directly answer when to leave, whether a connection is realistic, or whether a position was actually measured. Background operation also cannot depend on a visible Flutter lifecycle.

Responsibility

Product design, Flutter application, journey and freshness models, globe rendering, native Android services, notifications, backend polling and Play release preparation.

Outcome

An Android 1.5 development build with documented device states, a native live notification and a scheduler for constrained data requests.

Contrail displays a live flight with location-labelled times for Zakynthos and London.
Real review capture of the time model. Airport times carry location codes; parts of the status bar have limited contrast in this capture.
Android lock-screen live update for a flight saying it lands in 26 minutes.
Native Android 16 output. The compact lock-screen surface comes from the same watch state as the open application.

An estimate must never look like an observation

Current code defines five data states: live, estimated, scheduled, stale and offline. Each state carries age, last error and last successful request. Only genuinely fresh live data may pulse. A measurement leaves the live state after two minutes, while the wider freshness window depends on proximity to departure.

This avoids two kinds of false confidence. A flight days away need not poll every minute to look healthy. A flight at the gate cannot show the same state for hours. The interface reflects operational urgency rather than merely printing a request timestamp.

If a fresh ADS-B fix disappears, Contrail first advances the last real position using measured speed toward the destination. Only without a useful last position does it interpolate origin to destination from elapsed flight time. The aircraft therefore does not jump from the end of its actual track back onto an ideal line.

That projection has limits. It runs for no more than two hours toward the destination. If the measured heading differs by more than 90 degrees, it continues for at most 20 minutes and then freezes. Ground fixes stay still, impossible speeds are rejected, and a fix without a timestamp remains measured rather than projected. The estimate retains its basis and frozen state through to presentation.

Time has a place

Contrail represents travel time as a UTC instant plus IANA zone and location code. Durations use UTC; display names the place. Calendar-day changes are calculated separately.

This prevents a concrete class of mistakes around device time, airport time and library defaults. Existing tests cover both directions of the date line, daylight-saving changes, fractional offsets and missing or invalid zones. The honest claim is a centralised travel-time model with explicit locations, not a proof that every possible UI string is typed against ambiguity.

The notification survives the Flutter UI

The live notification is not a screenshot of a running widget. Flutter derives the current focus from trip, legs and an explicit clock: active leg, relevant moment, headline, connection assessment and door-to-gate plan. On backgrounding, it hands a flat contract to Android and deliberately stops its own poll loop.

Kotlin persists watched flight IDs, reconstructs them when the foreground service starts and can continue after a system restart with a null intent. The native watcher prefers the arrival anchor supplied by Flutter. Only when it is no longer useful does it calculate a simpler remaining time from distance and speed.

Tests cover one notification per trip, persistence when a single leg is removed and cleanup after the final leg. Another contract test compares method-channel calls in Dart with handlers in Kotlin, a boundary neither compiler can verify alone.

A constrained data allowance is a scheduling problem

The provider limit documented during implementation equated to roughly 300 requests per key per month. That is historical project configuration, not a current pricing claim. The architecture question remains: a constant poll rate would spend the allowance on a few flights regardless of urgency.

The scheduler assigns watched flights to urgency classes. Overdue, arriving, landed, active and soon-departing flights receive different intervals. User presence, watcher count, disruption and age of the last successful result affect selection. A backend tick caps candidates, spaces requests and records the decision separately from any push actually sent.

Failure handling distinguishes exhausted allowance, rate limits, missing records, malformed data and timeouts. Only confirmed exhaustion moves to another key within the attempt; retrying a timeout through a reserve route would likely pay for the same absence again.

What this demonstrates for client work

Contrail combines three capabilities: a legible product model for uncertain data, native background execution beyond a cross-platform lifecycle, and backend scheduling that translates resource limits into domain priorities.

The same approach applies to delivery status, sensors or field operations. The reusable principle is to model observation, assumption, age and failure as distinct states and carry those distinctions consistently through interface, native service and operations.

Technology and what it is for

Flutter
Interface and travel model
Kotlin
Native Android services
Android Foreground Service
Survives process death
Android 16 Live Updates
Lock-screen status
Cloud Functions
Backend request scheduling
ADS-B
Position data
Zoned time model
Local time at every airport