Flutter上传多张图片到Firebase对象存储空间

开场废话

Hello,大家好,很久没来写心得了,一是最近自己工作比较忙,二是最近一直在折腾如何把之前给老婆做的diy活动app的内容搬到云端。为什么要放在云端呢,因为做产品的都懂的,用户的需求是永远不会断的。有一次老婆在用app记录活动的时候问我,如果换手机或者手机坏了,丢了数据还有吗? 这。。。因为之前我是用的本地数据库,所以如果手机丢了坏了数据还真没了。所以本着满足用户一切需求的想法,我想办法给她把数据搬云端去。

废话少说,先看东西

用苹果的捷径直接视频转gif的,所以清晰度可能有点差,大家见谅


IMG_0516.GIF

最后结束的时候可能你发现突然少了一张,那是我点击的右上角X,想演示删除操作的。

一、图片云端存储方案

1、需求分析

老婆要数据不丢失(这次我们先解决图片的问题)

2、原来图片存储方式

之前是通过两种方式存储

1、每次复制一张图片到app文档目录,然后把地址存入本地数据库文件。
2、直接把图片原始数据uint8List数据存入数据库
这里不讨论这两种方式的好坏,关键问题是一旦手机丢或坏,那么图片数据就都没了。

3、为什么我选择Firebase_storage对象存储

网上查了很多资料,国内的诸如阿里云、腾讯云虽然都有对象存储,但是都要依赖后端,是的,我就是一个自学flutter,不会后端的菜鸡,而Firebase的对象存储和数据库都是帮你把后端服务给做好了的,你直接使用就可以,只要简单看下api就可以上手使用。

二、app添加Firebase以及使用的相关插件

(往下的内容可能都需要科学上网)
1、应用配置
要在自己的app里应用firebase体系的东西需要先进行配置,配置方法我这里就不说了,我推荐去firebase官方学习文档里学习一下,都是中文说明,所以应该没有困难。我这里贴一个官方地址教你如何进行配置:
https://firebase.google.com/docs/flutter/setup?authuser=0
2、本次需要使用的插件

1、firebase_storage: ^1.0.4 //firebase对象存储插件,核心插件
2、flutter_image_compress: ^0.2.3 //图片压缩插件。非必选
3、cached_network_image: ^0.5.1 //网络图片加载插件,支持缓存
4、multi_image_picker: ^2.2.55 //图片多选插件
5、path_provider: ^0.4.0 //提供获得应用路径
6、path: ^1.6.2 //路径操作

三、撸码

撸码前我建议大家去看下firebase_storage的帮助文档,了解使用方法后理解更加深刻。
在开撸前我还是习惯先梳理业务流程,分为以下几步:

1、获得你自己的对象存储空间实例
2、获得一个对象存储空间的位置引用,用来存储图片,注意这个引用的位置需要包含你的文件完整名字。
3、本地相册选择图片,然后你可以选择压缩后上传,或者不压缩直接上传到对象存储空间
4、上传完成后获得上传图片的下载地址(当然还提供了获得诸如文件位置、名称、大小等各类信息的方法)
5、将获得图片下载地址存入数据库

