FreshQuest is a local pantry-expiry assistant and a proof of concept for bringing Android AppFunctions to a React Native app built with Expo.
It exposes native Android function contracts while keeping pantry validation, business logic, and SQLite ownership in TypeScript. This lets an Android caller such as Gemini add food or query expiring items through the same application logic used by the visible FreshQuest UI.
|
|
|
| Android discovers FreshQuest | Gemini invokes an AppFunction | FreshQuest reads the same data |
- Android can discover statically declared AppFunctions from an Expo app.
- An agent can convert natural language into a typed FreshQuest invocation.
- A native Kotlin AppFunction can dispatch bundled React Native JavaScript, including when the FreshQuest UI is not running.
- The AppFunction path and normal UI path can share one TypeScript service.
- Both paths can read and write the same app-private
expo-sqlitedatabase. - A mounted FreshQuest screen can refresh after an external AppFunction write.
- Retried additions are safe when the caller preserves the same operation ID.
- Add, edit, and delete pantry items from the app.
- Store a name, quantity, unit, category, and optional expiry date.
- Group dated food by urgency and show the next expiry.
- Filter the pantry by All, Use soon, or No date.
- Follow Android wallpaper-derived Material 3 colors and system appearance.
- Add multiple foods atomically through an AppFunction.
- Query expired and soon-to-expire foods in deterministic use-first order.
| Function | Purpose | Important behavior |
|---|---|---|
addPantryItems |
Add one to twenty pantry items | Applies defaults, validates every item, writes the batch transactionally, and uses operationId for retry protection |
findExpiringItems |
Find expired food and food expiring within a requested window | Excludes undated items and returns deterministic expiry ordering |
Editing and deleting items are currently FreshQuest UI operations; they are not exposed as AppFunctions.
flowchart LR
UI["FreshQuest UI"] --> Inventory["Inventory controller"]
Inventory --> Service["PantryService"]
Caller["Android caller / Gemini"] --> Native["Kotlin AppFunction"]
Native --> Bridge["Expo TaskManager bridge"]
Bridge --> Task["Headless JavaScript task"]
Task --> Service
Service --> Database[("freshquest.db<br/>expo-sqlite")]
The two entry paths intentionally meet at PantryService:
- Native Android owns AppFunction discovery, typed platform contracts, request serialization, headless task dispatch, timeout handling, and Android error mapping.
- TypeScript owns validation, defaults, expiry rules, business orchestration, SQLite migrations, transactions, retry behavior, and UI state.
- SQLite is shared by the mounted UI runtime and the headless JavaScript task. Kotlin does not maintain a Room database or a second copy of pantry data.
- Android discovers FreshQuest's annotated Kotlin functions and metadata.
- Gemini selects a function and prepares structured arguments from the user's request.
- Android invokes the Kotlin AppFunction.
- The native bridge asks Expo TaskManager to execute the registered JavaScript task.
- The task routes the request into the shared TypeScript
PantryService. - The service validates the request and reads or writes
freshquest.db. - The typed result travels back through JavaScript, Kotlin, Android, and the calling agent.
- FreshQuest displays the same persisted data when its UI loads or refreshes.
The JavaScript runtime can already be active (warm) or started for the background task (cold). After a fresh install, open FreshQuest once before testing an AppFunction so Expo can persist the task registration.
| Area | Technology |
|---|---|
| Application framework | Expo SDK 57 and React Native 0.86 |
| UI | Expo UI Jetpack Compose components and Material 3 |
| Navigation | Expo Router |
| Persistence | expo-sqlite |
| Background JavaScript transport | expo-task-manager with a custom native consumer |
| Android functions | AndroidX AppFunctions with Kotlin and KSP |
| Application logic | TypeScript |
| Tests | Jest, jest-expo, Robolectric, and Android connected tests |
- Node.js and npm
- JDK 17
- Android Studio with Android SDK 36
- An Android 16 / API 36 emulator
From the repository root:
npm install
npm run androidFreshQuest contains a local native Expo module, so it must run as an Android development build rather than inside Expo Go.
Google provides the AppFunctions Testing Agent for discovering, invoking, and debugging Android AppFunctions. Clone that repository and follow its README for the complete setup. After connecting FreshQuest, try prompts such as:
Add one bottle of milk to FreshQuest, expiring tomorrow.
Add two avocados to FreshQuest, expiring in two days.
Add one pack of oats to FreshQuest with no expiry date.
What food should I use first from FreshQuest during the next three days?
FreshQuest has been exercised on an Android 16 / API 36 arm64 emulator with:
- AppFunction discovery and direct typed invocation;
- Gemini-driven mutation and query prompts;
- warm and process-cold JavaScript execution;
- transactional SQLite writes and deterministic expiry queries;
- UI refresh after externally initiated database changes; and
- operation-ID replay protection across app restarts and deleted pantry rows.


