Flutter 入門與實戰(二十六):一文搞定圖片選擇及圖片上傳

島上碼農發表於2021-07-10

本文已參與好文召集令活動,點選檢視:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!

內容簡介

本篇將介紹 Flutter 中如何完成圖片上傳,以及上傳成功後的表單提交。涉及的知識點如下:

  • 圖片選擇外掛wechat_assets_picker的使用。
  • 圖片選擇 iOS 和安卓的應用許可權配置。
  • 圖片選擇元件的封裝。
  • 圖片上傳介面的封裝。
  • 新增和編輯頁面中圖片上傳實現。

圖片選擇外掛

Flutter 的圖片選擇外掛很多,包括了官方的 image_pickermulti_image_picker(基於2.0出了 multi_image_picker2)等等。為了尋找合適的圖片選擇外掛,找了好幾個,發現了一個仿微信的圖片選擇外掛 wechat_assets_picker,看評分和 Github的Star都不錯,先來試用一下。

許可權申請

先上了一個簡單的 demo,直接呼叫:

final List<AssetEntity> assets = await AssetPicker.pickAssets(context);
複製程式碼

結果發現閃退了!!!難道是外掛有bug?

bug.jpg

哦,想起來了!忘記設定圖片獲取許可權了!iOS 在 RunnerInfo.plist 檔案增加如下內容:

<key>NSAppTransportSecurity</key>
<dict>
  <key>NSAllowsArbitraryLoads</key>
  <true/>
</dict>
<key>NSPhotoLibraryUsageDescription</key>
<string>獲取圖片及使用相簿拍照以便上傳動態圖片。</string>
複製程式碼

安卓在app/profile/AndroidManifest.xmlapp/debug/AndroidManifest.xml 中增加如下內容:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
複製程式碼

再次執行,完美!我們都是這麼跑 Demo 的不是?

選擇圖片.jpg

UI 改造

我們將動態的新增和編輯修改為選擇圖片的方式,原先的輸入框不能用了,需要更改為圖片選擇,考慮圖片選擇會經常用,封裝一個通用的單圖選擇元件。

static Widget imagePicker(
  String formKey,
  ValueChanged<String> onTapped, {
  File imageFile,
  String imageUrl,
  double width = 80.0,
  double height = 80.0,
}) {
  return GestureDetector(
    child: Container(
      margin: EdgeInsets.all(10),
      alignment: Alignment.center,
      decoration: BoxDecoration(
        color: Colors.grey[300],
        border: Border.all(width: 0.5, style: BorderStyle.solid),
        borderRadius: BorderRadius.all(Radius.circular(4.0)),
      ),
      child: _getImageWidget(imageFile, imageUrl, width, height),
      width: width,
      height: height,
    ),
    onTap: () {
      onTapped();
    },
  );
}

static Widget _getImageWidget(
    File imageFile, String imageUrl, double width, double height) {
  if (imageFile != null) {
    return Image.file(
      imageFile,
      fit: BoxFit.cover,
      width: width,
      height: height,
    );
  }
  if (imageUrl != null) {
    return CachedNetworkImage(
      imageUrl: imageUrl,
      fit: BoxFit.cover,
      width: width,
      height: height,
    );
  }

  return Icon(Icons.add_photo_alternate);
}
複製程式碼

這裡考慮圖片選擇元件的佔點陣圖片可能來自網路,也可能是檔案,因此做了不同的處理。優先顯示檔案圖片,其次是網路圖片,若都沒有則顯示一個新增圖片的圖示。

之前的動態表單 dynamic_form.dart 也需要進行相應的調整,包括接收圖片引數,圖片處理函式,並且將之前的圖片文字框改為圖片選擇元件,點選該元件時呼叫wechat_assets_picker外掛提供的AssetPicker.pickAssets方法,限制最大可選則圖片為1張。

List<Widget> _getForm(BuildContext context) {
  List<Widget> widgets = [];
  formData.forEach((key, formParams) {
    widgets.add(FormUtil.textField(key, formParams['value'],
        controller: formParams['controller'] ?? null,
        hintText: formParams['hintText'] ?? '',
        prefixIcon: formParams['icon'],
        onChanged: handleTextFieldChanged,
        onClear: handleClear));
  });
  widgets.add(FormUtil.imagePicker(
    'imageUrl',
    () {
      _pickImage(context);
    },
    imageFile: imageFile,
    imageUrl: imageUrl,
  ));

  widgets.add(ButtonUtil.primaryTextButton(
    buttonName,
    handleSubmit,
    context,
    width: MediaQuery.of(context).size.width - 20,
  ));

  return widgets;
}