业务核心逻辑代码:
  //图片上传任务列表
  List<StorageUploadTask> _upLoadTask = [];
  //存放firestorage返回的图片下载地址
  List<String> _imageUrl = [];
  //存放压缩后的图片数据路径
  List<String> _imagePath = [];
  //选择图片并上传
  _pickImageUpLoad() async {
    //通过MultiImagePicker插件从本地相册选取图片,配置一次最多选择12张,禁止摄像头拍照
    var requestList = await MultiImagePicker.pickImages(
      maxImages: 12,
      enableCamera: false,
    );
    if (!mounted) return;
    //获得目前上传任务数量
    int _taskNum = _upLoadTask.length;
    //这里进行一下判断,是否选择了图片,如果没有选择图片不进行任何操作。
    if (requestList.length != 0) {
      for (int i = 0; i < requestList.length; i++) {
        //获得一个uuud码用于给图片命名
        final String uuid = Uuid().v1();
        //请求原始图片数据
        await requestList[i].requestOriginal();
        //获取图片数据,并转换成uint8List类型
        Uint8List imageData = requestList[i].imageData.buffer.asUint8List();
        print('开始压缩第$i张图片');
        //通过图片压缩插件进行图片压缩
        var result = await FlutterImageCompress.compressWithList(imageData,
            quality: 100);
        //获得应用临时目录路径
        final Directory _directory = await getTemporaryDirectory();
        final Directory _imageDirectory =
            await new Directory('${_directory.path}/image/')
                .create(recursive: true);
        _path = _imageDirectory.path;
        print('本次获得路径:${_imageDirectory.path}');
        //将压缩的图片暂时存入应用缓存目录
        File imageFile = new File('${_path}originalImage_$uuid.png')
          ..writeAsBytesSync(result);
        _imagePath.add(imageFile.path);
        print('图片$i的 本地路径是:${imageFile.path}');
        print('开始创建第$i个上传任务');
        //获得对象存储控件实例后获得图片引用地址
        StorageReference storageReference =
            FirebaseStorage.instance.ref().child('image').child('image_${uuid}.jpg');
        //将图片上传至对象存储空间
        StorageUploadTask storageUploadTask =
            storageReference.putFile(imageFile);
        setState(() {
          _upLoadTask.add(storageUploadTask);
        });
        //释放图片原始数据资源
        requestList[i].releaseOriginal();
      }
      //根据上传任务数量获得上传成功后的图片下载地址
      for (int i = _taskNum == 0 ? 0 : (_upLoadTask.length - _taskNum);
          i < _upLoadTask.length;
          i++) {
        StorageTaskSnapshot snapshot = await _upLoadTask[i].onComplete;
        String uri = await snapshot.ref.getDownloadURL();
        _imageUrl.add(uri);
        print('上传图片返回的url:$uri');
      }
    }
  }

以上代码是业务流程的核心逻辑代码
我基本都写了注释,在这里我再说几点注意点:

1、上传图片的方法目前支持上传file文件putFile(File file)和内存数据putData(Uint8List data),建议使用上传文件的方式,因为上传内存数据的话需要图片数据先在内存中编译完成,可能导致内存占用方面的问题。
2、我用的这个多图选择插件个人感觉是目前最好的一个,但是他返回的是图片的内存数据,所以记得一旦不用的时候要释放。
3、图片上传的时候是否可以对过程进行操作和监控呢?当然可以。请往下看。

UI代码

刚才上面的是逻辑层面的,我们需要继续编写UI内容用于展示选择图片后的可以看到的界面变化。就好像开头效果展示一样,下面会显示上传任务的容器,容器底下还会显示上传状态和上传进度的变化。

分析:无论有多少个上传任务,每个任务展示的UI其实是一样的,如何展示只是形式的问题,可能你喜欢竖着排,他可能喜欢横着排,这个看个人喜好。我这里主要放上单个上传任务的UI
upload_task.dart

import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/material.dart';

import 'package:transparent_image/transparent_image.dart';

class UpLoadTaskContainer extends StatelessWidget {
  UpLoadTaskContainer({
    this.task,
//    this.onDismissed,
    this.onCancel,
    this.imagePath,
//    this.index,
//    this.url
//    this.getUrl,
  });

  final StorageUploadTask task;
//  final VoidCallback onDismissed;
  final VoidCallback onCancel;
//  final String url;
  final String imagePath;
//  final int index;
//  final VoidCallback getUrl;

  get status {
    String result;
    if (task.isComplete) {
      if (task.isSuccessful) {
        result = '成功';
      } else if (task.isCanceled) {
        result = '取消';
      } else
        result = '失败:${task.lastSnapshot.error}';
    } else if (task.isPaused) {
      result = '暂停';
    } else if (task.isInProgress) {
      result = '上传中';
    }
    return result;
  }

