Permission Handler and Location Access In Flutter

kcl

Nibesh Khadka

Posted on May 27, 2022

Permission Handler and Location Access In Flutter

Intro

Hello and Welcome to the 9th installment of the series Flutter App Development Tutorial. Before this, we have already made a splash screen, wrote a theme, made a custom widgets like app bar, bottom navigation bar, and drawer. We've also already made an authentication screen, set up a connection with firebase cloud and emulators, and authenticated users on firebase projects.

As a part of the user-screen flow, we are now at the stage where we need to access the user location. So, we'll ask for the user's location as soon as the user authenticates and reaches the homepage. We'll also Firebase Cloud Functions to save the user's location on the 'users/userId' document on Firebase Firestore. Find the source code to start this section from here.

Packages

In previous endeavors, we've already installed and set up Firebase packages. For now, we'll need three more packages: Location, Google Maps Flutter and Permission Handler. Follow the instruction on the packages home page or add just use the version I am using below.

The location package itself is enough to get both permission and location. However, permission_handler can get permission for other tasks like camera, local storage, and so on. Hence, we'll use both, one to get permission and another for location. For now, we'll only use the google maps package to use Latitude and Longitude data types.

Installation

On the command Terminal:

# Install location
flutter pub add location

# Install Permission Handler
flutter pub add permission_handler

# Install Google Maps Flutter
flutter pub add google_maps_flutter
Enter fullscreen mode Exit fullscreen mode

Setting Up Packages

Location Package

For the Location package, to be able to ask for the user's permission we need to add some settings.

Android

For android at "android/app/src/main/AndroidManifest.xml" before the application tag.

<!--
    Internet permissions do not affect the `permission_handler` plugin but are required if your app needs access to
    the internet.
    -->
    <uses-permission android:name="android.permission.INTERNET" />
   <!-- Permissions options for the `location` group -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />


<!-- Before application tag-->
 <application android:label="astha" android:name="${applicationName}" android:icon="@mipmap/launcher_icon">

Enter fullscreen mode Exit fullscreen mode
IOS

For ios, in "ios/Runner/Info.plist", add the following settings in the end of dict tag.

    <!-- Permissions list starts here -->
        <!-- Permission while running on backgroud -->
        <key>UIBackgroundModes</key>
        <string>location</string>
        <!-- Permission options for the `location` group -->
        <key>NSLocationWhenInUseUsageDescription</key>
        <string>Need location when in use</string>
        <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
        <string>Always and when in use!</string>
        <key>NSLocationUsageDescription</key>
        <string>Older devices need location.</string>
        <key>NSLocationAlwaysUsageDescription</key>
        <string>Can I have location always?</string>
        <!-- Permission options for the `appTrackingTransparency` -->
        <key>NSUserTrackingUsageDescription</key>
        <string>appTrackingTransparency</string>
        <!-- Permissions lists ends here -->
Enter fullscreen mode Exit fullscreen mode

Permission Handler

Android

For android on "android/gradle.properties" add these settings if it's already not there.

android.useAndroidX=true
android.enableJetifier=true
Enter fullscreen mode Exit fullscreen mode

On "android/app/build.gradle" change compiled SDK version to 31 if you haven't already.

android {
    compileSdkVersion 31
...
}
Enter fullscreen mode Exit fullscreen mode

As for the permission API, we've already added them in the AndroidManifest.XML file.

IOS

We've already added permissions on info.plist already. Unfortunately, I am using VS Code and could not find the POD file on the ios directory.

Google Maps Flutter

To use google maps you'll need an API key for it. Get it from Google Maps Platform. Follow the instructions from the package's readme on how to create an API key. Create two credentials each for android and ios. After that, we'll have to add it to both android and ios apps.

Android

Go to the AndroidManifest.xml file again.

<manifest ...
  <application ...
    <meta-data android:name="com.google.android.geo.API_KEY"
               android:value="YOUR KEY HERE"/>
<activity ...
Enter fullscreen mode Exit fullscreen mode

In the " android/app/build.gradle" file change the minimum SDK version to 21 if you haven't already.

