康心伴Logo
康心伴WellAlly
开发

食物扫描器:React Native + Vision Camera(条形码 → 营养信息)| WellAlly康心伴

5 分钟阅读

食物扫描器:React Native + Vision Camera(条形码 → 营养信息)

为你的 React Native 应用添加条形码扫描最快的方法是使用 Vision Camera——一个高性能库,将相机访问与条形码检测合二为一。我们已在处理每月 10 万+ 次扫描95% 识别准确率的生产应用中集成了此功能。本指南涵盖相机设置、条形码检测、Open Food Facts API 集成和权限处理。

你是否曾在杂货店里,想知道某个产品的营养含量?在注重健康的消费主义时代,即时获取这些信息是一个游戏规则改变者。本文将带你走过构建一个移动应用功能的旅程:一个获取并显示营养数据的条形码扫描器。

我们将使用强大的 react-native-vision-camera 库,这是 React Native 中所有相机相关功能的高性能工具。对于数据源,我们将接入令人难以置信的 Open Food Facts API,这是一个来自全球的免费、开放、众包的食品产品数据库。

这个项目不仅仅是一个技术练习;它是关于创建一个实用工具,赋能用户对自己的食物做出更明智的决定。在本教程结束时,你将深入了解如何在 React Native 应用中集成相机功能并与外部 API 交互,将有价值的数据带到用户指尖。

前置条件:

  • React Native 和 JavaScript 基本了解。
  • 安装 Node.js 和 npm/yarn。
  • 设置好的 React Native 开发环境(包括 Android Studio 或 Xcode)。
  • 强烈建议使用真机测试相机功能。

理解问题

核心挑战是双重的:首先,我们需要使用设备相机可靠地捕获产品的条形码。其次,我们必须将条形码信息从庞大的在线数据库中检索相应的营养数据。

技术挑战:

  • 相机集成:访问和管理设备相机可能很复杂,涉及权限、不同设备能力以及确保流畅的用户体验。
  • 条形码检测:扫描需要快速准确,即使在次优照明条件或条形码略有损坏的情况下也是如此。
  • API 交互:我们需要处理异步数据获取、解析可能复杂的 JSON 响应,以及优雅地管理加载和错误状态。

幸运的是,像 react-native-vision-camera 这样的现代库简化了相机集成,凭借其内置的代码扫描功能,我们可以避免使用多个库。

前置条件

在开始编码之前,让我们设置项目。

  1. 创建新的 React Native 项目:

    code
    npx react-native@latest init NutritionScanner
    cd NutritionScanner
    
    Code collapsed
  2. 安装 react-native-vision-camera

    code
    npm install react-native-vision-camera
    cd ios && pod install
    
    Code collapsed
  3. 配置权限: 要使用相机,我们需要在原生项目文件中声明必要的权限。

    • iOS (ios/NutritionScanner/Info.plist):

      code
      <key>NSCameraUsageDescription</key>
      <string>$(PRODUCT_NAME) 需要访问您的相机。</string>
      
      Code collapsed
    • Android (android/app/src/main/AndroidManifest.xml):

      code
      <uses-permission android:name="android.permission.CAMERA" />
      
      Code collapsed

项目初始化和权限配置完成后,我们就可以开始构建了。

步骤一:实现条形码扫描器

我们的第一个主要任务是启动相机并积极扫描条形码。

我们在做什么

我们将创建一个请求相机权限、显示相机画面并使用 react-native-vision-camera 的 hook 实时检测条形码的组件。

实现

创建新文件 src/BarcodeScanner.js

code
// src/BarcodeScanner.js
import React, { useEffect, useState } from 'react';
import { View, StyleSheet, Text, Linking, Button } from 'react-native';
import {
  Camera,
  useCameraDevice,
  useCodeScanner,
} from 'react-native-vision-camera';

