Flutter : HTTP POSTと結果JSONの処理

Flutter + Firebase でアプリ作成のためのパーツあつめ。

1.ここまでに確認したこと

  1. Flutter+Firebase環境設定
  2. 画面遷移
  3. Dialog
  4. Httpクライアント
  5. AndroidX対応
  6. 画像の選択
  7. 画像の切り抜き

2.今回確認すること

  1. Image Pickボタン押下:画像を選択し、画像を画面に表示
  2. OCRボタン押下:dioパッケージ を使用して、HTTP POST で、OCRサービス(Google Cloud VIsioin API)に画像をアップロードし、結果をJOSNで得る
  3. 結果のJSONを処理してテキストに表示

3.ソース

3.1 pubspwc.yaml

dependencies:
   image_picker: ^0.6.1+8
   dio: ^3.0.3

3.2 テーマに該当する部分のみ抜粋。

import 'dart:convert';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:dio/dio.dart';

class SecondRoute extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _SecondRoute();
  }
}
class _SecondRoute extends State<SecondRoute>{
  Image _defaultImage = Image.network('https://flutter.github.io/assets-for-api-docs/assets/widgets/owl-2.jpg');
  File _selectedFile;
  final _textController = TextEditingController();

  @override
  void initState() {
    super.initState();
//    _textController.addListener(_ocrText);
  }

  @override
  void dispose() {
    _textController.dispose();
    super.dispose();
  }

  Future _handleImagePick(BuildContext context) async {
    var imageFile = await ImagePicker.pickImage(source: ImageSource.gallery);
    setState(() {
      _selectedFile = imageFile;
    });
  }

  Future _handleOcr(BuildContext context) async {
    var dio = new Dio();
    var formData = new FormData.fromMap({
      "fileName": "ocrfile",
      "file": await MultipartFile.fromFile(_selectedFile.path ,filename: "selected_file")
    });
    var result = dio.post(
        "https://{google cloud vision api を実装したWebサービスアプリのURL}",
        options: Options(responseType:  ResponseType.json),
        data: formData);

    result.then((Response response){
      print("<status> ${response.statusCode}");
      response.headers.forEach((k, v){
        print("<header> $k:$v");
      });
      var json = response.data;
      print(json['text']);
      setState(() {
        _textController.text = json['text'];
      });
    });
    return null;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: LayoutBuilder(
        builder: (BuildContext context, BoxConstraints viewportConstraints) {
          return SingleChildScrollView(
            child: ConstrainedBox(
              constraints: BoxConstraints(
                minHeight: viewportConstraints.maxHeight,
              ),
              child: IntrinsicHeight(
                child: Column(
                  children: <Widget>[
                    Container(
                      // A fixed-height child.
                      color: const Color.fromARGB(255, 255, 255, 255),
                      height: 120.0,
                    ),
                    Expanded(
                      // A flexible child that will grow to fit the viewport but
                      // still be at least as big as necessary to fit its contents.
                      child: Container(
                        color: const Color(0xffffffff),
                        height: 240.0,
                        constraints: BoxConstraints(
                            minWidth: viewportConstraints.maxWidth,
                            minHeight: double.infinity
                        ),
                        child: Column(
                          children: <Widget>[
                            RaisedButton(
                              onPressed: () {
                                _handleImagePick(context);
                              },
                              child: Text('Image Pick'),
                            ),
                            RaisedButton(
                              onPressed: (){
                                _handleOcr(context);
                              },
                              child: Text('OCR'),
                            ),
                            (_selectedFile == null)?_defaultImage:Image.file(_selectedFile),
                            TextField(
                                controller: _textController,
                                minLines: 6,
                                maxLines: 15,
                                decoration: InputDecoration(
                                  border: OutlineInputBorder(),
                                ),
                            ),
                          ],
                        ),
                      ),
                    ),
                  ],
                ),
              ),
            ),
          );
        },
      ),
    );
  }

}

4.実行

Image Pick で画像をロード

flutter_ocr01

OCRボタンで、画像をサービスに送信して、結果のJSONをテキストフィールドに表示

flutter_ocr02

Flutter : 画像の切り抜き

Flutter + Firebase でアプリ作成のためのパーツあつめ。

まで、Android、iOSでの簡易動作確認ができたので、画像切り抜きパッケージ、image_cropper を試す。

1.例によってエラー対応

pub.devのimage_cropperページのExampleに従い、コードを記述し実行するが、androidx.core.widget.TintableCompoundDrawableView が存在しないエラー

I/pea.favo_phras( 6224): Rejecting re-init on previously-failed class java.lang.Class: java.lang.NoClassDefFoundError: Failed resolution of: Landroidx/core/widget/TintableCompoundDrawablesView;
I/pea.favo_phras( 6224):   at java.lang.Object java.lang.Class.newInstance() (Class.java:-2)
I/pea.favo_phras( 6224):   at android.app.Activity android.app.AppComponentFactory.instantiateActivity(java.lang.ClassLoader, java.lang.String, android.content.Intent) (AppComponentFactory.java:69)
I/pea.favo_phras( 6224):   at android.app.Activity androidx.core.app.CoreComponentFactory.instantiateActivity(java.lang.ClassLoader, java.lang.String, android.content.Intent) (CoreComponentFactory.java:43)

