Hello and welcome back to Flutter App Dev Tutorial Series. Before this, we have already made a splash Screen, defined a theme, made global widgets, made an authentication screen, and more.
As a part of the user-screen flow, we already have access to the user location. On this 10th installment, we'll use the user's location to fetch the nearest twenty temples using google's Places API. We'll fetch places with the HTTP package, then we'll write another HTTPS Callable with Firebase Functions to store those temples in FireStore. Since we're using an API key, we'll use the flutter_dotenv package to keep it secret. Find the source code to start this section from here.
So, first lets install flutter_dotenv package.
flutter pub add flutter_dotenv
Create a .env file at the root of your project.
touch .env
Add the .env file in .gitignore file.
#DOT ENV
*.env
Initialize .env file in main() method of our main.dart file.
import 'package:flutter_dotenv/flutter_dotenv.dart';
void main() async {
....
// Initialize dot env
await dotenv.load(fileName: ".env");
// Pass prefs as value in MyApp
runApp(MyApp(prefs: prefs));
}
We also need to add the .env file to the assets section of the pubspec file.
#find assets section
assets:
# Splash Screens
- assets/splash/om_splash.png
- assets/splash/om_lotus_splash.png
- assets/splash/om_lotus_splash_1152x1152.png
# Onboard Screens
- assets/onboard/FindTemples.png
- assets/onboard/FindVenues.png
# Auth Screens
- assets/AuthScreen/WelcomeScreenImage_landscape_2.png
// add this here
# Dotenv flie
- .env
To use the google places API we'll need the places API key. For that please go to google cloud console, set up a billing account, and create a new API key for Goog Places API. Then add that API key in the .env file.
.env
#Without quotes
GMAP_PLACES_API_KEY = Your_API_KEY
Install the HTTP package.
flutter pub add http
There's no need for extra setup with the HTTP package.
Like Home and Auth Folders, the temples directory will have all the files associated with temples. So, let's create various files and folders we'll use to fetch & display temples.
# Make folders
mkdir lib/screens/temples lib/screens/temples/providers lib/screens/temples/screens lib/screens/temples/widgets lib/screens/temples/models lib/screens/temples/utils
# Make files
touch lib/screens/temples/providers/temples_provider.dart lib/screens/temples/screens/temples_screen.dart lib/screens/temples/widgets/temples_item_widget.dart lib/screens/temples/models/temple.dart lib/screens/temples/utils/temple_utils.dart
Like the blogs before it, we'll keep our all apps logic inside the provider file. On top of that, we'll also need a utils file to store a few functions that we'll use on the provider class. So, first, let's create two simple functions of temple_utils.dart
temple_utils
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class TemplesUtils {
// Base url for google maps nearbysearch
// #1
static const String _baseUrlNearBySearch =
"https://maps.googleapis.com/maps/api/place/nearbysearch/json?";
// get api key
// #2
final String _placesApi = dotenv.env['GMAP_PLACES_API_KEY'] as String;
// Create a method that'll parse complete url and return it using http package
// #3
Uri searchUrl(LatLng userLocation) {
// Create variables that'll pass maps API parmas as string
// # 4
//===================//
final api = "&key=$_placesApi";
final location =
"location=${userLocation.latitude},${userLocation.longitude}";
const type = "&type=hindu_temple";
// Closest first
// #5
const rankBy = "&rankby=distance";
//=====================//
// Parse URL to get a new uri object
// #6
final url =
Uri.parse(_baseUrlNearBySearch + location + rankBy + type + api);
return URL;
}
}
Google Maps NearbySearch takes several parameters, among them, we'll use location(required parameter), rankby, and type .
Import API key from .env file.
Search Url is a function that'll take user location then, combine the base URL acceptable parameter by search API and return parsed URI.
The API key is a must, on every URL to get a result back from API.
The rankby="distance" sorts the search results based on distance. When we're using rankby the type parameter is required.
The final URL is to be used by the HTTP package to search for temples.
If you're from a place that doesn't have temples(some or not at all) you probably won't see any results. So, use something else for the establishment type.
Another method will be a simple mapper, its sole purpose is to map the incoming list into a list of TempleModels(which we'll create next) and return it as such. This will make our code later much cleaner.
List<TempleModel> mapper(List results) {
final newList = results
.map(
(temple) => TempleModel(
name: temple['name'],
address: temple['address'],
latLng: LatLng(
temple['latLng']['lat'],
temple['latLng']['lon'],
),
imageUrl: temple['imageRef'],
placesId: temple['place_id'],
),
)
.toList();
return newList;
}
The Temple model class will define a framework for the information to be stored about the temple. On the temple file inside models let's create a temple model.
import 'package:google_maps_flutter/google_maps_flutter.dart';
class TempleModel {
// name of temple
final String name;
// the address
final String address;
// geo location
final LatLng latLng;
// ImageUrls
final String imageUrl;
// id given to each item by places api
final String placesId;
const TempleModel(
{required this.name,
required this.address,
required this.latLng,
required this.imageUrl,
required this.placesId});
}
Each temple that'll be saved in Firestore will have a name, address, geographical coordinates, imageUrl, and an ID given by google's place API.
Now, it's time to write a provider class that'll take care of fetching the nearby temples list. This will be a long file with many things to explain. So, we'll go piece by piece codes from top to bottom.
import 'dart:convert';
import 'dart:math';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:cloud_functions/cloud_functions.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_storage/firebase_storage.dart' as firbase_storage;
import 'package:flutter/foundation.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:http/http.dart' as http;
//custom modules
import 'package:temple/screens/temples/models/temple.dart';
import 'package:temple/screens/temples/utils/temple_utils.dart';
class TempleProvider with ChangeNotifier {}
// Instantiate FIbrebase products
final FirebaseAuth auth = FirebaseAuth.instance;
final FirebaseFunctions functions = FirebaseFunctions.instance;
final FirebaseFirestore firestore = FirebaseFirestore.instance;
// Estabish sotrage instance for bucket of our choice
// once e mulator runs you can find the bucket name at storage tab
final firbase_storage.FirebaseStorage storage =
firbase_storage.FirebaseStorage.instanceFor(
bucket: 'astha-being-hindu-tutorial.appspot.com');
With Firebase Storage we will not use a default bucket to store images whose URL will be fetched to save on Firestore. So, what's up with this logic? You see google places API doesn't provide images, it's provided by details API. We'll not be using it. But instead, I have some random(4 in numbers) Hindu images that I downloaded from Unsplash, which I'll store in storage and fetch a random URL among images and assign it to the temple model. You don't have to do it and provide the hardcoded image URL for imageRef, but it's to practice reading storage.
// Instantiate Temple Utils
final TemplesUtils templesUtils = TemplesUtils();
// Create the fake list of temples
List<TempleModel>? _temples = [];
// User location from db
LatLng? _userLocation;
// Getters
List<TempleModel> get temples => [..._temples as List];
LatLng get userLocation => _userLocation as LatLng;
// List of Images
static const List<String> imagePaths = [
'image_1.jpg',
'image_2.jpg',
'image_3.jpg',
'image_4.jpg',
];
The imagePaths is a list of literally the name of images that I've uploaded in a folder named "TempleImages" inside our bucket we referenced earlier in Emulator's Storage.
// Future method to get temples
Future<List<TempleModel>?> getNearyByTemples(LatLng userLocation) async {
// Get urls from the temple utils
// #1
Uri url = templesUtils.searchUrl(userLocation);
try {
// Set up references for firebase products.
// Callable getNearbyTemples
// #2
HttpsCallable getNearbyTemples =
functions.httpsCallable('getNearbyTemples');
// Collection reference for temples
// # 3
CollectionReference templeDocRef = firestore.collection('temples');
// Get one doc from temples collection
// #4
QuerySnapshot querySnapshot = await templeDocRef.limit(1).get();
// A reference to a folder in storage that has images.
// #5
firbase_storage.Reference storageRef = storage.ref('TempleImages');
// We'll only get nearby temples if the temple's collection empty
// #6
if (querySnapshot.docs.isEmpty) {
print("Temple collection is empty");
// get the result from api search
// #7
final res = await http.get(url);
// decode to json result
// #8
final decodedRes = await jsonDecode(res.body) as Map;
// get result as list
// #9
final results = await decodedRes['results'] as List;
// Get random image url from available ones to put as images
// Since we have 4 images we'll get 0-3 values from Random()
// #10
final imgUrl = await storageRef
.child(imagePaths[Random().nextInt(4)])
.getDownloadURL();
// Call the function
// #11
final templesListCall = await getNearbyTemples.call(<String, dynamic>{
'templeList': [...results],
'imageRef': imgUrl,
});
// map the templesList returned by https callable
// we'll use utils mapper here
// #12
final newTempleLists = templesUtils.mapper(templesListCall.data['temples']);
// update the new temples list
// #13
_temples = [...newTempleLists];
} else {
// If the temples collection already has temples then we won't write
// but just fetch temples collection
// #14
print("Temple collection is not empty");
try {
// get all temples documents
final tempSnapShot = await templeDocRef.get();
// fetch the values as list.
final tempList = tempSnapShot.docs[0]['temples'] as List;
// map the results into a list
final templesList = templesUtils.mapper(tempList);
// update temples
_temples = [...templesList];
} catch (e) {
// incase of error temples list in empty
// # 15
_temples = [];
}
}
} catch (e) {
// incase of error temples list in empty
_temples = [];
}
// notify all the listeners
notifyListeners();
// #16
return _temples;
}
Alright, now the main method that'll do everything we've worked on so far in this blog getNearyByTemples has been created. Let's go by numbers:
Inside the index.js file, we'll now create another HTTPS callable function "getNearbyTemples". This method will create an array with the list of temple objects and then save it to the temples collection.
exports.getNearbyTemples = functions.https.onCall(async (data, _) => {
try {
// Notify function's been called
functions.logger.log("Add nearby temples function was called");
// Create array of temple objects.
let temples = data.templeList.map((temple) => {
return {
'place_id': temple['place_id'],
'address': temple['vicinity'] ? temple['vicinity'] : 'Not Available',
'name': temple['name'] ? temple['name'] : 'Not Available',
'latLng': {
'lat': temple.hasOwnProperty('geometry') ? temple['geometry']['location']['lat'] : 'Not Available', 'lon': temple.hasOwnProperty('geometry') ? temple['geometry']['location']['lng'] : 'Not Available',
'dateAdded': admin.firestore.Timestamp.now()
},
'imageRef': data.imageRef
}
}
);
// save the temples array to temples collection as one document named temples
await db.collection('temples').add({ temples: temples });
} catch (e) {
// if error return errormsg
return { 'Error Msg': e };
}
// If everything's fine return the temples array.
return temples;
});
I have not allocated memory for this operation. It was very tricky and time-consuming. If you want you can experiment. A firebase document can store up to 1MB in size. So, our list at least for this app will never grow beyond 20. So, inside the temple's collection, we are not saving 20 documents but one document with 20 items as a field "temples", db.collection('temples').add({ temples: temples }).
Let's say the user changed location or a new temple has been added to the google database. It should be reflected in the Firestore Temples collection. But we should handle updates carefully and only write new documents if there are any changes to the old ones. For our temples collection, we can just match the places_id, and only take action accordingly. Firebase provides an onUpdate() trigger to handle this type of work. Now, let's write some code on index.js.
// When the temple List Updates
exports.updateNearbyTemples = functions.runWith({
timeoutSeconds: 120,
memory: "256MB"
}).firestore.document('temples/{id}').onUpdate(async (change, context) => {
// If theres both new and old value
if (change.before.exists && change.after.exists) {
// temples list both new and old
let newTemplesList = change.after.data()['temples'];
let oldTemplesList = change.before.data()['temples'];
// Places Id list from both new and old list
let oldTemplesIdList = oldTemplesList.map(temple => temple['place_id']);
let newTemplesIdList = newTemplesList.map(temple => temple['place_id']);
// Lets find out if theres new temples id by filtering with old one
let filteredList = newTemplesIdList.filter(x => !oldTemplesIdList.includes(x));
// if the length are not same of fileted list has
//length of 0 then nothing new is there so just return
if (oldTemplesIdList.length != newTemplesIdList.length || filteredList.length == 0) {
functions.logger.log("Nothing is changed so onUpdate returned");
return;
}
// If somethings changed then
try {
functions.logger.log("On Update was called ");
// Make new list of temples
let temples = newTemplesList.map((temple) => {
return {
'place_id': temple['place_id'],
'address': temple['vicinity'] ? temple['vicinity'] : 'Not Available',
'name': temple['name'] ? temple['name'] : 'Not Available',
'latLng': {
'lat': temple.hasOwnProperty('geometry') ? temple['geometry']['location']['lat'] : 'Not Available', 'lon': temple.hasOwnProperty('geometry') ? temple['geometry']['location']['lng'] : 'Not Available',
'dateAdded': admin.firestore.Timestamp.now()
}
}
}
);
// use the current context id to update temples, no need to merge
await db.collection('temples').doc(context.params.id).set({ 'palces_id_list': newTemplesIdList, temples: temples });
}
catch (e) { throw e; }
return { 'status': 200 };
}
// return nothing
return null;
});
Changes.before.data gives values already in the Firestore and changes.after.data gives value newly gotten from the API. With this update, the function will not run every time the user loads the temples screen. It will save us lots of money on production mode.
Now, our classes are ready for work. So, let's make them available by updating the MultiProviders list in the app file.
MultiProvider(
providers: [
...
ChangeNotifierProvider(create: (context) => TempleProvider()),
...
],
...
Now, the GetNearbyTemples method is accessible for all the descendants of MultiProviders. So, where exactly are we going to call this method? Well in the next blog, We'll make our home page a little bit better looking. On that homepage, there will be a link to Temple List Screen. In method will be executed when the link is clicked. For now, let's end this blog before we derail from the main theme of the blog.
temple_provider
import 'dart:convert';
import 'dart:math';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:cloud_functions/cloud_functions.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_storage/firebase_storage.dart' as firbase_storage;
import 'package:flutter/foundation.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:http/http.dart' as http;
//custom modules
import 'package:temple/screens/temples/models/temple.dart';
import 'package:temple/screens/temples/utils/temple_utils.dart';
class TempleProvider with ChangeNotifier {
// Instantiate FIbrebase products
final FirebaseAuth auth = FirebaseAuth.instance;
final FirebaseFunctions functions = FirebaseFunctions.instance;
final FirebaseFirestore firestore = FirebaseFirestore.instance;
// Estabish sotrage instance for bucket of our choice
// once e mulator runs you can find the bucket name at storage tab
final firbase_storage.FirebaseStorage storage =
firbase_storage.FirebaseStorage.instanceFor(
bucket: 'astha-being-hindu-tutorial.appspot.com');
// Instantiate Temple Utils
final TemplesUtils templesUtils = TemplesUtils();
// Create the fake list of temples
List<TempleModel>? _temples = [];
// User location from db
LatLng? _userLocation;
// Getters
List<TempleModel> get temples => [..._temples as List];
LatLng get userLocation => _userLocation as LatLng;
// List of Images
static const List<String> imagePaths = [
'image_1.jpg',
'image_2.jpg',
'image_3.jpg',
'image_4.jpg',
];
// Future method to get temples
Future<void> getNearyByTemples(LatLng userLocation) async {
// Get urls from the temple utils
Uri url = templesUtils.searchUrl(userLocation);
try {
// Set up references for firebase products.
// Callable getNearbyTemples
HttpsCallable getNearbyTemples =
functions.httpsCallable('getNearbyTemples');
// COllection reference for temples
CollectionReference templeDocRef = firestore.collection('temples');
// Get one doc from temples collection
QuerySnapshot querySnapshot = await templeDocRef.limit(1).get();
// A reference to a folder in storage that has images.
firbase_storage.Reference storageRef = storage.ref('TempleImages');
// We'll only get nearby temples if the temples collection empty
if (querySnapshot.docs.isEmpty) {
print("Temple collection is empty");
// get the result from api search
final res = await http.get(url);
// decode the json result
final decodedRes = await jsonDecode(res.body) as Map;
// get result as list
final results = await decodedRes['results'] as List;
// Get random image url from available ones to put as images
// Since we have 4 images we'll get 0-3 values from Random()
final imgUrl = await storageRef
.child(imagePaths[Random().nextInt(4)])
.getDownloadURL();
// Call the function
final templesListCall = await getNearbyTemples.call(<String, dynamic>{
'templeList': [...results],
'imageRef': imgUrl,
});
// map the templesList restured by https callable
final newTempleLists = templesListCall.data['temples']
.map(
(temple) => TempleModel(
name: temple['name'],
address: temple['address'],
latLng: LatLng(
temple['latLng']['lat'],
temple['latLng']['lon'],
),
imageUrl: temple['imageRef'],
placesId: temple['place_id'],
),
)
.toList();
// update the new temples list
_temples = [...newTempleLists];
} else {
// If the temples collection already has temples then we won't write
// but just fetch temples collection
print("Temple collection is not empty");
try {
// get all temples documents
final tempSnapShot = await templeDocRef.get();
// fetch the values as list.
final tempList = tempSnapShot.docs[0]['temples'] as List;
// map the results into a list
final templesList = tempList
.map(
(temple) => TempleModel(
name: temple['name'],
address: temple['address'],
latLng: LatLng(
temple['latLng']['lat'],
temple['latLng']['lon'],
),
imageUrl: temple['imageRef'],
placesId: temple['place_id'],
),
)
.toList();
// update temples
_temples = [...templesList];
} catch (e) {
// incase of error temples list in empty
_temples = [];
}
}
} catch (e) {
// incase of error temples list in empty
_temples = [];
}
// notify all the listeners
notifyListeners();
}
}
index
// 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;
});
exports.getNearbyTemples = functions.https.onCall(async (data, _) => {
try {
// Notify function's been called
functions.logger.log("Add nearby temples function was called");
// Create array of temple objects.
let temples = data.templeList.map((temple) => {
return {
'place_id': temple['place_id'],
'address': temple['vicinity'] ? temple['vicinity'] : 'Not Available',
'name': temple['name'] ? temple['name'] : 'Not Available',
'latLng': {
'lat': temple.hasOwnProperty('geometry') ? temple['geometry']['location']['lat'] : 'Not Available', 'lon': temple.hasOwnProperty('geometry') ? temple['geometry']['location']['lng'] : 'Not Available',
'dateAdded': admin.firestore.Timestamp.now()
},
'imageRef': data.imageRef
}
}
);
// save the temples array to temples collection as one document named temples
await db.collection('temples').add({ temples: temples });
} catch (e) {
// if error return errormsg
return { 'Error Msg': e };
}
// If everything's fine return the temples array.
return temples;
});
// When the temple List Updates
exports.updateNearbyTemples = functions.runWith({
timeoutSeconds: 120,
memory: "256MB"
}).firestore.document('temples/{id}').onUpdate(async (change, context) => {
// If theres both new and old value
if (change.before.exists && change.after.exists) {
// temples list both new and old
let newTemplesList = change.after.data()['temples'];
let oldTemplesList = change.before.data()['temples'];
// Places Id list from both new and old list
let oldTemplesIdList = oldTemplesList.map(temple => temple['place_id']);
let newTemplesIdList = newTemplesList.map(temple => temple['place_id']);
// Lets find out if theres new temples id by filtering with old one
let filteredList = newTemplesIdList.filter(x => !oldTemplesIdList.includes(x));
// if the length are not same of fileted list has
//length of 0 then nothing new is there so just return
if (oldTemplesIdList.length != newTemplesIdList.length || filteredList.length == 0) {
functions.logger.log("Nothing is changed so onUpdate returned");
return;
}
// If somethings changed then
try {
functions.logger.log("On Update was called ");
// Make new list of temples
let temples = newTemplesList.map((temple) => {
return {
'place_id': temple['place_id'],
'address': temple['vicinity'] ? temple['vicinity'] : 'Not Available',
'name': temple['name'] ? temple['name'] : 'Not Available',
'latLng': {
'lat': temple.hasOwnProperty('geometry') ? temple['geometry']['location']['lat'] : 'Not Available', 'lon': temple.hasOwnProperty('geometry') ? temple['geometry']['location']['lng'] : 'Not Available',
'dateAdded': admin.firestore.Timestamp.now()
}
}
}
);
// use the current context id to update temples, no need to merge
await db.collection('temples').doc(context.params.id).set({ 'palces_id_list': newTemplesIdList, temples: temples });
}
catch (e) { throw e; }
return { 'status': 200 };
}
// return nothing
return null;
});
Before we leave let's summarize what we did so far.
Alright, this is it for this time. This series, is still not over, on the next upload we'll redesign our ugly-looking mundane homepage, create a Temple List Screen and execute the method we created today.
Please like 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