Сканирование штрихкодов с 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 — отсканируйте штрихкод, и остальной учёт кухни последует из этого. Подробнее на странице приложения.
// По теме
Ещё из журнала
Jetpack DataStore в 2026 году: переход с SharedPreferences без потери ни одной настройки
Практическое руководство по переходу с SharedPreferences на Jetpack DataStore на Android — асинхронные ловушки, путь миграции, сохраняющий существующие значения, и как это протестировать.
Миграции баз данных Room в 2026 году: как менять схему, не теряя ни одной строки
Практическое руководство по миграциям баз данных Room на Android — AutoMigration, написанные вручную объекты Migration и как протестировать миграцию до того, как её найдут ваши пользователи.
Baseline Profiles в Android: что на самом деле влияет на время холодного запуска в 2026 году
Практическое руководство по Android Baseline Profiles — генерация через Macrobenchmark, подключение в Gradle, честное измерение выигрыша и ещё три вещи, которые сильнее влияют на холодный запуск.