開発者がDockerを使うメリット
あらまし
開発は自社でやるけれど、運用は協力会社にお任せ!といったシーンが多くあると、 Dockerをはじめとしたコンテナ技術が、自分の開発業務にどう役立つかのイメージができないことがあります(ありました)。
そこで、開発・設計や技術検討、小さめのプロダクトでどう役立つかという視点で、Dockerを紹介します。
巷のウェブサービスのお試し
巷にはたくさんのウェブサービスが転がっていて、試してみたくてもなかなか忙しくて環境構築ができず試せないことが多いです。
コンテナが使えると、パッと環境作って試して、パッと環境ごと捨てて、何事もなかったかのようにいつもの業務に戻ることができます。
コンテナは手元の環境と独立しているので、手元のMySQLやPHPのバージョンの設定にも全く影響がないので、3秒くらいですんなりお別れできるようになってます。
WordPressの例
WordPressはPHPで開発されており、データベース管理システムとしてMySQLを利用している。 そのため動作環境として、最低でも以下を構築する必要がある。 - Apache(Webサーバーソフトウェア) - MySQL(データベース) - PHP(プログラミング言語)
Dockerを使わない一般的な構築手順
XAMPPを使ってローカル環境にWordPressをインストールする方法
- ApacheをまずOSにいれる(多くの場合XAMPP)
- PHPの指定バージョンが使えるように環境を設定する
- ファイアーウォールの設定を変更する
- MySQLをインストールする
- MySQL上に、WordPress用のDBを作る
- WordPressをApacheのフォルダにおく
- MySQLのDB・ユーザー・パスワード・ホストを設定する
- ブラウザでApacheで解放されているポートにアクセスする
っていう手順を、一つ一つミスがないように実施する必要があります。
また、ApacheのバージョンやMySQLのバージョンについて、知識を持って気をつけないと他の開発環境を汚染してしまいます。
さらに、お試しが終わった後にアンインストールする際に、Apache・MySQL・WordPress・ファイアーウォールの設定などを全て元に戻す必要があってとても大変です。(そして多くの場合、戻すほうが大変)
学生時代、言われるがままにXAMPPいれて、環境を戻せずOS再インストールした記憶があります。
といった感じで、ただWordPressのCMSの触りごごちや見た目を試してみたいだけなのに、かなりのリスクと手間をかける必要が出てきてしまいます。
Dockerを使う方法
- Dockerを入れる https://www.docker.com/
- どっかに適当なフォルダを作る
- そのフォルダに以下をコピペしたdocker-compose.ymlというファイルを作る
- shellでそのフォルダに入って、
docker-compose up -d
を実行 - ブラウザでlocalhost:8000にアクセスする
version: '3.3' services: db: image: mysql:5.7 volumes: - db_data:/var/lib/mysql restart: always environment: MYSQL_ROOT_PASSWORD: somewordpress MYSQL_DATABASE: wordpress MYSQL_USER: wordpress MYSQL_PASSWORD: wordpress wordpress: depends_on: - db image: wordpress:latest ports: - "8000:80" restart: always environment: WORDPRESS_DB_HOST: db:3306 WORDPRESS_DB_USER: wordpress WORDPRESS_DB_PASSWORD: wordpress WORDPRESS_DB_NAME: wordpress volumes: db_data: {}
これだけで、WordPressを以下のように試せるようになります。
Dockerが入っている環境だったら、1分もかかりません。
さらに、お試しがおわって環境を捨てたいときは、 docker-compose down
をshellで実行すれば、WordPressもMySQLもApacheも丸ごと消えてくれるので安心。
コンテナは手元の環境と独立しているので、手元のMySQLやPHPのバージョンの設定にも全く影響がないので、3秒くらいですんなりお別れできるようになってます。
WordPress以外を試すとき
大体の有名なサービスは誰かがdocker-compose.ymlを作って置いてあることが多いです。
- WordPress: https://docs.docker.com/compose/wordpress/
- Django: https://docs.docker.com/compose/django/
- Joomla: https://hub.docker.com/r/bitnami/joomla/
- Dokuwiki: https://hub.docker.com/r/bitnami/dokuwiki
- TensorFlow+TensorBoard: https://qiita.com/Nersch/items/7f1f5a3c780fab3ae1e4
- その他いろいろ: https://docs.docker.com/compose/
まとめ
最近話題のサービスを試したり、開発環境構築するときにぜひ使ってみてください!
Flutterでカメラアプリを作る〜写真保存編〜
前置き
FlutterAdventCalender2018 #2 に参加させていただきました。
この記事の前と、そのさらに前の合計3記事でワンセットになります。
blog.aftercider.com blog.aftercider.com
前に投稿した2つの記事は他の方も紹介していたり、ライブラリのExampleでだいたい理解できるところではあるのですが、今回のテーマとした「写真の保存」については個人的にも苦戦したため、AdventCalendarのエントリとしてまとめてみました。
環境
- Flutter (Channel stable, v1.0.0, on Mac OS X 10.14.1 18B75, locale ja-JP)
- Android toolchain - develop for Android devices (Android SDK 28.0.3)
- iOS toolchain - develop for iOS devices (Xcode 10.1)
- Android Studio (version 3.2)
記事書いてる途中でFlutter 1.0.0が出て嬉しいかぎりです!
想定する読者
- iOS開発/Android開発がなんとなくわかる
- Flutterは初心者(なので、クラス分割は行わず、ワンソースで処理を追えるようにします)
やりたいこと
以前の記事で、カメラプレビューの表示・撮影、そしてアプリケーション領域への撮影画像の保存をまとめました。
今回は撮影画像の保存先を他のアプリやPCからアクセスできるようにしていきます。
Androidであればシステム外部のストレージ領域、iOSであればPhotoライブラリ領域(別名 写真領域)に保存していきます。
ファイル保存のパーミッション設定
アプリケーション領域と異なり、Android・iOSそれぞれアプリケーション管轄外の保存領域に書き込むため、Permissionの取得が必要です。
パーミッション管理モジュールは、simple_permissionモジュールを使います。
(ちなみに最初、permission_handlerモジュールを最初使ったのですが、iOS環境でビルドするのに難儀したため、使用するのを断念しました。)
https://pub.dartlang.org/packages/permission_handler
Androidの設定
まずは設定の簡単なAndroidの方から進めます。
しばらく色々触った感想なのですが、Androidではサクッと動くモジュールも、iOSになると何か手を入れないと動かない、みたいなことが多いような気がしています。 それもあって、まずはAndroidから作るようなプロセスを今のところやっています。
以下のように、android/app/src/main/AndroidManufest/xmlに WRITE_EXTERNAL_STORAGE
のパーミッションを要求する宣言を追加します。
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.aftercider.cameraapp"> <!-- The INTERNET permission is required for development. Specifically, flutter needs it to communicate with the running application to allow setting breakpoints, to provide hot reload, etc. --> <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <!-- io.flutter.app.FlutterApplication is an android.app.Application that calls FlutterMain.startInitialization(this); in its onCreate method.
iOSの設定
iOSではPhotoLibraryにアクセスするためには、Info.plistに用途について記述することが必要です。
以下のように項目を追加します。詳しくはこちらの記事を。
ちなみに私の環境だと、simple_permissionを導入するとpodでエラーが発生するようになったため、ios/podfileの2行目を以下のように変更を入れる必要がありました。 Issueにも上がってるみたいです。
# Uncomment this line to define a global platform for your project platform :ios, '10.0' use_frameworks!
コードの追加
AndroidであればWRITE_EXTERNAL_STORAGE、iOSであればPhotoLibraryへのアクセスについて、パーミッションの確認・要求を行う処理を追加します。
// カメラを準備する void setUpCamera(CameraDescription cameraDescription) async { if (controller != null) { await controller.dispose(); } controller = CameraController(cameraDescription, ResolutionPreset.high); // カメラの情報が更新されたら呼ばれるリスナー設定 controller.addListener(() { if (mounted) setState(() {}); // 準備終わったらbuildし直す。 if (controller.value.hasError) { showInSnackBar('Camera error ${controller.value.errorDescription}'); } }); await controller.initialize(); // パーミッションの確認・要求 if (Platform.isAndroid && !await SimplePermissions.checkPermission(Permission.WriteExternalStorage)) { SimplePermissions.requestPermission(Permission.WriteExternalStorage); } else if (Platform.isIOS && !await SimplePermissions.checkPermission(Permission.PhotoLibrary)) { SimplePermissions.requestPermission(Permission.PhotoLibrary); } if (mounted) { setState(() {}); } }
これで、アプリケーションが起動して、setUpCameraが呼ばれるタイミングでパーミッションの要求が実行されるようになります。
撮影画像の保存
次は、許可が取れた領域に撮影した画像を保存する処理を追加していきます。
Androidについては非常に簡単で、 path_provider
モジュールがほとんどよしなにやってくれます。
getApplicationDocumentsDirectoryで取得していたディレクトリを、getExternalStorageDirectoryで取得するように変更すればOKです。
この際、WRITE_EXTERNAL_STORAGEのpermissionが取得できていない場合、保存に失敗するのでご注意ください。
// Before final Directory extDir = await getApplicationDocumentsDirectory(); // アプリケーション領域 final String dirPath = '${extDir.path}/Pictures/flutter_test'; await Directory(dirPath).create(recursive: true); final String filePath = '$dirPath/${timestamp()}.jpg';
// After final Directory extDir = await getExternalStorageDirectory(); // 外部領域 final String dirPath = '${extDir.path}/Pictures/flutter_test'; await Directory(dirPath).create(recursive: true); final String filePath = '$dirPath/${timestamp()}.jpg';
iOSでの処理
Androidで使用したgetExternalStorageDirectoryの関数は、残念ながらiOSでは使うことができません。
そこで、iOSのPhotoLibrary領域に写真を保存するために、 image_picker_saver
モジュールを追加します。
pubspec.yamlにimage_picker_saverを以下のように追加します。
environment: sdk: ">=2.0.0-dev.68.0 <3.0.0" dependencies: flutter: sdk: flutter camera: ^0.2.6 path_provider: ^0.4.1 simple_permissions: ^0.1.9 image_picker_saver: ^0.1.0
image_picker_saverは、保存したいバイト配列を渡すと、PhotoLibrary領域に画像を保存してくれる機能を持っています。
ということで、iOSでは、写真撮影した画像をtemporary領域に一時画像として保存し、image_picker_saverを使って、PhotoLibrary領域にコピーするといった流れで、撮影画像を保存していきます。
// 画像撮影・保存処理 Future<String> takePicture() async { if (!controller.value.isInitialized) { return null; } Directory dir; if (Platform.isAndroid) { dir = await getExternalStorageDirectory(); // 外部ストレージに保存 } else if (Platform.isIOS) { dir = await getTemporaryDirectory(); // 一時ディレクトリに保存 } else { return null; } final String dirPath = '${dir.path}/Pictures/flutter_test'; await Directory(dirPath).create(recursive: true); String filePath = '$dirPath/${timestamp()}.jpg'; if (controller.value.isTakingPicture) { return null; } await controller.takePicture(filePath); // filePathに保存されたデータをiOSならPhotoLibrary領域にコピーする if (Platform.isIOS) { String tmpPath = filePath; var savedFile = File.fromUri(Uri.file(tmpPath)); filePath = await ImagePickerSaver.saveFile( fileData: savedFile.readAsBytesSync()); } return filePath; }
動作確認
Android
AndroidEmulatorで動作させるとこんな感じの動作画面です。画面下部に表示された撮影先ファイルパスもちゃんと外部領域になっています。
iOS
iOSはシミュレーターだとカメラが動かせないので、手持ちのiPad miniで動作させました。 Androidと同じく、撮影されたファイルパスがきちんとPhotoLibrary領域になっています。撮影された画像も写真アプリからちゃんと閲覧することができています。
まとめ
コード量としては10行ちょっとの追加で、保存先を外部領域/PhotoLibrary領域に保存することができました。 Flutter 1.0.0が出たばっかりとはいえ、ライブラリ群がかなり準備されているおかげです。
コードについても、Dartがそこまで癖がある言語ではないこともあり、コード補完で大体出来上がった感じでした。 ただ、Podfileの変更や、AndroidManufest・Info.plistなど、各プラットフォームの開発経験がないと結構つまづきやすいところもあるので、その辺の知識はFlutter開発するにあたっても必須になるなと感じました。
また、今回コードをワンソースで見れるっていう形で作ったこともありOSでの分岐がどうしても発生してしまいましたが、クラス・モジュール設計をきちんとやるとロジック側ではあまりOS感の違いを意識しない作りにすることも可能だと思います。
もちろん、プロダクションレベルではモジュールとして隠蔽できるようにした作りにするのがいいかと思います。
書いたコード
作ったプロジェクトについてはGithubにおいておきました。ここには書ききれなかったコードの細かい変更等はそちらでご確認いただけると幸いです。
Flutterでカメラアプリを作る〜撮影編〜
Flutterという、ワンソースでiOSアプリもAndroidアプリもビルドできるGoogle製フレームワークがありまして、そのFlutterを使ってカメラのプレビュー・撮影・保存までのやり方をまとめました。
以下の記事では、環境設定とプレビューまでをまとめました。
ちなみに本記事の次では、保存に関してまとめてあります。
前提
- Flutter 0.11.3
- MacBookPro(Mac Mojave)環境
- Flutterは初めてさわる
撮影コード追加
package導入
まず、package.yamlにファイルパスをサポートしてくれるpath_provider
を導入して、$ flutter packages get
します。
name: hello_world_project description: HelloWorldProject version: 1.0.0+1 environment: sdk: ">=2.0.0-dev.68.0 <3.0.0" dependencies: flutter: sdk: flutter cupertino_icons: ^0.1.2 english_words: ^3.1.0 camera: ^0.2.4 path_provider: ^0.4.1 dev_dependencies: flutter_test: sdk: flutter flutter: uses-material-design: true
プログラムを書く
プレビュー編で行なった通りcameraモジュールの動作要件に合わせ、AndroidのminSdkVersionは21にしておきましょう。
cameraモジュールのExampleにサンプルコードが掲載されているんですが、 静止画撮影・動画撮影の両方のコードが書いてあり、ボリュームも大きいので初心者にはちょっと難易度が高いです。
また、FlutterのUIに関するコードも結構な量あるため、どこがカメラ制御用のコードなのかわからないっていう難しさもあります。
ということで、今回はFlutterでカメラを使えるようになることを目的として、
- 例外処理・UI装飾をできるだけなくして見通しをよくする
- 録画関連・サムネイル表示のコードを削除
- コメントを追加
といった対応を入れて、撮影・保存のみできるようにします。
main.dartを以下のようにしていきました。
import 'dart:async'; // 非同期処理(async/await) import 'dart:io'; // ファイルの入出力 import 'package:camera/camera.dart'; // カメラモジュール import 'package:flutter/material.dart'; // マテリアルデザイン import 'package:path_provider/path_provider.dart'; // ファイルパスモジュール List<CameraDescription> cameras; // 使用できるカメラのリスト // ここから始まる Future<Null> main() async { cameras = await availableCameras(); runApp(CameraApp()); } // 親玉のApp class CameraApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: CameraWidget(), ); } } // 親玉の中身 class CameraWidget extends StatefulWidget { @override _CameraWidgetState createState() { return _CameraWidgetState(); } } // 実際はこれがやることやる class _CameraWidgetState extends State<CameraWidget> { CameraController controller; final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>(); String timestamp() => DateTime.now().millisecondsSinceEpoch.toString(); // ファイル名にはタイムスタンプ入れる。 void showInSnackBar(String message) => _scaffoldKey.currentState .showSnackBar(SnackBar(content: Text(message))); // SnackBarでメッセージ表示 @override Widget build(BuildContext context) { Scaffold sc = Scaffold( key: _scaffoldKey, body: Column( children: <Widget>[ Expanded( child: Container( child: Padding( padding: const EdgeInsets.all(1.0), child: Center( child: _cameraPreviewWidget(), // カメラのプレビューを表示するWidget ), ), ), ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisSize: MainAxisSize.max, children: <Widget>[ IconButton( // カメラの撮影ボタン icon: const Icon(Icons.camera_alt), onPressed: controller != null && controller.value.isInitialized ? onTakePictureButtonPressed // 撮影ボタンを押された時にコールバックされる関数 : null, ), ], ) ], ), ); // カメラのセットアップ。セットアップが終わったらもう一回buildが走るので、 // controllerがnullかどうかで処理実施有無を判定。 if (controller == null) { setUpCamera(cameras[0]); } return sc; } /// カメラプレビューを表示するWidget Widget _cameraPreviewWidget() { if (controller == null || !controller.value.isInitialized) { // カメラの準備ができるまではテキストを表示 return const Text('Tap a camera'); } else { // 準備ができたらプレビュー表示 return AspectRatio( aspectRatio: controller.value.aspectRatio, child: CameraPreview(controller), ); } } // カメラを準備する void setUpCamera(CameraDescription cameraDescription) async { if (controller != null) { await controller.dispose(); } controller = CameraController(cameraDescription, ResolutionPreset.high); // カメラの情報が更新されたら呼ばれるリスナー設定 controller.addListener(() { if (mounted) setState(() {}); // 準備終わったらbuildし直す。 if (controller.value.hasError) { showInSnackBar('Camera error ${controller.value.errorDescription}'); } }); await controller.initialize(); if (mounted) { setState(() {}); } } // 撮影ボタンが押されたら撮影して、画像を保存する void onTakePictureButtonPressed() { takePicture().then((String filePath) { if (mounted) { setState(() {}); if (filePath != null) showInSnackBar('Picture saved to $filePath'); } }); } // 画像保存処理 Future<String> takePicture() async { if (!controller.value.isInitialized) { return null; } final Directory extDir = await getApplicationDocumentsDirectory(); final String dirPath = '${extDir.path}/Pictures/flutter_test'; await Directory(dirPath).create(recursive: true); final String filePath = '$dirPath/${timestamp()}.jpg'; if (controller.value.isTakingPicture) { return null; } await controller.takePicture(filePath); return filePath; } }
実機で実行
実機で実行すると、カメラプレビューと撮影ボタンがこんな感じで表示されます。
課題
撮影してみるとわかるんですが、iOSもAndroidもtakePicture
のなかで保存先ファイルパスをgetApplicationDocumentsDirectory
で取得していて、アプリ領域に画像を保存しているためため、ギャラリーアプリや写真アプリで撮影画像を見ることができません。
次回はギャラリーアプリや写真アプリで見れる領域に保存する方法をまとめます。
Android/iOSクロス開発フレームワーク Flutter入門
- 作者: 掌田津耶乃
- 出版社/メーカー: 秀和システム
- 発売日: 2018/09/14
- メディア: 単行本
- この商品を含むブログを見る
Flutterでカメラアプリを作る〜プレビュー表示編〜
Flutterという、ワンソースでiOSアプリもAndroidアプリもビルドできるGoogle製フレームワークがありまして、そのFlutterを使ってカメラのプレビュー・撮影・保存までのやり方をまとめました。
この記事ではプレビューまでをまとめ、次の記事では撮影・保存をまとめます。
前提
- Flutter 0.11.3
- MacBookPro(Mac Mojave)環境
- Flutterは初めてさわる
インストール
公式ドキュメントにステップバイステップで書いてあるので、これを見ながら導入します。
https://flutter.io/docs/get-started/install
AndroidStudid, XCode, VisualStudioCodeは導入していたので、プラグインだけ設定して完了です。
pubspec.yamlにcameraモジュールを追加
Androidでいうところのapp/buld.gradle
、Nodejsでいうところのpackage.json
にあたるのが、pubspec.yaml
です。
YAMLはインデントを使って構造を表現するようになっているので、ぱっと見で大体わかります。
今回はcameraモジュールを追加しましょう。
dependenciesの中にcamera: ^0.2.4
を追加します。
name: hello_world_project description: HelloWorldProject version: 1.0.0+1 environment: sdk: ">=2.0.0-dev.68.0 <3.0.0" dependencies: flutter: sdk: flutter cupertino_icons: ^0.1.2 english_words: ^3.1.0 camera: ^0.2.4 dev_dependencies: flutter_test: sdk: flutter flutter: uses-material-design: true
AndroidStudioの右上からPackages getを実行して、モジュール群を取得します。AndroidでいうところのSyncですね。
(Androidのみ)minSDKVersionを21以上にする
cameraモジュールのスペック要件がminSdkVersion:21以上なので、android/app/build.gradle
を開き、android
-> defaultConfig
-> minSdkVersion
を21にします。(筆者環境下での初期値は16になってました。)
カメラのプレビューを表示する
こんな感じで、main.dartを作ります。
import 'dart:async'; import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; // カメラ情報のリスト List<CameraDescription> cameras; // ここから始まる Future<Null> main() async { cameras = await availableCameras(); runApp(new CameraApp()); } // カメラを表示するStatefulWidget class CameraApp extends StatefulWidget { @override _CameraAppState createState() => new _CameraAppState(); } class _CameraAppState extends State<CameraApp> { CameraController controller; @override void initState() { super.initState(); // 背面カメラを高解像度で初期化して表示スタート controller = new CameraController(cameras[0], ResolutionPreset.high); controller.initialize().then((_) { if (!mounted) { return; } setState(() {}); }); } @override void dispose() { // 使い終わったらカメラを解放 controller?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { if (!controller.value.isInitialized) { return new Container(); } return new AspectRatio( aspectRatio: controller.value.aspectRatio, child: new CameraPreview(controller)); } }
実機で実行
端末をつなぎ、画面上の緑色の実行ボタンをクリックして実行します。
実行すると、カメラなどのパーミッション要求もきちんとやってくれて、アプリが立ち上がります。
IDE側のLogcatもちゃんと見れるようになっていて、Android開発者にも優しいです。
ちなみにプレビューはこんな感じです。(縦長になってしまうのはカメラの縦横比とライブビューの縦横比が違うため)
次の記事
次の記事では撮影・保存を行います。