flutter_offline_background_location 1.0.0
flutter_offline_background_location: ^1.0.0 copied to clipboard
Offline background location tracking plugin. Records location when app is closed or without internet.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:flutter_offline_background_location/flutter_background_location.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Background Location Example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
useMaterial3: true,
),
home: const PermissionWrapper(child: HomeScreen()),
);
}
}
class PermissionWrapper extends StatefulWidget {
const PermissionWrapper({super.key, required this.child});
final Widget child;
@override
State<PermissionWrapper> createState() => _PermissionWrapperState();
}
class _PermissionWrapperState extends State<PermissionWrapper> {
bool _checked = false;
@override
void initState() {
super.initState();
_requestPermissions();
}
Future<void> _requestPermissions() async {
await Permission.locationWhenInUse.request();
await Permission.locationAlways.request();
if (await Permission.notification.isDenied) {
await Permission.notification.request();
}
if (mounted) setState(() => _checked = true);
}
@override
Widget build(BuildContext context) {
if (!_checked) {
return const Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Requesting permissions…'),
],
),
),
);
}
return widget.child;
}
}
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
bool _isLoading = false;
String? _error;
bool _tracking = false;
final _plugin = FlutterBackgroundLocation.instance;
Future<void> _startTracking() async {
setState(() {
_error = null;
_isLoading = true;
});
try {
await _plugin.configure(const BackgroundLocationConfig(
intervalMinutes: 1,
accuracy: LocationAccuracy.high,
notificationTitle: 'Location tracking',
notificationBody: 'Recording your position.',
retentionDays: 30,
maxRecords: 10000,
));
await _plugin.initialize();
await _plugin.startTracking();
if (mounted) {
setState(() {
_isLoading = false;
_error = null;
_tracking = true;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Tracking started.')),
);
}
} catch (e) {
if (mounted) {
setState(() {
_isLoading = false;
_error = e.toString();
});
}
}
}
Future<void> _stopTracking() async {
setState(() {
_error = null;
_isLoading = true;
});
try {
await _plugin.stopTracking();
if (mounted) {
setState(() {
_isLoading = false;
_tracking = false;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Tracking stopped.')),
);
}
} catch (e) {
if (mounted) {
setState(() {
_isLoading = false;
_error = e.toString();
});
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Background Location'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (_error != null) ...[
Card(
color: Theme.of(context).colorScheme.errorContainer,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Text(_error!, style: const TextStyle(color: Colors.black87)),
),
),
const SizedBox(height: 16),
],
if (!_tracking)
ElevatedButton.icon(
onPressed: _isLoading ? null : _startTracking,
icon: const Icon(Icons.play_arrow),
label: const Text('Start tracking'),
style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
),
if (_tracking)
ElevatedButton.icon(
onPressed: _isLoading ? null : _stopTracking,
icon: const Icon(Icons.stop),
label: const Text('Stop tracking'),
style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
),
const SizedBox(height: 12),
OutlinedButton.icon(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (context) => const LocationHistoryScreen(),
),
);
},
icon: const Icon(Icons.history),
label: const Text('Location history'),
style: OutlinedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 16)),
),
if (_isLoading)
const Padding(
padding: EdgeInsets.only(top: 24),
child: Center(child: CircularProgressIndicator()),
),
],
),
),
);
}
}
class LocationHistoryScreen extends StatefulWidget {
const LocationHistoryScreen({super.key});
@override
State<LocationHistoryScreen> createState() => _LocationHistoryScreenState();
}
class _LocationHistoryScreenState extends State<LocationHistoryScreen> {
List<LocationRecord> _locations = [];
bool _loading = true;
String? _error;
final _plugin = FlutterBackgroundLocation.instance;
Future<void> _load() async {
setState(() {
_loading = true;
_error = null;
});
try {
final list = await _plugin.getLocations(limit: 200);
if (mounted) {
setState(() {
_locations = list;
_loading = false;
_error = null;
});
}
} catch (e) {
if (mounted) {
setState(() {
_loading = false;
_error = e.toString();
});
}
}
}
@override
void initState() {
super.initState();
_load();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Location history'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _loading ? null : _load,
),
],
),
body: _buildBody(),
);
}
Widget _buildBody() {
if (_loading) {
return const Center(child: CircularProgressIndicator());
}
if (_error != null) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_error!, textAlign: TextAlign.center),
const SizedBox(height: 16),
ElevatedButton(onPressed: _load, child: const Text('Retry')),
],
),
),
);
}
if (_locations.isEmpty) {
return const Center(
child: Text('No locations yet. Start tracking to record.'),
);
}
return RefreshIndicator(
onRefresh: _load,
child: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: _locations.length,
itemBuilder: (context, index) {
final loc = _locations[index];
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: ListTile(
title: Text(
'${loc.latitude.toStringAsFixed(6)}, ${loc.longitude.toStringAsFixed(6)}',
style: const TextStyle(fontFamily: 'monospace'),
),
subtitle: Text(
_formatTimestamp(loc.timestamp),
style: Theme.of(context).textTheme.bodySmall,
),
),
);
},
),
);
}
String _formatTimestamp(int ms) {
final dt = DateTime.fromMillisecondsSinceEpoch(ms);
return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')} '
'${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}:${dt.second.toString().padLeft(2, '0')}';
}
}