tight_flutter

Flutter bridge to the Tight mileage tracking SDKs for iOS and Android, exposed to Dart through platform channels.

Inspired in structure by intercom_flutter: a plain Dart API over MethodChannel for calls and an EventChannel stream for the native TightDelegate callbacks.

What it does (and does not) do

  • ✅ Initialize the SDK, request permissions, set the detection mode, start/stop semi-automatic drives, read the current drive, manage Bluetooth trigger devices.
  • ✅ Surface native drive/permission callbacks as a Stream<TightEvent>.
  • ✅ Bluetooth trigger devices (Android only — see below).
  • ❌ No trip history. The SDK only exposes the current drive (getCurrentDrive). Full history is served by the Tight backend, not this plugin.
  • ❌ No GPS polyline / map. TightDrive carries only start/end coordinates
    • distance. The Tight SDK v6.0.0 exposes no map/route view on either platform, so this plugin does not provide one.

Requirements & configuration

This package is a thin port of the public Tight SDKs (v6.0.0 on both platforms). It ships no credentials: the integrating app provides its own TightClientId (build config) and per-user accessToken (passed to initialize). The Tight SDKs are publicly distributed — no private access is required to build or use this package.

Detection mode parity

The native detection-mode enums differ, so the Dart MileageDetectionMode is a unified abstraction:

Dart iOS (OFF/AUTO) Android (OFF/AUTO/BLUETOOTH)
disabled OFF OFF
automatic AUTO AUTO
bluetooth unsupported → throws BLUETOOTH

Bluetooth device management (getBluetoothDevices, saveBluetoothDevice, requestBluetoothPermissions) is Android-only; these throw a TightException on iOS.

iOS

  • The Tight iOS SDK (module Tight, entry point SDK.manager) is Swift Package Manager only and requires iOS 17.0. On older iOS versions every call throws a TightException with code unsupported_os (see the gating notes in ios/tight_flutter/Package.swift).
  • Enable Flutter's SPM support in the host app: flutter config --enable-swift-package-manager.
  • Info.plist keys required by the host app:
    • TightClientId (provided by Tight)
    • NSLocationAlwaysAndWhenInUseUsageDescription
    • NSLocationWhenInUseUsageDescription
    • NSLocationUsageDescription
    • NSMotionUsageDescription
    • UIBackgroundModes = [location]

Android

  • Distributed via Maven Central (no private credentials). minSdk 28.
  • Host app resources:
    • string tight_client_id (required)
    • mileage_notification_icon_color, mileage_notification_icon (optional)
  • Host app AndroidManifest.xml permissions (background location, activity recognition and Bluetooth are needed for automatic detection). Confirm the exact list against the Tight Android SDK before shipping:
    • ACCESS_FINE_LOCATION, ACCESS_BACKGROUND_LOCATION
    • ACTIVITY_RECOGNITION
    • BLUETOOTH_CONNECT, BLUETOOTH_SCAN
  • The host Activity must be a ComponentActivity (Flutter's default FlutterActivity is) so the permission launcher works.

Notes & open items

  • Drive resumption after app kill / device reboot is handled by the native SDK; verify it on-device as part of QA.
  • Battery savers can suppress background tracking — surfaced via TightPermissions.osBatterySaver / appBatterySaver.

Usage

final tight = TightFlutter();

await tight.initialize(accessToken: backendMintedToken);
final permissions = await tight.requestMileagePermissions();
await tight.setMileageDetectionMode(MileageDetectionMode.automatic);

final sub = tight.events.listen((event) {
  switch (event) {
    case DidStartDrive(:final drive):   /* ... */
    case DidStopDrive(:final drive):    /* ... */
    case DidResumeDrive(:final drive):  /* ... */
    case DidPermissionsChange(:final permissions): /* ... */
    case DidPermissionsError():         /* ... */
    case TightUnknownEvent():           break;
  }
});

await tight.startSemiAutoDrive();
// ...
await tight.stopDrive();

Status

POC, verified against the public v6.0.0 SDKs (SPM module Tight / com.tight:tight-android-sdk:6.0.0, both via the SDK.manager singleton).

Verified end-to-end:

  • Android — example/ builds against the real SDK, runs on an emulator, and the method channel round-trips into the SDK (initialize reaches native and surfaces a typed TightException when no client id is configured).
  • iOS — example/ builds against the real SDK (SPM binary target, simulator slices present) and runs on the iOS 17+ simulator.

Before shipping, still to validate on-device with real credentials (TightClientId + backend accessToken): the full permission / drive lifecycle, background tracking, and drive resumption after app kill / reboot. Note the SPM manifest pins the plugin to iOS 17, so host apps must target iOS 17 (or vendor a prebuilt xcframework via the podspec to stay lower).