Flutter Live Location Tracking in Google Map— with Geolocator Plugin

Ahmet Can Poyraz
8 min readSep 28, 2022

In this article, I will show that how to real-time location updates with drawing route polylines.

Preview of Live Location Tracking in Google Map

Note: You should have the maps set up in your project using the Google Maps Flutter Package and your own Google Maps API key. If not, follow this link on how to set up your Flutter project to work with Google Maps.

Setup

Before adding the geolocator packages prep your environment accordingly to enable live location tracking on both iOS and Android by following the steps in the package’s README regarding the Android manifest file and the iOS Info.plist.

Add packages to dependencies in pubspec.yaml


dependencies:
flutter_polyline_points: ^1.0.0
google_maps_flutter: ^2.2.0
stop_watch_timer: ^1.5.0
geolocator: ^9.0.2
path_provider: ^2.0.11
sqflite: ^2.0.1
intl: ^0.17.0

Let’s Code

We will save our tracking records to database because of we will use sqflite package.Let’s start by creating the database class as db.dart first. This class have methods thats are onCreate,init,query and insert for database process.

db.dart

import 'dart:async';

import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart' as p;
import 'entry.dart';

class DB {
late Database _db;
static int get _version => 1;

Future<void> init() async {
try {
String _path = await getDatabasesPath();
String _dbpath = p.join(_path, 'database.db');
_db = await openDatabase(_dbpath, version: _version, onCreate: onCreate);
} catch (ex) {
print(ex);
}
}

static FutureOr<void> onCreate(Database db, int version) async {
await db.execute('''
CREATE TABLE entries (
id INTEGER PRIMARY KEY NOT NULL,
date STRING,
duration STRING,
speed REAL,
distance REAL
)
''');
}

Future<List<Map<String, dynamic>>> query(String table) async =>
await _db.query(table);
Future<int> insert(String table, Entry item) async =>
await _db.insert(table, item.toMap());
}

Then we will create Entry class.This class is entries model class.

entry.dart

class Entry {
static String table = "entries";

int? id;
String? date;
String? duration;
double? speed;
double? distance;


Entry({this.id, this.date, this.duration, this.speed, this.distance});

Map<String, dynamic> toMap() {
Map<String, dynamic> map = {
'date': date,
'duration': duration,
'speed': speed,
'distance': distance,
};

if (id != null) {
map['id'] = id;
}

return map;
}

static Entry fromMap(Map<String, dynamic> map) {
return Entry(
id: map['id'],
date: map['date'],
duration: map['duration'],
speed: map['speed'],
distance: map['distance']);
}
}

Now talk about what we are doing in main class. I will say piece by piece what we are doing as comments in code below.

Home Class screen

main.dart

import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:live_location_tracking/entry.dart';
import 'package:live_location_tracking/widgets.dart';
import 'db.dart';
import 'map_page.dart';

void main() {
runApp(const MaterialApp(
home: Home(),
));
}

class Home extends StatefulWidget {
const Home({super.key});

@override
State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {
late List<Entry> _data;
List<EntryCard> _cards = [];
late DB db;
// initialize DB and fetch entries for showing items in list
void initState() {
db = DB();
WidgetsBinding.instance.addPostFrameCallback((_){
db.init().then((value) => _fetchEntries());
});
super.initState();
}

void _fetchEntries() async {
_cards = [];

try{
List<Map<String, dynamic>> _results = await db.query(Entry.table);
_data = _results.map((item) => Entry.fromMap(item)).toList();
_data.forEach((element) => _cards.add(EntryCard(entry: element)));
setState(() {});
}catch (e){
print(e.toString());
}
}

void _addEntries(Entry en) async {
db.insert(Entry.table, en);
_fetchEntries();
}
// This method checks is location permission granted and Location
// services enable when press floating action button if return value
// is not null, you Navigator.push works and you go MapPage
Future<Position?> getPermission() async{
bool serviceEnabled;
LocationPermission permission;
serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
return null;
}

permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
return null;
}
}

