本文已參與好文召集令活動,點選檢視:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!」
內容簡介
本篇將介紹 Flutter 中如何完成圖片上傳,以及上傳成功後的表單提交。涉及的知識點如下:
- 圖片選擇外掛
wechat_assets_picker
的使用。 - 圖片選擇 iOS 和安卓的應用許可權配置。
- 圖片選擇元件的封裝。
- 圖片上傳介面的封裝。
- 新增和編輯頁面中圖片上傳實現。
圖片選擇外掛
Flutter 的圖片選擇外掛很多,包括了官方的 image_picker
,multi_image_picker
(基於2.0出了 multi_image_picker2
)等等。為了尋找合適的圖片選擇外掛,找了好幾個,發現了一個仿微信的圖片選擇外掛 wechat_assets_picker
,看評分和 Github的Star都不錯,先來試用一下。
許可權申請
先上了一個簡單的 demo,直接呼叫:
final List<AssetEntity> assets = await AssetPicker.pickAssets(context);
複製程式碼
結果發現閃退了!!!難道是外掛有bug?
哦,想起來了!忘記設定圖片獲取許可權了!iOS 在 Runner
的 Info.plist
檔案增加如下內容:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSPhotoLibraryUsageDescription</key>
<string>獲取圖片及使用相簿拍照以便上傳動態圖片。</string>
複製程式碼
安卓在app/profile/AndroidManifest.xml
和 app/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 的不是?
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);
}
}
複製程式碼
看看效果怎麼樣?看起來一切正常,接下來看如何上傳。
圖片上傳
圖片上傳和獲取介面之前已經完成,可以先拉取最新的後臺程式碼:基於 ExpressJs 的後臺程式碼。介面地址分別為:
- 單張圖片上傳介面地址:http://localhost:3900/api/upload/image ,Post 請求,欄位名為
image
,成功後返回圖片檔案id
。 - 圖片獲取介面:http://localhost:3900/api/upload/image/:id ,利用圖片檔案
id
即可獲取圖片檔案流。
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 網路相關示例程式碼,執行前務必拉取最新的後臺程式碼並啟動後臺,歡迎在評論區交流。從效果可以看到,每次更新完內容後需要再重新整理才會更新介面,下一篇我們將介紹完詳情頁面時會附帶完成父子頁面之間的同步。