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, and made some global widgets like the app bar, bottom navigation bar, and drawer. We've also already made a UI and wrote the backend for registration and sign-in methods to use with the Firebase project.
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.
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.
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
For the Location package, to be able to ask for the user's permission we need to add some settings.
For android "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">
For ios, in "ios/Runner/Info.plist", add the following settings at 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 -->
For android on "android/gradle.properties" add these settings if it's already not there.
android.useAndroidX=true
android.enableJetifier=true
On "android/app/build.gradle" change compiled SDK version to 31 if you haven't already.
android {
compileSdkVersion 31
...
}
As for the permission API, we've already added them in the AndroidManifest.XML file.
We've already added permissions to the info.plist already. Unfortunately, I am using VS Code and could not find the POD file on the ios directory.
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.
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 ...
In the " android/app/build.gradle" file change the minimum SDK version to 21 if you haven't already.
...
defaultConfig {
...
minSdkVersion 21
...
In the 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")
...
}
DO NOT SHARE YOUR API KEY, ADD ANDROID MANIFEST AND APPDELEGATE FILE TO GITIGNORE BEFORE PUSHING
Check out the read-me-in packages pages if anything doesn't work.
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.
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
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
enum AppPermissions {
granted,
denied,
restricted,
permanentlyDenied,
}
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;
}
}
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;
Let's explain codes, shall we?
Our getLocation method for AppPermissionProvider, which we'll create later, will call for HTTPS callable inside of it. So, let's head over to 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;
});
In the addUserLocation callable function above we are:
With our callable ready, let's now create a Future method that'll be used by the app. In app_permission_provider 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();
}
}
What we did here was:
Now, that 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 file.
app
...
providers: [
...
ChangeNotifierProvider(create: (context) => AppPermissionProvider()),
...
],
Our operation to get the location of the user is an asynchronous one that returns a Future. The 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 the home 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")),
);
}
})),
),
...
In the home Widget after importing AppPermissionProvider class we returned FutureBuilder as the child of the Safe Area widget. In there we:
app_permission_provider
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();
}
}
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;
});
home
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")),
);
}
})),
),
);
}
}
This blog was dedicated to permission handling and location access. Tasks accomplished in this blog are as follows:
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 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.
Also published here.