Flutter
Long Ping for Flutter is the long_ping package. It runs the same native iOS and Android engine as the React
Native package, so everything else in these docs applies to it unchanged: the clock, stop detection, the
upload queue, the HTTP contract, remote config, the on-device log, the permission flow and the platform
limits. Only your app code is different: it is Dart.
Requirements
Section titled “Requirements”| Minimum | |
|---|---|
| Flutter | 3.24 (Dart 3.5) |
| iOS | 15.1, with CocoaPods |
| Android | 7.0 (API 24), Google Play Services |
1. Add the package
Section titled “1. Add the package”flutter pub add long_pingRebuild the app after adding it (flutter run). A hot restart does not load native code.
2. iOS: Podfile and Info.plist
Section titled “2. iOS: Podfile and Info.plist”The long_ping pod needs iOS 15.1. Set the platform at the top of your Podfile:
platform :ios, '15.1'Flutter runs pod install for you on the next build.
Then add the two location usage strings and the location background mode:
<key>NSLocationWhenInUseUsageDescription</key><string>Your location is shared with dispatch while you are on duty.</string><key>NSLocationAlwaysAndWhenInUseUsageDescription</key><string>Your location is shared with dispatch while you are on duty, including when the app is closed.</string><key>UIBackgroundModes</key><array> <string>location</string></array>These are the same keys as in a React Native app. What each one is for, what happens without the background mode, and the privacy manifest the pod ships are on the Installation page.
3. Android: notification permission
Section titled “3. Android: notification permission”The plugin’s manifest brings the location, foreground-service and boot permissions. Declare one more, so the tracking notification can show on Android 13 and newer:
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />Your app asks for it at runtime; Installation
has an example with permission_handler.
Long Ping needs minSdk 24. Projects that use flutter.minSdkVersion get that from current Flutter
releases. If your android/app/build.gradle.kts (or build.gradle) sets a lower number, raise it to 24.
The optional battery-optimization permission and the Google Play Services requirement are the same as for React Native; see Installation. Google Play also asks for a background location declaration before release; see App Store & Google Play review.
4. Quick start
Section titled “4. Quick start”This records a fix every 30 seconds while moving and every 3 minutes while parked, and uploads to your endpoint once a minute.
import 'package:long_ping/long_ping.dart';
// On every app launch (safe to repeat: never starts, stops or prompts).Future<void> setUpTracking(String token) async { await LongPing.configure(const Config( movingIntervalSec: 30, parkedIntervalSec: 180, http: HttpConfig( url: 'https://api.example.com/locations', batch: true, uploadIntervalSec: 60, ), notification: NotificationConfig( title: 'On duty', text: 'Sharing your location with dispatch', ), ));
await LongPing.setAuthorization(Authorization(accessToken: token));}
// When the driver goes on duty.Future<void> goOnDuty() async { // Your own rationale screen first, then: await LongPing.requestPermission(PermissionLevel.always); await LongPing.start();}
// When the driver goes off duty.Future<void> goOffDuty() async { await LongPing.stop(); // queued records still upload}Plugin calls need the Flutter binding, so call setUpTracking() after ensureInitialized() if you run it
before runApp():
import 'package:flutter/widgets.dart';
import 'tracking.dart';
Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); await setUpTracking(await readSavedToken()); // your own auth runApp(const MyApp());}And in a widget, if you want to show what is happening:
import 'dart:async';
import 'package:flutter/material.dart';import 'package:long_ping/long_ping.dart';
class LastFix extends StatefulWidget { const LastFix({super.key});
@override State<LastFix> createState() => _LastFixState();}
class _LastFixState extends State<LastFix> { StreamSubscription<Location>? _sub; Location? _last;
@override void initState() { super.initState(); _sub = LongPing.onLocation.listen((location) => setState(() => _last = location)); }
@override void dispose() { _sub?.cancel(); super.dispose(); }
@override Widget build(BuildContext context) { final last = _last; if (last == null) return const Text('No fix yet'); return Text('${last.coords.latitude}, ${last.coords.longitude} at ${last.timestamp.toLocal()}'); }}What each call does, step by step, is on the Quick start page. Ask for permission the way Permissions describes, with your own rationale screen first.
Differences from the React Native API
Section titled “Differences from the React Native API”Behaviour, config keys, defaults and error codes are the same. The Dart API differs in these names and shapes:
| React Native | Flutter |
|---|---|
import LongPing from 'react-native-long-ping' |
import 'package:long_ping/long_ping.dart'; every call is static on LongPing |
configure({ http: { url, batch: true } }) |
configure(const Config(http: HttpConfig(url: …, batch: true))) |
'high', 'debug', 'PUT', 'automotive' |
Accuracy.high, LogLevel.debug, HttpMethod.put, IosActivityType.automotive |
notification.priority: 'default' |
NotificationPriority.normal |
lowBattery: { belowPct: 20, intervalSec: 300 } |
LowBattery(belowPct: 20, intervalSec: 300) |
setConfig({ lowBattery: null }) |
setConfig(const Config(lowBattery: LowBattery.off)) |
State |
LongPingState |
AuthorizationStatus.ALWAYS (a number) |
LocationAuthorizationStatus.always (.value is the same number) |
requestPermission('always') |
requestPermission(PermissionLevel.always) |
A rejected promise with code: 'PERMISSION_DENIED' |
A thrown LongPingException with code: LongPingErrorCode.permissionDenied (.wire is 'PERMISSION_DENIED') |
getCurrentPosition({ persist: true }) |
getCurrentPosition(persist: true) |
logger.getLog({ sinceMs, limit, order: 'desc' }) |
logger.getLog(since: aDateTime, limit: …, order: LogOrder.desc) |
const sub = LongPing.onLocation(cb), then sub.remove() |
final sub = LongPing.onLocation.listen(cb), then sub.cancel() |
onEnabledChange gives { enabled } |
onEnabledChange is a Stream<bool> |
onConfigChange gives a ResolvedConfig |
onConfigChange is a Stream<Config> with every field set |
location.is_moving, location.battery.is_charging |
location.isMoving, location.battery.isCharging |
location.timestamp, an ISO 8601 string |
location.timestamp, a UTC DateTime; location.toMap() gives the upload JSON |
state.lastLocationAt, epoch milliseconds |
state.lastLocationAt, a UTC DateTime? |
Every reference page shows both: pick the Flutter tab once and the docs remember it.
Example app
Section titled “Example app”The repository has a complete example app, with a button for every command, the event stream and the on-device log: flutter/example.