astamuse Lab

astamuse Labとは、アスタミューゼのエンジニアとデザイナーのブログです。アスタミューゼの事業・サービスを支えている知識と舞台裏の今を発信しています。

Flutterでお勉強時間管理用のタイマーアプリを作った

こんにちは、rinoguchi です。過去二回は、Dataprocの話名寄せの話 などを書いたりしてました。

今回は趣向を変えて、子供のお勉強時間を管理するために iOS/Androidで動くネイティブアプリを Flutter で作ったので、その話をしようと思います。

最終的に作ったアプリは、以下のようなものです。

f:id:astamuse:20210418151708g:plain

Flutter の特徴

まずは、Flutter を知らない人のために、Flutter の特徴を紹介しておきます。

  • 【○】一つのコードベースで、クロスプラットフォーム(Android/MacOS/Windows/Linux/iOS/Web)で動作するアプリを作れるため、開発・運用・学習コストが小さい
  • 【○】特に UI に関しては、独自のレンダリングエンジンで動くため、端末・OSに依存しない
  • 【○】flutter 独自の UI コンポーネント(Widget)が豊富で、Widget を組み合わせて簡単に UI を作れる
  • 【×】一般的な端末固有機能は対応しているが、全て対応してるわけではなく、最新機能の対応は遅れる
  • 【○】Google がリードする OSS なので、開発継続性の観点で安心感がある
  • 【○】Adobe XD からコードをジェネレートできる(らしい)

今回は、マテリアルデザインの UI が個人的に好きなのと、我が家にもiOS/Androidユーザがいるのでクロスプラットフォームにしたかったので、Flutter を採用することにしました。

作ったもの

我が家では「勉強した時間だけ iPad/Switch で遊んでいい」というルールを設けているのですが、勉強時間は過大報告し、遊び時間は過小報告するという事案が多発しています。
これを解決するため、以下の要件を満たすシンプルなタイマーアプリを作ることにしました。

  • 勉強している時は時間をプラスできて、遊んでる時は時間をマイナスできるタイマー
  • 時間が00:00:00になったら通知してアラームを鳴らす

この記事では、本アプリ構築を通じて学んだ「Flutter アプリの基礎的な構成」と「苦労した技術要素」について解説してみようと思います。

基礎的な構成を確認する

最初期の段階で作った「Startボタンでタイマーを起動して、Stopボタンで停止するだけのタイマーアプリ」を元に、Flutter アプリの基礎的な構成を確認してみたいと思います。知ってるよという方は読み飛ばしてください。

f:id:astamuse:20210418153256g:plain

ソースコードは以下の通り。60行程度の比較的シンプルなコードです。

import 'dart:async';

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

void main() {
  runApp(MaterialApp(home: TimerSamplePage())); // ここが Widget ツリーの起点
}

class TimerSamplePage extends StatefulWidget { // 状態を持ちたいので StatefulWidget を継承
  @override
  _TimerSamplePageState createState() => _TimerSamplePageState();
}

class _TimerSamplePageState extends State<TimerSamplePage> {
  Timer _timer; // この辺が状態
  DateTime _time;

  @override
  void initState() { // 初期化処理
    _time = DateTime.utc(0, 0, 0);
    super.initState();
  }

  @override
  Widget build(BuildContext context) { // setState() の度に実行される
    return Scaffold(
        body: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
      Text(
        DateFormat.Hms().format(_time),
        style: Theme.of(context).textTheme.headline2,
      ),
      Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          FloatingActionButton(
            onPressed: () { // Stopボタンタップ時の処理
              if (_timer != null && _timer.isActive) _timer.cancel();
            },
            child: Text("Stop"),
          ),
          FloatingActionButton(
            onPressed: () { // Startボタンタップ時の処理
              _timer = Timer.periodic(
                Duration(seconds: 1), // 1秒毎に定期実行
                (Timer timer) {
                  setState(() { // 変更を画面に反映するため、setState()している
                    _time = _time.add(Duration(seconds: 1));
                  });
                },
              );
            },
            child: Text("Start"),
          ),
        ],
      )
    ]));
  }
}

ここからは、このコードの解説をしていこうと思います。

まず、runApp()で、MaterialApp という Widget を画面にアタッチしています。 この Widget は一般的なマテリアルデザインのアプリケーションを提供するためのもので、ここを起点にUIを構成する Widget ツリーを組み立てています。

void main() {
  runApp(MaterialApp(home: TimerSamplePage()));
}

このシンプルなタイマーアプリの Widget ツリーは、以下の通りです。ソースコードと照らし合わせてみてください。なお、この Widget ツリーは、Flutter Devtools で簡単に確認できます。各Widgetの配置やサイズを確認できるのでとても便利です。

