You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Flutter中实现Firebase Realtime Database单引用合并存储多类数据

实现方案

核心逻辑说明

  • Firebase Realtime Database 仅适合存储结构化文本数据,因此你拍摄的图片需要先上传到 Firebase Storage,拿到图片的公共下载链接后,再和扫码结果一起存入 Realtime Database 的同一个节点下
  • 提前为本次存储生成唯一的节点key,后续新增条码、上传的图片链接都统一写入该节点,实现数据关联

步骤1:改造 BarcodesResultPreviewWidget 页面

首先将原Stateless组件改为Stateful,新增状态管理当前待存储的条码列表、图片链接列表,以及本次专属的存储节点引用:

import 'package:firebase_storage/firebase_storage.dart' as firebase_storage;

class BarcodesResultPreviewWidget extends StatefulWidget {
  final BarcodeScanningResult preview;
  const BarcodesResultPreviewWidget(this.preview, {Key? key}) : super(key: key);

  @override
  State<BarcodesResultPreviewWidget> createState() => _BarcodesResultPreviewWidgetState();
}

class _BarcodesResultPreviewWidgetState extends State<BarcodesResultPreviewWidget> {
  FirebaseFirestore firestore = FirebaseFirestore.instance;
  final firestoreInstance = FirebaseFirestore.instance;
  final DatabaseReference databaseRef = FirebaseDatabase.instance.reference().child('ScannedResults');
  // 本次存储的专属节点引用
  late DatabaseReference _currentRef;
  // 存储所有扫码结果
  late List<BarcodeItem> _barcodeItems;
  // 存储上传后的图片下载链接
  final List<String> _imageUrls = [];

  @override
  void initState() {
    super.initState();
    // 提前生成唯一的存储节点,后续所有数据都写入该节点
    _currentRef = databaseRef.push();
    // 初始化扫码结果列表
    _barcodeItems = widget.preview.barcodeItems;
  }

  // 上传图片到Storage并返回下载链接
  Future<String> _uploadImage(File image) async {
    String fileName = DateTime.now().millisecondsSinceEpoch.toString();
    firebase_storage.Reference ref = firebase_storage.FirebaseStorage.instance
        .ref()
        .child('scanned_images/$fileName');
    await ref.putFile(image);
    return await ref.getDownloadURL();
  }