...
 defaultConfig {
     ...
        minSdkVersion 21
...
Enter fullscreen mode Exit fullscreen mode
IOS

In ios/Runner/AppDelegate.swift file add the api key for ios.

// import gmap
import GoogleMaps

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  ...
-> Bool {
// Add api key don't remove anything else
    GMSServices.provideAPIKey("API KEY Here")
    ...
}

Enter fullscreen mode Exit fullscreen mode

DO NOT SHARE YOUR API KEY, ADD ANDROID MANIFEST AND APPDELEGATE FILE TO GITIGNORE BEFORE PUSHING

Reminder: Check out the read me in packages pages if anything doesn't work.

Acess Location of User

Let's go over the series of events that'll occur in the tiny moment user goes from the authentication screen to the home screen.

  1. User will be asked for permission to grant location access.
  2. If permission is positive, the user's location will be accessed and given to an HTTPS callable cloud function.
  3. Callable will then get the current user's id. With that ID callable will read the correct document from the "users" collections.
  4. It'll check if the location field is either empty or not.
  5. If it's empty it'll write a new document merge to add location.
  6. If it's not empty, then the function will just not write anything and return.

Since as the app grows the number of app permissions needed can also keep on increasing and permission is also a global factor, let's create a provider class that'll handle permissions in "globals/providers" folders.

On your terminal

# Make folder
mkdir lib/globals/providers/permissions
// make file
touch lib/globals/providers/permissions/app_permission_provider.dart

Enter fullscreen mode Exit fullscreen mode

App's Permission status is of four types: which is either granted, denied, restricted or permanently denied. Let's first make an enum to switch these values in our app.

app_permission_provider.dart

enum AppPermissions {
  granted,
  denied,
  restricted,
  permanentlyDenied,
}
Enter fullscreen mode Exit fullscreen mode

Let's create a provider class right below the enum. As mentioned earlier, we'll use permission_handler to get permission and the location package to get the location.

import 'package:cloud_functions/cloud_functions.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/foundation.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:location/location.dart'
    as location_package; // to avoid confusion with google_maps_flutter package


class AppPermissionProvider with ChangeNotifier {
  // Start with default permission status i.e denied
// #1
  PermissionStatus _locationStatus = PermissionStatus.denied;

  // Getter
// #2
  get locationStatus => _locationStatus;

// # 3
  Future<PermissionStatus> getLocationStatus() async {
    // Request for permission
    // #4
     final status = await Permission.location.request();
    // change the location status
   // #5
    _locationStatus = status;
    print(_locationStatus);
    // notify listeners
    notifyListeners();
    return status;

  }
}



Enter fullscreen mode Exit fullscreen mode
  1. We start with default permission which is denied.
  2. A getter of Location status.
  3. A method that returns a future of the type Permission Status. We'll need it later on.
  4. A method from Permission Handler(request) that asks for the user's permission.
  5. Assign new status and then notify listeners.

Now, let's move to the next step of the mission, which is actually to fetch the location and save it on Firestore. We're going to add some new variables and instances that'll help us achieve it. Add the following code before getLocationStatus method.

 // Instantiate FIrebase functions
// #1
  FirebaseFunctions functions = FirebaseFunctions.instance;
  // Create a LatLng type that'll be user location
 // # 2
  LatLng? _locationCenter;
  // Initiate location from location package
 // # 3
  final location_package.Location _location = location_package.Location();
 // # 4
  location_package.LocationData? _locationData;

  // Getter
 // # 5
  get location => _location;
  get locationStatus => _locationStatus;
  get locationCenter => _locationCenter as LatLng;

Enter fullscreen mode Exit fullscreen mode

Let's explain codes, shall we?

  1. Firebase functions instance is needed because after this we'll create a https callable function to handle the location submission.
  2. Location of the user that'll be returned by HTTPS callable function.
  3. Instantiate location package.
  4. Location to be given by Location package.
  5. The getters for fetching private values from this class.

Firebase Function: HTTPS Callable

Our getLocation method for AppPermissionProvider, which we'll create later, will call for HTTPS callable inside of it. So, let's head over to the index.js to create the onCall method from the firebase function.

index.js

// Create a function named addUserLocation 
exports.addUserLocation = functions.runWith({ 
    timeoutSeconds: 60,  // #1
    memory: "256MB" //#1
}).https.onCall(async (data, context) => {

    try {
       // Fetch correct user document with user id.
       // #2
        let snapshot = await db.collection('users').doc((context.auth.uid)).get();
       // functions.logger.log(snapshot['_fieldsProto']['userLocation']["valueType"] === "nullValue");
        // Get Location Value Type
        // #3
        let locationValueType = snapshot['_fieldsProto']['userLocation']["valueType"];
        // Check if field value for location is null
       // # 4
        if (locationValueType == 'nullValue') {
    // # 5
            await db.collection('users').doc((context.auth.uid)).set({ 'userLocation': data.userLocation }, { merge: true });

            functions.logger.log(`User location added ${data.userLocation}`);
        }
        else {
    // # 6
            functions.logger.log(`User location not changed`);

        }

    }
    catch (e) {
    // # 7
        functions.logger.log(e);
        throw new functions.https.HttpsError('internal', e);
    }
    // #7
    return data.userLocation;
});
Enter fullscreen mode Exit fullscreen mode

In the addUserLocation callable function above we are:

  1. Provide memory allocation for functions with runWith() method.
  2. Get the user document based on the user id provided by EventContext.
  3. Get the value type of the Location field. If you remember we saved the filed userLocation as null during the registration process in our auth_state_provider.dart file. This long snapshot['_fieldsProto']['userLocation']["valueType"] is something I got from experimenting and printing values. That's why is best to use emulators.
  4. If the locationValueType is null then means that the user location has never been saved before. Hence, we'll proceed to write a new document.
  5. Update the user document with the userLocation from the data property of the onCall method. It is the same location that'll be passed from the provider class to this function. Yes, the same one was fetched by the location package.
  6. If the locationValueType is not null then, we won't write a new document.
  7. Return the user location. It's important to end the callables with a return, if not function might end up running longer resulting in memory consumption that can cause extra bills from Firebase among other things.

Using HTTPS Callable with Location Package in Flutter

With our callable ready, let's now create a Future method that'll be used by the app. In app_permission_provider.dart file after the getLocationStatus method create getLocation method.

Future<void> getLocation() async {
    // Call Location status function here
    // #1
    final status = await getLocationStatus();
    // if permission is granted or limited call function
    // #2
    if (status == PermissionStatus.granted ||
        status == PermissionStatus.limited) {
      try {        
        // assign location data that's returned by Location package
        // #3
        _locationData = await _location.getLocation();
        // Check for null values
       // # 4
        final lat = _locationData != null
            ? _locationData!.latitude as double
            : "Not available";
        final lon = _locationData != null
            ? _locationData!.longitude as double
            : "Not available";

        // Instantiate a callable function
       // # 5
        HttpsCallable addUserLocation =
            functions.httpsCallable('addUserLocation');

        // finally call the callable function with user location
         // #6
        final response = await addUserLocation.call(
          <String, dynamic>{
            'userLocation': {
              'lat': lat,
              'lon': lon,
            }
          },
        );
        // get the response from callable function
        // # 7
        _locationCenter = LatLng(response.data['lat'], response.data['lon']);
      } catch (e) {
        // incase of error location witll be null
          // #8
        _locationCenter = null;
        rethrow;
      }
    }
// Notify listeners
    notifyListeners();
  }
}
Enter fullscreen mode Exit fullscreen mode

