Chapter 08

Flutterスマホアプリ・MultiPart(ファイル項目、画像項目がある)の例

kazpgm
kazpgm
2022.06.14に更新

■使用できる項目登録更新画面 item_register_amend.dart

flutter_app\lib\affairs\item\item_register_amend.dart

item_register_amend.dart
/// 使用できる項目登録更新画面
class ItemRegisterAmend extends StatefulWidget {
・・・途中省略・・・
    contentWidgets.add(
      // 画像
      Container(
        margin: const EdgeInsets.fromLTRB(10.0, 0, 10.0, 5),
        padding: const EdgeInsets.fromLTRB(10.0, 0, 10.0, 0),
        decoration: CommUtils.commBoxDecoration(),
        child:Column (
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: Container(
                margin: const EdgeInsets.fromLTRB(10.0, 0, 10.0, 5),
                padding: const EdgeInsets.fromLTRB(10.0, 0, 10.0, 0),

                child: (_itemForm.item27File == null && _itemForm.item27.isEmpty)
                ? const Text('画像  *を選んでください。')
                : Row(children:[
                    const Text("画像  * : ",
                    textAlign: TextAlign.left,
                    overflow: TextOverflow.clip,),

                    (() {
                      Widget _contentWidget;
                      // 画面上画像が選ばれた時
                      if (_itemForm.item27File != null) {
                        _contentWidget = Row(children: [
                          const Text("画像 : ",
                            textAlign: TextAlign.left,
                            overflow: TextOverflow.clip,),
                          Image.file(_itemForm.item27File!, fit: BoxFit.cover,
                            width: 80.0,
                            height: 80.0,
                            alignment: Alignment.centerLeft,)
                        ]);
                        // 画像登録済みの時
                      } else if (_itemForm.item27.isNotEmpty) {
                        _contentWidget = Container(
                          margin: const EdgeInsets.fromLTRB(5, 0, 5, 5),
                          padding: const EdgeInsets.fromLTRB(5, 0, 5, 0),
                          width: 80.0,
                          height: 80.0,
                          decoration: BoxDecoration(
                            shape: BoxShape.rectangle,
                            image: DecorationImage(


ファイルがキャッシュされるといやなので日時をつけている

                              image: NetworkImage(
                                  Consts.myHttpUrl + '/upload/item/' + _itemForm.item01 +  '/' + _itemForm.item27 + '?' + DateFormat('yyyyMMddHHmmss').format(DateTime.now()),

'/upload/' フォルダはADMINユーザーしか見れないのでheadersでcsrfとJsessionを渡している。

                                  headers: widget.headers
                              ),
                              fit: BoxFit.cover,
                            ),
                          ),
                        );
                      } else {
                      // ダミー
                      _contentWidget = Container();
                      }
                      return _contentWidget;
                    })()
                  ]),
              ),
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              children: [

                FloatingActionButton(
                  heroTag: 'add_a_photo',
                  onPressed: () async {
                    File? _file = await CommUtils.getImageFromAnyOne(
                        context, Consts.cameraKbn, Consts.maxFileSize,
                        Consts.imageExt);
                    if (_file != null) {
                      setState(() {
                        _itemForm.item27File = _file;
                      });
                    }
                  },
                  tooltip: 'Pick Image From Camera',
                  child: const Icon(Icons.add_a_photo),
                ),
                FloatingActionButton(
                  heroTag: 'photo_library',
                  onPressed:  () async {
                    File? _file = await CommUtils.getImageFromAnyOne(
                        context, Consts.galleryKbn, Consts.maxFileSize,
                        Consts.imageExt);
                    if (_file != null) {
                      setState(() {
                        _itemForm.item27File = _file;
                      });
                    }
                  },
                  tooltip: 'Pick Image From Gallery',
                  child: const Icon(Icons.photo_library),
                ),
              ],
            ),
            const SizedBox(
            height : 10.0,
            ),
            Text(
              // HTTPで返却されたエラーメッセージを表示する。(エラーなしはText高=0にしている)
              _itemForm.itemErrForm.item27FileErr.join("\n") +
                  _itemForm.itemErrForm.checkItem27HissuErr.join("\n") +
                  _itemForm.itemErrForm.checkItem27Err.join("\n"),
              textAlign: TextAlign.left,
              overflow: TextOverflow.clip,
              style: TextStyle(height:(_itemForm.itemErrForm.item27FileErr==[] ||
                  _itemForm.itemErrForm.checkItem27HissuErr==[] ||
                  _itemForm.itemErrForm.checkItem27Err.isEmpty)?0:1.2,
                  fontSize: 12, fontWeight: FontWeight.normal, color: Colors.red),
            ),
          ],
        ),
      ),
    );
    contentWidgets.add(
      // ファイル
      Container(
        margin: const EdgeInsets.fromLTRB(10.0, 0, 10.0, 5),
        padding: const EdgeInsets.fromLTRB(10.0, 0, 10.0, 0),
        decoration: CommUtils.commBoxDecoration(),
        child:Column (
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: Container(
                margin: const EdgeInsets.fromLTRB(10.0, 0, 10.0, 5),
                padding: const EdgeInsets.fromLTRB(10.0, 0, 10.0, 0),

                child: (_itemForm.item28File == null && _itemForm.item28.isEmpty)
                ? const Text('ファイル  を選んでください。')
                : (() {
                    Widget _contentWidget;

                    // 画面上ファイルが選ばれた時
                    if (_itemForm.item28File != null) {
                      _contentWidget = Text("ファイル : " + _itemForm.item28File!.path,
                        textAlign: TextAlign.left,
                        overflow: TextOverflow.clip,);
                      // ファイル登録済みの時
                    } else if (_itemForm.item28.isNotEmpty) {
                      _contentWidget =

                      Row(children:[
                        const Text("ファイル   : ",
                        textAlign: TextAlign.left,
                        overflow: TextOverflow.clip,),
                        Container(
                          margin: const EdgeInsets.fromLTRB(5, 0, 5, 5),
                          padding: const EdgeInsets.fromLTRB(5, 0, 5, 0),
                          child: ElevatedButton(
                            onPressed: () async {
                              var _httpClient = http.Client();

ファイルがキャッシュされるといやなので日時をつけている

                              final http.Response res = await _httpClient.get(Uri.parse(Consts.myHttpUrl + '/upload/item/' + _itemForm.item01 +  '/' + _itemForm.item28 + '?' + DateFormat('yyyyMMddHHmmss').format(DateTime.now())),

'/upload/' フォルダはADMINユーザーしか見れないのでheadersでcsrfとJsessionを渡している。

                                  headers: widget.headers);
                              if (res.statusCode != 200) {
                                String? _str = await CommUtils.openDialogOkComm(context, 'ファイルを読み込めませんでした。') ;
                              } else {
                                String _extension = _itemForm.item28.toLowerCase().substring(_itemForm.item28.length - 3);
                                if (_extension=="txt" || _extension=="pdf") { // Spring側で、txt、pdfしか入力OKにしていないので、これ以外はありえないのだけどチェックする。
                                  Uint8List? _pdfData;
                                  String _data = "";
                                  if (_extension=="txt") {


txtは、漢字が文字化けします。すみません。解決できていません。

                                    _data = String.fromCharCodes(res.bodyBytes);
                                  } else {
                                    _pdfData = res.bodyBytes;
                                  }
                                  Navigator.of(context).push(
                                    MaterialPageRoute(
                                      builder: (context) =>


PDFは正しく表示されます。

                                          PdfTxtDspComm(extension: _extension,
                                              pdfData: _pdfData,
                                              txtData: _data),
                                    ),
                                  );
                                }
                              }
                            },
                            style: ElevatedButton.styleFrom(
                              primary: Colors.blue,
                            ),
                            child: const Text("ファイルを開く"),
                          ),
                        ),
                      ]);
                    } else {
                      // ダミー
                      _contentWidget = const Text("ファイル   : ",
                        textAlign: TextAlign.left,
                        overflow: TextOverflow.clip,);
                    }
                    return _contentWidget;
                  })()
              ),
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              children: [

                FloatingActionButton(
                  heroTag: 'folder',
                  onPressed: () async {
                    File? _file = await CommUtils.getFileFromFilePicker(context,
                        Consts.maxFileSize,
                        Consts.fileExt);
                    if (_file != null) {
                        setState(() {
                          _itemForm.item28File = _file;
                        });
                    }
                  },
                  tooltip: 'Pick File',
                  child: const Icon(Icons.folder),
                ),
              ],
            ),
            (() {
              Widget _contentWidget;
              if (_itemForm.item28.isNotEmpty) {
                //チェックボックス
                _contentWidget = Column(

                  children: [CheckboxListTile(
                    activeColor: Colors.orange,
                    title: const Text("ファイルを削除"),
                    controlAffinity: ListTileControlAffinity.leading,
                    value: _itemForm.item28Del=="1"?true:false,
                    onChanged: (value) {
                      setState(() {
                        _itemForm.item28Del = (value==true?"1":"0");
                      });
                    }
                )]);
              } else {
                // ダミー
                _contentWidget = const SizedBox(
                  height : 0.0,
                );
              }
              return _contentWidget;
            })(),
            const SizedBox(
              height : 10.0,
            ),
            Text(
              // HTTPで返却されたエラーメッセージを表示する。(エラーなしはText高=0にしている)
              _itemForm.itemErrForm.item28FileErr.join("\n") +
                  _itemForm.itemErrForm.checkItem28HissuErr.join("\n") +
                  _itemForm.itemErrForm.checkItem28Err.join("\n"),
              textAlign: TextAlign.left,
              overflow: TextOverflow.clip,
              style: TextStyle(height:(_itemForm.itemErrForm.item28FileErr==[] ||
                  _itemForm.itemErrForm.checkItem28HissuErr==[] ||
                  _itemForm.itemErrForm.checkItem28Err.isEmpty)?0:1.2,
                  fontSize: 12, fontWeight: FontWeight.normal, color: Colors.red),
            ),
          ],
        ),
      ),
    );