f:id:astamuse:20210416092849p:plain

タイマーで計測中の時間を画面上に表示するために状態を持つ必要があるため、TimerSamplePageStatefulWidgetを継承しています。通常の Widget は、状態を持つことはできません。

class TimerSamplePage extends StatefulWidget {
  @override
  _TimerSamplePageState createState() => _TimerSamplePageState();
}

状態を保持する_TimerSamplePageStateクラスでは、initState()で状態を初期化した後にbuild()を実行して UI を構築します。
その後は、setState()が呼ばれるたびbuild()を実行して UI に変更を反映します。
Flutter は、build()の再実行を高速化するように最適化されているため、Widget のインスタンスを個別に変更する必要はありません。

class _TimerSamplePageState extends State<TimerSamplePage> {
  Timer _timer; // こらが状態
  DateTime _time; // こらが状態

  @override
  void initState() {
    _time = DateTime.utc(0, 0, 0);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // 省略
    );
  }
}

この部分で、現在の時間(_time)を00:00:00形式で表示しています。

Text(
  DateFormat.Hms().format(_time),
  style: Theme.of(context).textTheme.headline2,
),

最後に、Startボタンをタップした際に、1秒毎に定期的に処理を実行する Timer を起動しています。
この Timer により、定期的に「_timeに1秒追加する関数」が実行され、さらにsetState()することでbuild()が再実行され、UI に変更が反映されます。

FloatingActionButton(
  onPressed: () {
    _timer = Timer.periodic(
      Duration(seconds: 1),
      (Timer timer) {
        setState(() {
          _time = _time.add(Duration(seconds: 1));
        });
      },
    );
  },
  child: Text("Start"),
)

ここまでで、Flutter アプリの基礎的な構成の説明は終わりですが、いかがでしたでしょうか。
次の項からは、実装していて苦労した技術要素について紹介していきます。

アラームを鳴らす方法

「時間が00:00:00になったらアラームを鳴らす」という要件のために、アラームを鳴らす方法を 探したところ Android Alarm Manager を発見したのですが、これはもちろん iOS では動きません。

調べていくと Flutter Ringtone Player という、 Android でも iOS でも動く着信音を鳴らすプラグインを見つけたので、これを利用することにしました。

依存関係を pubspec.yaml を記載して

dependencies:
  flutter_ringtone_player: ^3.0.0

dartファイルにimport文を追加し、

import 'package:flutter_ringtone_player/flutter_ringtone_player.dart';

以下のようにFlutterRingtonePlayerplay()playNotification()などの関数を呼び出すことでアラームを鳴らすことができます。とてもシンプルですね。

// アラームを開始する
FlutterRingtonePlayer.play(
  android: AndroidSounds.notification, // Android用のサウンド
  ios: const IosSound(1023), // iOS用のサウンド
  looping: true, // Androidのみ。ストップするまで繰り返す
  asAlarm: true, // Androidのみ。サイレントモードでも音を鳴らす
  volume: 0.1, // Androidのみ。0.0〜1.0
);

// 以下の3つは典型的なパターンへのショートカット
FlutterRingtonePlayer.playNotification(); // 一度だけ音を鳴らす
FlutterRingtonePlayer.playAlarm(); // アラーム音を止めるまで鳴らし続ける。Androidの場合はサイレントモードでも音が出る
FlutterRingtonePlayer.playRingtone(); // 着信音を止めるまで鳴らし続ける。Androidでもサイレントモード時は音が出ない

// アラームを停止する
FlutterRingtonePlayer.stop();

とはいえ、多少の注意点があります。iOS ではlooping: trueが効かず、一回実行しただけでアラームが止まってしまう点です。
これではアラームに気づかない可能性があるので、iOS の場合は Timer で定期的にFlutterRingtonePlayer.playAlarm()を実行する形にすることで対処しました。

import 'dart:async';
import 'dart:io';
import 'package:flutter_ringtone_player/flutter_ringtone_player.dart';

class Alarm {
  Timer _timer;

  /// アラームをスタートする
  void start() {
    FlutterRingtonePlayer.playAlarm();
    if (Platform.isIOS) {
      _timer = Timer.periodic(
        Duration(seconds: 4),
        (Timer timer) => {FlutterRingtonePlayer.playAlarm()},
      );
    }
  }

  /// アラームをストップする
  void stop() {
    if (Platform.isAndroid) {
      FlutterRingtonePlayer.stop();
    } else if (Platform.isIOS) {
      if (_timer != null && _timer.isActive) _timer.cancel();
    }
  }
}

通知を行う方法

次はアプリがフォアグラウンドにいない時でも、タイマーが終了したことに気づけるように通知を行うことにしました。Flutter Local Notification というプラグインが良さそうです。
これはサーバを介さずにローカル通知を行うことができるプラグインで、iOS / Android に対応しています。