What we did here was:

  1. Ask for the user's permission to access the location.
  2. If the permission is Granted/Limited i.e always allow/allow while using the app, then we'll try and access the location.
  3. User Location packages getLocation method to access location data. It'll return a LatLng object type.
  4. Check if the data returned is null or not, if so then handle it appropriately.
  5. (5&6)Inside the try block, we instantiate HTTPS callable function as described by the FlutterFire package. Our callable function takes the parameter "userLocation" as a dictionary with lat and lon as keys. After this function is called in the background, it then returns a LatLng object, which can be accessed from the data object of response.
  6. In case of error the user location is determined null.

Now, the user location is updated the corresponding widgets listening to the method will be notified. But for widgets to access the Provider, we'll need to add the provider in the list of MultiProvider in our app.dart file.

...
 providers: [
        ...
        ChangeNotifierProvider(create: (context) => AppPermissionProvider()),
        ...
      ],
Enter fullscreen mode Exit fullscreen mode

FutureBuilder To The Rescue

Our operation to get the location of the user is an asynchronous one that returns a Future. Future can take time to return the result, hence normal widget won't work. FutureBuilder class from flutter is meant for this task.

We'll call the getLocation method from the Home widget in home.dart file as the future property of FutureBuilder class. While waiting for the location to be saved we can just display a progress indicator.

// Import the provider Package
import 'package:temple/globals/providers/permissions/app_permission_provider.dart';

