Practice and reinforce the concepts from Lesson 5
Build a geofencing application that monitors user location, triggers notifications when entering or leaving defined geographic areas, and provides a visual interface for creating and managing geofences on a map.
A reminder app that alerts users when they enter or leave specific locations (like "Buy milk when near grocery store" or "Remember to call when arriving home"). The app will show geofence boundaries on a map and send local notifications when boundaries are crossed.

Install required packages and configure permissions:
cd M3-Activity-05
npm install expo-location expo-notifications expo-task-manager
Configure app.json for background location and notifications:
{
"expo": {
"plugins": [
[
"expo-location",
{
"locationAlwaysAndWhenInUsePermission": "Allow this app to use your location to send reminders when you arrive at places.",
"locationAlwaysPermission": "Allow this app to use your location to send reminders when you arrive at places.",
"locationWhenInUsePermission": "Allow this app to use your location to send reminders when you arrive at places."
}
],
[
"expo-notifications",
{
"sounds": ["notification.wav"]
}
]
],
"android": {
"permissions": [
"ACCESS_COARSE_LOCATION",
"ACCESS_FINE_LOCATION",
"ACCESS_BACKGROUND_LOCATION",
"POST_NOTIFICATIONS"
]
},
"ios": {
"infoPlist": {
"NSLocationAlwaysAndWhenInUseUsageDescription": "We need your location to send reminders when you arrive at places.",
"NSLocationWhenInUseUsageDescription": "We need your location to send reminders when you arrive at places."
}
}
}
}
Set up storage and data structures for geofences:
// utils/geofenceManager.js
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as Location from 'expo-location';
import * as Notifications from 'expo-notifications';
const GEOFENCES_KEY = '@geofences';
export const saveGeofence = async (geofence) => {
try {
const existing = await getGeofences();
const updated = [...existing, { ...geofence, id: Date.now().toString() }];
await AsyncStorage.setItem(GEOFENCES_KEY, JSON.stringify(updated));
return updated;
} catch (error) {
console.error('Error saving geofence:', error);
return [];
}
};
export const getGeofences = async () => {
try {
const data = await AsyncStorage.getItem(GEOFENCES_KEY);
return data ? JSON.parse(data) : [];
} catch (error) {
console.error('Error loading geofences:', error);
return [];
}
};
export const deleteGeofence = async (id) => {
try {
const existing = await getGeofences();
const updated = existing.filter(g => g.id !== id);
await AsyncStorage.setItem(GEOFENCES_KEY, JSON.stringify(updated));
return updated;
} catch (error) {
console.error('Error deleting geofence:', error);
return [];
}
};
export const updateGeofence = async (id, updates) => {
try {
const existing = await getGeofences();
const updated = existing.map(g =>
g.id === id ? { ...g, ...updates } : g
);
await AsyncStorage.setItem(GEOFENCES_KEY, JSON.stringify(updated));
return updated;
} catch (error) {
console.error('Error updating geofence:', error);
return [];
}
};
export const isPointInCircle = (point, center, radius) => {
const R = 6371e3; // Earth radius in meters
const lat1 = point.latitude * Math.PI / 180;
const lat2 = center.latitude * Math.PI / 180;
const deltaLat = (center.latitude - point.latitude) * Math.PI / 180;
const deltaLon = (center.longitude - point.longitude) * Math.PI / 180;
const a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) +
Math.cos(lat1) * Math.cos(lat2) *
Math.sin(deltaLon / 2) * Math.sin(deltaLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
const distance = R * c;
return distance <= radius;
};
💡 Tip: Background location tracking significantly impacts battery life. Use the smallest radius and least frequent update interval that meets your needs.
Create a task that monitors location in the background:
// tasks/geofenceTask.js
import * as TaskManager from 'expo-task-manager';
import * as Notifications from 'expo-notifications';
import { getGeofences, updateGeofence, isPointInCircle } from '../utils/geofenceManager';
const GEOFENCE_TASK = 'GEOFENCE_BACKGROUND_TASK';
TaskManager.defineTask(GEOFENCE_TASK, async ({ data, error }) => {
if (error) {
console.error('Geofence task error:', error);
return;
}
if (data) {
const { locations } = data;
const currentLocation = locations[0].coords;
// Check all geofences
const geofences = await getGeofences();
for (const geofence of geofences) {
const isInside = isPointInCircle(
currentLocation,
geofence.coordinate,
geofence.radius
);
const wasInside = geofence.isInside || false;
// Detect entry
if (isInside && !wasInside) {
await updateGeofence(geofence.id, {
isInside: true,
lastTriggered: Date.now()
});
if (geofence.notifyOnEntry) {
await sendGeofenceNotification(geofence, 'entry');
}
}
// Detect exit
if (!isInside && wasInside) {
await updateGeofence(geofence.id, {
isInside: false,
lastTriggered: Date.now()
});
if (geofence.notifyOnExit) {
await sendGeofenceNotification(geofence, 'exit');
}
}
}
}
});
const sendGeofenceNotification = async (geofence, eventType) => {
await Notifications.scheduleNotificationAsync({
content: {
title: eventType === 'entry' ? '📍 Entered Location' : '🚶 Left Location',
body: geofence.reminder,
data: { geofenceId: geofence.id },
sound: true,
},
trigger: null, // Send immediately
});
};
export { GEOFENCE_TASK };
Build an interface for users to add geofences:
// components/GeofenceCreator.js
import React, { useState } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
Switch,
Modal,
} from 'react-native';
const GeofenceCreator = ({ visible, coordinate, onSave, onCancel }) => {
const [reminder, setReminder] = useState('');
const [radius, setRadius] = useState('100');
const [notifyOnEntry, setNotifyOnEntry] = useState(true);
const [notifyOnExit, setNotifyOnExit] = useState(false);
const handleSave = () => {
if (!reminder.trim()) {
alert('Please enter a reminder');
return;
}
const radiusNum = parseInt(radius);
if (isNaN(radiusNum) || radiusNum < 50 || radiusNum > 5000) {
alert('Radius must be between 50 and 5000 meters');
return;
}
onSave({
coordinate,
reminder: reminder.trim(),
radius: radiusNum,
notifyOnEntry,
notifyOnExit,
isInside: false,
createdAt: Date.now(),
});
// Reset form
setReminder('');
setRadius('100');
setNotifyOnEntry(true);
setNotifyOnExit(false);
};
return (
<Modal
visible={visible}
animationType="slide"
transparent={true}
onRequestClose={onCancel}
>
<View style={styles.overlay}>
<View style={styles.container}>
<Text style={styles.title}>Create Geofence Reminder</Text>
<Text style={styles.label}>Reminder Message</Text>
<TextInput
style={styles.input}
placeholder="e.g., Buy milk"
value={reminder}
onChangeText={setReminder}
multiline
/>
<Text style={styles.label}>Radius (meters)</Text>
<TextInput
style={styles.input}
placeholder="100"
value={radius}
onChangeText={setRadius}
keyboardType="numeric"
/>
<View style={styles.switchRow}>
<Text style={styles.switchLabel}>Notify on Entry</Text>
<Switch
value={notifyOnEntry}
onValueChange={setNotifyOnEntry}
trackColor={{ true: '#4ECDC4' }}
/>
</View>
<View style={styles.switchRow}>
<Text style={styles.switchLabel}>Notify on Exit</Text>
<Switch
value={notifyOnExit}
onValueChange={setNotifyOnExit}
trackColor={{ true: '#4ECDC4' }}
/>
</View>
<View style={styles.buttonRow}>
<TouchableOpacity
style={[styles.button, styles.cancelButton]}
onPress={onCancel}
>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, styles.saveButton]}
onPress={handleSave}
>
<Text style={styles.saveButtonText}>Save</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
);
};
const styles = StyleSheet.create({
overlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'center',
alignItems: 'center',
},
container: {
backgroundColor: 'white',
borderRadius: 15,
padding: 20,
width: '90%',
maxWidth: 400,
},
title: {
fontSize: 20,
fontWeight: 'bold',
marginBottom: 20,
},
label: {
fontSize: 14,
fontWeight: '600',
color: '#666',
marginBottom: 8,
marginTop: 12,
},
input: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
padding: 12,
fontSize: 16,
},
switchRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: '#f0f0f0',
},
switchLabel: {
fontSize: 16,
},
buttonRow: {
flexDirection: 'row',
marginTop: 20,
gap: 10,
},
button: {
flex: 1,
padding: 15,
borderRadius: 10,
alignItems: 'center',
},
cancelButton: {
backgroundColor: '#f0f0f0',
},
cancelButtonText: {
color: '#666',
fontSize: 16,
fontWeight: '600',
},
saveButton: {
backgroundColor: '#4ECDC4',
},
saveButtonText: {
color: 'white',
fontSize: 16,
fontWeight: '600',
},
});
export default GeofenceCreator;
Combine all components in the main app:
// App.js
import React, { useState, useEffect } from 'react';
import { StyleSheet, View, TouchableOpacity, Text, FlatList, Alert } from 'react-native';
import MapView, { Marker, Circle } from 'react-native-maps';
import * as Location from 'expo-location';
import * as Notifications from 'expo-notifications';
import GeofenceCreator from './components/GeofenceCreator';
import {
saveGeofence,
getGeofences,
deleteGeofence
} from './utils/geofenceManager';
import { GEOFENCE_TASK } from './tasks/geofenceTask';
// Configure notification handler
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: false,
}),
});
export default function App() {
const [location, setLocation] = useState(null);
const [geofences, setGeofences] = useState([]);
const [showCreator, setShowCreator] = useState(false);
const [selectedCoordinate, setSelectedCoordinate] = useState(null);
const [isTracking, setIsTracking] = useState(false);
useEffect(() => {
setupLocationTracking();
loadGeofences();
}, []);
const setupLocationTracking = async () => {
// Request permissions
const { status: foregroundStatus } =
await Location.requestForegroundPermissionsAsync();
if (foregroundStatus !== 'granted') {
Alert.alert('Permission denied', 'Location permission is required');
return;
}
// Get current location
const currentLocation = await Location.getCurrentPositionAsync({});
setLocation({
latitude: currentLocation.coords.latitude,
longitude: currentLocation.coords.longitude,
});
// Request background permission
const { status: backgroundStatus } =
await Location.requestBackgroundPermissionsAsync();
if (backgroundStatus === 'granted') {
startGeofenceTracking();
}
// Request notification permission
await Notifications.requestPermissionsAsync();
};
const startGeofenceTracking = async () => {
await Location.startLocationUpdatesAsync(GEOFENCE_TASK, {
accuracy: Location.Accuracy.Balanced,
distanceInterval: 50, // Update every 50 meters
showsBackgroundLocationIndicator: true,
foregroundService: {
notificationTitle: 'Geofence Active',
notificationBody: 'Monitoring your location for reminders',
},
});
setIsTracking(true);
};
const stopGeofenceTracking = async () => {
await Location.stopLocationUpdatesAsync(GEOFENCE_TASK);
setIsTracking(false);
};
const loadGeofences = async () => {
const loaded = await getGeofences();
setGeofences(loaded);
};
const handleMapLongPress = (event) => {
setSelectedCoordinate(event.nativeEvent.coordinate);
setShowCreator(true);
};
const handleSaveGeofence = async (geofence) => {
await saveGeofence(geofence);
await loadGeofences();
setShowCreator(false);
};
const handleDeleteGeofence = async (id) => {
Alert.alert(
'Delete Geofence',
'Are you sure you want to delete this reminder?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Delete',
style: 'destructive',
onPress: async () => {
await deleteGeofence(id);
await loadGeofences();
},
},
]
);
};
return (
<View style={styles.container}>
<MapView
style={styles.map}
region={location ? {
...location,
latitudeDelta: 0.01,
longitudeDelta: 0.01,
} : undefined}
onLongPress={handleMapLongPress}
showsUserLocation={true}
>
{geofences.map((geofence) => (
<React.Fragment key={geofence.id}>
<Circle
center={geofence.coordinate}
radius={geofence.radius}
fillColor="rgba(78, 205, 196, 0.2)"
strokeColor="rgba(78, 205, 196, 0.7)"
strokeWidth={2}
/>
<Marker
coordinate={geofence.coordinate}
onCalloutPress={() => handleDeleteGeofence(geofence.id)}
>
<View style={styles.markerContainer}>
<Text style={styles.markerText}>📍</Text>
</View>
</Marker>
</React.Fragment>
))}
</MapView>
<View style={styles.statusBar}>
<Text style={styles.statusText}>
{isTracking ? '✓ Tracking Active' : '✗ Tracking Inactive'}
</Text>
<TouchableOpacity
onPress={isTracking ? stopGeofenceTracking : startGeofenceTracking}
style={styles.toggleButton}
>
<Text style={styles.toggleButtonText}>
{isTracking ? 'Stop' : 'Start'}
</Text>
</TouchableOpacity>
</View>
<View style={styles.geofenceList}>
<Text style={styles.listTitle}>
Active Reminders ({geofences.length})
</Text>
<FlatList
data={geofences}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<TouchableOpacity
style={styles.geofenceItem}
onLongPress={() => handleDeleteGeofence(item.id)}
>
<Text style={styles.reminderText}>{item.reminder}</Text>
<Text style={styles.radiusText}>{item.radius}m radius</Text>
</TouchableOpacity>
)}
/>
</View>
<GeofenceCreator
visible={showCreator}
coordinate={selectedCoordinate}
onSave={handleSaveGeofence}
onCancel={() => setShowCreator(false)}
/>
<Text style={styles.instruction}>
Long press on map to create a geofence
</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
map: {
width: '100%',
height: '100%',
},
statusBar: {
position: 'absolute',
top: 50,
left: 10,
right: 10,
backgroundColor: 'white',
borderRadius: 10,
padding: 15,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 5,
elevation: 5,
},
statusText: {
fontSize: 16,
fontWeight: '600',
},
toggleButton: {
backgroundColor: '#4ECDC4',
paddingHorizontal: 20,
paddingVertical: 8,
borderRadius: 20,
},
toggleButtonText: {
color: 'white',
fontWeight: 'bold',
},
geofenceList: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
backgroundColor: 'white',
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
padding: 20,
maxHeight: '30%',
shadowColor: '#000',
shadowOffset: { width: 0, height: -3 },
shadowOpacity: 0.1,
shadowRadius: 5,
elevation: 10,
},
listTitle: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 10,
},
geofenceItem: {
padding: 12,
borderBottomWidth: 1,
borderBottomColor: '#f0f0f0',
},
reminderText: {
fontSize: 16,
marginBottom: 4,
},
radiusText: {
fontSize: 14,
color: '#666',
},
markerContainer: {
backgroundColor: 'white',
padding: 5,
borderRadius: 20,
},
markerText: {
fontSize: 20,
},
instruction: {
position: 'absolute',
bottom: 200,
alignSelf: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.7)',
color: 'white',
padding: 10,
borderRadius: 5,
},
});
Problem: Notifications don't trigger Solution: Verify notification permissions are granted. Check that the geofence task is registered before starting location updates. Ensure notifications are properly configured in app.json.
Problem: Background location doesn't work
Solution: Background location requires a development build or EAS Build, it won't work in Expo Go. Use npx expo run:android or npx expo run:ios to test.
Problem: Battery drains quickly Solution: Increase distanceInterval to update less frequently. Use Location.Accuracy.Low instead of High. Remove geofences when not needed.
Problem: Geofence triggers immediately after creation Solution: Ensure isInside is initialized to false. The first trigger should only happen when crossing the boundary, not when already inside.
Problem: Task not found error Solution: Make sure the task is defined before calling startLocationUpdatesAsync. Import the task file at the top of App.js.
For advanced students:
Time-based Geofences: Add time restrictions so geofences only trigger during certain hours or days of the week
Categories: Group geofences by category (Home, Work, Shopping) with different colors and icons
History Log: Track and display when geofences were triggered with timestamps
Smart Suggestions: Use Places API to suggest common locations (Home, Work, Gym) for geofence creation
Geofence Import/Export: Allow users to share geofence configurations via JSON export/import
In this activity, you:
In the next lesson, you'll explore ARCore fundamentals to add augmented reality features to your apps. You'll learn how to detect surfaces, place virtual objects, and create immersive AR experiences.