if (permission == LocationPermission.deniedForever) {
return null;
}
Position? position = await Geolocator.getLastKnownPosition();{
if(position != null){
return position;
}else{
return await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
}
}

}

// Alert dialog opening when getPermission() return null
Future<void> showMyDialog() async {
return showDialog<void>(
context: context,
barrierDismissible: false, // user must tap button!
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Attention'),
content: SingleChildScrollView(
child: ListBody(
children: const <Widget>[
Text('Please Open Your Location'),
],
),
),
actions: <Widget>[
TextButton(
child: const Text('Ok'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
children:[
SizedBox(height: 20,),
SizedBox(child: FloatingActionButton(
onPressed: () async {
Position? pos = await getPermission();
if (pos != null) {
// When pop page in MapPage _addEntries(value) method works and
// saving records to db and showing in HomePage
Navigator.push(
context, MaterialPageRoute(builder: (context) => const MapPage()))
.then((value) => _addEntries(value));
}else{
showMyDialog();
}
} ,
backgroundColor:Theme.of(context).primaryColor,
child: Icon(Icons.add,size: 32,),
),),
SizedBox(height: 20,),
SizedBox(
height: 300,
child: ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemCount: _cards.length,
itemBuilder: (BuildContext context, int index) {
return Card(
elevation: 5,
child: Padding(
padding: EdgeInsets.all(10),
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(5)),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(_cards[index].entry.date.toString(),style: TextStyle(fontSize: 14,color: Theme.of(context).primaryColor,fontWeight: FontWeight.bold),),
Text("Duration : ${_cards[index].entry.duration}",style: TextStyle(fontSize: 14,color: Colors.black,fontWeight: FontWeight.w600)),
],),
SizedBox(height: 10,),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("Distance (Km) : ${(_cards[index].entry.distance! / 1000).toStringAsFixed(2)}",style: TextStyle(fontSize: 14,color: Colors.grey,fontWeight: FontWeight.w600)),
Text("Speed (Km/Hours) : ${_cards[index].entry.speed!.toStringAsFixed(2)}",style: TextStyle(fontSize: 14,color: Colors.grey,fontWeight: FontWeight.w600)),
],)
],
),
),
),
);
}
),
),
]
),
),
);
}
}

Code of entries widget appears in homePage in below

widgets.dart

import 'package:flutter/material.dart';
import "entry.dart";

class EntryCard extends StatelessWidget {
final Entry entry;
EntryCard({required this.entry});

@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.all(10),
child: Container(
padding: EdgeInsets.all(20),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(entry.date!, style: TextStyle(fontSize: 18)),
Text("${(entry.distance! / 1000).toStringAsFixed(2)} km",
style: TextStyle(fontSize: 18)),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(entry.duration!,
style: TextStyle(fontSize: 14)),
Text("${entry.speed!.toStringAsFixed(2)} km/h",
style: TextStyle(fontSize: 14)),
],
)
],
)),
);
}
}

Now we come to the best part of the subject that is MapPage.

MapPage Screen

In map page there is a function as getCurrentPosition that does almost everything. I will explain step by step.

Let me show you before starting explain the function what is our object and variables and what we are doing in initState of Map Page.

final Set<Polyline> polyline = {};
List<LatLng> route = [];
double dist = 0;
late String displayTime;
late int time;
late int lastTime;
double speed = 0;
double avgSpeed = 0;
int speedCounter = 0;
late bool loadingStatus;
late double appendDist;
LatLng sourceLocation = LatLng(38.432199, 27.177221);
Position? currentPosition;
final Completer<GoogleMapController?> controller = Completer();
final StopWatchTimer stopWatchTimer = StopWatchTimer();
var mapStyle;
late StreamSubscription<Position> positionStream;

