Skip to content
abhinavbhatia
Sheet 02 / 05 — Blogs← Archive

Meal Tracker: log a meal by taking a photo

A phone app that reads your plate. Point the camera at dinner and it works out what it is and roughly how much. What made it work was handling the times the AI gets it wrong.

The reason food logging apps get abandoned is data entry. Every one of them asks you to search a database of 40,000 items for the thing you just ate, pick the closest wrong answer, then guess a portion size. Three days of that and everybody quits.

So this one has one input: a photograph, a line of text, or both. Expo and React Native, Firebase underneath, and a multimodal call that turns “the thing on the plate” into a name and a number.

The contract

The whole app rests on one call returning something a program can use. Not something a person can read — something JSON.parse will accept, first time, without a regex.

The prompt is blunt about it:

Return ONLY a valid JSON object in this exact format: {"food": "...", "calories": number}. Do not use markdown blocks or formatting.

That last sentence exists because of a specific failure. Models wrap JSON in triple-backtick fences constantly — it’s the shape of most of the JSON they’ve ever seen. You get back something that looks perfect in a log line and throws on parse, because character one is a backtick.

Photo or text expo-image-picker base64 Gemini, multimodal "estimate the portion size shown" strip fences ```json → "" then JSON.parse {food, calories} → Firestore mealLogs parse failed one sentence, type it in instead THE RULE THIS TAUGHT ME Treat a model's output as an untrusted external API response, because that is exactly what it is. Never hand a raw completion straight to a parser without a way to survive it not parsing.

the request path from photo or text through Gemini to a parsed JSON result, with the failure branches

So the response gets its fences stripped before it goes near the parser, and the whole thing sits in a try/catch that ends in a plain sentence asking you to type the number yourself. Every AI feature needs the version of itself that works when the model is unavailable, wrong, or having an opinion about markdown. Here that fallback is a text field, which is what the app would have been anyway.

The estimate is an estimate, and the app says so — logged entries from the model are marked as such rather than presented as measurement. A photograph cannot tell you how much oil went in the pan. Anything that claims otherwise is lying with more decimal places.

React Native, briefly

Six hundred lines, one App.js, no navigation library. Expo handles the build, expo-image-picker handles the camera roll, and Firebase Auth and Firestore do what they do everywhere.

The friends feature is three collections — users, friendRequests, friends — and it’s the part I’d redo. Mutual-consent relationships modelled as documents get fiddly the moment you want “pending”, “accepted” and “blocked” to be consistent from both sides, and I ended up with reads in more places than I’d like. A single relationships document keyed by a sorted pair of user IDs would have collapsed most of it. Next time.

Why it’s still on my phone

Because the entry cost is one photo. Every other one I’ve tried died on the search box.

That’s not a machine-learning insight, it’s a product one, and it’s the same lesson as everything else I build: the technology was never the reason people stopped using the thing. The friction was.