・・・途中省略・・・

情報登録、情報変更、情報一覧へのhttpアクセス

  /// 情報登録、情報変更、情報一覧へのhttpアクセス
  void httpForInfo(String _modeBase) async {
    try {
      // TextFormField値を、Formに設定。補足:TextFormField以外は直接Formを見ている。
・・・途中省略・・・
        // ファイル項目、画像項目が1つでもある場合の処理
        _mode = (_itemForm.messageForm.mode == 'ins' || _itemForm.messageForm.mode == 'ins_do')?'ins_do':'upd_do';
        widget.headers["content-type"]= "multipart/form-data";
        widget.headers["X-XSRF-TOKEN"]= "${widget.cookies['XSRF-TOKEN']}";
        final url = Uri.parse("${Consts.myHttpUrl}/members/admin/item/item/" + _mode);

ファイル項目、画像項目が1つでもある場合、http.MultipartRequestメソッドで行う。

        var request = http.MultipartRequest("POST", url);

ファイル、画像以外の項目はrequest.fieldsに設定する。

        request.fields['formComm.page'] = _itemForm.messageForm.page.toString();
        request.fields['itemForm.item01'] = _itemForm.item01;
        request.fields['itemForm.item02'] = _itemForm.item02;
        request.fields['itemForm.item03'] = _itemForm.item03!=null?DateFormat("yyyy/MM/dd").format(_itemForm.item03!):"";
        request.fields['itemForm.item04'] = _itemForm.item04;
・・・途中省略・・・
        request.fields['itemForm.item26'] = _itemForm.item26;
        if (_itemForm.item27File != null) {
          final mimeTypeData = lookupMimeType(
              _itemForm.item27File!.path, headerBytes: [0xFF, 0xD8])!.split('/');

ファイル、画像のアップロードは、http.MultipartFile.fromPathメソッドを使い、 request.files.add(file)でrequest.fieldsに設定する。

          final file = await http.MultipartFile.fromPath(
              'itemForm.item27File', _itemForm.item27File!.path,
              contentType: MediaType(mimeTypeData[0], mimeTypeData[1]));
          request.files.add(file);
        }
        request.fields['itemForm.item27'] = _itemForm.item27;
        request.fields['itemForm.item27Del'] = _itemForm.item27Del;
        if (_itemForm.item28File != null) {
          final mimeTypeData = lookupMimeType(
              _itemForm.item28File!.path, headerBytes: [0xFF, 0xD8])!.split('/');
          final file = await http.MultipartFile.fromPath(
              'itemForm.item28File', _itemForm.item28File!.path,
              contentType: MediaType(mimeTypeData[0], mimeTypeData[1]));
          request.files.add(file);
        }
        request.fields['itemForm.item28'] = _itemForm.item28;
        request.fields['itemForm.item28Del'] = _itemForm.item28Del;
        request.fields['itemForm.item31'] = _itemForm.item31;

headersでcsrfとJsessionを渡している。

        request.headers.addAll(widget.headers);

responseはrequest.sendを行った後、http.Response.fromStreamメソッドで取得する。

        final streamedResponse = await request.send();
        response = await http.Response.fromStream(streamedResponse);
        widget.headers["content-type"]= "application/json; charset=UTF-8";
      }
      if (response.statusCode != 200) {
・・・以降省略・・・