@override
void initState() {
route.clear();
polyline.clear();
dist = 0;
time = 0;
lastTime = 0;
speed = 0;
avgSpeed = 0;
speedCounter = 0;
appendDist = 0;
stopWatchTimer.onExecute.add(StopWatchExecute.reset);
WidgetsBinding.instance.addPostFrameCallback((_) {
getCurrentPosition();
});
stopWatchTimer.onExecute.add(StopWatchExecute.start);
super.initState();
}

Now we can look the method.First of all, getting current position of user,

currentPosition = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);

then we define location settings for stream of position, distanceFilter : 5 means every 5 meter changes stream will update

locationSettings = const LocationSettings(
accuracy: LocationAccuracy.high, distanceFilter: 5);

then we can listening location changes of user, in below you can see how works adding route, adding polyline and changes according to what distance,speed,time

Geolocator.getPositionStream(locationSettings: locationSettings)
.listen((Position? position) async {
currentPosition = position;

if (route.isNotEmpty) {
appendDist = Geolocator.distanceBetween(route.last.latitude,
route.last.longitude, position!.latitude, position.longitude);
dist = dist + appendDist;
int timeDuration = (time - lastTime);

if (lastTime != null && timeDuration != 0) {
speed = (appendDist / (timeDuration / 100)) * 3.6;
if (speed != 0) {
avgSpeed = avgSpeed + speed;
speedCounter++;
}
}
}
lastTime = time;

if (route.isNotEmpty) {
if (route.last != LatLng(position!.latitude, position.longitude)) {
route.add(LatLng(position.latitude, position.longitude));

polyline.add(Polyline(
polylineId: PolylineId(position.toString()),
visible: true,
points: route,
width: 6,
startCap: Cap.roundCap,
endCap: Cap.roundCap,
color: Colors.blue));
}
} else {
route.add(LatLng(position!.latitude, position.longitude));
}

setState(() {});
});

and full code of MapPage below

map_page.dart

import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:intl/intl.dart';
import 'package:live_location_tracking/entry.dart';
import 'package:stop_watch_timer/stop_watch_timer.dart';

class MapPage extends StatefulWidget {
const MapPage({Key? key}) : super(key: key);

@override
State<MapPage> createState() => _MapPageState();
}

class _MapPageState extends State<MapPage> {
final Set<Polyline> polyline = {};
List<LatLng> route = [];
double dist = 0;
late String displayTime;
late int time;
late int lastTime;
double speed = 0;
double avgSpeed = 0;
int speedCounter = 0;
late bool loadingStatus;
late double appendDist;
LatLng sourceLocation = LatLng(38.432199, 27.177221);
Position? currentPosition;
final Completer<GoogleMapController?> controller = Completer();
final StopWatchTimer stopWatchTimer = StopWatchTimer();
var mapStyle;
late StreamSubscription<Position> positionStream;

@override
void initState() {
route.clear();
polyline.clear();
dist = 0;
time = 0;
lastTime = 0;
speed = 0;
avgSpeed = 0;
speedCounter = 0;
appendDist = 0;
stopWatchTimer.onExecute.add(StopWatchExecute.reset);
stopWatchTimer.clearPresetTime();
WidgetsBinding.instance.addPostFrameCallback((_) {
getCurrentPosition();
});
stopWatchTimer.onExecute.add(StopWatchExecute.start);
super.initState();
}

@override
void dispose() {
super.dispose();
stopWatchTimer.dispose();
}

Future<void> getCurrentPosition() async {
currentPosition = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high);
setState(() {});
if (currentPosition != null) {
late LocationSettings locationSettings;
locationSettings = const LocationSettings(
accuracy: LocationAccuracy.high, distanceFilter: 5);

positionStream =
Geolocator.getPositionStream(locationSettings: locationSettings)
.listen((Position? position) async {
print(position == null
? 'Unknown'
: '${position.latitude.toString()}, ${position.longitude.toString()}');
currentPosition = position;

if (route.isNotEmpty) {
appendDist = Geolocator.distanceBetween(route.last.latitude,
route.last.longitude, position!.latitude, position.longitude);
dist = dist + appendDist;
int timeDuration = (time - lastTime);

if (lastTime != null && timeDuration != 0) {
speed = (appendDist / (timeDuration / 100)) * 3.6;
if (speed != 0) {
avgSpeed = avgSpeed + speed;
speedCounter++;
}
}
}
lastTime = time;

if (route.isNotEmpty) {
if (route.last != LatLng(position!.latitude, position.longitude)) {
route.add(LatLng(position.latitude, position.longitude));

polyline.add(Polyline(
polylineId: PolylineId(position.toString()),
visible: true,
points: route,
width: 6,
startCap: Cap.roundCap,
endCap: Cap.roundCap,
color: Colors.blue));
}
} else {
route.add(LatLng(position!.latitude, position.longitude));
}

setState(() {});
});
setState(() {});
}
}