まずは依存関係を pubspec.yaml に追記します。

dependencies:
  flutter_local_notifications: ^5.0.0+1

次に、Android用に /android/app/src/main/res/drawable/ フォルダ配下に通知アイコンの画像(app_icon.png)を配置します。

後は、FlutterLocalNotificationsPluginのインスタンスを生成して、initialize()して、zonedSchedule()を実行すると、ローカル通知がスケジュールされます。

ボタンを押したら5秒後にローカル通知を行うだけのサンプルアプリを作りました。

f:id:astamuse:20210418153849g:plain

実際にソースコードは以下の通りです。

import 'dart:math' as math;

import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/data/latest.dart' as tz;
import 'package:timezone/timezone.dart' as tz;

void main() async {
  await setup();
  runApp(MaterialApp(home: NotificationSamplePage()));
}

Future<void> setup() async {
  tz.initializeTimeZones();
  var tokyo = tz.getLocation('Asia/Tokyo');
  tz.setLocalLocation(tokyo);
}

class NotificationSamplePage extends StatelessWidget {
  // インスタンス生成
  final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();

  /// ローカル通知をスケジュールする
  void _scheduleLocalNotification() async {
    // 初期化
    flutterLocalNotificationsPlugin.initialize(
      InitializationSettings(
          android: AndroidInitializationSettings('app_icon'), // app_icon.pngを配置
          iOS: IOSInitializationSettings()),
    );
    // スケジュール設定する
    int id = (new math.Random()).nextInt(10);
    flutterLocalNotificationsPlugin.zonedSchedule(
        id, // id
        'Local Notification Title $id', // title
        'Local Notification Body $id', // body
        tz.TZDateTime.now(tz.local).add(const Duration(seconds: 5)), // 5秒後設定
        NotificationDetails(
            android: AndroidNotificationDetails('my_channel_id', 'my_channel_name', 'my_channel_description',
                importance: Importance.max, priority: Priority.high),
            iOS: IOSNotificationDetails()),
        uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Center(
            child: FloatingActionButton(
      onPressed: _scheduleLocalNotification, // ボタンを押したら通知をスケジュールする
      child: Text("Notify"),
    )));
  }
}

いくつかポイントを説明しておきます。

  • 通知をzonedSchedule()を使用してスケジュール設定する場合は、TZDateTime でタイミングを指定する必要があるため、main()の中でsetup()を呼んでロケーションを設定している
  • Android用の設定のAndroidInitializationSettings('app_icon')の部分で先ほど配置したアイコン画像を指定している
  • IOSInitializationSettings()には何も指定してないが、ここで callback 関数を渡すことで、通知を受け取った際に何らかの処理を実行できる。しかし、これは iOS 10 以前のオプションなので使えない
  • zonedSchedule()で通知のIDを指定しますが、同じIDで複数の通知をスケジュールすると上書きされて最後の1回だけが実行される。またこのIDは通知スケジュールをcancel()際にも利用する
  • AndroidNotificationDetails()で通知チャネルIDを指定している
    • Android8.0以降の場合、音とバイブレーションは通知チャネルに関連付けられており、最初に作成されたときにのみ構成できる。通知を表示/スケジュールすると、指定したIDのチャネルがまだ存在しない場合は作成される。別の通知で同じチャネルIDが指定して、別の音やバイブレーションパターンを指定しても、何も変更されない
  • リアルタイムで通知を実行したい場合は、zonedSchedule()ではなくshow()を利用する

すこし癖がありますが、ドキュメントに詳しく書いてあるので、なんとなく理解できた気がします。

懺悔

これまで紹介した技術を使って、冒頭の動画のお勉強時間管理用のタイマーアプリを作ったわけですが、、、

実は、このアプリは iOS および Android ともに、エミュレータ上での動作確認では意図通りに動いていたのものの、実際のデバイスではバックグラウンドに遷移した際に上手く動かないことが分かりました。
これはバックグラウンドに遷移した際に Timer 自体が停止するためです。

これを解決する方法としては、

  • 案1) Timer をバックグラウンドタスクとして実行する
  • 案2) アプリがバックグラウンドに遷移したら時間記録+通知スケジュール登録して、フォアグラウンドに復帰したら差分時間をタイマー反映+通知スケジュール解除する

というのが思いつくものの、どちらも一筋縄ではいかなそうなので、とりあえずはフォアグラウンドでは上手く動いているので、普段使わない古いiPadで画面ロックしないよう設定した上で起動しっぱなしで運用してみようと思います。

[追記] 上記の案2)で上手く解決できたので、別記事を記載しました。
Flutter でバックグラウンドでも動くタイマーアプリを作った -rinoguchiブログ-

