Stocky बनाना: आपकी रसोई में क्या खत्म होने वाला है, इसका पूर्वानुमान — वह भी डिवाइस पर ही
Stocky एक बारकोड स्कैन और थोड़े-से उपयोग इतिहास को ऐसी शॉपिंग लिस्ट में कैसे बदल देता है जो आपके खत्म होने से पहले ही सामने आ जाती है — और सारा गणित, सारा डेटा, आपके फोन पर ही रहता है।
ज़्यादातर पैंट्री ऐप्स बस सजी-धजी चेकलिस्ट होती हैं। आप दूध जोड़ते हैं, दूध पर निशान लगाते हैं, फिर से दूध जोड़ते हैं। ऐप कभी कुछ नहीं सीखता। Stocky एक अलग सवाल से शुरू हुआ: अगर ऐप यह भांप ले कि आप चीज़ों को असल में कितनी तेज़ी से इस्तेमाल करते हैं, और खत्म होने से पहले ही आपकी शॉपिंग लिस्ट फिर से बना दे तो?
बस यही एक विचार — एक छोटा, ईमानदार पूर्वानुमान — ने ज़्यादातर इंजीनियरिंग को आकार दिया। यहाँ बताया गया है कि यह कैसे काम करता है, और यह सब स्थानीय रूप से क्यों चलता है।
एक स्कैन एक टाइमस्टैम्प वाली घटना है, कोई पंक्ति नहीं
भोला-भाला मॉडल एक quantity कॉलम वाली वस्तुओं की तालिका है, जिसे आप बढ़ाते-घटाते रहते हैं। यह सरल है, और जिस पल आप इसे संपादित करते हैं, यह सब कुछ भूल जाता है।
इसके बजाय Stocky घटनाएँ संग्रहीत करता है। हर स्कैन, हर “थोड़ा इस्तेमाल किया”, हर पुनःपूर्ति केवल-जोड़ने वाला रिकॉर्ड है:
@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
)
वर्तमान मात्रा बस घटनाओं पर एक फोल्ड है। यह बर्बादी जैसा लगता है, पर पूरी चतुराई यही है: क्योंकि मैं इतिहास रखता हूँ, मैं ऐसे सवाल पूछ सकता हूँ जिनका जवाब एक चलता योग नहीं दे सकता — जैसे “यह घर इसका कितने मिलीलीटर प्रतिदिन इस्तेमाल कर लेता है?”
पूर्वानुमान जानबूझकर उबाऊ है
यहाँ कोई न्यूरल नेटवर्क नहीं है। किसी घर की जैतून तेल की खपत ऐसी समस्या नहीं जिसके लिए transformer चाहिए — इसे बस एक ऐसा औसत चाहिए जो एक बड़े सप्ताहांत पर हद से ज़्यादा प्रतिक्रिया न दे।
हर उत्पाद के लिए मैं एक पिछली अवधि (trailing window) पर खपत दर की गणना करता हूँ, हाल की घटनाओं को थोड़ा अधिक भार देते हुए:
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। जब यह मान किसी सीमा से नीचे गिरता है, तो वह वस्तु चुपचाप आपकी शॉपिंग लिस्ट में आ जाती है। अभी तक कोई दर नहीं? तो कोई पूर्वानुमान नहीं — Stocky अंदाज़ा लगाने के बजाय कुछ नहीं कहता। एक शांत ऐप ग़लत होने के बजाय चुप रहना पसंद करेगा।
आंशिक उपयोग, क्योंकि रसोइयाँ एनालॉग होती हैं
जिस फ़ीचर में सबसे ज़्यादा माथापच्ची लगी वह गणित नहीं था — वह आपको यह कहने देना था कि “मैंने इसका करीब एक-तिहाई पी लिया।” लोग पूर्णांकों में मात्रा नहीं घटाते। जूस का डिब्बा एक बार में 200 ml करके इस्तेमाल होता है, और अगर ऐप आपको या तो इसे “भरा” रखने या “खत्म” चिह्नित करने पर मजबूर करे, तो पूर्वानुमान बेकार हो जाता है।
इसलिए उपभोग शीट असली इकाइयों में बात करती है और उत्पाद के पैकेज आकार को याद रखती है, ताकि “एक गिलास” या “आधा” किसी ऐसी चीज़ से मेल खाए जिसे दर फ़ंक्शन इस्तेमाल कर सके। यह इंटरफ़ेस का एक नन्हा-सा हिस्सा है जो चुपचाप पूरे मॉडल को भरोसेमंद बना देता है।
यह सब फोन से बाहर क्यों नहीं जाता
ऊपर की हर संख्या आपकी घटनाओं से निकली है, और आपकी घटनाएँ आपकी ज़िंदगी को असहज कर देने वाले ब्यौरे में बयान करती हैं — आप कब खाते हैं, कितना, कितनी बार खरीदारी करते हैं। यही तो वह डेटा है जिसे मैं अपने पास रखना नहीं चाहता।
इसलिए न कोई खाता है, न कोई सर्वर जो आपकी पैंट्री संग्रहीत करे। सब कुछ एक स्थानीय Room डेटाबेस में रहता है। उत्पाद की खोज Open Food Facts (और तुर्की उत्पादों के एक सामुदायिक सेट) से होती है, पर उस अनुरोध में केवल एक बारकोड होता है — कभी आपका इतिहास नहीं। पूर्वानुमान डिवाइस पर चलता है क्योंकि डेटा पहले से वहीं है, और उसे वहीं रहना चाहिए।
यहाँ लोकल-फ़र्स्ट कोई मार्केटिंग का चेकबॉक्स नहीं है। यही वह वजह है जिससे इस ऐप का ईमानदार संस्करण संभव हो पाया: मैं ऐसा डेटाबेस न खो सकता हूँ, न बेच सकता हूँ, न लीक कर सकता हूँ जिसे मैंने कभी इकट्ठा ही नहीं किया।
मैं क्या अलग करता
अगर मैं आज दर मॉडल फिर से बनाता, तो मैं एक हल्का मौसमी पद जोड़ता — कुछ चीज़ें गर्मियों में बढ़ जाती हैं — और प्रति-श्रेणी एक prior जोड़ता, ताकि एक बिलकुल नई वस्तु एक हफ़्ते तक चुप रहने के बजाय एक समझदार शुरुआती दर उधार ले ले। दोनों छोटे हैं। किसी को क्लाउड की ज़रूरत नहीं।
Stocky Google Play पर उपलब्ध है। अगर आप इसे आज़माएँ, तो अपने-आप सामने आने वाली वह शॉपिंग लिस्ट वही हिस्सा है जिस पर मुझे सबसे ज़्यादा गर्व है — और वही हिस्सा जिसे, उम्मीद है, आप भूल जाएँगे कि यह कितना चतुर है।
// संबंधित पठन
जर्नल से और भी
Granyn बनाना: बिना बैंक लॉगिन वाला बजट ट्रैकर, बस तीन टेबल में
Granyn एक भी बैंक अकाउंट जोड़े बिना कई मुद्राओं में खर्च कैसे ट्रैक करता है और आवर्ती बिलों को कैसे पकड़ता है — इसके पीछे का Room स्कीमा, और इससे जुड़े समझौते।
2026 में लोकल-फर्स्ट Android: SQLite, Room, और यूज़र डेटा को डिवाइस पर ही रखना
Room और SQLite के साथ लोकल-फर्स्ट Android ऐप बनाने की 2026 गाइड — स्कीमा डिज़ाइन, माइग्रेशन, WAL, एक्सपोर्ट, और सिंक कब जोड़ें (और कब नहीं)।
Android पर प्राइवेसी-फर्स्ट OCR: Subly बिना क्लाउड के बिल कैसे पढ़ता है
2026 के लिए Android पर ऑन-डिवाइस OCR की व्यावहारिक गाइड, ML Kit Text Recognition के साथ — सेटअप, कोड, और वे फ़ील्ड-एक्सट्रैक्शन तरकीबें जो यूज़र के डेटा को आपके सर्वर से दूर रखती हैं।