image_cropper のGithubのissueに同じ事象の報告および解決策情報あり。

https://github.com/hnvn/flutter_image_cropper/issues/78

build.gradle の dependencies に以下を追記

implementation 'androidx.core:core:1.0.2'
implementation 'androidx.appcompat:appcompat:1.0.2'

とのことだが、それだけでは解決しないため、エラーログを見ながら以下のように調整。

AndroAndroidXの互換性対応で、この手のエラー解決のコツがなんとなくつかめてきたが、、、若干不安。

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'com.google.firebase:firebase-analytics:16.0.0'
    implementation 'androidx.core:core:1.0.0'
    implementation 'androidx.appcompat:appcompat:1.1.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

configurations.all {
    resolutionStrategy.eachDependency { DependencyResolveDetails details ->
        print(details.requested.group)
        if (details.requested.group ==~ /^androidx\.(core|viewpager|drawlayout|interpolator|fragment|versionedparcelable|appcompat)/
        ) {
            details.useVersion '1.0.0'
            details.because 'API needs higher versions'
        }
        if (details.requested.group == 'androidx.lifecycle'
            ) {
            details.useVersion '2.1.0'
            details.because 'API needs higher versions'
        }
    }
}

2.Flutter

上記エラー対応によって、pub.devのimage_cropperページのExampleのコードが動いた。

2.1 pubspec.yaml(/)

に以下を追記

dependencies:
    :
  image_cropper: ^1.1.0

2.2 AndroidManifest.xml(/android/app/main/res)

に以下のActivityを追加。

<application>
    <activity
        android:name="com.yalantis.ucrop.UCropActivity"
        android:screenOrientation="portrait"
        android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
</application>

2.3 ソースコード

import 'dart:convert';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:image_cropper/image_cropper.dart';
import 'package:image_picker/image_picker.dart';

class SecondRoute extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _SecondRoute();
  }
}
class _SecondRoute extends State<SecondRoute>{
  Image _image = Image.network('https://flutter.github.io/assets-for-api-docs/assets/widgets/owl-2.jpg');