// Inside Scaffold body 
...
body: SafeArea(
        child: FutureBuilder(
            // Call getLocation function as future
            // its very very important to set listen to false
           // #1
            future: Provider.of<AppPermissionProvider>(context, listen: false)
                .getLocation(),
            // don't need context in builder for now
            builder: ((_, snapshot) {
              // if snapshot connectinState is none or waiting
             // # 2
              if (snapshot.connectionState == ConnectionState.waiting ||
                  snapshot.connectionState == ConnectionState.none) {
                return const Center(child: CircularProgressIndicator());
              } else {
                // if snapshot connectinState is active
                // # 3
                if (snapshot.connectionState == ConnectionState.active) {
                  return const Center(
                    child: Text("Loading..."),
                  );
                }
                // if snapshot connectinState is done
              // #4
                return const Center(
                  child: Directionality(
                      textDirection: TextDirection.ltr,
                      child: Text("This Is home")),
                );
              }
            })),
      ),
...
Enter fullscreen mode Exit fullscreen mode

In the home Widget after importing AppPermissionProvider class we returned FutureBuilder as the child of the Safe Area widget. In there we:

  1. User getLocation of AppPermissionProvider as future. It's very important to remember to set listen to false. Otherwise, the build will keep on reloading and functions will get executed again and again.

  2. We return CircularProgressIndicator while waiting for the result to be finished in the background. For now, it seems that there's no point in this because we're not using the location of the user in our app. So, why the progress indicator? It's for later, where we'll again use this moment to fetch another data from firebase which will also be asynchronous.

  3. When the future is active, we display text that says loading.

  4. After the future is done we load or simple home page.

Final Code

app_permission_provider.dart

import 'package:cloud_functions/cloud_functions.dart';
import 'package:flutter/foundation.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:location/location.dart'
    as location_package; // to avoid confusion with google_maps_flutter package

enum AppPermissions {
  granted,
  denied,
  restricted,
  permanentlyDenied,
}

class AppPermissionProvider with ChangeNotifier {
  // Start with default permission status i.e denied
  PermissionStatus _locationStatus = PermissionStatus.denied;

  // Instantiate FIrebase functions
  FirebaseFunctions functions = FirebaseFunctions.instance;

  // Create a LatLng type that'll be user location
  LatLng? _locationCenter;
  // Initiate location from location package
  final location_package.Location _location = location_package.Location();
  location_package.LocationData? _locationData;

  // Getter
  get location => _location;
  get locationStatus => _locationStatus;
  get locationCenter => _locationCenter as LatLng;

  Future<PermissionStatus> getLocationStatus() async {
    // Request for permission
    final status = await Permission.location.request();
    // change the location status
    _locationStatus = status;
    // notiy listeners
    notifyListeners();
    print(_locationStatus);
    return status;
  }

  Future<void> getLocation() async {
    // Call Location status function here
    final status = await getLocationStatus();
    print("I am insdie get location");
    // if permission is granted or limited call function
    if (status == PermissionStatus.granted ||
        status == PermissionStatus.limited) {
      try {
        // assign location data that's returned by Location package
        _locationData = await _location.getLocation();
        // Check for null values
        final lat = _locationData != null
            ? _locationData!.latitude as double
            : "Not available";
        final lon = _locationData != null
            ? _locationData!.longitude as double
            : "Not available";

        // Instantiate a callable function
        HttpsCallable addUserLocation =
            functions.httpsCallable('addUserLocation');

        // finally call the callable function with user location
        final response = await addUserLocation.call(
          <String, dynamic>{
            'userLocation': {
              'lat': lat,
              'lon': lon,
            }
          },
        );
        // get the response from callable function
        _locationCenter = LatLng(response.data['lat'], response.data['lon']);
      } catch (e) {
        // incase of error location witll be null
        _locationCenter = null;
        rethrow;
      }
    }
// Notify listeners
    notifyListeners();
  }
}


Enter fullscreen mode Exit fullscreen mode

index.js

// Import modiules
const functions = require("firebase-functions"),
    admin = require('firebase-admin');

// always initialize admin 
admin.initializeApp();

// create a const to represent firestore
const db = admin.firestore();


// Create a new background trigger function 
exports.addTimeStampToUser = functions.runWith({
    timeoutSeconds: 240,  // Give timeout 
    memory: "512MB" // memory allotment 
}).firestore.document('users/{userId}').onCreate(async (_, context) => {
    // Get current timestamp from server
    let curTimeStamp = admin.firestore.Timestamp.now();
    // Print current timestamp on server
    functions.logger.log(`curTimeStamp ${curTimeStamp.seconds}`);

    try {
        // add the new value to new users document i
        await db.collection('users').doc(context.params.userId).set({ 'registeredAt': curTimeStamp, 'favTempleList': [], 'favShopsList': [], 'favEvents': [] }, { merge: true });
        // if its done print in logger
        functions.logger.log(`The current timestamp added to users collection:  ${curTimeStamp.seconds}`);
        // always return something to end the function execution
        return { 'status': 200 };
    } catch (e) {
        // Print error incase of errors
        functions.logger.log(`Something went wrong could not add timestamp to users collectoin ${curTimeStamp.seconds}`);
        // return status 400 for error
        return { 'status': 400 };
    }
});

