2026年、AndroidにおけるML Kitバーコードスキャン:Stockyが1秒未満でパントリーに商品を追加する仕組み
ML KitとCameraXによる端末内バーコードスキャンの実践的な2026年版ガイド。フォーマットの絞り込み、オフラインの商品検索、そしてパントリーアプリを支える部分使用の計算まで。
食料品をアプリに手入力するのは、買うたびに支払う小さな税金のようなものだ。名前、ブランド、サイズ、時にはカテゴリー — キッチンの在庫を管理する人にとって、それは所有するすべての商品にかかる30秒の摩擦を掛け合わせたものになる。使う価値のあるパントリーアプリはこの計算に勝たなければならず、それはつまりバーコードがほぼすべての作業を担う必要があるということだ。
これがStockyが組み立てられている前提だ。カメラをバーコードに向ければ、1秒後には商品がパントリーに入っている — 名前、サイズ、カテゴリー、栄養データ、すべて自動で埋まる。ここでは、その1秒の裏側にあるパイプラインと、スキャン自体よりも仕上げに時間がかかった部分を解説する。
スキャンのパイプライン:CameraX + ML Kit Barcode Scanning
ML KitのBarcode Scanning APIはText Recognitionとは別のモデルで、より小さく、より高速で、たった一つの仕事のために作られている——カメラのフレーム内にあるバーコードのフォーマットとエンコードされた値を見つけることだ。セットアップはGradleの1行と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のいずれかに収まる。この4フォーマットに絞ることで、各フレームの無駄な処理が減るだけでなく、背景の偶発的なパターンによる誤検出も抑えられる——こちらの方が実は効いてくる。
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) }
}
ここでの2つの判断は、コードが示す以上の重みを持っている。
- ネットワーク優先ではなく、キャッシュ優先。 あるバーコードが一度でも解決されれば——同じ端末での過去のスキャンを含め、誰によるものでも——それはRoomに入っており、次のスキャンは即座かつオフラインで完了する。パントリーアプリでは、同じ十数種類の商品(牛乳、卵、いつものパンのブランド)が繰り返しスキャンされるため、キャッシュのヒット率はすぐに上がっていく。
- 見つからないことは行き止まりではない。 ノーブランド品や地域限定の商品は、常にOpen Food Factsに存在するとは限らない。検索結果が空だった場合、Stockyは単なるエラー表示ではなく、バーコードがあらかじめ入力された短い手動入力フォームに直接遷移する。ユーザーは2つの項目を埋めるだけで完了し——同意があれば——その入力が、次に同じバーコードをスキャンする誰かのためにコミュニティのデータセットを強化することになる。
「1秒未満」を誠実に保つ
スキャンモデル自体は、ミドルレンジのスマートフォンでも100ミリ秒を大きく下回る時間でバーコードを解決する。体感される遅延の予算のほとんどは、周辺の処理に費やされる——カメラのウォームアップ、アナライザーのフレームレート、そして重複検出の回避だ。
体感の質に本当に効いてくるのはこの点だ。デバウンス処理がなければ、構えたままのカメラは毎フレームでアナライザーを発火させる——手が安定するまでの間に、同じバーコードが20回以上検出されることになる。そのたびに検索・触覚フィードバック・UI更新が走れば、それは速さではなく、もたつきとして体感される。
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秒のデバウンスウィンドウを設けるだけで、一連の検出の連発を1回のスキャンにまとめられる。それでいて、ユーザーが一度カメラを離してまた向け直すことで、同じ商品を意図的にすぐ再スキャンすること(たとえばヨーグルトをもう1個パントリーに追加するなど)は妨げない。
部分使用という問題
パントリーの商品は「ある・ない」の二値フラグではない——そしてこれこそが、バーコードスキャンだけでは解決できない部分だ。Stockyの実際のデータモデルは、各パッケージについて残量を追跡する。1リットルの牛乳パックは「在庫あり」でも「在庫切れ」でもなく、まず1000mlであり、朝食後には700mlになり、やがて0になって静かに買い物リストへ移動する。同じパッケージのバーコードを再スキャンすることで、ユーザーは「300ml飲んだ」という部分的な消費を記録できる——冷蔵庫にまだ半分残っているものに対して、「使い切った、新しいものを追加する」という完全なサイクルを強いる代わりに。
この一つの設計判断こそが、後の在庫切れ予測を誠実なものにしている。毎回ゼロに戻り、消費のペースについて何も語らない粗雑な在庫あり/なしの切り替えではなく、商品ごとの本物の消費曲線の上に成り立っているからだ。
端末に残るもの、残らないもの
フレームは決してスマートフォンの外へ出ない——ML Kitのバーコードモデルは、テキスト認識のきょうだいと同様に完全に端末内で動作する。ネットワークを通過するのはバーコードの数字そのものだけで、それは他の何百万人もの買い物客がすでにスキャンしたことのある商品を検索するために送られる。写真は一切キャプチャも保存もされず、誰がスキャンしたかに関する情報がリクエストに付随することもない。パントリーの中身、消費履歴、支出は端末内にとどまり、共有されるのは匿名の商品検索だけだ。
これはOCRよりも小さいプライバシー面であり、機能のためにどのビジョンAPIを選ぶかを考えるときに覚えておく価値がある。必要な価値が固定された構造化された識別子——文章のページではなくバーコード——であるなら、スキャナーモデルは、誰かの文書を読み取らなければならない認識モデルよりも、実装が速く、プライバシーポリシー上でも説明しやすい。
Stockyは現在Google Playで公開中だ——バーコードをスキャンすれば、あとのキッチン管理はそこから続いていく。アプリページで詳しく見る。
// 関連記事
ジャーナルの他の記事
2026年のJetpack DataStore:設定を一つも失わずにSharedPreferencesから移行する
SharedPreferencesからJetpack DataStoreへの移行に関する実践ガイド — 非同期の落とし穴、既存の値を保持する移行パス、そしてそれをどうテストするか。
2026年のRoomデータベースマイグレーション: 1行もデータを失わずにスキーマ変更を出荷する
AndroidのRoomデータベースマイグレーションに関する実践ガイド — AutoMigration、手書きのMigrationオブジェクト、そしてユーザーがバグを見つける前にマイグレーションをテストする方法。
Androidのベースラインプロファイル: 2026年、コールドスタート時間を実際に左右するもの
Android Baseline Profilesの実践ガイド — Macrobenchmarkでの生成方法、Gradleへの組み込み、実際の効果測定、そしてコールドスタートを左右する他の3つの要素。