  Future _handleImageCrop(BuildContext context) async {

    var imageFile = await ImagePicker.pickImage(source: ImageSource.camera);
    var croppedFile = await ImageCropper.cropImage(
        sourcePath: imageFile.path,
        aspectRatioPresets: [
          CropAspectRatioPreset.square,
          CropAspectRatioPreset.ratio3x2,
          CropAspectRatioPreset.original,
          CropAspectRatioPreset.ratio4x3,
          CropAspectRatioPreset.ratio16x9
        ],
        androidUiSettings: AndroidUiSettings(
            toolbarTitle: 'Cropper',
            toolbarColor: Colors.deepOrange,
            toolbarWidgetColor: Colors.white,
            initAspectRatio: CropAspectRatioPreset.original,
            lockAspectRatio: false),
        iosUiSettings: IOSUiSettings(
          minimumAspectRatio: 1.0,
        )
    );
    setState(() {
      _image = Image.file(croppedFile);
    });
  }

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (BuildContext context, BoxConstraints viewportConstraints) {
        return SingleChildScrollView(
          child: ConstrainedBox(
            constraints: BoxConstraints(
              minHeight: viewportConstraints.maxHeight,
            ),
            child: IntrinsicHeight(
              child: Column(
                children: <Widget>[
                  Container(
                    // A fixed-height child.
                    color: const Color.fromARGB(255, 255, 255, 255),
                    height: 120.0,
                  ),
                  Expanded(
                    // A flexible child that will grow to fit the viewport but
                    // still be at least as big as necessary to fit its contents.
                    child: Container(
                      color: const Color(0xffffffff),
                      height: 240.0,
                      constraints: BoxConstraints(
                          minWidth: viewportConstraints.maxWidth,
                          minHeight: double.infinity
                      ),
                      child: Column(
                        children: <Widget>[
                          RaisedButton(
                            onPressed: () {
                              _handleImageCrop(context);
                            },
                            child: Text('Image Crop'),
                          ),
                          RaisedButton(
                            onPressed: () {
                              Navigator.pop(context);
                            },
                            child: Text('Back'),
                          ),
                          _image,
                        ],
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
        );
      },
    );
  }

}

3.実行

3.1 Android

「Image Crop」ボタン押下で、_handleImageCrop() を呼び出し、画像選択 を経由(カメラを起動)して取得したファイル

var imageFile = await ImagePicker.pickImage(source: ImageSource.camera);

を、

var croppedFile = await ImageCropper.cropImage()

に引き渡す。

flutter_image_crop01

画像を切り抜く

flutter_image_crop02

範囲を選択し、画面右上のチェック

flutter_image_crop03

切り抜かれた範囲を画面に表示。OK!!

flutter_image_crop04

3.2 iOS

Android同様のことをiPhoneエミュレーターで。

特に変更はなし。

・・・エミュレータのため、カメラ起動できないようで、OKでエラーになるので、

var imageFile = await ImagePicker.pickImage(source: ImageSource.camera);

var imageFile = await ImagePicker.pickImage(source: ImageSource.gallery);

に変更して確認

flutter_image_crop_ios01

Android同様に画像の切り抜きUIが起動。

flutter_image_crop_ios02

Doneで、選択部分が画面表示された!

flutter_image_crop_ios03

Flutterもエラーの嵐でうんざりしかけたが、AndroAndroidXの互換性対応で、何とか乗り切れそうか!?

macの操作がぎこちないので、本でも買おうかしら。



Flutter : 画像の選択

Flutter + Firebase でアプリ作成のためのパーツあつめ。

画像選択のパッケージ、image_picker を試す。

https://pub.dev/packages/image_picker

のだが、AndroidX互換性問題に遭遇し難儀したが、何とか解決

1.Flutter

1.1 pubspec.yaml (/) にimage_pickerを追記

dependencies:
    :
  image_picker: ^0.6.1+8

1.2 second.dart

画面遷移で起動した画面に、画像選択し、画像表示するウィジェットを追加する (関係ないコードは除外)

ウィジェットのルートにScaffoldを使用したままだと、写真を選択したときにサイズがはみ出ると、下図のように、はみで多分に黒と黄色の車線がはいって表示され、以下のようなエラーログが吐かれる。

════════ Exception caught by rendering library ═════════════════════════════════════════════════════
The following assertion was thrown during layout:
A RenderFlex overflowed by 236 pixels on the bottom.

flutter_pick_image_err

スクロールできるようにする必要があるようなので、以下のサンプルソースに合わせて、スクロールビューの中に入れる。

https://api.flutter.dev/flutter/widgets/SingleChildScrollView-class.html

import 'dart:convert';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';


class SecondRoute extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _SecondRoute();
  }
}
class _SecondRoute extends State<SecondRoute>{
  Image _image = Image.network('https://flutter.github.io/assets-for-api-docs/assets/widgets/owl-2.jpg');

  Future _handleImagePick(BuildContext context) async {
    var imageFile = await ImagePicker.pickImage(source: ImageSource.gallery);
    var image = Image.file(imageFile);
    setState(() {
      _image = image;
    });
  }

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (BuildContext context, BoxConstraints viewportConstraints) {
        return SingleChildScrollView(
          child: ConstrainedBox(
            constraints: BoxConstraints(
              minHeight: viewportConstraints.maxHeight,
            ),
            child: IntrinsicHeight(
              child: Column(
                children: <Widget>[
                  Container(
                    // A fixed-height child.
                    color: const Color.fromARGB(255, 255, 255, 255),
                    height: 120.0,
                  ),
                  Expanded(
                    // A flexible child that will grow to fit the viewport but
                    // still be at least as big as necessary to fit its contents.
                    child: Container(
                      color: const Color(0xffffffff),
                      height: 240.0,
                      constraints: BoxConstraints(
                        minWidth: viewportConstraints.maxWidth,
                        minHeight: double.infinity
                      ),
                      child: Column(
                        children: <Widget>[
                          RaisedButton(
                            onPressed: () {
                              _handleImagePick(context);
                            },
                            child: Text('Image Pick'),
                          ),
                          _image,
                        ],
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
        );
      },
    );
  }

}

2.Android

画像の初期表示には、Flutter Imageクラスのサンプル画像を表示。

flutter_pick_image01

「Image Pick」ボタン押下でギャラリーから画像を選択できる。

var imageFile = await ImagePicker.pickImage(source: ImageSource.gallery);

のImageSource を、ImageSource.camera に変更することで、カメラが起動する!

flutter_pick_image02

選択すると、Image コンポーネントに選択された画像がロードされる!

flutter_pick_image03

3.iOS

Androidに対応して、少し変更する必要がある。

info.plist(/ios/Runner) に以下を追記。それぞれの機能(key)を利用するときに、目的(string)を表示しユーザーに許可を求める。

<dict>
    <key>NSPhotoLibraryUsageDescription</key>
    <string>What purpose to use</string>
    <key>NSCameraUsageDescription</key>
    <string>What purpose to use</string>
    <key>NSMicrophoneUsageDescription</key>
    <string>What purpose to use</string>
    <key>CFBundleDevelopmentRegion</key>
    :
</dict>

エミュレーターで実行。Androidと同様に表示された。

flutter_pick_image_ios01

「Image Pick」ボタンで、ユーザーに確認。OK押下。

flutter_pick_image_ios02

ギャラリー?が表示されるので、画像を選択。

flutter_pick_image_ios03

選択した画像が、表示された!

flutter_pick_image_ios04

AndroidX対応で、結構はまったが、行けそうか!?

iOSについても勉強する必要あるな。