感想

今回、初めて Flutter でネイティブアプリを実際に作ってみましたが、Flutter はとても良くできていて UI の構築部分は API を眺めながらサクサク作れました。
問題は、OS・端末固有の事情で Flutter 及びプラグインが要件を満たす機能を提供してくれていない時で、そのようなケースの解決はかなり大変な印象です。また、普段からネイティブアプリを作ってる人には常識なのかもしれませんが、エミュレータでは動くけど実機で動かないケースも怖いですね。

ソースコード環境構築手順 も別途公開してますので、興味があればご覧ください。

さいごに

アスタミューゼでは、エンジニア・デザイナー・PM・コンサルタントを募集中です。ご興味のある方は遠慮なく採用サイトからご応募ください。お待ちしています。

DenoのハイパフォーマンスなWebフレームワーク

お久しぶりです、植木です。前回記事を書いてから、かれこれ一年が経ち、また当番が回ってきました。

現在も、ICPのエンジニアを担当しており、バックエンドとフロントエンド問わずに両方やってます。

はじめに

さて、今回はJS界隈では話題のDenoに関するトピックスです。

弊社でもSwaggerで設計した内容を実際のコードにきちんとエンドポイントに落とし込んでいるのかを確認するツールや、Slack botで活用しており、徐々に浸透が広まっております。
個人的にもコードレビューでの気になるコードの動作確認をDenoのReplで行ったり、書き捨てのコードが必要な時もJSでささっと書いてDenoで実行するケースは多いです。

Denoはこの一年でますます勢いを増し、deno.land/xを見てみると、サードパーティモジュールの数は約2000にも及んでいます。

今回は、そのサードパーティモジュールの中でも人気があったり、パフォーマンスを一番発揮するようなフレームワークはどれなの?という疑問に対して、比較検証した結果を紹介します!

準備

下記を使用して、検証しました。(インストール方法は割愛)

  • Deno 1.8.2
  • autocannon v7.0.5

autocannonでは、Req/Sec、Byte/Sec、Latencyを計測できるので、それを中心に測定していきましょう

Webフレームワークを選定する

個人的に運営しているdeno-x-rankingを活用しながら、最近人気がありそうな11個を選定しました。
deno.land/xではGithubのdescriptionまで見れないので、deno-x-rankingで読んでWebフレームワークと言えるか判断していきいました。

ノミネートはこちら。A-Z順に並べます

abc
alosaur
aqua
deno-drash ※ 通称はdrashですが、この記事ではリポジトリ名の方で統一します dragon
mandarinets
oak
opine
pogo
reno
servest

この中では、oakが1番人気で聞いたことがある人も多いかもしれません。かく言う僕も心の中でもoakを上回るのはどれかなと楽しみです。

コードを書く

Hello worldを返すサンプルコードを1つづつ用意していきます。
ついでに、一言ずつコメントと、スター数とフォーク数(※2021/03/31時点)をつけて紹介していきます。計測結果だけ知りたい方は飛ばしてください。

abc

expressライクに書けて、分かりやすいです。2018年からあるフレームワークで、メンテされ続けてます。スター数も今回紹介する中では5番目とやや多め。
Star: 515 / Fork: 45

import { Application } from "https://deno.land/x/abc@v1.3.0/mod.ts";

const app = new Application();

console.log("http://localhost:8080/");

app
  .get("/", (c) => {
    return "Hello, World!";
  })
  .start({ port: 8080 });

alosaur

デコレーターで定義されてるタイプです。Javaのアノテーションとよく似てますが、JSでは定義するときはただの関数と変わりません @Controller
ロードマップがあるので、今後も機能追加されていくことでしょう。スター数は4番手。

Star: 515 / Fork: 45

import {
  App,
  Area,
  Controller,
  Get,
} from "https://deno.land/x/alosaur@v0.29.3/mod.ts";

@Controller() // or specific path @Controller("/home")
export class HomeController {
  @Get() // or specific path @Get("/hello")
  text() {
    return "Hello World!";
  }
}

// Declare module
@Area({
  controllers: [HomeController],
})
export class HomeArea {}

// Create alosaur application
const app = new App({
  areas: [HomeArea],
});

app.listen('0.0.0.0:8080');

aqua

今回紹介する中ではコード量が少なくシンプルです。こちらもExpressライクです。
READMEにあるベンチマーク紹介では、abcやdeno-drashを上回ると書いてあるので、期待が高まります

Star: 121 / Fork: 4

import Aqua from "https://deno.land/x/aqua@v1.1.0/mod.ts";

const app = new Aqua(8080);

app.get("/", (req) => {
    return "Hello World!";
});

