Field Track 360

Developer guide

Integrate the SDK

Native SDKs for Android and iOS, with bridges for React Native and Flutter. Pick your platform - the setup genuinely differs, so these are not one page.

Install

A bridge, not a port. Every decision happens in the native SDKs.

The Kalman filter, the acceptance pipeline, the motion state machine, stop consolidation, spline smoothing and arrow placement all happen natively. This package moves values across a platform channel and gives them Dart types. It reimplements nothing.

Background tracking, including app-killed tracking, is 100% native: Android uses the SDK's own foreground service, boot receiver and watchdog workers; iOS uses CLBackgroundActivitySession, significant-location-change relaunch and a BGTaskScheduler backstop. Dart never needs to be running for capture to continue.

dependencies:
  trackit_flutter:
    path: ../trackit-flutter

Android requirements

These are inherited from the SDK's AARs, not chosen by the plugin.

  • minSdk 26 - otherwise manifest merge fails naming trackit-core
  • compileSdk 37 - otherwise checkDebugAarMetadata reports "requires minCompileSdk 37"
  • Kotlin 2.4.x and JDK 17
  • A JitPack read token
  • SDK v0.1.1-alpha03 or later, or getGeofences and getCurrentLocation fail as NOT_IMPLEMENTED_ON_PLATFORM

iOS requirements

iOS 17.0 - the SDK's floor, for @Observable state, CLBackgroundActivitySession and Swift 6 binaries with library evolution. Set it in the Podfile, in the post_install block, and on the Runner target itself; Flutter templates default to 13.0 and the build then fails on the module's minimum deployment target.

The AGP 9 problem, and the workaround

As shipped, the SDK and Flutter Gradle have mutually exclusive requirements. Here is the pin that breaks the tie.

trackit-core pulls androidx.core 1.19.0, whose AAR metadata demands AGP 9.1 or higher. Flutter's Gradle plugin (through 3.38) cannot run under AGP 9 - it throws an NPE, because AGP 9 ships Kotlin support built in while Flutter still expects org.jetbrains.kotlin.android to be applied separately.

Forcing androidx.core down breaks the tie, and it is safe rather than merely convenient: the only androidx.core APIs the SDK touches are NotificationCompat, ServiceCompat, ContextCompat and the three Location*Compat classes, all stable since core 1.7.

allprojects {
    repositories {
        google()
        mavenCentral()
        maven(url = "https://jitpack.io") {
            credentials { username = trackitAuthToken }
        }
    }

    // Remove when Flutter supports AGP 9.
    configurations.configureEach {
        resolutionStrategy.force(
            "androidx.core:core:1.16.0",
            "androidx.core:core-ktx:1.16.0",
        )
    }
}

Note this belongs in android/build.gradle.kts, not settings.gradle.kts: Flutter declares project-level repositories on :app, and Gradle ignores dependencyResolutionManagement for any project that does.

The real fixes are a Flutter release whose Gradle plugin supports AGP 9, or the SDK lowering its androidx floor. Delete the pin when either lands.

What your app must implement

The plugin shows no UI, prompts nothing and draws nothing. Skipping any of this is a bug that looks like the SDK misbehaving.

1. Startup order - subscribe first, but do not block your first frame

ready() is what reports a session a crash left open, and the event stream has no replay, so a listener attached afterwards misses it.

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  TrackIt.instance.events.listen(handleEvent);   // subscribe FIRST

  // Started here, awaited in the UI. Not awaited before runApp.
  final ready = TrackIt.instance.ready().timeout(const Duration(seconds: 15));

  runApp(MyApp(ready: ready));
}

Awaiting a platform-channel call before runApp means any stall on the native side shows up as an app frozen on its splash screen, with nothing on screen to say why. Paint first, resolve readiness behind a visible state, and keep SDK-dependent controls disabled until it answers.

2. Permissions are yours to trigger

The SDK decides which permissions this OS version needs and in what order; your app decides when to ask and provides the rationale. Nothing is requested automatically. Requesting the whole ladder at launch is the reliable way to get permanently denied.

3. Use the SDK's own one-shot fix for map centring

A second location package answers from its own permissions, provider choice and accuracy settings, so it will quietly disagree with the puck the engine draws.

4. Rendering is yours; the geometry is not

Decode with decodePolyline at track.precision and place track.arrows where the SDK put them rather than computing your own.

5. Recover interrupted sessions at launch

Handle it by event and by pull - a session can still be open after a crash or kill.

6. Branch on the typed error code, never the message

Drawing a track - two mistakes that silently ruin the render

Precision 6, not 5. And green is the fastest band, not the slowest.

final track = await TrackIt.instance.buildTrack(
  query: PointQuery(sessionId: id),
);

// Precision 6, NOT the 5 most decoders assume. Read it off the track.
final line = decodePolyline(track.encodedPolyline, precision: track.precision);

for (final s in track.segments.where((s) => s.isTravel)) {
  draw(decodePolyline(s.encodedPolyline, precision: track.precision), color(s.speedBand));
}

for (final stop in track.stops)  marker(stop.position, stop.dwell, pulse: stop.isOngoing);
for (final a in track.arrows)    arrow(a.position, rotation: a.bearing);

Why decodePolyline ships in this package

TrackIt encodes at precision 6 and the common decoders default to 5. That does not fail - it returns coordinates ten times too small and plots the track into the sea.

speedBand: green is the fastest

The names follow the traffic-map convention, so green is the fastest band and red the slowest. Colouring green as "slow" inverts every drawn track.

Live tracking: check the sequence