  _bytePercent(StorageTaskSnapshot snapshot) {
    return '${((snapshot.bytesTransferred / snapshot.totalByteCount) * 100).toInt()}%';
  }

  @override
  Widget build(BuildContext context) {
    return new StreamBuilder<StorageTaskEvent>(
      stream: task.events,
      builder: (BuildContext context,
          AsyncSnapshot<StorageTaskEvent> asyncSnapShot) {
        Widget subtitle;
        if (asyncSnapShot.hasData) {
          StorageTaskEvent event = asyncSnapShot.data;
          StorageTaskSnapshot snapshot = event.snapshot;
          subtitle = new Text(
            '$status:${_bytePercent(snapshot)}',
            style: new TextStyle(color: Colors.white),
          );
        } else
          subtitle = new Text('准备上传');
        return new Stack(
          alignment: Alignment.bottomCenter,
          children: <Widget>[
            new FadeInImage(
              placeholder: new MemoryImage(kTransparentImage),
              image: AssetImage(
                imagePath,
              ),
              fit: BoxFit.cover,
              width: (MediaQuery.of(context).size.width - 32.0 - 8.0) / 3,
              height: (MediaQuery.of(context).size.width - 32.0 - 8.0) / 3,
            ),
            new Positioned(
              right: 0.08,
              top: 0.08,
              child: new GestureDetector(
                onTap: onCancel,
                child: new Container(
                  decoration: new BoxDecoration(
                    color: Colors.black45,
                    shape: BoxShape.circle,
                  ),
                  child: new Icon(
                    Icons.close,
                    color: Colors.white,
                    size: 20.0,
                  ),
                ),
              ),
            ),
            new Positioned(
                child: new Container(
              alignment: Alignment.center,
              height: 20.0,
              width: (MediaQuery.of(context).size.width - 32.0 - 8.0) / 3,
              color: Colors.black45,
              child: subtitle,
            )),
          ],
        );
      },
    );
  }
}

代码简析:

1、该类构造函数接收三个参数,上传任务task,回调函数onCancel用于删除上传任务,imagePath用于临时显示上传任务的展示
2、get status方法用于获取上传任务的状态
3、StreamBuilder<StorageTaskEvent>是一个上传任务事件类型的数据流构造方法,stream数据流就是任务事件。通过判断任务快照是否在进行数据传输显示任务状态。
4、最上层的ui就是一个stack层叠控件,包裹着图片显示,右上角的删除任务按钮和底下的带有上传状态和进度的控件。

总结

至此基本上上传多个图片到Firebase_storage就实现了,上传后你可以在你的存储空间看到上传的图片。当然了后续业务还包括下载图片、删除图片等,原理差不多,都是先获得图片在对象控件的引用,然后进行相关操作,这里我就不多介绍,感兴趣的朋友可以自己操作一下。

本文毕竟贴了核心代码,所以如果你在操作的过程出现问题,可以去我项目地址查看全部源码。地址是:
https://gitee.com/xusujun33/activity_record_jia
本人是一个产品经理自学开发的小学生,所以代码简陋、错误难免,还望各位海涵。下个目标是把diy活动信息部分也搬到firebase上,这样就实现了所有数据的云端化,再也不怕丢数据了。

不聊了,老婆又提新需求了,我赶紧溜了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  • 关于Mongodb的全面总结 MongoDB的内部构造《MongoDB The Definitive Guide》...
    中v中阅读 31,894评论 2 89
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,084评论 1 32
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,016评论 4 62
  • Pod Unable to find a specification for `SGQRCode` podrepo...
    adalillian阅读 154评论 0 0
  • 从前我在一个酒店兼职 去吃饭。厨师就会欺负外来兼职的 从前我不喜欢吃牛肉 但是我不知道那是牛肉 他就当所有人 呵斥...
    meng1234阅读 174评论 0 0