Skip to content

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.

Minimum
Flutter 3.24 (Dart 3.5)
iOS 15.1, with CocoaPods
Android 7.0 (API 24), Google Play Services
Terminal window
flutter pub add long_ping

Rebuild the app after adding it (flutter run). A hot restart does not load native code.

The long_ping pod needs iOS 15.1. Set the platform at the top of your Podfile:

ios/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:

ios/Runner/Info.plist
<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.

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:

android/app/src/main/AndroidManifest.xml
<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.

This records a fix every 30 seconds while moving and every 3 minutes while parked, and uploads to your endpoint once a minute.

lib/tracking.dart
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():

lib/main.dart
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:

lib/last_fix.dart
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.

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.

The repository has a complete example app, with a button for every command, the event stream and the on-device log: flutter/example.