TrackIt.instance.liveTrack.listen((frame) {
  if (frame.sequence <= lastDrawn) return;   // REQUIRED
  lastDrawn = frame.sequence;
});

Frames can arrive out of order across a dispatcher hop and a platform channel; drawing a stale one makes the puck jump backwards. Never re-smooth frozenTailPolyline - it is already smoothed. And puck.headingDeg is null when velocity is too small to have a direction: hold your last rotation rather than snapping to a fabricated 0 degrees.

Background tracking, and the Android OEM asterisk

Capture survives a swipe-kill on stock Android. Several OEM skins override that, and no SDK can defeat it from inside the app.

Nothing to build - the SDKs own it. There is no background isolate, no headless callback and no second notification.

  • Backgrounded for a 30-minute drive: full trace on both platforms.
  • Swipe-kill mid-session: Android capture continues (stopWithTask="false"). On iOS capture stops, but a significant-location change or region exit relaunches the app in the background and capture resumes, with no Dart involved.
  • Reboot: Android restores per ServiceConfig.startOnBoot; iOS resumes at the next launch or SLC trigger.
  • Force-quit on iOS: no reliable relaunch. That is OS policy, not an SDK gap - surface it in tracking-health UI and never build correctness on it.

The OEM problem, stated honestly

"Capture continues after a swipe-kill" is what stock Android does. Several Chinese OEM skins - Xiaomi/Redmi, Oppo/Realme, Vivo, Huawei, and Samsung to a lesser degree - override it and kill the whole process, foreground service included.

No SDK can defeat this from inside the app; it is enforced above it. What works is per-device user settings, and a tracking app has to ask for them:

  • Autostart / Auto-launch - enable it
  • Battery saver - set to No restrictions
  • Lock the app in Recents - the padlock on the task card

Symptoms when they are not set: the notification disappears within seconds or minutes of the app being swiped away, the session stays open with a gap in its points, and a relaunch reports it via SessionInterruptedEvent.

Known limitations

Stated, not discovered.

  • changePace is iOS-only; feedIngestor on getCurrentLocation is ignored on Android.
  • HeartbeatEvent is declared on both but historically only iOS emits it - its absence is not a liveness signal.
  • Geofencing and getCurrentLocation landed on Android in v0.1.1-alpha03; with an older AAR they fail as NOT_IMPLEMENTED_ON_PLATFORM.
  • iOS getCurrentLocation collapses every failure into FIX_TIMEOUT - timeout, missing authorization and a concurrent call share one code. Only the message distinguishes them, so do not treat it as "retry later".
  • iOS track exports carry less than Android's: point src and mock are absent, and maxSpeedMps, avgMovingSpeedMps, pointCount and stopCount read 0 on iOS-built tracks.
  • A Dart-implemented RoadSnapProvider is impossible - the native interface is called inside buildTrack. OSRM or nothing, on both platforms.
  • offerFix and fixture replay are not bridged.
  • No hosted iOS dist exists yet, so the XCFrameworks are built from a local SDK checkout by the fetch script.

Geofence differences the bridge does not paper over

notifyOnEntry and notifyOnExit are honoured on iOS and ignored on Android, where both crossings always fire. dwellAfterMs is synthesised on iOS and has no equivalent on Android. onEnterEvent labels are carried on Android and ignored on iOS. Neither is emulated, because a faked difference is worse than a documented one.

Read stored history rather than only the stream: a crossing can be delivered to a process with no Dart listener, and the stream has no replay on either platform. getGeofenceEvents() is complete; stream events are a foreground convenience.

Why we verify licences on the device, not on our servers

A tracking app that stops because a licence server was unreachable is worse than a licence that survives a few hours too long.

Every Field Track 360 licence is verified on the device, with no network call. The token carries its own signature, the SDK carries the public key, and the check happens locally in milliseconds.

The trade we made

We also check periodically to see whether a licence has been revoked. That check can fail - no signal, a flat DNS, our own outage. When it does, the SDK keeps working on its cached verdict.

That is deliberate, and it costs us something: a revoked licence can survive until the device next reaches the network. We think that is the right side to fail on. A field team's tracking going dark mid-shift because a server blinked is a far worse outcome than a licence lasting a few hours longer than it should.

What it means for you

Your app starts tracking on a plane, on first launch, in a tunnel, after a reboot. There is no licence server in the path of your users doing their jobs.

Application keys: what one key actually covers

One key, one app identifier, both platforms, forever. Here is why it is bound that way and what to do when the id changes.

An application key licenses one app identifier - your Android applicationId or your iOS CFBundleIdentifier - on both platforms.

Why is it bound to the identifier? Because the identifier is cryptographically signed into the token itself. That is what makes a key impossible to copy into a second app: change the identifier and the signature no longer matches. It also means the binding cannot be edited afterwards, by us or by anyone else.

Debug and staging builds

Variants are included, as long as they extend your identifier with a dot. com.acme.app.dev and com.acme.app.staging are covered by a key for com.acme.app. com.acme.appX is a different application and is not - the dot is what separates a variant from a namesake.

When the identifier changes

It happens: a rebrand, a store migration, a typo caught late. Generate a corrected key from your account and the old one is revoked automatically. There is a limit per plan, because a revoked key still verifies offline, and support can help beyond it.

Try it before you buy

A 30-day trial licence for one application, issued instantly. Development builds are licence-waived, so you can evaluate the whole SDK first.

Get a trial key

Verification API

The SDK handles licensing for you. This is documented for tooling.

POST https://fieldtrack360-sdk.devstree.in/api/v1/verify