Flutter
GoogleDriveAPI

Flutter+Google Driveで健康診断記録アプリを数時間で開発する方法

健康診断記録

「健康診断の結果ちゃんと管理してないな」

   ↓

「スキャンしてGoogle Driveに保存すると良いよ!」 ← 神の声

   ↓

「そういえばGoogle Drive + Flutterやったこと無いな」

   ↓

「Flutterで健康診断結果をGoogle Driveに保存するアプリを作ろう」

ということで作ってみました

開発は帰宅してから寝るまでという制約で短時間で出来ることのみに絞って開発しました。求める機能は「簡単にGoogle Driveにアップロード」と「簡単にアップした画像を閲覧」という2点です。

設計図

まずはお絵かき
設計図

まずはイラストに落としてみると頭の整理がしやすいので自分さえ理解できれば良いのでアナログですが絵を書いてみることをオススメします。

完成したアプリ

ログイン直後

右下のアイコンからカメラかギャラリーを選択できます
Flutter健康診断記録アプリ

一覧画面

健康診断レポートを2枚アップロードした状態の一覧表示
Flutter健康診断記録アプリ

詳細画面

上の一覧画面からレポートをタップして詳細を開いた画面。画像の拡大縮小をすることが出来ます
Flutter健康診断記録アプリ

※サンプル用画像なので私の健康状態とは関係ありません

必要な機能の一覧

次に設計図を元に必要な実装すべき機能を洗い出します。ここも人に伝えるわけではないので、自分が粒度は適当。

  • Google Login
  • FABアイコン(スピードダイアル)
  • 写真選択
  • 写真撮影
  • Google Driveに画像保存
  • Google Driveからファイル読み込み
  • ListViewの画像をタップすると画像詳細画面
  • 写真は拡縮できるようにする

最低限この辺りの機能はアプリを便利に使うために必要。

やろうと思ったけどやめたこと

  • Firebaseにファイル名、パス、日付タイトルを保存
  • AppBarに編集ボタン
  • 編集画面(フォーム入力)
  • スワイプでファイル削除

リストをしっかりと管理(編集、削除)出来ないようにしました。
Firebaseでデータを管理するほど扱う情報が無いため、画像+ファイル名で管理
ファイル名は撮影時刻で保存することにしてアプリからの変更は無し(Google Drive上でファイル名の変更等を行えば反映できる)
編集画面も同様に無し(Google Drive上でファイル名の変更等を行えば反映できる)
ファイル削除機能も無し(Google Drive上で削除すれば一覧から消すことが出来る)

という事で、アップロードと閲覧を簡単に出来るようにする以外の機能はGoogle Driveの持つ機能をそのまま利用することで実装を回避しました

Flutterでのgoogle-services.jsonの置き場所(Android)

Screen Shot 0030-07-11 at 22.03.45.png

Flutterの記事なのでGoogle API Console等の使い方は割愛

Google Login

google_sign_in 3.0.4
https://pub.dartlang.org/packages/google_sign_in

googleapis 0.51.0
https://pub.dartlang.org/packages/googleapis

pubspec.yaml
google_sign_in: "^3.0.4"
googleapis: "^0.51.0"
main.dart
import 'package:google_sign_in/google_sign_in.dart';
import 'package:googleapis/drive/v3.dart';

GoogleSignInAccount _currentUser;

// ログイン時に要求するパーミッションを指定
GoogleSignIn _googleSignIn = new GoogleSignIn(
  scopes: <String>[
    DriveApi.DriveFileScope,
  ],
);

  @override
  void initState() {
    super.initState();
    _googleSignIn.onCurrentUserChanged.listen((GoogleSignInAccount account) {
      setState(() {
        _currentUser = account;
      });
      if (_currentUser != null) {
        _handleGetFiles();
      }
    });
    _googleSignIn.signInSilently();
  }

  // google sign in
  Future<Null> _handleSignIn() async {
    try {
      await _googleSignIn.signIn();
    } catch (error) {
      print(error);
    }
  }

のよう必要なパーミッションの列挙、ログイン済みだった場合の自動ログイン処理、ログイン済みユーザーのアカウント情報を取得します。ログイン後は、_currentUser.authHeadersから認証用のHTTPヘッダー等と取得することが出来るので通信時に使用したり、

main.dart
ListTile(
  leading: GoogleUserCircleAvatar(
    identity: _currentUser,
  ),
  title: Text(_currentUser.displayName),
  subtitle: Text(_currentUser.email),
)

のようにしてログインユーザーの表示に利用することが出来ます

スピードダイアル

flutter_speed_dial 1.0.4
https://pub.dartlang.org/packages/flutter_speed_dial

こちらの記事でも紹介したFABメニューです。今回やりたいことが簡単に実現できるので採用

Flutterウィークリー #26
https://qiita.com/aoinakanishi/items/27f3d60440dc3caf9158#flutter-speed-dial

