開始編寫一個Flutter web 文件上傳組件 WebFileUploadWidget

最近想用flutter作一個簡單的web後臺管理系統,須要文件上傳功能,無賴網上找了半天資料,沒有web版的上傳組件,只有本身動手豐衣足食了。html

既然是web,那天然想到的就是須要dart:html這個包了,基本步驟主要有三個:web

  • 選擇文件
  • 上傳文件
  • UI設計

UI

不用設計了,將就看吧,咱們重在功能 api

選擇文件

首先導入包bash

import 'dart:html' as html;
import 'package:flutter/material.dart';
複製代碼

組件上面部分,點擊彈出選擇文件框app

var picker = InkWell(
  hoverColor: Colors.blue[50],
  splashColor: Colors.blue[10],
  onTap: _selectFile,
  child: Container(
    decoration: BoxDecoration(
      border: Border.all(color: Colors.blue),
      borderRadius: BorderRadius.all(Radius.circular(5.0)),
    ),
    padding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0),
    child: Text(
      'Click to select file',
      style: TextStyle(fontWeight: FontWeight.w500, color: Colors.grey),
    ),
  ),
);
複製代碼

重點來了,下面是如何選擇文件的代碼:async

_selectFile() {
    html.InputElement uploadInput = html.FileUploadInputElement();
    uploadInput.multiple = false;
    uploadInput.click();
    uploadInput.onChange.listen((e) {
      final files = uploadInput.files;
      var fileItem = UploadFileItem(files[0]);
      setState(() {
        _files.add(fileItem);
      });
    });
}
複製代碼

這裏獲取到文件是一個dart:html裏面的一個File實例,自定義一個UploadFileItem包裝一下:ide

import 'dart:html' as html;

enum FileStatus {
  normal,
  uploading,
  success,
  fail,
}

class UploadFileItem {
  html.File file;
  FileStatus fileStatus;
  double progress = 0.0;

  UploadFileItem(this.file) {
    fileStatus = FileStatus.normal;
  }
}
複製代碼

到這裏,如何選擇文件咱們已經會了,而且獲得了文件對象,那麼如何實現上傳呢?優化

上傳文件

網上查詢了好多資料,基本上都是用dart:html裏面的FileReader讀取文件後再經過http或者dio這個包上傳文件內容,可是要把文件內容都讀取出來,真的好麼?我要上傳的多是大文件,試了試,果然卡得起飛😂。ui

😔就快要放棄了this

最後仍是在StackOverflow找到了一個條有用的信息

原來解決辦法仍是用dart:html,這個包好強大有沒有,竟然有一個HttpRequest,使用還黑方便😃

直接看代碼:

final html.FormData formData = html.FormData()..appendBlob('file', file);

var url = 'http://localhost:8088/api/file/upload';
html.HttpRequest.request(
  url,
  method: 'POST',
  sendData: formData,
  onProgress: onProgress,
).then((httpRequest) {
  handleRequest(httpRequest);
}).catchError((e) {
  setState(() {
    widget.fileItem.fileStatus = FileStatus.fail;
  });
});
複製代碼

啊,勝利的曙光終於來了,寫完代碼,跑一遍,速度夠快,運行很流暢。

完工!

最後附上源碼,總共三個文件:

file_item.dart

import 'dart:html' as html;

enum FileStatus {
  normal,
  uploading,
  success,
  fail,
}

class UploadFileItem {
  html.File file;
  FileStatus fileStatus;
  double progress = 0.0;

  UploadFileItem(this.file) {
    fileStatus = FileStatus.normal;
  }
}
複製代碼

upload_widget.dart

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'dart:html' as html;
import 'package:web_file_upload/file_item.dart';
import 'package:web_file_upload/file_item_widget.dart';

class UploadWidget extends StatefulWidget {
  @override
  _UploadWidgetState createState() => _UploadWidgetState();
}

class _UploadWidgetState extends State<UploadWidget> {
  List<UploadFileItem> _files = [];