// Create a function named addUserLocation 
exports.addUserLocation = functions.runWith({ 
    timeoutSeconds: 60,  
    memory: "256MB"
}).https.onCall(async (data, context) => {

    try {
       // Fetch correct user document with user id.
        let snapshot = await db.collection('users').doc((context.auth.uid)).get();
        // Check if field value for location is null
        // functions.logger.log(snapshot['_fieldsProto']['userLocation']["valueType"] === "nullValue");
        let locationValueType = snapshot['_fieldsProto']['userLocation']["valueType"];
        if (locationValueType == 'nullValue') {
            await db.collection('users').doc((context.auth.uid)).set({ 'userLocation': data.userLocation }, { merge: true });
            functions.logger.log(`User location added    ${data.userLocation}`);
                return data.userLocation;

        }
        else {
            functions.logger.log(`User location not changed`);

        }

    }
    catch (e) {
        functions.logger.log(e);
        throw new functions.https.HttpsError('internal', e);
    }
    return data.userLocation;

});
Enter fullscreen mode Exit fullscreen mode

home.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// Custom
import 'package:temple/globals/providers/permissions/app_permission_provider.dart';
import 'package:temple/globals/widgets/app_bar/app_bar.dart';
import 'package:temple/globals/settings/router/utils/router_utils.dart';
import 'package:temple/globals/widgets/bottom_nav_bar/bottom_nav_bar.dart';
import 'package:temple/globals/widgets/user_drawer/user_drawer.dart';

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

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

class _HomeState extends State<Home> {
  // create a global key for scafoldstate
  final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // Provide key to scaffold
      key: _scaffoldKey,
      // Changed to custom appbar
      appBar: CustomAppBar(
        title: APP_PAGE.home.routePageTitle,
        // pass the scaffold key to custom app bar
        // #3
        scaffoldKey: _scaffoldKey,
      ),
      // Pass our drawer to drawer property
      // if you want to slide right to left use
      endDrawer: const UserDrawer(),
      bottomNavigationBar: const CustomBottomNavBar(
        navItemIndex: 0,
      ),
      primary: true,
      body: SafeArea(
        child: FutureBuilder(
            // Call getLocation function as future
            // its very very important to set listen to false
            future: Provider.of<AppPermissionProvider>(context, listen: false)
                .getLocation(),
            // don't need context in builder for now
            builder: ((_, snapshot) {
              // if snapshot connectinState is none or waiting
              if (snapshot.connectionState == ConnectionState.waiting ||
                  snapshot.connectionState == ConnectionState.none) {
                return const Center(child: CircularProgressIndicator());
              } else {
                // if snapshot connectinState is active

                if (snapshot.connectionState == ConnectionState.active) {
                  return const Center(
                    child: Text("Loading..."),
                  );
                }
                // if snapshot connectinState is done
                return const Center(
                  child: Directionality(
                      textDirection: TextDirection.ltr,
                      child: Text("This Is home")),
                );
              }
            })),
      ),
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

Khadka's Coding Lounge

Summary

This blog was dedicated to permission handling and location access. Tasks accomplished in this blog are as follows:

  1. Installed three packages: Location, Permission Handler, and Google Maps Flutter.
  2. Spend a short time updating the settings required to use these packages.
  3. Created a provider class that'll ask for the user's permission to access the location.
  4. Same class also has a method that will access location and call the HTTPS callable function.
  5. Created HTTPS function which will update the user's location on Firebase Firestore.
  6. Implemented provider class with the help of FutureBuilder in our app.

Show Support

Alright, this is it for this time. This series, is still not over, on the next upload we'll dive deeper with Google Places API, Firebase Firestore, and Firebase Cloud Functions.

So, please do like, comment, and share the article with your friends. Thank you for your time and for those who are subscribing to the blog's newsletter, we appreciate it. Keep on supporting us. This is Nibesh from Khadka's Coding Lounge, a freelancing agency that makes websites and mobile applications.

Like and Subscribe

💖 💪 🙅 🚩
kcl
Nibesh Khadka

Posted on May 27, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related