Get Your Location On: A Step-by-Step Guide to Creating a Flutter App with Riverpod and Geolocator with Clean Architecture

Intro:

While I was working on our upcoming app GotInfo. I needed to handle location permissions for my user since we use a google map to show the nearby posts pertinent to the user.

While I could just use the default permission_handler and be done with it. Unfortunately it's not so simple in a production environment as we have to handle a lot of location variables.

Here are the instances that we are going to cover through the article:

  • Initial Permission Request

  • Permission Denied

  • Permission Revoked

  • Location Services Disabled

Initial packages setup

We are going to keep our list of packages to minimum. We just need two packages : geolocator and flutter_riverpod

So let's get the latest versions for both the packages in our project by running the following commands:

flutter pub add flutter_riverpod
flutter pub add geolocator
flutter pub add app_settings

Finally run the following to make sure all the packages are in sync with our framework:

flutter pub get

Setup Permissions:

Geolocator has a very detailed usage guide as to how to setup location permissions. So let's see them:

Android Setup:

Android X:

The geolocator plugin requires the AndroidX version of the Android Support Libraries. This means you need to make sure your Android project supports AndroidX.

So we need to do things to get Android X Support:

  1. Add the following to your android/gradle.properties file:
android.useAndroidX=true
android.enableJetifier=true
  1. Make sure you set the compileSdkVersion in your "android/app/build.gradle" file to 34:
android {
  compileSdkVersion 34

  ...
}

Permissions:

On Android you'll need to add either the ACCESS_COARSE_LOCATION or the ACCESS_FINE_LOCATIONpermission to your Android Manifest.

So let's go to our AndroidManifest.xml file located in android/app/src/main folder and the following permissions as children of <mainfest> tag:

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

You can find my AndroidManifest in this gist.

iOS Setup:

info.Plist File:

On iOS you'll need to add the following entry to your Info.plist file (located under ios/Runner) in order to access the device's location.

Simply open your Info.plist file and add the following (make sure you update the description so it is meaningful in the context of your App):

<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>We use your location data to send you notifications when you check in to certain places to enhance your experience in our app.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>Our app may also use your location in the background to send you timely notifications about important community alerts and updates relevant to your area, even when you're not actively using the app.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Our app uses your location to connect you with your local community, allowing you to discover nearby events, groups, and marketplace listings. For example, enabling location helps us show you the neighborhood happenings and items for sale in your vicinity.</string>

You can find my complete info.plist in the following gist.

Code Setup:

So we have bunch of files where we are going to write the code. Since are using flutter_riverpod. we need to make sure that we add ProviderScope for our MyApp class

So this is our main.dart:

void main() {
  runApp(const ProviderScope(
    child: MyApp(),
  ));
}

Location State:
Initially we'll create a LocationState to get the status of the service. We essentially want to check if the service is enabled and what is the permission status.

You can find the complete code for location state in the following gist.

import 'package:geolocator/geolocator.dart';

class GetLocationState {
  final Position? position;
  final LocationPermission permission;

  GetLocationState({
     this.position,
    required this.permission,
  });

  factory GetLocationState.initial() {
    return GetLocationState(
      permission: LocationPermission.unableToDetermine,
    );
  }

  GetLocationState copyWith({
    Position? position,
    LocationPermission? permission,
  }) {
    return GetLocationState(
      position: position ?? this.position,
      permission: permission ?? this.permission,
    );
  }
}

Location Repository:

Now e create the Location Repository where we can checkPermissions and get CurrentLocation

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:geolocator/geolocator.dart';

final locationRepositoryProvider = Provider<LocationRepository>((ref) => LocationRepository());

class LocationRepository {
  Future<Position?> getCurrentLocation() async {
    try {
      LocationPermission permission = await checkPermissions();
      if (permission == LocationPermission.denied || permission == LocationPermission.deniedForever) {
        return null;
      }
      return await Geolocator.getCurrentPosition();
    } catch (e) {
      print('Error: $e');
      return null;
    }
  }

