ほねっとのぶろぐ

アニメとAndroidが好きなほねっとのブログです。

開発者がDockerを使うメリット

f:id:aftercider:20190316234633p:plain

あらまし

開発は自社でやるけれど、運用は協力会社にお任せ!といったシーンが多くあると、 Dockerをはじめとしたコンテナ技術が、自分の開発業務にどう役立つかのイメージができないことがあります(ありました)。

そこで、開発・設計や技術検討、小さめのプロダクトでどう役立つかという視点で、Dockerを紹介します。

巷のウェブサービスのお試し

巷にはたくさんのウェブサービスが転がっていて、試してみたくてもなかなか忙しくて環境構築ができず試せないことが多いです。

コンテナが使えると、パッと環境作って試して、パッと環境ごと捨てて、何事もなかったかのようにいつもの業務に戻ることができます。

コンテナは手元の環境と独立しているので、手元のMySQLやPHPのバージョンの設定にも全く影響がないので、3秒くらいですんなりお別れできるようになってます。

WordPressの例

f:id:aftercider:20190316234338p:plain

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分もかかりません。

f:id:aftercider:20190316234020p:plain
wordpress

さらに、お試しがおわって環境を捨てたいときは、 docker-compose down をshellで実行すれば、WordPressもMySQLもApacheも丸ごと消えてくれるので安心。

コンテナは手元の環境と独立しているので、手元のMySQLやPHPのバージョンの設定にも全く影響がないので、3秒くらいですんなりお別れできるようになってます。

WordPress以外を試すとき

大体の有名なサービスは誰かがdocker-compose.ymlを作って置いてあることが多いです。

まとめ

最近話題のサービスを試したり、開発環境構築するときにぜひ使ってみてください!

Flutterでカメラアプリを作る〜写真保存編〜

f:id:aftercider:20181208230023p:plain

前置き

FlutterAdventCalender2018 #2 に参加させていただきました。

qiita.com

この記事の前と、そのさらに前の合計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モジュールを使います。

pub.dartlang.org

(ちなみに最初、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に用途について記述することが必要です。

以下のように項目を追加します。詳しくはこちらの記事を。

dev.classmethod.jp

ちなみに私の環境だと、simple_permissionを導入するとpodでエラーが発生するようになったため、ios/podfileの2行目を以下のように変更を入れる必要がありました。 Issueにも上がってるみたいです。

github.com

# 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 モジュールを追加します。

pub.dartlang.org

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で動作させるとこんな感じの動作画面です。画面下部に表示された撮影先ファイルパスもちゃんと外部領域になっています。

f:id:aftercider:20181208225318p:plain
Android動作イメージ

iOS

iOSはシミュレーターだとカメラが動かせないので、手持ちのiPad miniで動作させました。 Androidと同じく、撮影されたファイルパスがきちんとPhotoLibrary領域になっています。撮影された画像も写真アプリからちゃんと閲覧することができています。

f:id:aftercider:20181208225356p:plain
iOS動作イメージ

まとめ

コード量としては10行ちょっとの追加で、保存先を外部領域/PhotoLibrary領域に保存することができました。 Flutter 1.0.0が出たばっかりとはいえ、ライブラリ群がかなり準備されているおかげです。

コードについても、Dartがそこまで癖がある言語ではないこともあり、コード補完で大体出来上がった感じでした。 ただ、Podfileの変更や、AndroidManufest・Info.plistなど、各プラットフォームの開発経験がないと結構つまづきやすいところもあるので、その辺の知識はFlutter開発するにあたっても必須になるなと感じました。

また、今回コードをワンソースで見れるっていう形で作ったこともありOSでの分岐がどうしても発生してしまいましたが、クラス・モジュール設計をきちんとやるとロジック側ではあまりOS感の違いを意識しない作りにすることも可能だと思います。

もちろん、プロダクションレベルではモジュールとして隠蔽できるようにした作りにするのがいいかと思います。

書いたコード

作ったプロジェクトについてはGithubにおいておきました。ここには書ききれなかったコードの細かい変更等はそちらでご確認いただけると幸いです。

github.com

Flutterでカメラアプリを作る〜撮影編〜

f:id:aftercider:20181116165525p:plain Flutterという、ワンソースでiOSアプリもAndroidアプリもビルドできるGoogle製フレームワークがありまして、そのFlutterを使ってカメラのプレビュー・撮影・保存までのやり方をまとめました。

以下の記事では、環境設定とプレビューまでをまとめました。

blog.aftercider.com

ちなみに本記事の次では、保存に関してまとめてあります。

前提

  • 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;
  }
}

実機で実行

実機で実行すると、カメラプレビューと撮影ボタンがこんな感じで表示されます。

f:id:aftercider:20181116165705p:plain

課題

撮影してみるとわかるんですが、iOSもAndroidもtakePictureのなかで保存先ファイルパスをgetApplicationDocumentsDirectoryで取得していて、アプリ領域に画像を保存しているためため、ギャラリーアプリや写真アプリで撮影画像を見ることができません。

次回はギャラリーアプリや写真アプリで見れる領域に保存する方法をまとめます。

Android/iOSクロス開発フレームワーク Flutter入門

Android/iOSクロス開発フレームワーク Flutter入門

Flutterでカメラアプリを作る〜プレビュー表示編〜

f:id:aftercider:20181114175259p:plain

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ですね。

f:id:aftercider:20181114174256p:plain

(Androidのみ)minSDKVersionを21以上にする

cameraモジュールのスペック要件がminSdkVersion:21以上なので、android/app/build.gradleを開き、android -> defaultConfig -> minSdkVersion を21にします。(筆者環境下での初期値は16になってました。)

f:id:aftercider:20181114174255p:plain

カメラのプレビューを表示する

こんな感じで、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));
  }
}

実機で実行

端末をつなぎ、画面上の緑色の実行ボタンをクリックして実行します。

f:id:aftercider:20181114174249p:plain

実行すると、カメラなどのパーミッション要求もきちんとやってくれて、アプリが立ち上がります。

IDE側のLogcatもちゃんと見れるようになっていて、Android開発者にも優しいです。

ちなみにプレビューはこんな感じです。(縦長になってしまうのはカメラの縦横比とライブビューの縦横比が違うため)

f:id:aftercider:20181114174225p:plain

次の記事

次の記事では撮影・保存を行います。

水樹奈々ブログウィジェット&リーダーをリニューアル!!!

f:id:aftercider:20170127204810p:plain

2年以上ぶりのアプリ更新

学生時代にリリースし、2014年に最終アップデートを行い、はや2年以上。

なかなかいいかんじで動いてくれていましたが、この度アップデートを行いました!

ダウンロードは↑こちらから!

以下では機能や今回のリニューアル内容を解説します。

続きを読む