  _selectFile() {
    html.InputElement uploadInput = html.FileUploadInputElement();
    uploadInput.multiple = false;
    uploadInput.click();
    uploadInput.onChange.listen((e) {
      final files = uploadInput.files;
      var fileItem = UploadFileItem(files[0]);
      setState(() {
        _files.add(fileItem);
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    var picker = InkWell(
      hoverColor: Colors.blue[50],
      splashColor: Colors.blue[10],
      onTap: _selectFile,
      child: Container(
        decoration: BoxDecoration(
          border: Border.all(color: Colors.blue),
          borderRadius: BorderRadius.all(Radius.circular(5.0)),
        ),
        padding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0),
        child: Text(
          'Click to select file',
          style: TextStyle(fontWeight: FontWeight.w500, color: Colors.grey),
        ),
      ),
    );

    var filesList = _files.map(_fileItem);

    return Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: <Widget>[picker, SizedBox(height: 10.0), ...filesList],
    );
  }

  _delete(UploadFileItem fileItem) {
    print('delete ${fileItem.file.name}');
    setState(() {
      _files.remove(fileItem);
    });
  }

  Widget _fileItem(UploadFileItem fileItem) {
    return FileItemWidget(
      fileItem: fileItem,
      onDeleteFile: _delete,
    );
  }
}
複製代碼

file_item_widget.dart

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'dart:html' as html;
import 'package:web_file_upload/file_item.dart';

typedef void DeleteFile(UploadFileItem fileItem);

class FileItemWidget extends StatefulWidget {
  final UploadFileItem fileItem;

  final DeleteFile onDeleteFile;

  const FileItemWidget({Key key, this.fileItem, this.onDeleteFile}) : super(key: key);

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

class _FileItemWidgetState extends State<FileItemWidget> {
  @override
  void initState() {
    super.initState();
// _readFile();
    _sendFormData(widget.fileItem.file);
  }

  _sendFormData(final html.File file) async {
    setState(() {
      widget.fileItem.fileStatus = FileStatus.uploading;
    });

    final html.FormData formData = html.FormData()..appendBlob('file', file);

    handleRequest(html.HttpRequest httpRequest) {
      print('upload resp: ${httpRequest.responseText}');
      switch (httpRequest.status) {
        case 200:
          setState(() {
            widget.fileItem.fileStatus = FileStatus.success;
          });
          return;
        default:
          setState(() {
            widget.fileItem.fileStatus = FileStatus.fail;
          });
          break;
      }
    }

    onProgress(e) {
      print('upload sending: ${e.loaded} ${e.total}');
      double progress = e.lengthComputable ? (e.loaded * 100 ~/ e.total) / 100.0 : e.loaded / 100.0;

      print('upload sending: $progress');
      if (widget.fileItem.progress == progress) return;
      setState(() {
        widget.fileItem.progress = progress;
      });
    }

    // var url = 'https://www.mocky.io/v2/5cc8019d300000980a055e76';
    var url = 'http://localhost:8088/api/file/upload';

    html.HttpRequest.request(
      url,
      method: 'POST',
      sendData: formData,
      onProgress: onProgress,
    ).then((httpRequest) {
      handleRequest(httpRequest);
    }).catchError((e) {
      setState(() {
        widget.fileItem.fileStatus = FileStatus.fail;
      });
    });

// final html.HttpRequest httpRequest = html.HttpRequest();
// httpRequest
// ..onProgress.listen(onProgress)
// ..onLoadEnd.listen((e) {
// handleRequest(httpRequest);
// })
// ..open('POST', url)
// ..send(formData);
  }

  @override
  Widget build(BuildContext context) {
    UploadFileItem fileItem = widget.fileItem;

    var prefixIcon;
    if (fileItem.fileStatus == FileStatus.uploading || fileItem.fileStatus == FileStatus.normal) {
      prefixIcon = CupertinoActivityIndicator(
        animating: true,
        radius: 8.0,
      );
    } else {
      prefixIcon = Icon(
        Icons.attach_file,
        color: Colors.lightBlue,
        size: 20.0,
      );
    }

    return InkWell(
      hoverColor: Colors.blue[100],
      onTap: () {},
      child: Container(
        padding: const EdgeInsets.symmetric(horizontal: 16.0),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            Row(
              mainAxisSize: MainAxisSize.max,
              children: <Widget>[
                prefixIcon,
                SizedBox(width: 8.0),
                Expanded(
                  child: Text(
                    '${fileItem.file.name}',
                    style: TextStyle(color: fileItem.fileStatus == FileStatus.fail ? Colors.red : Colors.black87, fontSize: 18.0),
                  ),
                ),
                IconButton(
                  padding: EdgeInsets.all(2.0),
                  iconSize: 18.0,
                  icon: Icon(Icons.delete),
                  onPressed: () {
                    widget.onDeleteFile(fileItem);
                  },
                  color: Colors.red[500],
                ),
              ],
            ),
            if (fileItem.fileStatus == FileStatus.uploading)
              LinearProgressIndicator(
                value: fileItem.progress,
                backgroundColor: Colors.transparent,
              ),
          ],
        ),
      ),
    );
  }
}
複製代碼

功能算實現了,還有好多優化的地方,好比作一個統一平臺的插件,支持移動端,你們以爲有用就本身去優化吧😎

相關文章
相關標籤/搜索