[flutter]Quill(rich text editor)で端末から画像アップロードする方法

quillエディタで端末から画像アップロードする方法をお教えします。

結論から言うとquillでは画像のリンクから画像アップロードすることはできますが、**端末からアップロードする方法はなく自作するしかありません**（少なくともドキュメントはない）

※前提として状態管理はriverpodを使用しています。




## Quillエディタ公式ドキュメント


[公式ドキュメントはこちら](https://pub.dev/packages/flutter_quill "quill公式library")
ライブラリのインポート等は公式ドキュメントを参照してください。
ここでは省略します。

## カスタムボタンの追加

```dart:カスタムボタンの追加
QuillToolbar.basic(
    customButtons: [
                  QuillCustomButton(
                      icon:Icons.camera_alt,
                      onTap: () {
                        getImageFromGallery(ref);
                        ///ここで端末のフォルダを参照している

                      }
                  ),

                ],
           )
```

上記のようにquillのツールバーにはカスタムボタンを作れるのでますは任意のアイコンと押下時のロジックを用意しておきましょう。

## imagePickerを追加

[公式ドキュメントはこちら](https://pub.dev/packages/image_picker)

ライブラリのインポート等は公式ドキュメントを参照してください。
ここでは省略します。

## 実装



```dart:カスタムボタンの追加
Future getImageFromGallery(WidgetRef ref) async {
  final pickedFile = await picker.pickImage(source: ImageSource.gallery,maxHeight: 380, maxWidth: 340);//デフォルトサイズは一旦こんな感じ

    if(pickedFile != null) {
      ref.watch(imageProvider.notifier).update((state) =>  File(pickedFile.path));
    }
  final index = _controller.selection.baseOffset;
  final length = _controller.selection.extentOffset - index;

  print(File(pickedFile!.path));

  _controller.replaceText(index, length, BlockEmbed.image(File(pickedFile.path).path), null);
 // BlockEmbed.image(value)
}
```


## 全コード

<details><summary>サンプルコード</summary>


```dart
QuillController _controller = QuillController.basic();


class CommentWidget extends StatelessWidget {
  const CommentWidget({
    super.key,
  });

  @override
  Widget build(BuildContext context) {


    return Scaffold(
      body: Column(
        children: [
          SingleChildScrollView(
            child: Container(
              height: 56,
              decoration: const BoxDecoration(
                color: whiteColor,
                borderRadius: BorderRadius.only(
                  topLeft: Radius.circular(12.0),
                  topRight: Radius.circular(12.0),
                ),
              ),
              child: Stack(
                children: [
                  ConstrainedBox(
                    constraints: const BoxConstraints.expand(),
                    child: Row(
                        mainAxisAlignment: MainAxisAlignment.end,
                        children: [
                          IconButton(
                            icon: Transform.rotate(
                                angle: 45 * pi / 180,
                                child: const Icon(Icons.add,
                                    size: 45, color: blackColor)),
                            onPressed: () {
                              Navigator.pop(context);
                            },
                          ),
                          const SizedBox(
                            width: 6,
                          )
                        ]),
                  ),
                  // const Center(
                  //   child: Text('ボトムシート',
                  //       style: TextStyle(
                  //           fontWeight: FontWeight.bold,
                  //           color: Colors.white,
                  //           fontSize: 16)),
                  // )
                ],
              ),
            ),
          ),

          Consumer(

            builder: (BuildContext context, WidgetRef ref, Widget? child) {

              return QuillToolbar.basic(
           
                toolbarSectionSpacing: 0,
                toolbarIconCrossAlignment: WrapCrossAlignment.end,
                toolbarIconAlignment: WrapAlignment.start,
                color: whiteColor,
                toolbarIconSize: 20,
           

                customButtons: [
                  QuillCustomButton(
                      icon:Icons.camera_alt,
                      onTap: () {
                        getImageFromGallery(ref);
                        ///ここで画像取得できるかやってみる

                      }
                  ),

                ],
                controller: _controller,

                iconTheme: const QuillIconTheme(
                    borderRadius: 14,
                    iconSelectedFillColor: basedAccentColor
                ),
              );
            },

          ),

         // const SizedBox(height: 20,),

          Expanded(
            child: SingleChildScrollView(
              child: Column(
                children: [
                  Padding(
                    padding: const EdgeInsets.only(left: 10,bottom: 10,right: 10),
                    child: Container(

                      height: 300,
                      color: whiteColor,
                      child: QuillEditor(


                        embedBuilders: FlutterQuillEmbeds.builders(),
                        keyboardAppearance: Brightness.dark,
                        controller: _controller,
                        placeholder: 'タイトル',
                        readOnly: false,
                        focusNode: FocusNode(),
                        scrollController: ScrollController(),
                        scrollable: true,
                        padding: EdgeInsets.zero,
                        autoFocus: true,
                        expands: false,
                        // true for view only mode
                      ),
                    ),
                  ),


                  Padding(
                    padding: const EdgeInsets.only(left: 10,bottom: 10,right: 10),
                    child: Container(

                      height: 300,
                      color: whiteColor,
                      child: QuillEditor(


                        embedBuilders: FlutterQuillEmbeds.builders(),
                        keyboardAppearance: Brightness.dark,
                        controller: _controller2,
                        placeholder: 'みんなのためになる事を書きましょう。',
                        readOnly: false,
                        focusNode: FocusNode(),
                        scrollController: ScrollController(),
                        scrollable: true,
                        padding: EdgeInsets.zero,
                        autoFocus: true,
                        expands: false,
                        // true for view only mode
                      ),
                    ),
                  ),


                  Padding(
                    padding: const EdgeInsets.only(left: 10,bottom: 10,right: 10),
                    child: Container(

                      height: 300,
                      color: whiteColor,
                      child: QuillEditor(


                        embedBuilders: FlutterQuillEmbeds.builders(),
                        keyboardAppearance: Brightness.dark,
                        controller: _controller3,
                        placeholder: 'みんなのためになる事を書きましょう。',
                        readOnly: false,
                        focusNode: FocusNode(),
                        scrollController: ScrollController(),
                        scrollable: true,
                        padding: EdgeInsets.zero,
                        autoFocus: true,
                        expands: false,
                        // true for view only mode
                      ),
                    ),
                  ),


                ],
              ),
            ),
          ),





       //   const SizedBox(height: 30,),
          Padding(
            padding: const EdgeInsets.only(bottom: 10),
            child: SizedBox(

              width: 320,
              height: 40,
              child: DefaultButtonWidget(text: '投稿', width: 340,mainColor: basedAccentColor,textColor: Colors.white,
                  onPressed: () {
                 print(_controller.document.toDelta().toJson());



                 print('_controller.document.length${_controller.document.length}');

                //  final index = _controller.selection.baseOffset;
                //  final length = _controller.selection.extentOffset - index;
                // print(index);
                // print(length);
                 for(int i = 0;i<_controller.document.length; i++) {
                   print(_controller.document.length);
                   if (_controller.hasUndo) {
                     _controller.clear();
                   }
                   else {
                     print('for break');
                     break;
                   }
                 }




                Navigator.pop(context);

                // Navigator.push(
                //   context,
                //   MaterialPageRoute(builder: (context) => const UserReportView()),
                // );
              })
            ),
          ),
        ],
      ),
    );
  }
}


final imageProvider = StateProvider<File?>((ref) => null);
//画像を入れる変数
final picker = ImagePicker(); //Image Pickerをインスタンス化


Future getImageFromGallery(WidgetRef ref) async {
  final pickedFile = await picker.pickImage(source: ImageSource.gallery,maxHeight: 380, maxWidth: 340);//デフォルトサイズは一旦こんな感じ

    if(pickedFile != null) {
      ref.watch(imageProvider.notifier).update((state) =>  File(pickedFile.path));
    }
  final index = _controller.selection.baseOffset;
  final length = _controller.selection.extentOffset - index;

  print(File(pickedFile!.path));

  _controller.replaceText(index, length, BlockEmbed.image(File(pickedFile.path).path), null);
 // BlockEmbed.image(value)
}
```
</details>

## 最後に

リッチテキストエディタ要件を全て満たそうとすると途端に何度上がる印象です。。
もっといいライブラリがあればいいのですが、、

もし上記の記事に対するご指摘などあればぜひよろしくお願いします！！

最後まで読んでいただきありがとうございました。