  //Retrieving the data on console
  void getData(){
    databaseRef.once().then((DataSnapshot snapshot){
      Map<dynamic, dynamic> values = snapshot.value;
      values.forEach((key,values) {
        print(values["ScannedItem"]);
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    Widget previewView;
    var barcodeImageURI = widget.preview.barcodeImageURI;
    if (barcodeImageURI != null) {
      if (shouldInitWithEncryption) {
        previewView = EncryptedPageWidget(barcodeImageURI);
      } else {
        previewView = PageWidget(barcodeImageURI);
      }
    } else {
      previewView = Container();
    }
    return Scaffold(
        appBar: AppBar(
          iconTheme: IconThemeData(),
          leading: GestureDetector(
            child: Icon(
              Icons.arrow_back,
              color: Colors.black,
            ),
            onTap: () {
              Navigator.of(context).pop();
            },
          ),
          backgroundColor: Colors.white,
          title: const Text('Scanned barcodes',
              style: TextStyle(inherit: true, color: Colors.black)),
        ),
        body: Column(
          children: <Widget>[
            previewView,
            // 展示扫码结果
            Expanded(
              child: ListView.builder(
                itemCount: _barcodeItems.length,
                itemBuilder: (context, position) {
                  return BarcodeItemWidget(_barcodeItems[position]);
                },
              ),
            ),
            // 可选:添加展示已上传图片的区域
            if(_imageUrls.isNotEmpty)
              SizedBox(
                height: 100,
                child: ListView.builder(
                  scrollDirection: Axis.horizontal,
                  itemCount: _imageUrls.length,
                  itemBuilder: (context, index) => Padding(
                    padding: const EdgeInsets.all(4),
                    child: Image.network(_imageUrls[index], width: 80, fit: BoxFit.cover),
                  ),
                ),
              ),
            Wrap(
              children: [
                Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: FlatButton(
                    color: Colors.grey,
                    child: const Text('Add Another Item'),
                    onPressed: () async {
                      // 跳转扫码页,接收新扫到的条码
                      final newBarcode = await Navigator.push(
                        context,
                        MaterialPageRoute(builder: (context) => const MainPageWidget()),
                      );
                      if(newBarcode != null && newBarcode is BarcodeItem){
                        setState(() {
                          _barcodeItems.add(newBarcode);
                        });
                      }
                    },
                  ),
                ),
                Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: FlatButton(
                    color: Colors.grey,
                    child: const Text('Capture an Image'),
                    onPressed: () async {
                      // 跳转拍照页,接收拍摄的图片文件
                      final takenImages = await Navigator.push(
                        context,
                        MaterialPageRoute(builder: (context) => const TakePictureScreen(title: '')),
                      );
                      if(takenImages != null && takenImages is List<File>){
                        // 批量上传图片
                        for(var img in takenImages){
                          String url = await _uploadImage(img);
                          _imageUrls.add(url);
                        }
                        setState(() {});
                      }
                    },
                  ),
                ),
                Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: FlatButton(
                    color: Colors.grey,
                    child: const Text('Save'),
                    onPressed: () {
                      // 所有数据存入同一个节点
                      _currentRef.set({
                        'ScannedItem': _barcodeItems.map((barCodeItem) => barCodeItem.toJson()).toList(),
                        'imageUrls': _imageUrls,
                        'createTime': DateTime.now().millisecondsSinceEpoch
                      });
                      // 保存成功后可跳转回上一页
                      Navigator.pop(context);
                    },
                  ),
                ),
              ],
            ),
          ],
        ));
  }
}

步骤2:改造 TakePictureScreen 页面

新增回调逻辑,拍摄完成后点击返回时把所有拍摄的图片回传给上一页:

class TakePictureScreen extends StatefulWidget {
  const TakePictureScreen({Key? key, required this.title}) : super(key: key);
  final String title;

  @override
  _TakePictureScreenState createState() => _TakePictureScreenState();
}

class _TakePictureScreenState extends State<TakePictureScreen> {
  final photos = <File>[];

  void openCamera() {
    Navigator.push(
        context,
        MaterialPageRoute(
            builder: (_) => CameraCamera(
              onFile: (file) {
                photos.add(file);
                Navigator.pop(context);
                setState(() {});
              },
            )));
  }

  @override
  Widget build(BuildContext context) {
    final size = MediaQuery.of(context).size;
    return WillPopScope(
      // 拦截返回事件,把拍摄的图片列表回传给上一页
      onWillPop: () async {
        Navigator.pop(context, photos);
        return true;
      },
      child: Scaffold(
        backgroundColor: Colors.black,
        appBar: AppBar(
          title: Text(widget.title),
        ),
        body: GridView.builder(
          gridDelegate:
          const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
          itemCount: photos.length,
          itemBuilder: (_, index) => Padding(
            padding: const EdgeInsets.all(8.0),
            child: Container(
              width: size.width,
              child: Image.file(
                photos[index],
                fit: BoxFit.cover,
              ),
            ),
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: openCamera,
          child: const Icon(Icons.camera_alt),
        ),
      ),
    );
  }
}

步骤3:配置Firebase Storage权限

测试阶段可以先把Storage权限设为公开访问,生产环境请根据业务逻辑配置身份校验:

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read, write: if true;
    }
  }
}

最终存储到Realtime Database的结构如下:

ScannedResults
  └─ 随机生成的节点key
      ├─ ScannedItem: [条码1的JSON, 条码2的JSON...]
      ├─ imageUrls: [图片1下载链接, 图片2下载链接...]
      └─ createTime: 存储时间戳

内容的提问来源于stack exchange,提问作者neo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.03 10:09:01