pubspec.yaml
flutter_speed_dial: "^1.0.4"
main.dart
      return SpeedDial(
        animatedIcon: AnimatedIcons.menu_close,
        animatedIconTheme: IconThemeData(size: 22.0),
        curve: Curves.bounceIn,
        children: [
          // Select file
          SpeedDialChild(
            child: Icon(Icons.add_photo_alternate),
            backgroundColor: Colors.green,
            onTap: () {
              getImage(ImageSource.gallery);
            },
            label: 'Select',
            labelStyle: TextStyle(fontWeight: FontWeight.w500),
          ),
          // Take a picture
          SpeedDialChild(
            child: Icon(Icons.add_a_photo),
            backgroundColor: Colors.deepOrangeAccent,
            onTap: () {
              getImage(ImageSource.camera);
            },
            label: 'Camera',
            labelStyle: TextStyle(fontWeight: FontWeight.w500),
          ),
        ],
      );

写真撮影か、ファイル選択を選ぶメニューとして使いました

写真選択・写真撮影

image_picker 0.4.5
https://pub.dartlang.org/packages/image_picker

pubspec.dart
image_picker: "^0.4.5"
main.dart
// ギャラリーからファイルを選択
var image = await ImagePicker.pickImage(source: ImageSource.gallery);

// カメラで写真撮影
var image = await ImagePicker.pickImage(source: ImageSource.camera);

これを呼ぶだけでファイルのパスが入ってきてあとの処理はライブラリ側で処理してくれます

Google Driveに画像保存

main.dart
    var client = GoogleHttpClient(await _googleSignIn.currentUser.authHeaders);
    var api = DriveApi(client);

    // ファイル名を日付+時刻にする
    uploadFile(api, image,
            DateTime.now().toIso8601String().substring(0, 19) + ".jpg")
        .whenComplete(() => client.close());

  // upload file to Google drive
  Future uploadFile(DriveApi api, io.File file, String filename) {
    var media = Media(file.openRead(), file.lengthSync());
    return api.files
        .create(File.fromJson({"name": filename}), uploadMedia: media)
        .then((File f) {
      print('Uploaded $file. Id: ${f.id}');
    }).whenComplete(() {
      // reload content after upload the file
      _handleGetFiles();
    });
  }

Google Driveからファイル読み込み

main.dart
    // Google DriveのAPIを叩く
    final Response response = await get(
      'https://www.googleapis.com/drive/v3/files',
      headers: await _currentUser.authHeaders,
    );

    // jsonデコード処理
    final Map<String, dynamic> data = json.decode(response.body);

    // ファイル情報を取得
    for (var i = 0; i < data['files'].length; i++) {
      print(data['files'][i]['name']);
      print(data['files'][i]['id']);
    }

ListViewに画像を表示

main.dart
FadeInImage(
    // 読み込み中の画像を指定
    placeholder: AssetImage('images/placeholder.png'),
    // Google Driveからファイルを取得するURLを指定
    image: NetworkImage(
        "https://www.googleapis.com/drive/v3/files/" +
            data['files'][i]['id'] +
            "?alt=media",
        // ヘッダーを指定
        headers: await _googleSignIn.currentUser.authHeaders),
    fadeOutDuration: new Duration(milliseconds: 300),
    fadeOutCurve: Curves.decelerate,
    height: photoHeight,
    width: photoWidth,
    fit: BoxFit.fitWidth,
)

という書き方をすることで、ローディング画像を表示しておいてダウンロードが完了すると画像がフェードして入れ替わります。

タップすると詳細画面

main.dart
GestureDetector(
  child: // 画像表示は上で解説したので省略
  onTap: () async {
    var headers = await _googleSignIn.currentUser.authHeaders;
    Navigator.push(
      context,
      MaterialPageRoute(
          builder: (context) =>
              DetailScreen(data['files'][i], headers)),
    );
  }
),

呼び出されるWidgetは以下の表にしました

main.dart
// Photo viewer
class DetailScreen extends StatelessWidget {
  var _data;
  Map<String, String> _headers;
  DetailScreen(this._data, this._headers);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(_data['name']),
        ),
        body: // 下のZoomableImageを使用
    );
  }
}

詳細画面では拡縮できるようにする

zoomable_image 1.2.0
https://pub.dartlang.org/packages/zoomable_image

pubspec.yaml
zoomable_image: "^1.2.0"
main.dart
ZoomableImage(
    NetworkImage(
        "https://www.googleapis.com/drive/v3/files/" +
            _data['id'] +
            "?alt=media",
        headers: _headers
    ),
    placeholder: Center(child: CircularProgressIndicator()),
    backgroundColor: Colors.white
)

ソース

https://github.com/aoinakanishi/flutter-healthreport-logger

まとめ

苦労した点は特になく、スムーズに作業をすることが出来ました。あえて書くとすれば、FlutterからGoogle Drive APIを使用した例が無く、プラグインを使用した例を探すことが出来なかった点がありますが、プラグインもオープンソースで提供されているのでソースを読めば大丈夫。

良かった点はFlutterからGoogle Driveのファイルを扱う方法がわかったこと。改めてFlutterは便利なので今後も色々な自分用アプリを作っていこうかと思えたことです。やりたいことは大体標準のAPIとして組み込まれていたり既に誰かがプラグインを公開していることが多いです。

最新のFlutter情報に触れたい場合は
https://qiita.com/tags/flutterweekly
とかをウォッチしておくと毎週Flutterに関する新しいレポートが読めるかも?

ちなみに、普段メモ用途のアプリに「 Google Keep 」というサービスを使っているのですが、そこに写真を保存しておけば画像にメモも残せるし、検索も出来るし便利なのでオレオレアプリを時間かけて作っるよりもオススメです!!!