@override
Widget build(BuildContext context) {
return Scaffold(
body: currentPosition == null
? Center(
child: CircularProgressIndicator(),
)
: Stack(children: [
GoogleMap(
polylines: polyline,
myLocationEnabled: true,
myLocationButtonEnabled: false,
zoomControlsEnabled: false,
initialCameraPosition: CameraPosition(
target: LatLng(currentPosition!.latitude,
currentPosition!.longitude),
zoom: 13.5),
onMapCreated: (mapController) {
mapController.setMapStyle(mapStyle);
if (controller.isCompleted) {
controller.complete(mapController);
setState(() {});
}
setState(() {});
},
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
width: double.infinity,
margin: EdgeInsets.fromLTRB(10, 0, 10, 10),
height: 125,
padding: EdgeInsets.fromLTRB(10, 10, 10, 0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10)),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Column(
children: [
Text(
"Speed (Km/Hours)",
style: Theme.of(context)
.textTheme
.headline3!
.copyWith(
fontSize: 12,
fontWeight: FontWeight.w300),
),
Text(
speed.toStringAsFixed(2),
style: Theme.of(context)
.textTheme
.headline3!
.copyWith(
fontSize: 24,
fontWeight: FontWeight.w300),
)
],
),
Column(
children: [
Text(
"Duration",
style: Theme.of(context)
.textTheme
.headline3!
.copyWith(
fontSize: 12,
fontWeight: FontWeight.w300),
),
StreamBuilder<int>(
stream: stopWatchTimer.rawTime,
initialData: 0,
builder: (context, snap) {
time = snap.data!;
displayTime =
"${StopWatchTimer.getDisplayTimeHours(time)}:${StopWatchTimer.getDisplayTimeMinute(time)}:${StopWatchTimer.getDisplayTimeSecond(time)}";
return Text(
displayTime,
style: Theme.of(context)
.textTheme
.headline3!
.copyWith(
fontSize: 24,
fontWeight: FontWeight.w300),
);
},
)
],
),
Column(
children: [
Text(
"Distance (Km)",
style: Theme.of(context)
.textTheme
.headline3!
.copyWith(
fontSize: 12,
fontWeight: FontWeight.w300),
),
Text(
(dist / 1000).toStringAsFixed(2),
style: Theme.of(context)
.textTheme
.headline3!
.copyWith(
fontSize: 24,
fontWeight: FontWeight.w300),
)
],
)
],
),
const Divider(),
InkWell(
child: Icon(
Icons.stop_circle_outlined,
size: 45,
color: Colors.redAccent,
),
onTap: () async {
Entry en = Entry(
date: DateFormat.yMMMMd()
.format(DateTime.now()),
duration: displayTime,
speed: speedCounter == 0
? 0
: avgSpeed / speedCounter,
distance: dist);
// positionStream.cancel();
Navigator.pop(context, en);
},
),
],
),
))
]));
}
}

Thanks for reading this article

I wish it was helpful for you, for any question you can feel free to disturb poyrazahmetcan@hotmail.com

Read my other articles:

--

--

Ahmet Can Poyraz

Yasar University/Computer Engineering — Flutter Developer