DEV Community

Cover image for A quick guide: obtaining current location on Google Maps using Riverpod in Flutter
uuta
uuta

Posted on • Updated on

A quick guide: obtaining current location on Google Maps using Riverpod in Flutter

Goal: Get the current location and set a marker

Google Maps on Flutter

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.

  1. Show Google Map by google_maps_flutter
  2. Obtain the current location
  3. 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>
Enter fullscreen mode Exit fullscreen mode

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)
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
    }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)));
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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 []);
Enter fullscreen mode Exit fullscreen mode

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)));
Enter fullscreen mode Exit fullscreen mode

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));
  }
}
Enter fullscreen mode Exit fullscreen mode

LocationController defines three functions to call from main.dart.

void onMapCreated(GoogleMapController controller) {}
Future<void> getCurrentLocation() async {}
Future<void> getNewLocation() async {}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Run the following command with build_runner.

$ flutter pub run build_runner build --delete-conflicting-outputs
Enter fullscreen mode Exit fullscreen mode

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;

    serviceEnabled = await Geolocator.isLocationServiceEnabled();
    if (!serviceEnabled) {
      return Future.error('Location services are disabled.');
    }

    permission = await Geolocator.checkPermission();
    if (permission == LocationPermission.denied) {
      permission = await Geolocator.requestPermission();
      if (permission == LocationPermission.denied) {
        return Future.error('Location permissions are denied');
      }
    }

    if (permission == LocationPermission.deniedForever) {
      return Future.error(
          'Location permissions are permanently denied, we cannot request permissions.');
    }

    return await Geolocator.getCurrentPosition(
        desiredAccuracy: LocationAccuracy.high);
  }
}
Enter fullscreen mode Exit fullscreen mode

References

Top comments (0)