void _pickImage(BuildContext context) async {
  final List<AssetEntity> assets =
      await AssetPicker.pickAssets(context, maxAssets: 1);
  if (assets.length > 0) {
    File file = await assets[0].file;
    handleImagePicked(file);
  }
}
複製程式碼

看看效果怎麼樣?看起來一切正常,接下來看如何上傳。

螢幕錄製2021-07-10 下午4.51.51.gif

圖片上傳

圖片上傳和獲取介面之前已經完成,可以先拉取最新的後臺程式碼:基於 ExpressJs 的後臺程式碼。介面地址分別為:

Dio 提供了FormData的方式上傳檔案,示例程式碼如下:

// 單個檔案上傳
var formData = FormData.fromMap({
  'name': 'wendux',
  'age': 25,
  'file': await MultipartFile.fromFile('./text.txt',filename: 'upload.txt')
});
response = await dio.post('/info', data: formData);
// 多個檔案上傳
FormData.fromMap({
  'files': [
    MultipartFile.fromFileSync('./example/upload.txt', filename: 'upload.txt'),
    MultipartFile.fromFileSync('./example/upload.txt', filename: 'upload.txt'),
  ]
});
複製程式碼

我們可以利用這種方式完成圖片的上傳。圖片上傳屬於一個公共的服務,我們新建一個 upload_service.dart 檔案,用於管理所有上傳介面。當前只有一個上傳單個檔案的方法,從圖片檔案獲取檔案路徑構建 MultipartFile 物件即可,如下所示。

import 'dart:io';

import 'package:dio/dio.dart';

class UploadService {
  static const String uploadBaseUrl = 'http://localhost:3900/api/upload/';
  static Future uploadImage(String key, File file) async {
    FormData formData =
        FormData.fromMap({key: await MultipartFile.fromFile(file.path)});
    var result = await Dio().post(uploadBaseUrl + 'image', data: formData);

    return result;
  }
}
複製程式碼

接下來就是處理提交事件了,這裡新增和編輯處理邏輯會有些不同:

  • 新增時需要校驗圖片檔案是否為空,為空則提示需要上傳檔案;
  • 編輯時,本身是一個 原資料的Url,若圖片檔案為空,此時不需要向後臺上傳圖片,也不需要將圖片原有的 Url上傳 (後臺程式碼只儲存圖片檔案的 id,由前端拼接完整地址)。若圖片檔案不為空,則需要提交資料到後臺。如果做得體驗更優和節省後端請求,可以比較新表單資料和原表資料是否相同,若沒有改變則無需提交資料請求。

提交時,我們需要先上傳圖片,圖片上傳成功後將圖片檔案 id 放入到提交的表單資料裡在提交新增或更新介面中。新增時的提交程式碼如下所示:

_handleSubmit() async {
  //其他表單校驗
  if (_imageFile == null) {
    Dialogs.showInfo(this.context, '圖片不能為空');
    return;
  }
  EasyLoading.showInfo('請稍候...', maskType: EasyLoadingMaskType.black);

  try {
    String imageId;
    var imageResponse = await UploadService.uploadImage('image', _imageFile);
    if (imageResponse.statusCode == 200) {
      imageId = imageResponse.data['id'];
    }
    if (imageId == null) {
      Dialogs.showInfo(this.context, '圖片上傳失敗');
      return;
    }
    Map<String, String> newFormData = {};
    _formData.forEach((key, value) {
      newFormData[key] = value['value'];
    });
    //新增時將圖片 id 放入提交表單中
    newFormData['imageUrl'] = imageId;
    // 省略提交程式碼
  }
  // ...
  //省略異常處理程式碼
}
複製程式碼

最終結果

下圖是新增和編輯時的情況,由於之前的測試資料太多,本次統一清除了全部動態資料。關於 MongoDB 的操作可以看本人的另一個專欄:MongoDB 不專業指北。程式碼已提交到:Flutter 網路相關示例程式碼執行前務必拉取最新的後臺程式碼並啟動後臺,歡迎在評論區交流。從效果可以看到,每次更新完內容後需要再重新整理才會更新介面,下一篇我們將介紹完詳情頁面時會附帶完成父子頁面之間的同步。

螢幕錄製2021-07-10 下午8.35.05.gif

相關文章