Сканирование штрихкодов с ML Kit на Android в 2026 году: как Stocky добавляет товар в кладовую меньше чем за секунду
Практическое руководство 2026 года по сканированию штрихкодов на устройстве с ML Kit и CameraX: настройка форматов, офлайн-поиск товара и математика частичного расхода в приложении для кладовой.
Вручную вводить продукт в приложение — это небольшой налог, который вы платите каждый раз при покупке. Название, бренд, размер, иногда категория — для того, кто пополняет запасы на кухне, это тридцать секунд трения, умноженные на каждый товар, который у него есть. Единственная версия приложения для кладовой, которую стоит использовать, обязана победить эту математику, а значит, почти всю работу должен делать штрихкод.
Именно на этом ограничении построен Stocky: наведите камеру на штрихкод — и через секунду товар уже в кладовой: название, размер, категория, данные о питании — всё заполнено. Вот конвейер, который стоит за этой секундой, и те части, доводка которых заняла больше времени, чем само сканирование.
Конвейер сканирования: CameraX + ML Kit Barcode Scanning
API ML Kit Barcode Scanning — это отдельная модель от Text Recognition: меньше, быстрее и заточенная ровно под одну задачу — найти формат и закодированное значение штрихкода внутри кадра камеры. Настройка — одна строка в Gradle плюс анализатор CameraX:
// app/build.gradle.kts
dependencies {
implementation("com.google.mlkit:barcode-scanning:17.3.0")
implementation("androidx.camera:camera-camera2:1.4.0")
implementation("androidx.camera:camera-lifecycle:1.4.0")
}
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import com.google.mlkit.vision.common.InputImage
import androidx.camera.core.ImageProxy
private val options = BarcodeScannerOptions.Builder()
.setBarcodeFormats(
Barcode.FORMAT_EAN_13,
Barcode.FORMAT_EAN_8,
Barcode.FORMAT_UPC_A,
Barcode.FORMAT_UPC_E,
)
.build()
private val scanner = BarcodeScanning.getClient(options)
@androidx.camera.core.ExperimentalGetImage
fun analyzeFrame(image: ImageProxy, onFound: (String) -> Unit) {
val media = image.image ?: return image.close()
val input = InputImage.fromMediaImage(media, image.imageInfo.rotationDegrees)
scanner.process(input)
.addOnSuccessListener { barcodes ->
barcodes.firstOrNull()?.rawValue?.let(onFound)
}
.addOnCompleteListener { image.close() }
}
Вызов setBarcodeFormats значит больше, чем кажется на первый взгляд. Оставленный по умолчанию, сканер также ищет QR-коды, PDF417, Aztec и ещё полдюжины форматов, которые он никогда не увидит на банке томатов — и каждый кадр платит за этот поиск. Розничные товары почти целиком укладываются в EAN-13, EAN-8, UPC-A и UPC-E. Ограничение этими четырьмя форматами снижает лишнюю работу на каждом кадре и, что ещё полезнее, уменьшает число ложных срабатываний от случайных узоров на заднем плане.
От 13-значного числа к товару, который можно показать
Штрихкод — это просто цифры. Реальный товар — название, бренд, размер упаковки, данные о питании — должен приходить откуда-то ещё. Stocky разрешает это через Open Food Facts, открытую базу данных товаров, наполняемую сообществом, дополненную турецкими товарами, добавленными пользователями, которые общий датасет часто упускает.
Сам поиск — это обычный REST-запрос по отсканированному коду:
suspend fun lookupProduct(barcode: String): ProductInfo? {
localCache.find(barcode)?.let { return it }
return runCatching {
api.getProduct(barcode).toProductInfo()
}.getOrNull()?.also { localCache.save(barcode, it) }
}
Здесь два решения весят больше, чем кажется по коду:
- Сначала кэш, а не сеть. Как только штрихкод был распознан хотя бы раз — кем угодно, включая предыдущее сканирование на том же устройстве, — он оказывается в Room, и следующее сканирование происходит мгновенно и офлайн. Для приложения кладовой одни и те же десяток товаров (молоко, яйца, тот же бренд хлеба) сканируются постоянно; процент попаданий в кэш быстро растёт.
- Промах — не тупик. Товары без бренда или специфичные для региона не всегда есть в Open Food Facts. Когда поиск возвращает пусто, Stocky сразу переходит к короткой форме ручного ввода, уже заполненной штрихкодом, вместо голой ошибки. Пользователь завершает ввод, заполнив всего два поля, и — с его согласия — эта запись может усилить датасет сообщества для следующего, кто отсканирует тот же код.
Как сделать «меньше секунды» честной цифрой
Сама модель сканирования распознаёт штрихкод значительно быстрее 100 мс даже на телефоне среднего класса. Почти весь бюджет ощущаемой задержки уходит на всё, что вокруг: прогрев камеры, частота кадров анализатора и избегание повторных срабатываний.
Деталь, которая действительно влияет на ощущение: без дебаунса удерживаемая камера запускает анализатор на каждом кадре — более двадцати срабатываний одного и того же штрихкода за то время, пока рука успевает застыть. Запуск поиска, тактильного отклика и обновления интерфейса на каждое из них ощущается как подтормаживание, а не как скорость.
private var lastCode: String? = null
private var lastAt = 0L
fun onBarcodeFound(code: String, now: Long) {
if (code == lastCode && now - lastAt < 1_500) return
lastCode = code
lastAt = now
handleScan(code)
}
Окна дебаунса в 1,5 секунды на код достаточно, чтобы свернуть целую серию срабатываний в одно сканирование, но при этом позволить пользователю намеренно сразу пересканировать тот же товар (например, добавить в кладовую вторую баночку йогурта), убрав и снова наведя камеру.
Проблема частичного расхода
Товар в кладовой — не бинарный флаг «есть или нет», и именно эту часть одно только сканирование штрихкода не решает. Реальная модель данных Stocky отслеживает остаток по каждой упаковке: литровый пакет молока — это не «в наличии» или «закончился», это 1000 мл, затем 700 мл после завтрака, затем 0, и он тихо переезжает в список покупок. Повторное сканирование штрихкода на той же упаковке позволяет пользователю зафиксировать частичный расход — «выпил 300 мл» — вместо того чтобы заставлять его проходить полный цикл «закончилось, добавь новое» ради того, что всё ещё наполовину полно в холодильнике.
Именно это единственное дизайнерское решение делает честными последующие прогнозы окончания: они строятся на реальной кривой расхода для каждого товара, а не на грубом переключателе в наличии/закончилось, который каждый раз сбрасывается в ноль и ничего не говорит о реальном темпе.
Что остаётся на устройстве, а что — нет
Кадр никогда не покидает телефон — модель штрихкодов ML Kit работает полностью на устройстве, как и её родственная модель распознавания текста. Через сеть проходят только сами цифры штрихкода, отправленные, чтобы найти товар, который уже отсканировали миллионы других покупателей. Ни одна фотография не захватывается и не сохраняется, и ничего о том, кто сканирует, к запросу не прикладывается. Содержимое кладовой, история расхода и траты остаются локально; передаётся только анонимный поиск товара.
Это меньшая площадь для утечки приватности, чем у OCR, и об этом стоит помнить при выборе между API компьютерного зрения для новой функции: если нужное вам значение — это фиксированный, структурированный идентификатор — штрихкод, а не страница текста, — модель сканера и быстрее внедрить, и проще защитить в политике конфиденциальности, чем распознаватель, которому приходится читать чей-то документ.
Stocky уже доступен в Google Play — отсканируйте штрихкод, и остальной учёт кухни последует из этого. Подробнее на странице приложения.
// По теме
Ещё из журнала
WorkManager в 2026 году: уникальная работа, цепочки задач и тестирование фоновых заданий на Android
Практическое руководство по WorkManager на Android — политики уникальной работы, цепочки задач, ускоренное выполнение и как на самом деле протестировать фоновое задание перед релизом.
Android App Links в 2026 году: почему ваши https:// ссылки всё ещё открываются в браузере
Верификация Digital Asset Links, подводные камни assetlinks.json и adb-команды, которые объясняют, почему Android не передаёт ссылку вашему приложению.
Room TypeConverters в 2026 году: как хранить enum'ы, даты и списки, не повреждая схему
Практическое руководство по Room TypeConverters на Android — enum'ы, Instant/LocalDate и списки — а также ошибки, которые превращают конвертер в незаметный баг повреждения данных.