如何在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
相关产品推荐
相关产品推荐