  Future<LocationPermission> checkPermissions() async {
    LocationPermission permission = await Geolocator.checkPermission();

    if (permission == LocationPermission.denied ||
        permission == LocationPermission.deniedForever) {
      permission = await Geolocator.requestPermission();
    }

    return permission;
  }
}

Finally on the basis of LocationState and locationServiceStreamProvider we can write the LocationNotifier

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:geolocator/geolocator.dart';

// Assuming your LocationState and LocationRepository are in the same feature directory
import '../data/location_repository.dart';
import '../data/location_state.dart';

final locationProvider =
    StateNotifierProvider<LocationNotifier, GetLocationState>((ref) {
  final locationRepository = ref.read(locationRepositoryProvider);
  return LocationNotifier(locationRepository);
});

class LocationNotifier extends StateNotifier<GetLocationState> {
  final LocationRepository _locationRepository;

  LocationNotifier(this._locationRepository)
      : super(GetLocationState.initial());

  Future<void> getLocation(BuildContext context) async {

    final permission = await _locationRepository.checkPermissions();

if (permission == LocationPermission.denied || permission == LocationPermission.deniedForever) {
      _showPermissionDialog(context);
      return;
    }

    final position = await _locationRepository.getCurrentLocation();

    if (position != null) {
      state = state.copyWith(position: position);
    } else {
      // Handle location permission denied (explained below)
    }
  }

  void _showPermissionDialog(BuildContext context) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Location Permission Denied'),
        content: const Text(
            'This app needs location permission to access your location.'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('Cancel'),
          ),
          TextButton(
            onPressed: () async {
              final status = await Geolocator.requestPermission();
              if (status == LocationPermission.whileInUse ||
                  status == LocationPermission.always) {
                getLocation(context); // Retry fetching location if permission granted
              } else {
                // Handle user refusing permission (optional)
              }
              Navigator.pop(context);
            },
            child: const Text('Settings'),
          ),
        ],
      ),
    );
  }
}

And this our final LocationScreen where we get the latitude and longitude:

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:geolocator/geolocator.dart';

// Assuming your LocationState and LocationRepository are in the same feature directory
import '../data/location_repository.dart';
import '../data/location_state.dart';

final locationProvider =
    StateNotifierProvider<LocationNotifier, GetLocationState>((ref) {
  final locationRepository = ref.read(locationRepositoryProvider);
  return LocationNotifier(locationRepository);
});

class LocationNotifier extends StateNotifier<GetLocationState> {
  final LocationRepository _locationRepository;

  LocationNotifier(this._locationRepository)
      : super(GetLocationState.initial());

  Future<void> getLocation(BuildContext context) async {

    final permission = await _locationRepository.checkPermissions();

if (permission == LocationPermission.denied || permission == LocationPermission.deniedForever) {
      _showPermissionDialog(context);
      return;
    }

    final position = await _locationRepository.getCurrentLocation();

    if (position != null) {
      state = state.copyWith(position: position);
    } else {
      // Handle location permission denied (explained below)
    }
  }

  void _showPermissionDialog(BuildContext context) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Location Permission Denied'),
        content: const Text(
            'This app needs location permission to access your location.'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('Cancel'),
          ),
          TextButton(
            onPressed: () async {
              final status = await Geolocator.requestPermission();
              if (status == LocationPermission.whileInUse ||
                  status == LocationPermission.always) {
                getLocation(context); // Retry fetching location if permission granted
              } else {
                // Handle user refusing permission (optional)
              }
              Navigator.pop(context);
            },
            child: const Text('Settings'),
          ),
        ],
      ),
    );
  }
}

Finally you should be able to get the location in your screen. It should also handle location denied permissions too.

Source Code:

You can find the source code for my project here: Github Link

Did you find this article valuable?

Support Harish Kunchala by becoming a sponsor. Any amount is appreciated!