Flutterで画像アップロード機能を実装したので記事にした
PR
実装
以下を使用して実装 - image_picker | Flutter package - image_cropper | Flutter package - firebase_storage | Flutter package
まずはimage_pickerで画像選択部分を実装
初期設定は以下を参照
使い方は以下の通り
final picker = ImagePicker(); @override Widget build(BuildContext context) { final image = useState<File?>(null); Future<void> onTap() async { final pickedFile = await picker.pickImage(source: ImageSource.camera); if (pickedFile != null) { image.value = File(pickedFile.path); } }
picker.pickImage(source: ImageSource.camera)
を呼ぶと以下みたいな感じでカメラが起動する
次にimage_cropperで以下のように実装
Future<void> onTap() async { final pickedFile = await picker.pickImage(source: ImageSource.camera); if (pickedFile != null) { return; } final croppedFile = await ImageCropper().cropImage( sourcePath: pickedFile.path, uiSettings: [ AndroidUiSettings( toolbarTitle: '画像を切り取る', initAspectRatio: CropAspectRatioPreset.square, lockAspectRatio: true, aspectRatioPresets: [ CropAspectRatioPreset.square, ]), IOSUiSettings( title: '画像を切り取る', minimumAspectRatio: 1.0, aspectRatioPresets: [ CropAspectRatioPreset.square, ], aspectRatioLockEnabled: true, ), WebUiSettings( context: context, ), ], ); if (croppedFile != null) { image.value = File(croppedFile.path); } }
ImageCropper().cropImage
にpickImageで取得した画像のpathを指定すると以下のように画像のトリミングの画面が起動する
今回はアップロードする画像は正方形にしたいのでsquare
& アスペクト比を固定に設定
最後にfirebase_storageを使用してアップロードの処理を作成
onInputPressed() async { try { final uuid = const Uuid().v4(); final fileName = "$uuid.${image.value!.path.split('.').last}"; final res = await storageRef .child('category/$fileName}') .putFile(image.value!); final imageUrl = await res.ref.getDownloadURL(); imageURL.value = imageUrl; } catch (e) { print("error: $e"); return; } }
ファイル名はuuidで生成して、storageRef.child('category/$fileName}').putFile(image.value!)
で指定したファイルをFirebase Storageにアップローできる
上記の全部を実装すると以下のようなコードになって画像アップロードが実装できる
■ lib/features/category/components/input.dart
import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:sampleflutter/components/button/button.dart'; import 'package:firebase_storage/firebase_storage.dart'; import 'package:image_picker/image_picker.dart'; import 'package:uuid/uuid.dart'; import 'package:image_cropper/image_cropper.dart'; import 'package:flutter/cupertino.dart'; class InputCategory { late final String name; late final String? imageURL; InputCategory({ required this.name, this.imageURL, }); } class Input extends HookWidget { final void Function(InputCategory) onPressed; final InputCategory? defaultValue; final picker = ImagePicker(); Input({ super.key, this.defaultValue, required this.onPressed, }); @override Widget build(BuildContext context) { final inputText = useTextEditingController(text: defaultValue?.name ?? ''); final image = useState<File?>(null); final imageURL = useState<String?>(defaultValue?.imageURL); final storageRef = FirebaseStorage.instance.ref(); Future<void> cropImage(String path) async { final croppedFile = await ImageCropper().cropImage( sourcePath: path, uiSettings: [ AndroidUiSettings( toolbarTitle: '画像を切り取る', initAspectRatio: CropAspectRatioPreset.square, lockAspectRatio: true, aspectRatioPresets: [ CropAspectRatioPreset.square, ]), IOSUiSettings( title: '画像を切り取る', minimumAspectRatio: 1.0, aspectRatioPresets: [ CropAspectRatioPreset.square, ], aspectRatioLockEnabled: true, ), WebUiSettings( context: context, ), ], ); if (croppedFile != null) { image.value = File(croppedFile.path); } } void showPickImage() { showModalBottomSheet( context: context, builder: (BuildContext context) { return Padding( padding: const EdgeInsets.only( top: 10.0, bottom: 40.0, left: 10.0, right: 10.0), // 上に20、下に10の余白を追加 child: Wrap( children: <Widget>[ const ListTile( title: Text("画像をアップロード", style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold)), ), ListTile( leading: const Icon(Icons.camera_alt), title: const Text('カメラを起動する'), onTap: () async { Navigator.pop(context); final pickedFile = await picker.pickImage(source: ImageSource.camera); if (pickedFile != null) { cropImage(pickedFile.path); } }, ), ListTile( leading: const Icon(Icons.image_search), title: const Text('アルバムから選択する'), onTap: () async { Navigator.pop(context); final pickedFile = await picker.pickImage(source: ImageSource.gallery); if (pickedFile != null) { cropImage(pickedFile.path); } }, ), ], )); }, ); } onInputPressed() async { if (image.value != null) { try { final uuid = const Uuid().v4(); final fileName = "$uuid.${image.value!.path.split('.').last}"; final res = await storageRef .child('category/$fileName}') .putFile(image.value!); final imageUrl = await res.ref.getDownloadURL(); imageURL.value = imageUrl; //print("imageURL: $imageUrl"); } catch (e) { //print("error: $e"); showDialog( context: context, builder: (BuildContext contextDialog) { return CupertinoAlertDialog( title: const Text("エラーが発生しました"), content: Text("画像のアップロードに失敗しました。もう一度お試しください。(エラーコード: $e)"), actions: [ CupertinoDialogAction( child: const Text('OK'), onPressed: () => Navigator.pop(contextDialog), ), ], ); }); return; } } if (inputText.text.isEmpty) { showDialog( context: context, builder: (BuildContext contextDialog) { return CupertinoAlertDialog( title: const Text("入力エラー"), content: const Text("部屋の名前を入力してください。"), actions: [ CupertinoDialogAction( child: const Text('OK'), onPressed: () => Navigator.pop(contextDialog), ), ], ); }); return; } onPressed(InputCategory(name: inputText.text, imageURL: imageURL.value)); } return Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ Center( child: TextField( controller: inputText, cursorColor: Colors.white, style: const TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold), decoration: const InputDecoration( labelText: "部屋の名前", labelStyle: TextStyle(color: Colors.white, fontSize: 26), enabledBorder: UnderlineInputBorder( borderSide: BorderSide(color: Colors.white), ), focusedBorder: UnderlineInputBorder( borderSide: BorderSide(color: Colors.white), ), ))), Container( padding: const EdgeInsets.only(top: 30), child: InkWell( onTap: showPickImage, child: image.value != null ? Image.file(image.value!, width: 250, height: 250) : imageURL.value != null ? Image.network(imageURL.value!, width: 250, height: 250) : Card( color: Colors.black26, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.zero, // Cardの角を直角にする ), elevation: 0, child: SizedBox( width: 250, height: 250, child: Container( width: 40, height: 40, padding: const EdgeInsets.all(2), // ボーダーの幅を調整 child: const Icon( Icons.camera_alt, color: Colors.white, size: 40, ), )))), ), Flexible( child: Center( heightFactor: 3, child: Button(title: "保存", onPressed: onInputPressed))), ], ); } }
上記のコードで以下みたいな画面が実装できる