deno-drash

ドキュメントのチュートリアルがしっかりしてますDrash Land - Drash。ひっそりYoutubeもあります。
「サードパーティに依存しないHTTPサーバー」と紹介されてる通り、deps.tsの中をみてみると、https://deno.land/x/から始まるものが1つもありません
スター数は2番手。

Star: 806 / Fork: 24

import { Drash } from "https://deno.land/x/drash@v1.4.1/mod.ts";

class HomeResource extends Drash.Http.Resource {
  static paths = ["/"];
  public GET() {
    this.response.body = "Hello World!";
    return this.response;
  }
}

const server = new Drash.Http.Server({
  response_output: "text/html",
  resources: [HomeResource]
});

server.run({
  hostname: "0.0.0.0",
  port: 8080
});

dragon

リポジトリ作成日が2020/11/29となっていて、かなり新しいです。Readmeのベンチマークに関する記載があり、そこではoakを上回っているので、期待を込めて選びました

Star: 61 / Fork: 9

import {
  Application,
  HttpRequest,
  HttpResponse,
  RequestMethod,
} from "https://deno.land/x/dragon@v1.1.5/lib/mod.ts";

async function main(args: string[]): Promise<void> {
  const app = new Application();

  const r = app.routes({ maxRoutes: 1 });

  r.Path("/").withMethods(RequestMethod.GET).handleFunc(
    async function (Request: HttpRequest, ResponseWriter: HttpResponse) {
      ResponseWriter.withBody("Hello Dragon").end();
    },
  );

  app.listenAndServe({ port: 8080 });
}

await main(Deno.args);

mandarinets

MVCで、こちらもデコレーションが使われてます。
mandarineorgというorganizationで、orangemicrolemonという関連プロダクトがあります。
ドキュメントもかなりしっかりしてます MandarineTS

Star: 170 / Fork: 9

import { MandarineCore, Controller, GET } from "https://deno.land/x/mandarinets@v2.3.2/mod.ts";

@Controller()
export class MyController {

    @GET('/')
    public httpHandler() {
        return "Hello World!";
    }

}

new MandarineCore().MVC().run();

oak

サードパーティの中では一番人気なのがこれです。コードもスッキリしてます
Denoのメンテナでもある@kitsonkさんが主体で活動してるので、メンテナンスされていくことは確実でしょう。

Star: 3084 / Fork: 159

import { Application } from "https://deno.land/x/oak@v6.5.0/mod.ts";

const app = new Application();

app.use((ctx) => {
  ctx.response.body = "Hello World!";
});

await app.listen({ port: 8080 });

opine

こちらもexpressライクに書けるタイプです。
以前から、Reactのexampleをはじめexamples配下が充実しており、実装の際に参考になることでしょう。とはいえReactに関しては、aleph.jsが台頭する前は、と言うのがあるので、今だとaleph.js使うのが良いとは思います。

Star: 512 / Fork: 25

import { opine } from "https://deno.land/x/opine@1.2.0/mod.ts";

const app = opine();

app.get("/", function(req, res) {
  res.send("Hello World!");
});

app.listen(8080);

pogo

こちらもシンプルでいい感じで、routesの書き方もAdding routesで紹介されてるような書き方も自由です。
ReactとJSXもサポートしてます。

Star: 344 / Fork: 28

import pogo from 'https://deno.land/x/pogo@v0.5.2/main.ts';

const server = pogo.server({ hostname: '0.0.0.0', port : 8080 });

server.router.get('/', () => {
    return 'Hello World!';
});

server.start();

reno

Errorハンドリングがはじめから入っている分、少し長くは見えますが、複雑なところはないです。
そもそも標準ライブラリのhttp/server.tsを薄くラップする思想で作られているので、これが正しい姿と言えますね。

Star: 96 / Fork: 3

import { listenAndServe } from "https://deno.land/std@0.90.0/http/server.ts";

import {
  createRouter,
  createRouteMap,
  textResponse,
  NotFoundError,
} from "https://deno.land/x/reno@v1.3.11/reno/mod.ts";

function createErrorResponse(status: number, { message }: Error) {
  return textResponse(message, {}, status);
}

export const routes = createRouteMap([
  ["/", () => textResponse("Hello World!")],
]);

const notFound = (e: NotFoundError) => createErrorResponse(404, e);
const serverError = (e: Error) => createErrorResponse(500, e);

const mapToErrorResponse = (e: Error) =>
  e instanceof NotFoundError ? notFound(e) : serverError(e);

const router = createRouter(routes);

await listenAndServe(
  ":8080",
  async (req) => {
    try {
      const res = await router(req);
      return req.respond(res);
    } catch (e) {
      return req.respond(mapToErrorResponse(e));
    }
  },
);

