A quick guide: obtaining current location on Google Maps using Riverpod in Flutter
uuta
Posted on December 13, 2021
Goal: Get the current location and set a marker
Hi everyone! This article will look at how we get the current location of the user and set a marker as being a specific location on Flutter. This app has several features as shown below.
- Show Google Map by google_maps_flutter
- Obtain the current location
- Set the marker and shift for it after pushing the bottom right button
Get API key
At first, let's obtain your API key for Google Maps.
Fix AndroidManifest.xml
- android/app/src/main/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.googlemap.testgooglemap">
// Add this
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<application
android:label="testgooglemap"
android:icon="@mipmap/ic_launcher">
// Add this
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="<YOUR API KEY>" />
...
</application>
</manifest>
Modify AppDelegate.swift
- ios/Runner/AppDelegate.swift
import UIKit
import Flutter
import GoogleMaps
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GMSServices.provideAPIKey("<YOUR API KEY>")
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
Add a key in Info.plist
To show a dialog to allow the user gives the current location to app, put a pair of key and description into Info.plist
.
- ios/Runner/Info.plist
<dict>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Your location is required for this app</string>
...
</dict>
Upgrade minSdkVersion in build.gradle
You have to make minSdkVersion going up to 20 from 16 for working out well this app on google_maps_flutter package. In my case, I also had to fix compileSdkVersion to 31.
google_maps_flutter | Flutter Package
android {
compileSdkVersion 31 // up to 31 from 30
...
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.googlemap.testgooglemap"
minSdkVersion 20 // up to 20 from 16
targetSdkVersion 30
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
pubspec.yaml
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
google_maps_flutter: ^2.1.1
geolocator: ^8.0.0
riverpod: ^1.0.0
flutter_hooks: ^0.18.0
hooks_riverpod: ^1.0.0
http: ^0.13.4
freezed_annotation: ^1.0.0
json_annotation: ^4.3.0
flutter_secure_storage: ^5.0.1
geocoding: ^2.0.1
dev_dependencies:
flutter_test:
sdk: flutter
freezed: ^1.0.0
build_runner: ^2.1.5
json_serializable: ^6.0.1
main.dart
Let's modify your code in main.dart. Some errors would be shown on your text editor when pasting it, but don't have to be careful about that for now. We'll fix it up by adding other code.
- lib/main.dart
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import '/models/controllers/location/location_controller.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'dart:async';
void main() {
runApp(ProviderScope(
child: MyApp(),
));
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const Homepage(),
);
}
}
class Homepage extends HookConsumerWidget {
const Homepage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final locationState = ref.watch(locationNotifierProvider);
final locationNotifier = ref.watch(locationNotifierProvider.notifier);
useEffect(() {
Future.microtask(() async {
ref.watch(locationNotifierProvider.notifier).getCurrentLocation();
});
return;
}, const []);
return Scaffold(
body: locationState.isBusy
? const Center(child: CircularProgressIndicator())
: GoogleMap(
mapType: MapType.normal,
myLocationButtonEnabled: true,
myLocationEnabled: true,
zoomControlsEnabled: false,
initialCameraPosition: CameraPosition(
target: locationState.currentLocation,
zoom: 14.4746,
),
markers: locationState.markers,
onMapCreated: locationNotifier.onMapCreated,
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
locationNotifier.getNewLocation();
},
child: const Icon(Icons.location_searching)));
}
}
Let's take a look at that. We can handle the state on Flutter by Riverpod. In this sample app, locationState
for reading state and locationNotifier
for changing state are defined.
@override
Widget build(BuildContext context, WidgetRef ref) {
final locationState = ref.watch(locationNotifierProvider);
final locationNotifier = ref.watch(locationNotifierProvider.notifier);
useEffect
is useful to initialize something on every build. In here's sample, we call the getCurrentLocatoin()
function defined in location_controller.dart for state management.
useEffect(() {
Future.microtask(() async {
locationNotifier.getCurrentLocation();
});
return;
}, const []);
When user presses the bottom right button, FloatingActionButton()
call getNewLocation()
function that shifts the position of camera and sets a pin.
floatingActionButton: FloatingActionButton(
onPressed: () async {
locationNotifier.getNewLocation();
},
child: const Icon(Icons.location_searching)));
LocationController
As you can see, LocationController extends StateNotifier class.
- lib/models/controllers/location/location_controller.dart
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:riverpod/riverpod.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:flutter/material.dart';
import '/models/repositories/location/location_repository.dart';
import '/models/controllers/location/location_state.dart';
import 'dart:async';
final locationNotifierProvider =
StateNotifierProvider<LocationController, LocationState>(
(ref) => LocationController(),
);
class LocationController extends StateNotifier<LocationState> {
LocationController() : super(const LocationState());
final repository = LocationRepository();
final Completer<GoogleMapController> _mapController = Completer();
void onMapCreated(GoogleMapController controller) {
_mapController.complete(controller);
}
Future<void> getCurrentLocation() async {
state = state.copyWith(isBusy: true);
try {
final data = await repository.getCurrentPosition();
state = state.copyWith(
isBusy: false,
currentLocation: LatLng(data.latitude, data.longitude));
} on Exception catch (e, s) {
debugPrint('login error: $e - stack: $s');
state = state.copyWith(isBusy: false, errorMessage: e.toString());
}
}
Future<void> getNewLocation() async {
await _setNewLocation();
await _setMaker();
}
Future<void> _setNewLocation() async {
state = state.copyWith(newLocation: const LatLng(35.658034, 139.701636));
}
Future<void> _setMaker() async {
// Set markers
final Set<Marker> _markers = {};
_markers.add(Marker(
markerId: MarkerId(state.newLocation.toString()),
position: state.newLocation,
infoWindow:
const InfoWindow(title: 'Remember Here', snippet: 'good place'),
icon: BitmapDescriptor.defaultMarker));
state = state.copyWith(markers: _markers);
// Shift camera position
CameraPosition _kLake =
CameraPosition(target: state.newLocation, zoom: 14.4746);
final GoogleMapController controller = await _mapController.future;
controller.animateCamera(CameraUpdate.newCameraPosition(_kLake));
}
}
LocationController defines three functions to call from main.dart.
void onMapCreated(GoogleMapController controller) {}
Future<void> getCurrentLocation() async {}
Future<void> getNewLocation() async {}
Freezed for controller
This sample app uses Freezed package, making the data immutable easily. This is used for LocationController.
- lib/models/controllers/location/location_state.dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
part 'location_state.freezed.dart';
@freezed
class LocationState with _$LocationState {
const factory LocationState({
@Default(false) bool isBusy,
@Default(LatLng(35.658034, 139.701636)) LatLng currentLocation,
@Default(LatLng(35.658034, 139.701636)) LatLng newLocation,
@Default({}) Set<Marker> markers,
String? errorMessage,
}) = _LocationState;
}
Run the following command with build_runner.
$ flutter pub run build_runner build --delete-conflicting-outputs
Repository class for the error handling
We'll also set up getCurrentPosition()
in LocationRepository class, the function to handle errors on the permission regarding the location.
- lib/models/repositories/location/location_repository.dart
import 'package:geolocator/geolocator.dart';
class LocationRepository {
Future<Position> getCurrentPosition() async {
bool serviceEnabled;
LocationPermission permission;
<span class="n">serviceEnabled</span> <span class="o">=</span> <span class="k">await</span> <span class="n">Geolocator</span><span class="o">.</span><span class="na">isLocationServiceEnabled</span><span class="p">();</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">serviceEnabled</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="n">Future</span><span class="o">.</span><span class="na">error</span><span class="p">(</span><span class="s">'Location services are disabled.'</span><span class="p">);</span>
<span class="p">}</span>
<span class="n">permission</span> <span class="o">=</span> <span class="k">await</span> <span class="n">Geolocator</span><span class="o">.</span><span class="na">checkPermission</span><span class="p">();</span>
<span class="k">if</span> <span class="p">(</span><span class="n">permission</span> <span class="o">==</span> <span class="n">LocationPermission</span><span class="o">.</span><span class="na">denied</span><span class="p">)</span> <span class="p">{</span>
<span class="n">permission</span> <span class="o">=</span> <span class="k">await</span> <span class="n">Geolocator</span><span class="o">.</span><span class="na">requestPermission</span><span class="p">();</span>
<span class="k">if</span> <span class="p">(</span><span class="n">permission</span> <span class="o">==</span> <span class="n">LocationPermission</span><span class="o">.</span><span class="na">denied</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="n">Future</span><span class="o">.</span><span class="na">error</span><span class="p">(</span><span class="s">'Location permissions are denied'</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="n">permission</span> <span class="o">==</span> <span class="n">LocationPermission</span><span class="o">.</span><span class="na">deniedForever</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="n">Future</span><span class="o">.</span><span class="na">error</span><span class="p">(</span>
<span class="s">'Location permissions are permanently denied, we cannot request permissions.'</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">return</span> <span class="k">await</span> <span class="n">Geolocator</span><span class="o">.</span><span class="na">getCurrentPosition</span><span class="p">(</span>
<span class="nl">desiredAccuracy:</span> <span class="n">LocationAccuracy</span><span class="o">.</span><span class="na">high</span><span class="p">);</span>
}
}
References
Posted on December 13, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
December 13, 2021