FlutterでiOS、Android両方で動くカメラアプリを作る


Flutterを使ってマルチプラットフォームなカメラアプリを作っていく

「iOSでもAndroidでも動くアプリで、」
「スマホ内の画像にアクセスしてローカルのDBに保存可能」
「アプリ内でもカメラを起動できる」

っていう要件をFlutterで満たせるかの検証をバイト先に任されたからなー

# [Flutter](https://flutter.dev/docs)

iOS、Androidといった異なるプラットフォームのアプリを一つのコードで書くことができるGoogle製のオープンソースのクロスプラットフォームのフレームワーク

公式ドキュメントも充実・サードパーティー製のプラグインも日々増えているので、旬な技術だと思っている

[公式Youtube](https://www.youtube.com/watch?v=l-YO9CmaSUM)にはお洒落な感じデザインのやらWidgetやらの説明動画がたくさんあるので楽しい

環境構築は以下のサイトを参考にした。ありがとうございました

- [iPhone実機でFlutter開発を進めたい @tocomi ](https://qiita.com/tocomi/items/34949d64de9677c1e697)

ホットリロードが神すぎる

早速作っていきたいのだけど、Flutterは言語はdartでWidget単位でコードを完成させてアプリを作っていく

公式ドキュメントでもFlutterではWidgetが全てだよって言ってる

> In Flutter, “everything is a widget”! - [Flutter documentation](https://flutter.dev/docs)


特に重要なWidgetが [Stateless Widget](https://api.flutter.dev/flutter/widgets/StatelessWidget-class.html) と [Statefull Widget](https://api.flutter.dev/flutter/widgets/StatefulWidget-class.html) の２つで順番におさらいしていく

そのあと、画像ファイル・カメラへのアクセスを可能にするプラグインである[image_picker](https://pub.dev/packages/image_picker/example) と、データベースを扱うためのプラグイン[sqflite](https://pub.dev/packages/sqflite)の紹介に入って、最後にコードを説明して終わり

## [Stateless Widget](https://api.flutter.dev/flutter/widgets/StatelessWidget-class.html)

名前の通り状態を持たないWidget。

以下は公式YouTubeで取り上げられているコード例

```
class ItemCount extends StatelessWidget {
 final String name;
 final int count;

 ItemCount({this.name, this.count});

 @overide
 Widget build(BuildContext context) {
  return Text('$name: $count')
 }
}
```

String型のnameとint型のcountを与えると、それらをTextWidgetに渡して表示させるようなWidgetである。


## [Statefull Widget](https://api.flutter.dev/flutter/widgets/StatefulWidget-class.html)

状態をもつWidgetで、Stateが更新されたときに内容が更新される。

先ほどのコードをStatefulにすると以下のような感じになる。

```
class ItemCount extends StatefulWidget {
 final String name;

 ItemCounter({this.name});
 
 @override
 _ItemCounterState createState() => _ItemCounterState();
}

class _ItemCounterState extends State<ItemCounter> {
 int count = 0;

 @overide
 Widget build(BuildContext context) {
   return GestureDetector(
     onTap: () {
       setState(() {
         count++;
       });
     ),
     child: Text('${widget.name}: $count'),
   );
 }
}
```

setStateはその中に書かれた変数が変化した時にクラスに変更を伝える役割を持っている

これでStatelessだった画面が、タップするとcountが上がっていくプログラムになった


## [image_picker](https://pub.dev/packages/image_picker)

image_pickerははアプリ内でカメラを立ち上げたり、スマホ内のメディア（画像）にアクセスさせたりするのに使うプラグイン

> A Flutter plugin for iOS and Android for picking images from the image library, and taking new pictures with the camera. - [image_picker | pub.dev](https://pub.dev/packages/image_picker)

フラッター開発チームが作った純正なのでいろいろ安心

Flutterでのプラグインの導入は ```pubspec.yaml``` にプラグインの情報を追加して ```$ flutter pub get``` してあげるだけ

このプラグインではフォトライブラリへのアクセスを許可しないといけないので、iOSでは以下の変数に関する設定を``` ios/Runner/info.plist```に事前に書き込んでおく必要がある

|変数名|役割|
|---|---|
|```NSPhotoLibraryUsageDescription```|フォトライブラリへのアクセス	|
|```NSCameraUsageDescription```|カメラへのアクセス|
|```NSMicrophoneUsageDescription```|マイクへのアクセス	|

動画を取り込まないなら一番下の項目は無視できる

細かい設定の仕方は[公式ドキュメント](https://pub.dev/packages/image_picker)をご参照ください

公式ドキュメントをサンプルのコードに手を加え、ペッとmain.dartに貼り付けたら動くものを作った

動作環境はMacBookPro上の ```android-x86 emulator(Pixel_3a_API_30_x86)```である

例えばカメラの起動と写真の取得はたったこれだけ。

```dart:カメラの起動・写真の取得
final pickedFile = await picker.getImage(source: ImageSource.camera);
```

```ImageSource.gallery``` ならフォルダへのアクセスができる

かなり使えそう。

## [sqflite](https://pub.dev/packages/sqflite)

SQLite（データベース）を導入・利用するためのプラグイン

> Flutter plugin for SQLite, a self-contained, high-reliability, embedded, SQL database engine. - [sqflite | pub.dev](https://pub.dev/packages/sqflite)

導入は例に漏れず ```pubspec.yaml``` にプラグインの情報を追加して ```$ flutter pub get``` してあげるだけ

[公式Github](https://github.com/tekartik/sqflite/blob/master/sqflite/doc/opening_db.md)とか他の記事見ながら理解した

主な関数の説明を列挙していく

まずはデータベースへのパスを取得する必要があるらしいので、```import 'package:path/path.dart'```しておいて、getDatabasesPath関数でパスを取得する

それを用いると、データベースを開く処理はこんな感じでできる

```dart:openDatabase関数
String _path = join(await getDatabasesPath(), "mydb.db");
Database database = await openDatabase(_path, version: 1, onCreate: 処理);
```

新規にデータベースを作成してアクセスする時などは、openDatabase関数実行時にonCreateを設定すれば良い

onCreate関数は、データを事前準備しておきたい時にも使える

```dart:onCreate関数
    String _path = join(await getDatabasesPath(), "mydb.db");

    Database _database = await openDatabase(_path, version: 1,
        onCreate: (Database db, int version) async {
      await db.execute(
          "CREATE TABLE IF NOT EXISTS mydb (id INTEGER PRIMARY KEY, text TEXT)");
      // データを事前に流し込みたい時はここにinsertを記述すれば良い
    });
```

データベースの内容の書き換えなどで、transaction関数を利用できる

```dart:transaction関数
    await _database.transaction((txn) async {
      await txn.rawInsert('INSERT INTO mydb(text) VALUES("$textData")');
    });
```

レコードの取得にはrawQuery、保存にはrawInsert、更新はrawUpdate、削除はrawDeleteを使えば良い
rawQuery関数の結果はMap型のリストで返ってくる

```dart:rawQuery関数
    List<Map> _result = await _database.rawQuery('SELECT * FROM mydb');
```

SQLとかを発行しない処理であれば、execute関数で行える

# 本題

カメラとギャラリーから画像を取ってきて、DB(SQLite)に保存するアプリを作ってみた。

まずは完成したアプリをみて欲しい．

Androidエミュレーターでの動き

![andq.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296126/026c6c4f-6efa-6f64-7314-14012ae2153e.gif)

iPhoneエミュレーターでの動き

![iosq.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296126/bb79d484-778e-72e3-f820-623fefd23d98.gif)

Android、iphoneの両方で動作するアプリがであることが確認できる（ただし、iphoneエミュレーター上ではカメラは起動しない）

sqfliteに画像を保存する場合は画像情報をbase64でエンコードしたテキストとして写真の実体を保存する

自分はこちらを参考にした（Thank you!）

[Google’s Flutter Tutorial – Save Image as String in SQLite Database
](https://www.elasticfeed.com/dbeda2c33c1acd374f6fdcf04030d34b/)


画像を保存しておくためのテーブルは以下のように定義している

```dart

  initDB() async {
    io.Directory documentsDirectory = await getApplicationDocumentsDirectory();
    String path = join(documentsDirectory.path, DB_NAME);
    var db = await openDatabase(path, version: 1, onCreate: _onCreate);
    return db;
  }

  _onCreate(Database db, int version) async {
    await db.execute("CREATE TABLE $TABLE ($ID INTEGER, $DATA TEXT)");
  }
```

ギャラリーからピックアップしてきた画像をBase64でエンコードして保存

```dart

  _pickImageFromGallery() {
    final picker = ImagePicker();
    picker.getImage(source: ImageSource.gallery).then((imgFile) async {
      if (imgFile != null) {
        String imgString = Utility.base64String(await imgFile.readAsBytes());
        Photo photo = Photo(0, imgString);
        dbHelper.save(photo);
      }
    });
  }
```

完成したコードは[GitHub](https://github.com/okayu1230z/image_manage_app)においておきます