servest

Denoコントリビュータで、日本人(@keroxp)が作ってます。スター数も3番手。(すごい!)
デフォルト設定でログが出るので、公平を期すために今回はLoglevelをERRORで設定することで余計なログは抑えて、勝負する事にしました。

Star: 694 / Fork: 38

import { createApp, Loglevel, setLevel } from "https://deno.land/x/servest@v1.2.0/mod.ts";

const app = createApp();
setLevel(Loglevel.ERROR);
app.get("/", async (req) => {
  const body = await req.respond({
    status: 200,
    headers: new Headers({
      "content-type": "text/html",
    }),
    body: "Hello World!",
  });;
});

app.listen({ port: 8080 });

速度検証結果

レイテンシー分析

dragon、alosaur、aqua、deno-drash、oakのレイテンシーは平均値で全て50ms以内で安定して早いことが分かります。
また、99.0%においても50ms以内に返すことができているのは、dragonのみという結果でした

平均値

Latency Avg (ms)
dragon 27.14
alosaur 32.01
aqua 32.2
deno-drash 32.96
oak 38.41
reno 86.67
abc 91.89
pogo 93.77
mandarinets 96.21
servest 144.38
opine 157.62

f:id:astamuse:20210331185458p:plain

Req/Sec分析

ここでもdragon、alosaur、aqua、deno-drash、oakが速度が早く安定していることが分かります。
dragonとoakでは、1000rps程度違うので、この差は大きいです。
alosaurは、1.00%, 2.50%が上位5つで比べて低いので、たまにうまくリクエストを捌けていないようです

平均値

Req/Sec Avg(rps)
dragon 36190.6
alosaur 30767.3
aqua 30584.6
deno-drash 29884
oak 25702.8
reno 11469.4
abc 10819.8
pogo 10604.6
mandarinets 10334.21
servest 6894.3
opine 6318.6

f:id:astamuse:20210331185442p:plain

伝送速度分析(Byte/Sec)

ここでもdragon、alosaur、aqua、deno-drash、oakが速度が早く安定していることが分かります。
レイテンシー分析とReq/Sec分析では、deno-drash→oakでしたが、ここで初めてoak→deno-drashと順位が入れ替わりました。
平均値でdragonは、renoやservestの実に4倍のスコアが出ています。
alosaurは、1.00%, 2.50%が上位5つで比べて低いので、たまにうまく伝送できていないようです

平均値

Bytes/Sec Avg
dragon 3510000
alosaur 2800000
aqua 2780000
oak 2360000
deno-drash 2270000
abc 1010000
opine 967000
pogo 965000
mandarinets 940000
reno 883000
servest 779000

f:id:astamuse:20210331185434p:plain

検証時に発生したerror: Uncaught (in promise) BrokenPipe: Broken pipe (os error 32)というエラーに関して

下記のモジュールはで検証中に、実はautocannonの実行後にエラーが発生しました

alosaur
aqua
deno-drash
dragon
mandarinets
opine
reno

error: Uncaught (in promise) BrokenPipe: Broken pipe (os error 32)
    at handleError (deno:core/core.js:186:12)
    at binOpParseResult (deno:core/core.js:299:32)
    at asyncHandle (deno:core/core.js:223:40)
    at Array.asyncHandlers.<computed> (deno:core/core.js:238:9)
    at handleAsyncMsgFromRust (deno:core/core.js:207:32)

今回使用した、autocannonの-p/--pipeliningオプションを使用した際に起こるようで、Deno本体の問題かどうかの切り分けがされていない状態のIssueは存在しました。
(本題ではないこともあり、)今回の検証で発生した情報はIssueに提供しておき、情報を待つことにしました

github.com

結論

パフォーマンス存分に出す必要があるなら、dragonを選びましょう。
パフォーマンスと人気度(今後もメンテナンスされる可能性が高そう)両方を必要とするなら、oak, deno-drash, alosaur の中から、記法や機能差(ここは調査できてないのでご自身で調べてみてください)を基準に1つを選びましょう。
・上記2つに当てはまらない(ささっと、とりあえず動かしたいというような時など)パフォーマンスとコード量の両方を重視したいなら、aqua, oakがおすすめです。

Denoの今後の動き

これまで説明してきたWebフレームワークはdeno_std/httpを使用している前提の話でしたが、Deno namespace APIに直接httpが扱えるようになるPull Requestが出ています。
[WIP] feat: native HTTP bindings by bartlomieju · Pull Request #9935 · denoland/deno · GitHub

つまり、Denoのドキュメントで紹介されるように、

import { serve } from "https://deno.land/std@0.91.0/http/server.ts";

const server = serve({ hostname: "0.0.0.0", port: 8080 });

