Hello and welcome back to Flutter App Dev Tutorial Series. Before this, we have already made a , defined a , made made an , and . splash Screen theme global widgets , authentication screen more About 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 . We'll fetch places with the 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 package to keep it secret. Find the source code to start this section from . Places API HTTP flutter_dotenv here Packages DotEnv So, first lets install package. flutter_dotenv 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 method of our main.dart file. main() 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 file. pubspec #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 Google Maps Places API To use the google places API we'll need the places API key. For that please go to , set up a billing account, and create a new API key for Goog Places API. Then add that API key in the .env file. google cloud console .env #Without quotes GMAP_PLACES_API_KEY = Your_API_KEY HTTP Install the package. HTTP flutter pub add http There's no need for extra setup with the HTTP package. Folder Structures 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 Utilities 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 takes several parameters, among them, we'll use (required parameter), , and . NearbySearch location rankby 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 is a must, on every URL to get a result back from API. API key The rankby="distance" sorts the search results based on distance. When we're using the parameter is required. rankby type 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; } Temple Model The Temple model class will define a framework for the information to be stored about the temple. On the file inside models let's create a temple model. temple 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. Google Place API with HTTP and Provider 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 Modules and Create a provider class. 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 {} Inside the class, instantiate Firebase Products we will be using. // 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 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 API. We'll not be using it. But instead, I have some random(4 in numbers) Hindu images that I downloaded from , which I'll store in storage and fetch a URL among images and assign it to the temple model. You don't have to do it and provide the hardcoded image URL for , but it's to practice reading storage. Firebase Storage details Unsplash random imageRef Other Fields and Getters // 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 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. imagePaths Future To Return Places API Result // 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 has been created. Let's go by numbers: getNearyByTemples User the temple utils we created earlier to get the URL ready to be used by the HTTP package. Reference to the HTTP callable , which we'll create after this provider session. It's responsible to save the list of all the temples we fetch during this search. getNearyByTemples Reference to temple collection. Temple reference be used to read a single document from the collection. References to a folder named "TempleImages" inside the bucket of storage. We're checking if the temple doc we fetched earlier is empty. The logic is that we don't want to call for Place Api, Firestores, and Functions every time user uses our app. We'll only fetch temples and save them on FireStore if the temple's collection is empty or doesn't exist. HTTP get() method can be used to fetch the results. You can use software like the postman or just a chrome browser to see the results of the get request. parses strings to JSON data types. JSON Decode Function Places API provides the of a list of temples as a list with as its key. We'll extract that as the List type. response results Firebase Storage provided the means to from the reference. We're randomly downloading a URL and assigning it to imageRef property needed in our HTTPS callable. download the URL We call our HTTPS callable now, providing with temples list and image's Url. It'll save the list in Firesotre's Temples collection and return that list. The returned list will be now used to update our List of Temple Models using the mapper method of Temple Utils. This same list will be used by our app to display a beautiful list of temple cards on the temple’s screen. The else block only executes if the temple's collection already has a list of temples. In that case unlike in the, if block we do not fetch the temples list from API, we just read all the documents that are saved in the temple's collection. After this process is the same as above. In case of errors, the temples list will be empty. It is very important to return this new list. We will need this List as QurerySnapshot data fetched by FutureBuilder to display it on our app. Write Google Places API Search Results On FireStore With Firebase Functions Inside the 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 collection. index.js temples 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 }) Handle Updates With Firestore Triggers 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 trigger to handle this type of work. Now, let's write some code on . onUpdate() 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; }); gives values already in the Firestore and 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. Changes.before.data changes.after.data Making Provider Class available for Widgets Now, our classes are ready for work. So, let's make them available by updating the MultiProviders list in the file. app 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. Final Code 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; }); Summary Before we leave let's summarize what we did so far. We created google maps places API. By installing the Flutter_DotEnv package, we secured the API from being public. HTTP package was also added which played a vital role in fetching temples lists from API. We created a utility file though with just one util. Later on, if you want, you can write a distance calculator method from user to temple to represent in Google Maps. We then wrote a method in our provider class, that fetched a search list, and passes it to firebase cloud functions. The firebase function saves the temple lists to Firestore if the collection is empty. We then wrote an update trigger, that'll only run when the value is changed. Show Support 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. 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 , a freelancing agency that makes websites and mobile applications. Please like and share Khadka's Coding Lounge Also Published Here