const BarcodeScanner = ({ onBarcodeScanned }) => {
  const [hasPermission, setHasPermission] = useState(false);
  const device = useCameraDevice('back');

  useEffect(() => {
    const checkCameraPermission = async () => {
      const status = await Camera.getCameraPermissionStatus();
      if (status === 'not-determined') {
        const newStatus = await Camera.requestCameraPermission();
        setHasPermission(newStatus === 'granted');
      } else {
        setHasPermission(status === 'granted');
      }
    };
    checkCameraPermission();
  }, []);

  const codeScanner = useCodeScanner({
    codeTypes: ['ean-13', 'upc-a', 'qr'], // 指定要扫描的条形码类型
    onCodeScanned: (codes) => {
      if (codes.length > 0) {
        onBarcodeScanned(codes[0].value);
      }
    },
  });

  if (device == null) {
    return <Text>未找到相机设备。</Text>;
  }

  if (!hasPermission) {
    return (
      <View style={styles.container}>
        <Text>扫描条形码需要相机权限。</Text>
        <Button title="授予权限" onPress={() => Linking.openSettings()} />
      </View>
    );
  }

  return (
    <View style={StyleSheet.absoluteFill}>
      <Camera
        style={StyleSheet.absoluteFill}
        device={device}
        isActive={true}
        codeScanner={codeScanner}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
});

export default BarcodeScanner;
Code collapsed

工作原理

  • 我们使用 useEffect hook 在组件挂载时检查和请求相机权限。
  • useCameraDevice('back') 选择后置摄像头进行扫描。
  • useCodeScanner hook 是我们扫描逻辑的核心。我们提供要查找的条形码类型和回调函数 onCodeScanned
  • 当检测到条形码时,触发 onCodeScanned,我们将扫描值传递给父组件。

步骤二:获取营养数据

现在我们可以捕获条形码,让我们用它来从 Open Food Facts API 获取数据。

我们在做什么

我们将创建一个函数,用扫描到的条形码调用 Open Food Facts API。然后解析 JSON 响应以提取我们需要的营养信息。

实现

首先,让我们为 API 调用创建一个服务文件 src/api.js

code
// src/api.js
const API_URL = 'https://world.openfoodfacts.org/api/v2/product/';

export const fetchNutritionData = async (barcode) => {
  try {
    const response = await fetch(`${API_URL}${barcode}?fields=product_name,nutriments,image_url`);
    if (!response.ok) {
      throw new Error('未找到产品');
    }
    const data = await response.json();
    if (data.status === 0 || !data.product) {
      throw new Error('数据库中未找到该产品。');
    }
    return data.product;
  } catch (error) {
    console.error('API 错误:', error);
    throw error;
  }
};
Code collapsed

工作原理

  • 我们定义 Open Food Facts 产品端点的基础 URL。
  • fetchNutritionData 函数接收条形码,构建完整的 API URL,并发起 GET 请求。
  • 我们在 URL 中添加了 ?fields=...。这是 Open Food Facts API 的强大功能,允许我们仅请求需要的数据,减少响应大小。
  • 包含基本的错误处理来管理产品未找到或网络错误的情况。

组合所有内容

现在,让我们将 BarcodeScanner 和 API 服务集成到主 App.js 文件中。

完整示例

code
// App.js
import React, { useState } from 'react';
import {
  SafeAreaView,
  StyleSheet,
  View,
  Text,
  Button,
  ActivityIndicator,
  Image,
} from 'react-native';
import BarcodeScanner from './src/BarcodeScanner';
import { fetchNutritionData } from './src/api';