のようにimportしてから利用するのではなく直接、
Deno.http.createServer("127.0.0.1:8080")と書くことができるようになると言うことです。

これがmergeされることでパフォーマンスは上がるので、今回の検証結果に一泡吹かせる事になるかもしれません。 また、途中で紹介したエラーもstdに起因する原因であれば発生しなくなる可能性もあります

感想

使ったことがないWebフレームワークも多く、また最終的なパフォーマンス結果に差が思っていたよりあったので、検証してみて面白かったです!
中でもdragonaquaはスター数も少なく、この記事を読むまで知らない人が多かった事でしょう!隠れたヒーローを見つけたような気分になりました。
今回の作業内容はこちらのリポジトリにあります。 github.com

至らない点や、足りないフレームワークを見つけたら、Issueをあげてリクエストするか、Pull Requestでコードベースで投げつけてくださると幸いです。

最後までお読みいただきありがとうございました。
アスタミューぜでは引き続き、一緒に働いてくれるデザイナー・エンジニア、さらにはDenoを使って活躍したい方を大募集中です!
下記に採用サイトもありますので、是非ご覧ください。

参考

デザインガイドラインについて考えよう

f:id:astamuse:20210331101211p:plain

こんにちは。デザイン部の橋本です。

企業やブランド、サービスのデザインを行う際「デザインルール」や「デザインレギュレーション」、「デザインシステム」といったデザインのガイドラインをつくることがあると思います。

弊社でも上記のようなデザインガイドラインを作成し、業務に役立てています。今回はそんなデザインガイドラインについて、他社事例を含めて、いろいろと調査してみました。

デザインガイドラインでよく整備されているもの

デザインガイドラインとは、色・文字・レイアウトなど様々なデザイン要素について、ルールを綿密に定義したドキュメントのことです。 ※今回は主に “Webページやアプリにおけるデザインガイドライン” について調査しています。

デザインガイドラインで整備するものはプロジェクトの規模で変わってきますが、割と整備されていることが多いものをまとめてみました。

f:id:astamuse:20210330193235p:plain

Brand - ブランド
まずはサービスのブランドイメージを決定します。デザインガイドライン全体を通して、統一感を持たせて、共通のメッセージを発信するのに必要な土台となります。
言語化したり、写真を使用したりしてブランドイメージを伝えます。
Typography - タイポグラフィ
タイポグラフィーをスタイルガイドによってどのように最適化するかは、サイト全体のクオリティーを左右する重要なポイントです。
まず見出しの種類( h1〜h6) についてや、本文テキスト、太字やイタリックなどについての設定を行います。続いて、リンクやリード文などに使えるような小さめのカスタムフォント、そして文字のフォントや太さ、色についての検討を行います。
Color - 色
色はブランドイメージと強く関連づけて利用されることが多いので、カラーパレットの選定は、ブランドの追求結果にもとづいて、慎重に行われます。
カラーパレットを作成する際はプライマリーカラー(サービスを特徴づける色)は必ず設定しますが、セカンダリーカラーは設定していない場合もあります。ブランドの主色をより効果的に見せるために、白、灰色、黒などの中間色を加えます。
Icon - アイコン
アイコンは文字よりもはるかに高い伝達能力を発揮します。ユーザーはアイコンを見るだけで、今何が行われているのか、また次に何が起きるのかを瞬時に把握することができます。
アイコンを決定する際はブランドバリューとアイコンの適合性について考慮する必要があります。
Form - フォーム
フォームはユーザーとサイトを結ぶインタラクティブ性の高いパーツです。
ユーザーはフォームを介してデータ入力をし、サイト側はそれに対する操作を行います。起動時やホバー時の表示、また、パスワードが弱すぎることやIDが無効なことを伝えるためのエラーや警告、「支払い完了」などの成功……このようなフィードバック要素について、あらかじめ決定しておきます。
Button - ボタン
ボタンはカラーパレットやフォーム、トンマナを合わせて構成されるツールです。
ボタンデザインには統一感を持たせ、ユーザーに一貫したサイト体験を提供できるように工夫します。
Margin - 余白
統一された正確な余白は、Webサイト全体にまとまりと洗練された印象を生み出すと言われています。
見出しやボタン、画像、フォームにおける余白など、あらゆる要素の余白について設定します。具体的には余白の設定ルール(5の倍数、8の倍数など)を決めて、適応していくパターンが多いです。
UI Element - エレメント
カードデザインや、テーブルデザイン、ページャーなどのUIコンポーネントを収録します。更新頻度が最も高く、収録数も多い要素です。
Logo - ロゴデザイン
ロゴデザインについては、詳細な運用ルールは別紙でまとめられており、デザインガイドラインでは一部のルール(カラー、余白スペース、最小サイズ)だけ触れられているケースが多い気がします。
Photo - 写真
ビジュアルのデザインは、サービスのイメージを左右する重要な要素です。Webサイトのキービジュアルやサムネイル、挿絵などで使用する画像など、主なビジュアル要素についてもガイドラインを設定します。
写真の選定は感覚的な側面も強いため、はっきりとしたルール作りは難しいのですが、OK例とNG例を示すことで目指すエリアを浮かび上がらせることができます。
Illustrations - イラスト
イラストも写真と同様にサービスのイメージを左右する重要な要素です。使用できるイラストの方向性をあらかじめ決めておくと、サービス全体に統一感を持たせることができます。

