Technical case study: Cross-platform mobile app
A chat app with real-time messaging, offline-first architecture, image and location sharing, and screen reader support.
Built with
At a glance
4
weeks of development
2
platforms, iOS and Android
6
core features
3
Firebase services
Role
Developer (solo project)
Framework
React Native with Expo
Backend
Firebase Firestore, Auth, and Storage
Program
CareerFoundry Full-Stack Web Development
Overview
The app lets users chat in real time, share images, and send their location. Firebase Firestore stores and syncs every conversation, Firebase Cloud Storage hosts shared images, and the Gifted Chat library powers the chat interface, with screen reader compatibility built in throughout.
See it in action
A quick walkthrough of the start screen, real-time messaging, and image and location sharing.
Core features
Name entry and background color selection, built to spec with accessibility in mind.
Gifted Chat with Firebase Firestore for instant delivery and cross-device sync.
Firebase Authentication gives secure access without a sign-up barrier.
Camera capture and gallery selection, hosted in Firebase Cloud Storage.
GPS-based location sharing displayed in an interactive map view.
AsyncStorage keeps conversations available and syncs when the connection returns.
The approach
Challenge: one app that behaves the same on iOS and Android.
Solution: React Native with Expo, using native device features from a single codebase.
Challenge: instant messaging across unreliable connections.
Solution: Firestore listeners that update the chat live, falling back to cached messages offline.
Challenge: keeping conversations available without a connection.
Solution: AsyncStorage caching that loads saved messages whenever the device is offline.
In the code
Three snippets from the app’s source code, one for each core feature.
Choose a feature below to see its code
Firestore listeners deliver new messages instantly and cache them for offline use.
// Firebase Firestore real-time listener setup
useEffect(() => {
let unsubscribe;
if (isConnected === true) {
const q = query(
collection(db, "messages"),
orderBy("createdAt", "desc")
);
unsubscribe = onSnapshot(q, (docs) => {
let newMessages = [];
docs.forEach((doc) => {
newMessages.push({
id: doc.id,
...doc.data(),
createdAt: new Date(doc.data().createdAt.toMillis())
});
});
cacheMessages(newMessages);
setMessages(newMessages);
});
} else {
loadCachedMessages();
}
return () => unsubscribe && unsubscribe();
}, [isConnected]);AsyncStorage keeps conversations readable when the connection drops.
// AsyncStorage implementation for offline functionality
const cacheMessages = async (messagesToCache) => {
try {
await AsyncStorage.setItem(
"messages",
JSON.stringify(messagesToCache)
);
} catch (error) {
console.log(error.message);
}
};
const loadCachedMessages = async () => {
try {
const cachedMessages =
(await AsyncStorage.getItem("messages")) || "[]";
setMessages(JSON.parse(cachedMessages));
} catch (error) {
console.log(error.message);
}
};Photos upload to Firebase Cloud Storage, then post to the chat as a message.
// Firebase Cloud Storage integration
const uploadAndSendImage = async (imageURI) => {
const uniqueRefString = generateReference(imageURI);
const newUploadRef = ref(storage, uniqueRefString);
const response = await fetch(imageURI);
const blob = await response.blob();
uploadBytes(newUploadRef, blob).then(async (snapshot) => {
const imageURL = await getDownloadURL(snapshot.ref);
onSend([{
_id: generateMessageId(),
text: "",
createdAt: new Date(),
user: user,
image: imageURL,
}]);
});
};The results
Building offline-first showed how complex mobile state management can be, and why graceful degradation matters for any app that depends on a network connection.
Book a free 30-minute discovery call to talk through your idea.