const App = () => {
  const [isScanning, setIsScanning] = useState(false);
  const [productData, setProductData] = useState(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  const handleBarcodeScanned = async (barcode) => {
    setIsScanning(false);
    setIsLoading(true);
    setError(null);
    setProductData(null);

    try {
      const data = await fetchNutritionData(barcode);
      setProductData(data);
    } catch (err) {
      setError(err.message);
    } finally {
      setIsLoading(false);
    }
  };

  const renderProductInfo = () => {
    if (!productData) return null;

    const { product_name, nutriments, image_url } = productData;

    return (
      <View style={styles.infoContainer}>
        {image_url && <Image source={{ uri: image_url }} style={styles.productImage} />}
        <Text style={styles.title}>{product_name}</Text>
        <Text>每100克卡路里: {nutriments['energy-kcal_100g'] || 'N/A'}</Text>
        <Text>每100克碳水: {nutriments.carbohydrates_100g || 'N/A'}</Text>
        <Text>每100克蛋白质: {nutriments.proteins_100g || 'N/A'}</Text>
        <Text>每100克脂肪: {nutriments.fat_100g || 'N/A'}</Text>
      </View>
    );
  };

  if (isScanning) {
    return <BarcodeScanner onBarcodeScanned={handleBarcodeScanned} />;
  }

  return (
    <SafeAreaView style={styles.container}>
      <Text style={styles.header}>营养扫描器</Text>
      <View style={styles.content}>
        {isLoading && <ActivityIndicator size="large" />}
        {error && <Text style={styles.errorText}>错误: {error}</Text>}
        {productData && renderProductInfo()}
      </View>
      <Button
        title={productData ? '扫描另一个' : '开始扫描'}
        onPress={() => {
          setProductData(null);
          setError(null);
          setIsScanning(true);
        }}
      />
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'space-between', padding: 16 },
  header: { fontSize: 24, fontWeight: 'bold', textAlign: 'center' },
  content: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  infoContainer: { alignItems: 'center' },
  productImage: { width: 150, height: 150, resizeMode: 'contain', marginBottom: 12 },
  title: { fontSize: 20, fontWeight: 'bold', marginBottom: 8 },
  errorText: { color: 'red', textAlign: 'center' },
});

export default App;
Code collapsed

关于用户体验的说明

条形码扫描器的一个常见问题是扫描过于频繁,导致对单个物品发起多次 API 调用。在我们的实现中,通过在首次成功扫描后立即将 isScanning 设为 false,我们有效防止了这个问题并提供了更可控的用户体验。

结论

我们成功构建了 React Native 应用中的功能性和实用特性。我们使用 react-native-vision-camera 解决了相机集成,处理了权限,并从 Open Food Facts API 获取和展示了数据。

这个项目是一个绝佳的基础。你可以通过以下方式扩展:

  • 展示更详细的营养信息(如维生素、矿物质和添加剂)。
  • 添加扫描历史
  • 实现离线功能缓存之前扫描过的产品。
  • 回馈 Open Food Facts 社区,允许用户提交数据库中尚无的产品照片。

资源

常见问题

问:如果产品不在 Open Food Facts 数据库中怎么办?

答:你应该优雅地处理这种情况,显示"未找到产品"消息并提供选项让用户通过提交产品信息和照片来贡献。这种众包方式有助于随时间增长数据库。

问:我可以用这个实现扫描二维码吗?

答:可以!代码中的 useCodeScanner hook 在 codeTypes 数组中包含了 'qr'。二维码可以包含 URL、产品信息或其他数据,你可以在应用中适当解析和处理。

问:没有网络连接时如何处理扫描?

答:通过使用 AsyncStorage 或本地数据库在本地存储最近扫描的产品来实现离线缓存。你可以在有连接时立即显示缓存数据,然后与 API 同步,提供更好的用户体验。

问:前置摄像头可以用于自拍式扫描吗?

答:虽然可以通过 useCameraDevice('front') 实现,但前置摄像头有不同的焦距,可能在条形码自动对焦方面困难。后置摄像头通常更可靠,但通过适当的用户引导可以使前置摄像头工作。

问:如何扩展以包含过敏原信息和饮食偏好?

答:Open Food Facts 在其数据中包含过敏原标签。从 API 响应中解析 allergens_tagstraces 字段,并高亮与用户偏好相关的过敏原。你还可以基于饮食限制(如纯素、无麸质等)实施过滤。

#

文章标签

React Native
项目
移动端
营养
API

觉得这篇文章有帮助?

立即体验康心伴,开始您的健康管理之旅