整備するものに特別な決まりはなく、なかにはデザインガイドラインを作っていないプロジェクトもあります。

デザインガイドラインのメリット/デメリット

デザインガイドラインのメリット

f:id:astamuse:20210331102226p:plain

メリット1. デザイン品質の担保

制作対象の規模が大きくなると、デザインを複数人で行うことがあります。 こうした時にデザインガイドラインがあれば、個別に作業していても統一感のあるデザインで制作することが可能です。 新しいデザイナーが着任した時には、立ち上がりの拠り所になります。

メリット2. ユーザビリティの担保

デザインガイドラインによって統一感のあるわかりやすいUIが実現できていれば、ユーザーの学習コストが下がり、操作性も向上します。

メリット3. 制作コスト削減

デザインガイドラインによって制作のテンプレートができていれば、新規に制作するページであってもそれらを組み合わせたり引用したりすることで、制作コストを削減することができます。

また、デザイナーだけでなくプロダクトオーナーやディレクター、エンジニアなどデザイン関係者にもガイドラインを浸透させることができれば(ガイドライン制作時から多くの人を巻き込むのが理想的です…!)、共通言語ができることでコミュニケーションロスを最小化でき、デザインのミスも見つけてもらいやすくなります。

デザインガイドラインのデメリット

デメリット1. アップデートが必要

サービスというのは成長していくものなので、サービスが成長すれば、デザインガイドラインもアップデートしていく必要があります。デザインガイドラインの品質を保つためには、デザイナーがある程度手間をかけて運用していかなければなりません。

デメリット2. 表現の幅が狭まる

デザインガイドラインは統一性を高めることでブランディングにも繋がるものですが、細かく決め過ぎると、表現や制作の自由度を奪ってしまい、新しいことやその時々にふさわしい表現や提案ができなくなってしまうこともあります。また、視覚的に変化の乏しい画一的なデザインになってしまい、ユーザーの興味の持続が難しくなる可能性があります。

デザインガイドラインの事例

Material Design Guidelines(Google)

f:id:astamuse:20210330193353p:plain

https://material.io/

マテリアルデザインは、Googleが提唱するデザインシステム。リアルな世界の「物体(マテリアル)」が奥行と厚みを持つように、Web上に表現されたデザインでも奥行と厚みを見せることによって、より操作性の高いインターフェイスを実現しようとするものです。

Human Interface Guidelines(Apple)

f:id:astamuse:20210330193417p:plain

https://developer.apple.com/design/human-interface-guidelines/

Apple社が設けたデザインガイドライン。iPhoneをはじめとする様々なApple社デバイスで動作するアプリに対するルールが規定されています。1978年の初版から更新され続けている歴史あるデザインガイドラインです。

Spectrum(Adobe)

f:id:astamuse:20210330193431p:plain

https://spectrum.adobe.com/

Adobeのデザインシステム。制作ソフト寄りのアイコンが多くあり、見てみると面白いです。「International design」のページには、世界的にユーザーのいるAdobeだからこその配慮などが書かれています。

IBM Design Language(IBM)

f:id:astamuse:20210330193448p:plain

https://www.ibm.com/design/language/

タイポグラフィからモーションUIまでとても細かく表記されているデザインガイドライン。ギャラリーのページは、ムードボードのようになっており、見ているだけでIBMの世界観が伝わってきて、デザイナーが迷った時に、立ち戻って冷静になれるページだと思います。

Orbit(Kiwi.com)

f:id:astamuse:20210330193506p:plain

https://orbit.kiwi/

チェコを拠点とする航空予約サイトKiwi.comが開発したデザインシステム。UIコンポーネントの種類が豊富で、利用ルールも丁寧に定めているため、複雑な画面を設計する際のヒントになります。

さいごに

参考文献・資料

最後までお読みいただきありがとうございました。 アスタミューぜでは引き続き、一緒に働いてくれるデザイナー・エンジニアを大募集中です! 採用サイトもありますので、是非ご覧ください。

Copyright © astamuse company, ltd. all rights reserved.