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
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">
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 -->
Permission Handler
Android
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.
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 ...
In the " android/app/build.gradle" file change the minimum SDK version to 21 if you haven't already.
...
defaultConfig {
...
minSdkVersion 21
...
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")
...
}
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.
- User will be asked for permission to grant location access.
- If permission is positive, the user's location will be accessed and given to an HTTPS callable cloud function.
- Callable will then get the current user's id. With that ID callable will read the correct document from the "users" collections.
- It'll check if the location field is either empty or not.
- If it's empty it'll write a new document merge to add location.
- 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
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,
}
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;
}
}
- We start with default permission which is denied.
- A getter of Location status.
- A method that returns a future of the type Permission Status. We'll need it later on.
- A method from Permission Handler(request) that asks for the user's permission.
- 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;
Let's explain codes, shall we?
- Firebase functions instance is needed because after this we'll create a https callable function to handle the location submission.
- Location of the user that'll be returned by HTTPS callable function.
- Instantiate location package.
- Location to be given by Location package.
- 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;
});
In the addUserLocation callable function above we are:
- Provide memory allocation for functions with runWith() method.
- Get the user document based on the user id provided by EventContext.
- 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.
- 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.
- 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.
- If the locationValueType is not null then, we won't write a new document.
- 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();
}
}
What we did here was:
- Ask for the user's permission to access the location.
- If the permission is Granted/Limited i.e always allow/allow while using the app, then we'll try and access the location.
- User Location packages getLocation method to access location data. It'll return a LatLng object type.
- Check if the data returned is null or not, if so then handle it appropriately.
- (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.
- 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()),
...
],
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")),
);
}
})),
),
...
In the home Widget after importing AppPermissionProvider class we returned FutureBuilder as the child of the Safe Area widget. In there we:
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.
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.
When the future is active, we display text that says loading.
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();
}
}
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.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")),
);
}
})),
),
);
}
}
Summary
This blog was dedicated to permission handling and location access. Tasks accomplished in this blog are as follows:
- Installed three packages: Location, Permission Handler, and Google Maps Flutter.
- Spend a short time updating the settings required to use these packages.
- Created a provider class that'll ask for the user's permission to access the location.
- Same class also has a method that will access location and call the HTTPS callable function.
- Created HTTPS function which will update the user's location on Firebase Firestore.
- 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.
Top comments (0)