Construir Stocky: predecir qué se agota en tu cocina — en el propio dispositivo
Cómo Stocky convierte el escaneo de un código de barras y un poco de historial de uso en una lista de la compra que aparece antes de que te quedes sin nada — con todos los cálculos, y todos los datos, guardados en tu teléfono.
La mayoría de las apps de despensa son listas de tareas glorificadas. Añades leche, marcas la leche, vuelves a añadir leche. La app nunca aprende nada. Stocky partió de una pregunta distinta: ¿y si la app se diera cuenta de la rapidez con la que realmente consumes las cosas, y reconstruyera tu lista de la compra antes de que te quedes sin ellas?
Esa única idea — una predicción pequeña y honesta — dio forma a la mayor parte de la ingeniería. Así es como funciona, y por qué todo se ejecuta en local.
Un escaneo es un evento con marca de tiempo, no una fila
El modelo ingenuo es una tabla de artículos con una columna quantity que incrementas y decrementas. Es simple, y olvida todo en el mismo instante en que la editas.
En su lugar, Stocky almacena eventos. Cada escaneo, cada «usé un poco», cada reposición es un registro de solo añadir:
@Entity(tableName = "pantry_events")
data class PantryEvent(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val productId: String, // barcode / Open Food Facts id
val type: EventType, // ADDED, CONSUMED, DISCARDED
val amount: Double, // in the product's base unit (ml, g, pcs)
val at: Long, // epoch millis
)
La cantidad actual es simplemente un plegado (fold) sobre los eventos. Suena derrochador, pero ahí está todo el truco: como conservo el historial, puedo hacer preguntas que un total acumulado no puede responder — como «¿cuántos mililitros de esto consume la casa al día?»
La predicción es deliberadamente aburrida
Aquí no hay ninguna red neuronal. El consumo de aceite de oliva de una casa no es un problema que necesite un transformer — necesita un promedio que no reaccione de forma exagerada a un fin de semana intenso.
Para cada producto calculo una tasa de consumo sobre una ventana móvil, ponderando un poco más los eventos recientes:
fun dailyRate(events: List<PantryEvent>, now: Long): Double {
val consumed = events.filter { it.type == EventType.CONSUMED }
if (consumed.size < 2) return 0.0
val windowStart = now - 30.days.inWholeMilliseconds
val recent = consumed.filter { it.at >= windowStart }
if (recent.isEmpty()) return 0.0
val total = recent.sumOf { it.amount }
val spanDays = ((now - recent.first().at) / 1.days.inWholeMilliseconds)
.coerceAtLeast(1)
return total / spanDays
}
daysLeft = currentQuantity / dailyRate. Cuando ese valor cae por debajo de un umbral, el artículo aterriza discretamente en tu lista de la compra. ¿Aún no hay tasa? No hay predicción — Stocky no dice nada en lugar de adivinar. Una app tranquila prefiere quedarse en silencio antes que equivocarse.
Uso parcial, porque las cocinas son analógicas
La función que más ajustes requirió no fueron las matemáticas — fue dejarte decir «me bebí más o menos un tercio de esto». La gente no decrementa en números enteros. Un cartón de zumo se consume de 200 ml en 200 ml, y si la app te obliga a mantenerlo «lleno» o a marcarlo «terminado», la predicción es basura.
Así que la hoja de consumo habla en unidades reales y recuerda el tamaño del envase del producto, de modo que «un vaso» o «la mitad» se corresponden con algo que la función de tasa puede usar. Es un pedacito de interfaz que, sin hacer ruido, vuelve fiable todo el modelo.
Por qué nada de esto sale del teléfono
Cada número anterior se deriva de tus eventos, y tus eventos describen tu vida con un detalle incómodo — cuándo comes, cuánto, con qué frecuencia haces la compra. Esos son precisamente los datos que no quiero guardar.
Por eso no hay cuenta ni servidor que almacene tu despensa. Todo vive en una base de datos Room local. Las búsquedas de productos consultan Open Food Facts (y un conjunto comunitario de productos turcos), pero esa petición solo contiene un código de barras — nunca tu historial. La predicción se ejecuta en el dispositivo porque ahí es donde ya están los datos, y donde deberían quedarse.
Lo local primero no es aquí una casilla de marketing. Es la razón misma por la que la versión honesta de esta app pudo siquiera existir: no puedo perder, vender ni filtrar una base de datos que nunca recopilé.
Qué haría de otra manera
Si reconstruyera hoy el modelo de tasa, añadiría un término estacional suave — algunas cosas se disparan en verano — y un prior por categoría, para que un artículo recién estrenado tome prestada una tasa inicial razonable en lugar de quedarse callado durante una semana. Ambos son pequeños. Ninguno necesita la nube.
Stocky está disponible en Google Play. Si la pruebas, la lista de la compra que aparece por sí sola es la parte de la que más orgulloso estoy — y la parte que, con suerte, olvidarás que es siquiera ingeniosa.
// Lecturas relacionadas
Más del diario
Construir Granyn: un control de presupuesto sin inicio de sesión bancario, en tres tablas
Cómo Granyn controla el gasto en varias divisas y detecta facturas recurrentes sin vincular una sola cuenta bancaria — el esquema Room detrás de todo, y las concesiones que exige.
Android local-first en 2026: SQLite, Room y mantener los datos del usuario en el dispositivo
Una guía de 2026 para construir apps Android local-first con Room y SQLite — diseño de esquemas, migraciones, WAL, exportaciones y cuándo (y cuándo no) añadir sincronización.
OCR con la privacidad primero en Android: cómo Subly lee facturas sin la nube
Una guía práctica de 2026 sobre OCR en el dispositivo en Android con ML Kit Text Recognition: la configuración, el código y los trucos de extracción de campos que mantienen los datos del usuario fuera de tus servidores.