Stocky'yi Geliştirmek: mutfağınızda neyin biteceğini tahmin etmek — hem de cihazın üzerinde
Stocky bir barkod taramasını ve biraz kullanım geçmişini, siz daha bitirmeden önünüze düşen bir alışveriş listesine nasıl dönüştürüyor — tüm matematik ve tüm veriler telefonunuzda kalarak.
Çoğu kiler uygulaması, süslenmiş bir yapılacaklar listesinden ibarettir. Süt eklersiniz, sütü işaretlersiniz, sonra tekrar süt eklersiniz. Uygulama hiçbir şey öğrenmez. Stocky farklı bir soruyla yola çıktı: ya uygulama, şeyleri gerçekte ne kadar hızlı tükettiğinizi fark etseydi ve siz daha bitirmeden alışveriş listenizi yeniden kursaydı?
Bu tek fikir — küçük, dürüst bir tahmin — mühendisliğin büyük kısmını şekillendirdi. İşte nasıl çalıştığı ve neden hepsinin yerel olarak çalıştığı.
Bir tarama, bir satır değil, zaman damgalı bir olaydır
Naif model, artırıp azalttığınız bir quantity sütununa sahip bir ürün tablosudur. Basittir ve onu düzenlediğiniz anda her şeyi unutur.
Stocky bunun yerine olayları saklar. Her tarama, her “biraz kullanıldı”, her yeniden stoklama, yalnızca ekleme yapılan bir kayıttır:
@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
)
Mevcut miktar, sadece olayların bir toplamıdır (fold). Kulağa savurgan geliyor ama bütün numara bu: geçmişi sakladığım için, çalışan bir toplamın yanıtlayamayacağı soruları sorabiliyorum — örneğin “bu evde bundan günde kaç mililitre tüketiliyor?”
Tahmin bilinçli olarak sıradan
Burada sinir ağı yok. Bir evin zeytinyağı tüketimi, bir transformer gerektiren bir problem değildir — büyük bir hafta sonuna aşırı tepki vermeyen bir ortalamaya ihtiyaç duyar.
Her ürün için, geriye dönük bir pencere üzerinde, yakın zamandaki olaylara biraz daha ağırlık vererek bir tüketim oranı hesaplıyorum:
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. Bu değer bir eşiğin altına düştüğünde, ürün sessizce alışveriş listenize düşüyor. Henüz oran yok mu? Tahmin de yok — Stocky tahmin yürütmek yerine hiçbir şey söylemez. Sakin bir uygulama, yanlış olmaktansa sessiz kalmayı tercih eder.
Kısmi kullanım, çünkü mutfaklar analogdur
En çok uğraştıran özellik matematik değildi — “bunun aşağı yukarı üçte birini içtim” diyebilmenizi sağlamaktı. İnsanlar tam sayılarla azaltmaz. Bir kutu meyve suyu 200 ml’lik yudumlarla tüketilir ve uygulama sizi ya “dolu” tutmaya ya da “bitti” işaretlemeye zorlarsa, tahmin çöp olur.
Bu yüzden tüketim ekranı gerçek birimlerle konuşuyor ve ürünün ambalaj boyutunu hatırlıyor; böylece “bir bardak” ya da “yarısı”, oran fonksiyonunun kullanabileceği bir şeye karşılık geliyor. Bütün modeli sessizce güvenilir kılan minik bir arayüz parçası.
Neden bunların hiçbiri telefondan çıkmıyor
Yukarıdaki her sayı, olaylarınızdan türetiliyor ve olaylarınız hayatınızı rahatsız edici bir ayrıntıyla anlatıyor — ne zaman yediğinizi, ne kadar, ne sıklıkla alışveriş yaptığınızı. İşte tam da elimde tutmak istemediğim veri bu.
Bu yüzden ne bir hesap var ne de kilerinizi saklayan bir sunucu. Her şey yerel bir Room veritabanında yaşıyor. Ürün aramaları Open Food Facts’e (ve topluluk kaynaklı bir Türk ürünleri setine) gidiyor, ama o istek yalnızca bir barkod içeriyor — asla geçmişinizi değil. Tahmin cihaz üzerinde çalışıyor çünkü veri zaten orada ve orada kalması gerekiyor.
Yerel öncelikli olmak burada bir pazarlama kutucuğu değil. Bu uygulamanın dürüst sürümünü mümkün kılan şeyin ta kendisi: Hiç toplamadığım bir veritabanını kaybedemem, satamam ya da sızdıramam.
Neyi farklı yapardım
Oran modelini bugün yeniden kursaydım, nazik bir mevsimsel terim eklerdim — bazı şeyler yazın artar — ve kategori başına bir öncül (prior) eklerdim; böylece yepyeni bir ürün, bir hafta boyunca sessiz kalmak yerine makul bir başlangıç oranı ödünç alırdı. İkisi de küçük. İkisinin de buluta ihtiyacı yok.
Stocky Google Play’de yayında. Denerseniz, kendiliğinden beliren o alışveriş listesi en gurur duyduğum kısım — ve umarım akıllıca olduğunu bile unutacağınız kısım.
// İlgili okumalar
Günlükten dahası
Granyn'i Geliştirmek: banka girişi olmayan bir bütçe takipçisi, üç tabloda
Granyn tek bir banka hesabı bağlamadan, para birimleri arasında harcamaları nasıl takip ediyor ve tekrarlayan faturaları nasıl yakalıyor — altındaki Room şeması ve bunun getirdiği ödünleşimler.
2026'da local-first Android: SQLite, Room ve kullanıcı verisini cihazda tutmak
Room ve SQLite ile local-first Android uygulamaları geliştirmenin 2026 rehberi — şema tasarımı, migration'lar, WAL, dışa aktarma ve senkronizasyonu ne zaman eklemeli (ve ne zaman eklememeli).
Android'de gizlilik öncelikli OCR: Subly faturaları buluta göndermeden nasıl okuyor
ML Kit Text Recognition ile Android'de cihaz üzerinde OCR için pratik bir 2026 rehberi — kurulum, kod ve kullanıcı verisini sunucularınızdan uzak tutan alan çıkarma teknikleri.