タグ「プログラミング」の 記事 143 件中 1 ~ 100 件を表示しています。

この Blog は私が契約している VPS で動作しているが、それを重い腰を上げてようやく Ubuntu 20.04 から 24.04 にアップグレードした。 それで PHP バージョンが 7.4 から 8.3 に上がるということで Laravel のバージョンも上げなきゃ、そして Bootstrap も 4 から 5 に移行しなきゃということで結局全体的に一つ一つ見ながらコードを入れ替えた。 これが結構大変だ。 仕事でやっている時もそうだが、プログラムというのは書いたら終わりではなくフレームワークや言語、環境のアップデートがあった場合にちゃんと追従していかないとすぐ陳腐化してしまう。 特に PHP などはリリースされてから 3 年ほどで EOL になってしまうので 1 年経ったぐらいでもう一通り見直していかないと 3, 4 年経った段階で見直すと EOL になっていたり全体的に全然変わっていたりと修正が厳しくなってしまう。 以下今回ハマったこと、気になったことをつらつらと書いていこうと思う:

Laravel Mix から Vite への移行

以前はアセット・コンパイラとして Laravel Mix だったのだが最近の Laravel は Vite になっている。 なのでここは完全に書き換えたのだが、これの移行はそんなに難しくはなかった。 書き方が変わっているのでそこを調べて書き換えればよい。 npm run dev して開発してデプロイする時は npm build するような雰囲気でよい。 ただデプロイする時に本番向けビルドして DEBUG=false にしているはずなのに本番向けの CSS/JS が適用されなくて困った。 調べたらこれは /public/hot というファイルも間違えてデプロイしてしまっていたのが原因だった。 このファイルがあるとホットデプロイされていると Laravel が認識してしまって本番用のリソースが適用されなくなっていた。

ConoHa VPS の仕様変更

ConoHa を使用しているのだが、新しい VPS を契約してサーバ立ち上げて SSH の設定をしても何故か外部から接続できずにハマった。 これがどうも最近になってセキュリティグループの設定が必要になったようで公式にも書いてあった

認証周り

以前は Laravel の組み込みの認証の仕組みをそのまま使用していたが、最近のものは Laravel Sanctum だとか Laravel Jetstream だとかやたらと大仰な認証パッケージがついており、これを導入しようとするといろいろ余計なクラスが追加されてしまってあまり好ましくない。 この Blog はただ単に認証とログイン・ログアウトができればいいだけなので最低限で良い。 ということで普通に自作する形で対処した。 これらの認証フレームワークを使わなければならないのかと思ってちょっと構えていたが、そんなこともなかった。 自作しているといってもちゃんと認証には Laravel の Auth クラスを使用しているので問題ないはず。

脱 Homestead

以前の Laravel は Homestead といういわゆる Vagrant のラッパーを使用していた。 これが便利だったのだが最近は Laravel Sail を使う形 (要は Docker) になっているらしい。 これも認証と同じでそんな面倒なことしなくても普通に Django みたいに開発サーバだけでも良いのに、と思って Laravel の開発サーバを使ってみた。 php artisan serve で開発用の Web サーバが立ち上がり、別のウインドウで npm run dev しておけば CSS/JS は都度ホットデプロイされながら開発できる。 これで開発中に全然不足は感じなかったので、この程度の規模の開発であればこれで良いのだろう。

FCM (Firebase Cloud Messaging) は Android / iOS でプッシュ通知を実装する際に利用するサービスであるが、これが 2024 年 6 月 20 日から従来のレガシーな実装は廃止されるとメールで通知が来た。 なので FCM HTTP v1 API への移行を余儀なくされたわけだ。 移行に関しては公式ドキュメントに記載があるのでこの通りやればいいのだが、残念ながら PHP で書いた時の例がない。 Firebase Admin SDK を利用すれば比較的簡単に実装できるようだが、これも PHP 非対応である。 これだけメジャーで使われているのに何故こんなに PHP に対して塩対応なのか。 まあ言語仕様がアレだから Google 的には使わせたくないというのはわからないでもないが……。 ともかくこれをやる上で必要だったこと、ハマったことをここにメモっておく。

OAuth 2.0 token には Google APIs Client

前述の公式移行ドキュメントに記載のある通り HTTP ヘッダに OAuth 2.0 のアクセストークンを含める必要がある。 これを自前で発行・管理するのは面倒なので Google APIs Client Library for PHP を使ったらうまくいった。 尚、PHP が古い場合 composer require するライブラリバージョンを下げる必要がある。

// PHP 7.2 でも動くように古いバージョンを指定する
composer require google/apiclient:2.12.1

実装は以下のような感じ:

// Firebase Console から持ってきた秘密鍵 JSON を指定する. 本来は環境変数に書くべきところ
putenv('GOOGLE_APPLICATION_CREDENTIALS=hoge/fuga.json');

// まず Google API Client で OAuth 2.0 トークンを取得する
$client = new Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope('https://www.googleapis.com/auth/firebase.messaging');
$httpClient = $client->authorize();

// push する JSON を組み立てる
$json = [
    'message' => [
        'token' => $token,
        'notification' => [
            'title' => 'FCM message',
        ],
        'data' => $data,
    ],
];
$url = 'https://fcm.googleapis.com/v1/projects/${project-id}/messages:send';
$result = $httpClient->post($url, ['json' => $json]);

data には key-value pair として双方とも string を指定する

上記のような実装で何度か試してみると 400: Bad Request が返却されて正しく通らない。 何故なのかずっと試行錯誤していたところ $data に含める PHP 連想配列は key, value 共に string でなければならないことがわかった。 つまり、

$data = ['key' => '1'];  // これは送信できる
$data = ['key' => 1];  // これは送信できない (値が int)
$data = ['key' => ['key2' => '1']];  // このようにネストした連想配列も送信不可

ちなみに legacy FCM API では前述のすべての例が送信できたので、全然気づかずハマってしまった。 ということで旧 API との互換性がなく、もしクライアント側でこのようなネストした値を parse している場合移行する時に工夫する必要があるかもしれない。

今日 ConoHa で契約している VPS の割引きっぷの有効期限が 1 週間前になったとメールが来た。 なるほどそうか、どうしようかな、と思って ConoHa VPS サイトの割引きっぷの料金プランを見てみたら何だか以前と違っている。 デフォルトが VPS 割引きっぷ SSL セットになっており、独自 SSL のアルファ SSL とセットになっていてちょっと高い。 しかし VPS 割引きっぷシンプルという SSL なしの従来のものも用意されていて、そちらは今キャンペーンをやっているらしく結構安い。 よし、これでいいだろう、と思って既存の VPS をこれに変更しようとしたができず SSL セットのものしか選択できなくなっていた。 VPS 割引きっぷシンプルを選択するには新規で立ち上げるしかないようだ。 迷ったが料金が全然違うので Ubuntu 20.04 LTS で再度立ち上げてそちらに移行した。 一回 Ubuntu 22.04 LTS も試してみたが PHP のバージョンが違うため Laravel 8 をそのまま動かすことができず PHP をダウングレードしても何だかハマってしまってうまくいかなかった。 トラブルシューティングが面倒なので 20.04 LTS に戻して、3 年前にメモっていたサーバ設定通りに作業して何とかなったというわけだ。 しかしそのうち 20.04 LTS のサポートも切れてしまうだろうから、徐々に Laravel もバージョンアップしないと駄目だなと思った。

ちなみに今は GitHub も無料でプライベートリポジトリを持てるようになったので、ブログ・システムを動かすくらい別にレンタルサーバでもいいかなと思ってロリポップなど見てみた。 しかし下位のグレードだと SSH 接続もできないため composer でインストールをすることもできず、そうすると結局月 500 円くらいのグレードにせざるを得ずあまり節約になっていない。 だったら自由度の高い VPS のほうが良いかなと思った。

あと kojion.com の DNS 設定を変更するためにお名前.com にログインしたのだが、その時に kojion.com ドメインの延長手続きが出てきたのでついでに 5 年追加しておいた。 このドメインも私が学生の時からずっと付き合っているので、恐らく 20 年くらい使っていることになるだろう。 更新費用は当時より結構高くなってしまった印象だが、まあそれでも 5 年で 8,500 円程度ならまだ許容範囲内かなと思う。

何だかこのブログでは久々にプログラミングの話をするようだ。 若い頃は毎日のようにプログラミングの記事を書いていた (その片鱗が Qiita に残っている) のに、最近ではほとんど書かない。 若い時より学習意欲が減退してしまったというのもあるが、いくら勉強しても自分のやりたいような仕事ができるとは限らないので直接的に役に立つことが少ないというのがある (あれだけ勉強した Python / Django や Laravel も業務では一度も使えていない)。 それを言い出すとじゃあ転職したほうがいいんじゃないかとかなるかもしれないが、今更そんな気にもならない。

そんなこんなで最近になってようやく Vue.js 3 を試して使い始めた。 Vue 3 の目玉は Composition API らしく Vue 2 の Options API の書き方に比べて結構簡潔になるし何よりネストが浅くなるのがいい。 <script setup> 直下に書いた constfunction<template> 側でそのまま認識したのは感動した。 再利用性が高まるというメリットも書いてあったがそれはまだ感じていない。 Vue 2 で書かれたコードを Vue 3 の Composition API に移行するのはかなり大変そうだが、少なくとも一から書くぶんにはそんなに違和感もない。 強いて言えばググった時にバージョンによって記述が微妙に違うので記事の最新性を確認してから参考にするように務めるといったところか。 まあそれは Vue に限ったことではないのだが。

あと Vuex についてだが使ったプロジェクトで軒並み Vue と Vuex の役割分担がしっかりできていなくてコードがぐちゃぐちゃになるという問題があったので今回は使わずにやってみようかと思って試してみたが、とても無理だった。 Vue はコンポーネント間のデータのやり取りがかなり面倒な印象で、それは Vue 3 でも変わらなかった。 やっぱり Vuex はよっぽど小さいプロダクトでもない限り使わないというわけにはいかないようだ。 であればせめて Vuex は state と mutation のみ使用するとか、機能限定で使うとゴチャゴチャにならなそうな気がする。 今回のプロジェクトではそうしてみようと思った。

以前は chess.kojion.com というサブドメインを切って MkDocs で専用のコンテンツを配信したりなどしていたが、こちらはサブドメインと SSL を用意したりしなければならないのでちょっと面倒だなという印象は正直あった。 今回 MkDocs は mkdocs gh-deploy という専用コマンドが用意されていて、これを使うと簡単に GitHub Pages にデプロイすることができるというのを思い出して使ってみた。 GitHub を使ってさえいれば本当にコマンド一発でデプロイできて感動した。 当然だがサーバ、SSL などを用意する必要もなく楽だ。

今回 kojion.github.io/chess という URL でチェス関連の記事を配置してみることにした。 MkDocs 周りで調べた内容は上記サイトにも記載したので、同じように Markdown でどんどんドキュメントを書きたいのだがより良い使い方が知りたいという方に見ていただければと思う。 もう少しコンテンツをちゃんと作り込んでから、改めて発表したい。

MkDocs で Markdown でドキュメントを書くのは良いのだが git push した時に自動でデプロイされてほしい。 そうしないといちいちサーバにログインして git pull して mkdocs build する必要があり面倒だ。 これに関しては Git フックという仕組みを使えば git push 成功したタイミングでサーバサイドで処理を行うことができる。

ということでケイタブログ様の記事を参考に以下のように定義したらうまく動いた:

cd (リモート Git リポジトリ)/hooks
vi post-receive
#!/bin/sh
cd (デプロイ先ディレクトリ
git --git-dir=.git pull
cd (MkDocs のディレクトリ)
mkdocs build
chmod +x post-receive

--git-dir=.git は必要だった。 これがないとワーキングディレクトリで git pull してもエラーになる。

久々に Redmine をインストールしてみたのだが、いろいろ忘れていたことがあったのでメモ。

インストール

Ubuntu 20.04 LTS でも以前書いた Ubuntu 18.04 LTS に最短手順で Redmine インストールがそのまま通用した。

初期ユーザ

admin/admin が初期ユーザとして用意されているので、これでログインする。 初回ログイン時にパスワード変更を聞かれるのでそのようにする。 admin というユーザ名だと簡単に見破られてしまうので、適切なログイン ID に後で変更するとよいだろう。

DB を初期化する

設定している間に間違えてしまったりなどして最初の状態に戻したいとする。 SQLite 3 のファイルの実体は /var/lib/dbconfig-common/sqlite3/redmine/instances/default/redmine_default にあるので削除し、マイグレーションを再実行すればよい。 それぞれファイルが別のところにあるので注意。

rm /var/lib/dbconfig-common/sqlite3/redmine/instances/default/redmine_default
cd /usr/share/redmine  # Gemfile があるディレクトリ
sudo -u www-data RAILS_ENV=production bin/rake db:migrate

A1 Theme

デフォルトの Redmine の見た目はイマイチなので A1 Theme をインストールする。 インストール方法はリンク先に書いてある。

JavaScript で長押し時に実行するイベントを定義する機能というのはいわゆる言語標準としては実装されていない。 つまりクリックイベントであれば HTMLElement.onclick に設定すれば良い (若しくは HTMLElement.addEventListener('click', function)) が HTMLElement.onlongpressHTMLElement.addEventListener('longpress', function) 的なものは用意されていないということだ。 そのために長押し時に何かするライブラリが Vue.js や jQuery 向けに公開されているが、ピュア JavaScript 向けには見た感じ存在しない。 そしてそういったコードは検索するといくつか出てくるが、どれも何だか複雑なので今回わかりやすい形で自分で実装してみた。

定義

本当は HTMLElement.addEventListener('longpress', function) みたいな感じでコールできるようにしたかったが、それは addEventListener メソッドをオーバーライドしないといけなそうな感じに見えたので止めて HTMLElement.setOnLongPressListener() とコールバックを設定して実行させる形にした:

HTMLElement.prototype.setOnLongPressListener = function(listener, interval = 800) {
    let isInProgress = false;  // 長押し中かどうか
    this.addEventListener('mousedown', () => {
        isInProgress = true;
        setTimeout(() => {
            if (isInProgress) {
                listener();
            }
        }, interval);
    });
    this.addEventListener('mouseup', () => {
        isInProgress = false;
    });
};

ギミックは至って単純で mousedown した時にタイマーを起動して interval 経過後に mouseup されていなかったらコールバックを実行するというものだ。 コードを読めばすぐ分かると思う。 長押しを検出するミリ秒はデフォルト 0.8 秒とした。

使用例

document.querySelectorAll('table tbody td').forEach(element => {
    element.setOnLongPressListener(() => element.style.borderColor = 'blue');
});
document.querySelectorAll('table tbody td').forEach(element => {
    element.setOnLongPressListener(() => element.style.backgroundColor = 'red', 1500);
});

このコードを実行するとすべてのテーブルの tbody 内のセルを長押しすると 0.8 秒後に枠線が青くなり 1.5 秒後に背景が赤くなる。

昨日書いた Javascript.infoJavaScript の基礎の部分の一人読書会を実践した。

2.1 Hello, world!

<script>...</script> だけで良い。 今どきは type 属性を使わない。

<script type="text/javascript"><!--
    ...
//--></script>

のような JavaScript をサポートしないブラウザのためのコメントアウトも非常にレガシーなので使わない。

2.2 コード構造

下記のようなコードは想定通り機能しないので避ける:

alert("There will be an error")
[1, 2].forEach(alert)

JavaScript は角括弧 [...] の前にはセミコロンを想定しないため alert() の戻り値への配列添字アクセスのように解釈されてしまう。 こういった不測の事態を避けるためセミコロンを省略せずに常に付与するのが望ましい。

ショートカットキー Ctrl + / で 1 行コメントアウト、Ctrl + Shift + / で複数行コメントアウトができる。

2.3 モダンなモード, "use strict"

IDE が警告出してくれていたので使っていなかった、というより忘れていたが、使おう。

2.4 変数

定数 const は小文字キャメルケースのもの (いわゆる不変な参照) と大文字スネークケースの従来の定数を混在させて良い。 定数か不変な参照かどうかは下記のように実行時に変化するかをみるのがよい:

const BIRTHDAY = '18.04.1982';  // 定数
const age = someCode(BIRTHDAY);  // 不変な参照

2.5 データ型

Infinity (無限大) という定数が定義されているのは初めて知った。 BigInt も初めて知ったが、おそらく使う機会は無いだろう。

2.8 演算子

以下のように数値変換に + 演算子が使える:

let apples = "2";
let oranges = "3";

// 二項演算子プラスの処理の前に両方の値が数値に変換されます
alert(+apples + +oranges);  // 5

似たような考え方だが - 演算子でゼロを引いてもいいので、いつも apples - 0 + (oranges - 0) のようにしていた。

カンマ演算子の存在は改めて書かれるとなるほどと思った。 確かに for 文で使われる:

// 1 行に 3 つの演算子
for (a = 1, b = 3, c = a * b; a < 10; a++) {
 ...
}

昨日新しく出たオライリーの JavaScript 第 7 版が網羅的な学習に良いのでは、と書いたが今日ネットでいろいろ探していたら Javascript.info というサイトを見つけた。

Javascript.info は、ロシアで最大級のJavaScriptチュートリアルと学習プラットフォームである learn.javascript.ru のフォークとして作成されました。このチュートリアルの内容はオープンソースであり、誰もが貢献できます。

とのこと。 ちょっと見てみたが網羅的で詳しくてかなり良い。 今の時代はこんなコンテンツが無料で得られるのか、と感動する。 ということで高いお金を出してオライリーの本を買う必要はなさそうだ。 勿論本で勉強したほうが好みという人はいるだろうが、私はこれで十分だ。

久々に技術の話を書く。 オライリーの JavaScript 第 7 版が出ていた。 その存在によって ECMAScript 2015 (ES6) のモダンな記法で書くのを阻害されていた忌まわしき IE11 が今年 6 月 15 日にサポート終了されるとのことで、ようやく ES6 をトランスパイラなしで書けるようになりそうだ。 どちらにしろモジュールのインポートや SASS のコンパイルで Laravel Mix にはお世話になりそうだが、それにしても直に ES6 記法で書けるのは大きい。 そのために今一度 JavaScript を一から網羅的に学習するために、オライリーの JavaScript 本は必読だ。 値段がかなり高いが、それだけの価値はあると思う。

私は就寝が早いのだが、最近寝た後に通知が届いてその音で起きてしまうというのがよくある。 なので今更だがおやすみ時間モードを調べて導入した。 以前の OPPO Reno A の場合は確かサウンドか通知の設定項目にそういったものがあったのだが、今使っている OPPO Reno5 A にはそれが見当たらない。 まさか機能としてオミットされているのか? と考えて検索してみたところ Android 9 から Digital Wellbeing というアプリの使用状況を統計・グラフ表示する機能が実装され、その中の機能として用意されたらしいことが分かった。 ということで OPPO Reno5 A と TECLAST T40 Plus (タブレット) 共におやすみモードを設定したので、設定時間帯はサイレントモードになる。 これで安心だ。

まさか IntelliJ IDEA の正式な日本語化パックプラグインが提供される日が来るとは思わなかった。 最近になって Pleiades が適用できなくなっていたので仕方なく英語のまま使っていたのだが JetBrains チームからお使いの IDE が日本語に対応しましたというメールが来たのでそれを知った。 PhpStorm のような IntelliJ IDEA の派生 IDE であればプラグインから「日本語」で検索して日本語化パックをインストールすることができるようになっていた。 これは楽だ。 Android Studio に関してはこれができなかったが、これに関しても Android Studioを日本語化する(トラブルシューティング付き) という題でやり方をまとめてくださっている方がいたので、その通りにやったらできた。 感謝。

この Blog のバックアップデータを Google Drive に定期的に保存しようと思ってスクリプトを書いたのだが、バックアップデータが 1 GB 以上あるので転送時にメモリが足りなくて強制終了してしまう。 php.ini のメモリの設定も確認したがちゃんと -1 (制限なし) になっている。 これを改善するには VPS をもっと良いプランにしなければならないが、そこにお金を使う気にはなれないので断念。 クライアント側で定期的に cron 的な処理を叩いて転送するのが良さそうだが、時間がある時にまたトライすることにする。

昔このあたりは一回検討したことがあったのだが、どうにも使い勝手が良くないなあと感じてそのまま放置していた。 Android 用の SSH クライアントアプリはいくつかあるが ConnectBot というアプリは後述する日本語入力の切り替え (Ctrl + Space など) がうまく動かなかった。 JuiceSSH が画面のデザインやフォントの種類 (JetBrains Mono も設定できる) など細かく設定できるので良さそうだ。 普通にサーバに SSH 接続できたのだが、日本語入力が何故かできない。 というより SSH クライアント側が日本語入力に対応していないような気がする。 どうしたものかと思ってググってみたら Android 端末から SSH 経由で日本語入力したいがヒットした。 その記事の通りやったらうまくいったのでとても参考になった。 クライアントサイドで日本語 IME を起動するのではなく、サーバサイドで日本語 IME を起動するという解決法で快適とは言い難いが、とりあえず使えるのは助かる。

私は以前は MacBook Pro をメイン PC として使用していたのでそれがあればこのような考察は不要だった。 だが今のように在宅勤務メインになってしまうとほとんどが自宅の自室での使用となりノート PC として使いたい機会がほぼなくなってしまったのでデスクトップ PC に買い替えてしまった。 タブレットでも USB-C のアダプタさえ持っていけば普通の USB キーボードを接続して作業可能なのは助かる。 外出先でガッツリ開発作業を行うことはなく、この Blog を書いたりサーバを少し弄れればいいので調べてみたわけだ。

カレンダー UI を実装している時にその日が祝日かどうかまたはその日の祝日名が欲しいことがあると思う。 これが結構大変なのだが、私の場合はコジごみカレンダーというアプリで祝日判定に関して実装していたので、それを移植して祝日名を返すようにしたらできた。 2021 年は山の日、スポーツの日、海の日が特別ルールになっていたりといろいろと面倒だ。 あと振替休日判定が面倒。 ただ単に前の日が日曜日だったら、という判定では足りない。 尚 Laravel での実装前提なので日付を格納するインスタンスのクラスとして Carbon を使用している。 例えば素 PHP の場合は適当に $year $month $day などの int 型変数に書き換えればいいだろう。 また、今どきはこういう実装は JavaScript 側にすることが多いと思う。 そちらへの移植も難しくないだろう。

    /**
     * その日が祝日であれば祝日名を返す.
     * 祝日でなければ null を返す.
     *
     * @param Carbon $carbon 対象日付
     * @return string|null 祝日名 (祝日でない場合 null)
     */
    private function getHolidayName(Carbon $carbon): ?string
    {
        list($y, $m, $d, $w) = [$carbon->year, $carbon->month, $carbon->day, $carbon->dayOfWeek];
        if ($m === 1 && $d === 1) {
            return '元旦';
        } elseif (($y < 2000 && $m === 1 && $d === 15) || ($y > 1999 && $m === 1 && $d >= 8 && $d <= 14 && $w === Carbon::MONDAY)) {
            return '成人の日';
        } elseif ($m === 2 && $d === 11) {
            return '建国記念の日';
        } elseif (($y > 2018 && $m === 2 && $d === 23) || ($y > 1988 && $y < 2019 && $m === 12 && $d === 23)) {
            return '天皇誕生日';
        } elseif ($this->isShunbun($y, $m, $d)) {
            return '春分の日';
        } elseif ($m === 4 && $d == 29) {
            return '昭和の日';
        } elseif ($m === 5 && $d === 3) {
            return '憲法記念日';
        } elseif ($m === 5 && $d === 4) {
            return 'みどりの日';
        } elseif ($m === 5 && $d === 5) {
            return 'こどもの日';
        } elseif (($y > 1995 && $y < 2003 && $m === 7 && $d === 20) || ($y > 2002 && $y !== 2021 && $m === 7 && $d >= 15 && $d <= 21 && $w === Carbon::MONDAY) || ($y === 2021 && $m === 7 && $d === 22)) {
            return '海の日';
        } elseif (($y > 2015 && $y !== 2021 && $m === 8 && $d === 11) || ($y === 2021 && $m === 8 && $d === 8)) {
            return '山の日';
        } elseif (($y < 2003 && $m === 9 && $d === 15) || ($y > 2002 && $m == 9 && $d >= 15 && $d <= 21 && $w === Carbon::MONDAY)) {
            return '敬老の日';
        } elseif ($this->isShubun($y, $m, $d)) {
            return '秋分の日';
        } elseif (($y < 2000 && $m === 10 && $d === 10) || ($y > 1999 && $y !== 2021 && $m === 10 && $d >= 8 && $d <= 14 && $w === Carbon::MONDAY) || ($y === 2021 && $m === 7 && $d === 23)) {
            return $y > 2019 ? 'スポーツの日' : '体育の日';
        } elseif ($m === 11 && $d === 3) {
            return '文化の日';
        } elseif ($m === 11 && $d === 23) {
            return '勤労感謝の日';
        } else {
            return null;
        }
    }

    /**
     * 春分の日かどうかを判定して返す.
     *
     * @param int $y 年
     * @param int $m 月
     * @param int $d 日
     * @return bool 春分の日かどうか
     */
    private function isShunbun(int $y, int $m, int $d): bool
    {
        $arg1 = $y < 1980 ? 20.8357 : 20.8431;
        $arg2 = $y < 1980 ? 1983 : 1980;
        return $m === 3 && floatval($d) === floor($arg1 + 0.242194 * ($y - 1980) - floor(($y - $arg2) / 4.0));
    }

    /**
     * 秋分の日かどうかを判定して返す.
     *
     * @param int $y 年
     * @param int $m 月
     * @param int $d 日
     * @return bool 秋分の日かどうか
     */
    private function isShubun(int $y, int $m, int $d): bool
    {
        $arg1 = $y < 1980 ? 23.2588 : 23.2488;
        $arg2 = $y < 1980 ? 1983 : 1980;
        return $m === 9 && floatval($d) === floor($arg1 + 0.242194 * ($y - 1980) - floor(($y - $arg2) / 4.0));
    }

    /**
     * 対象日が振替休日かを判定して返す.
     *
     * @param Carbon $target 対象日
     * @return bool 振替休日かどうか
     */
    private function isExtra(Carbon $target): bool
    {
        // 前日が日曜かつ祝日であるならば振替休日
        $carbon = $target->copy();
        $carbon->subDay();
        if ($this->getHolidayName($carbon) !== null) {
            if ($carbon->dayOfWeek === Carbon::SUNDAY) {
                return true;
            }

            // 2007 年以後では日曜の祝日が発生した場合次の平日が振替休日となる: GW の振替休日判定が増える
            if ($carbon->year > 2006) {
                // 前日と後日が祝日であったならば挟まれた日も祝日
                $carbon->addDays(2);  // 対象日の後日に移動
                if ($this->getHolidayName($carbon) !== null) {
                    return true;
                }

                // 前々日から日曜の祝日を順に探索. 日曜でなく祝日だけ満たしていたらその前の日を探索し続ける. 祝日でなくなったら終了
                $carbon->subDays(3);  // 後日から前々日へシフト
                while ($this->getHolidayName($carbon) !== null) {
                    if ($carbon->dayOfWeek === Carbon::SUNDAY) {
                        return true;
                    }
                    $carbon->subDay();
                }
            }
        }
        return false;
    }

これを以下のように使用できる:

$carbon = Carbon::now();
$holidayName = $this->getHolidayName($carbon) ?? ($this->isExtra($carbon) ? '振替休日' : '');

$holidayName には祝日名もしくは振替休日という文字列、対象でない場合は null が格納される。

私は kojion.com ドメインを所有しており、この度チェスクラブ用に chess.kojion.com というサブドメインを切った。 kojion.com 側に Let's Encrypt を適用しているがこれを chess.kojion.com にも適用したい。 そういう時は以下のように指定すればいけた:

sudo certbot certonly --webroot --webroot-path (web ルートパス) -d kojion.com -d chess.kojion.com -m (メールアドレス)

要するに -d オプションで並べて定義する。 サブドメインと webroot が異なる場合どのように定義するのだろうと思ったのだが、どうも片方だけ存在すればいいようだ。

私は PhpStorm と Android Studio と PyCharm を使用しているのだがこれらの JetBrains 社製 IDE のアップデートが入ると全部のシリーズに同様の更新が入り、しかもパッチを当てると次から起動時にエラーが表示されてそのまま落ちてしまうような状況になってしまうので、その回避方法のメモ。 多分私が Pleiades という日本語化パッチを当てているせいで、アップデート時におかしくなるのだろうと思われる。

アップデート後に起動しなくなるのは IDE の設定ファイルが壊れている。 これは C:\Users\(ユーザ名)\AppData\Local\JetBrainsC:\Users\(ユーザ名)\AppData\Roaming\JetBrains に対象のディレクトリ (例えば PhpStorm2021.1) があるので、そこを丸ごと削除する。 そうするとプラグインも含めて設定が全部クリアされるが、起動できるようになる。

ちなみに Android Studio の場合は C:\Users\(ユーザ名)\AppData\Local\Google だった。

JavaScript 開発に必須

昨今の JavaScript 開発において ECMAScript 6 で記述してビルド時にブラウザで解釈できるレガシーな JavaScript に変換するために Babel などのトランスパイラの設定は必須になっていると思うが、私は以前より Laravel Mix という Laravel に付属している簡単に使えるトランスパイラを愛用している。 以前書いた記事だと Laravel プロジェクトからファイルを引っ張ってくる方法だったが、今回試した方法だともっとシンプルにいけたのでメモ。 尚、これを試すのに arms inc. Engineers' Blog 様の記事が大変参考になった。 前提条件として Node.js は既にインストールされているものとする。

1. 新規 Node.js プロジェクトを作成しインストール

まず PhpStorm で新規プロジェクトを作成する時に Node.js プロジェクトにする。 こうすると package.json のみが含まれたシンプルな Node.js プロジェクトが生成される。 ここで以下を叩き Laravel Mix と cross-env をインストールする:

npm install laravel-mix cross-env --save-dev

そして package.json に Laravel Mix のビルド用のスクリプトを配置する (各コマンドの説明は省略する):

{
  "scripts": {
    "dev": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
    "watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js --watch",
    "prod": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
  }
}

ちなみに私の環境だと --hide-modules オプションがついているとスクリプト実行時にエラーになった。 この場合 --hide-modules を削除すると実行できた。

2. webpack.mix.js ファイルの配置と動作確認

webpack.mix.js はビルド用の設定ファイルとなっている。 今回は JavaScript と SASS で書くことにする。 TypeScript や LESS を使いたかったりする場合は別途書き換える。 プロジェクト配下に webpack.mix.js というファイルを以下のように作る:

const mix = require('laravel-mix');
mix.js('src/app.js', 'dist/')
    .sass('src/app.scss', 'dist/')

この例の場合 src/app.js に置かれた JavaScript が dist/app.js にビルドされ src/app.scss に書かれた SCSS が dist/app.css にビルドされることとなる。 ということで srcdist ディレクトリを作成し src/app.jssrc/app.scss に適当な記述をしたファイルを作成し npm run dev を叩く。 webpack compiled successfully が表示され、正しく dist/app.jsdist/app.css が作成されることを確認する。 更に npm run watch を叩いて適当に src/app.jssrc/app.scss を書き換えて保存するとそれぞれビルドされて dist/app.jsdist/app.css が書き換わることを確認する。

Blog のバックアップデータを定期的にローカル HDD に scp で保存したいと思った。 こういうのは cron で行うのが普通だが Windows だと ***.bat ファイルを書いてタスクスケジューラに登録するというのが面倒くさい。 ということで Windows 10 上で駆動する Linux 環境である WSL を導入した。

インストール

WSL2(Windows Subsystem for Linux)導入手順の通りやったらできた。 作業途中で WslRegisterDistribution failed with error: 0x800701bc というエラーが表示されたので、こちらはググって Linux カーネルの更新プログラムをインストールして解決。 Linux ディストリビューションは記事と同様に私が慣れている Ubuntu 20.04 LTS を選択。

cron

起動時に cron が起動していないので WSL2 で cron を有効にして OpenSSH server を走らせるを参考に Windows 起動時にバッチファイルをキックして cron を起動するように設定。 sudo visudo で慣れない nano が起動して操作が分からなくて手間取ってしまった……。 今回は scp でデータバックアップを取得するという想定なので公開鍵認証を設定しておく。 このあたりは SSH公開鍵認証で接続するまでを参照した。

例えば d ドライブの backup ディレクトリにバックアップファイルをコピーする処理は以下となる:

scp (ホスト名):(バックアップディレクトリ)/* /mnt/d/backup

と思ったが最新のものが欲しいだけなので rsync でいいだろうと思った。 毎回 scp してしまうとオーバーヘッドがひどい。

rsync -avh (ホスト名):(バックアップディレクトリ) /mnt/d/backup/

ただの適材適所のような気がする

最近 jQuery でググると jQuery はレガシーで Vue や React, Angular はモダンというものばかり出てくる。 私も仕事やこの Blog において Vue で SPA を作成しており確かに便利だと思うのだが、本当に jQuery や Vanilla JS は終わった技術なのか、というのは気になるところだ。 その話をするところで jQuery は至るところでデータを持ち回してスパゲッティになりやすい (が Vue などはそうでない) といったような言い回しがされているのだが、私は全くそうは思わなかった。 むしろ Vue (Vuex) の方がどこに何があるかが分かりにくいし、PhpStorm でも Vue のコンポーネント開発があまり楽に書けない (JS のコード補完があまり頼りにならない)。 正直なところ技術がどうというより実装者のスキルに依存するほうが大きい。 他人が書いた Vue (Vuex) のコードなど読めたものではない。

jQuery はまだまだ Bootstrap 4 にも同梱されているし、あまり考えずに使っていけるのがよい。 レガシーなのだから、と毛嫌いして使わないのはもったいないような気がする。 jQuery 使うくらいなら Vanilla JS のほうがいい、という意見もあるかもしれないが、実際のところ jQuery で書いたほうがはるかにコードが短くなる。 それに jQuery を使わなくても Bootstrap で必要だったりして結局 jQuery のロードはされるケースが多い。 さすがに SPA は Vue 一択だと思うが、普通の Blog やマスタ管理のようなページ切り替えをメインとする Web アプリならば jQuery の方が実装しやすい気がする。 要は適材適所。

それにしても PHP はどんどん進化して書きやすくなっていっているが、JavaScript は Laravel Mix (Babel) を通して ES6 で書いてもイマイチ書きにくいと感じる。 ということで、なるべく楽をするために PHP 側で書ける処理はなるべく PHP 側で書くというふうにしている。

プロジェクトごとに Vagrant インスタンスを用意したほうが便利

2 年前に同様の記事を書いたのだが、当時は Vagrant インスタンスをローカルマシンに 1 つインストールしてそれを複数のプロジェクトで使い回す想定でいた。 いろいろ試行錯誤した結果、それよりも 1 つのプロジェクトごとに Vagrant インスタンスを別々に用意したほうがはるかに便利なことに気がついた。 前者だと複数のプロジェクトのインスタンスを同時に立ち上げる必要がある場合に 1 つだけ立ち上げれば済むのでリソース使用量が少なくて良いというのがあるが、そんなケースはほとんど発生しない。 大体がプロジェクト A に携わっている日にはプロジェクト A の仕事しかしない。 それよりは後者の方が仮想マシンが完全に分かれているので単純で安心だ。

前提

以下はインストールされているものとする:

  • PhpStorm (IntelliJ IDEA Ultimate で PHP プラグインを使用している状態でもよい)
  • Pleiades (PhpStorm 日本語化)
  • PHP 7.4 (PATH が通されており php.ini の設定も済んでいる状態)
  • Composer
  • Vagrant (Virtual Box)
  • Cygwin (PhpStorm のコンソールでも良いかも)

PhpStorm 上で Laravel プロジェクト作成

  1. PhpStorm で ファイル -> 新規プロジェクト
  2. プロジェクトの種類の中から Composer プロジェクトを選択
  3. ロケーションを適切に入力
  4. 'composer 実行可能ファイル' を選択
  5. パッケージに laravel/laravel と入力
  6. インストールするバージョンは <default> を選択。バージョン指定したい場合はそのようにする
  7. 作成を押下すると Composer の処理が走るのでしばらく待つ (2020/09/10 現在なぜか失敗する……が問題ない)

2020/09/10 現在 <default> 選択すると Laravel 8 がインストールされる。 いつからそうなったのかは分からないが、今の Laravel プロジェクトを Composer からインストールすると Laravel 側の .git ディレクトリも入ってしまう。 これだと自分の開発した Git のヒストリーと Laravel 本体のヒストリーが混じってしまう。 rm -fR .git で一旦削除した上で改めて Git 管理下に置きたい。

プロジェクトごとの Vagrant インスタンス

Laravel 公式の Homestead の説明プロジェクトごとにインストールのところをそのまま実施する。

composer require laravel/homestead --dev
vendor\bin\homestead make
vagrant up  # Box のダウンロードに時間がかかる

.gitignore

ルートの .gitignore に Homestead 関連と IntelliJ IDEA 関連のファイルを追記:

.idea/
.vagrant
after.sh
aliases
Vagrantfile
.phpstorm.meta.php
_ide_helper.php
_ide_helper_models.php

2 年前も最有力として採用した

仕事で詳細設計を書く段取りになりそうなので Markdown 記法ができるドキュメント生成用のパーサは今何があるのか再度調べてみた。 Sphinx の方も Markdown 対応が進んでいるようだが、やはりまだ 2 年前と同様に MkDocs が最有力のようだ。 ということで再度調べ直したが、やっぱり便利だ。 というより 2 年前に書いた自分の記事が参考になった。 自分で調べたことでも半年以上経つとほとんど覚えていない……。

build したファイルをローカルに置いて正しく見る設定

2 年前に調べた時に複数の Markdown ファイルをリンクする構成の場合 index.html へのリンクがうまく張られないという問題があった。 今もそうなのかと思って調べたところ、どうも 2 年前に考察したこんなことをしなくても mkdocs.yml への設定一発でできるようになっているようだった。 というより当時からできていて私が知らなかっただけなのかもしれない。

mkdocs.yml に以下のようにかく:

use_directory_urls: false

これは stamemo 様の MkDocs で生成したサイトをローカルで開くと index.html が開かれない問題という記事が非常に参考になった。 この設定を false にしておくと例えば hoge.md に対して HTML リンクが /hoge のようになってしまいディレクトリ配下の index.html を探索するサーバの挙動を期待するようなリンクの張られ方にならなくなる。 mkdocs パッケージのバージョンにもよるらしいが、私が今試したバージョンだと hoge.html としてビルドされて hoge.html へのリンクが張られた。

今だったらこのような mkdocs.yml にする

site_name: 'サイトネーム'
site_description: 'サイト説明'
site_author: '所有者名'
site_url: 'https://hoge.fuga.com/'
copyright: '著作表記'
use_directory_urls: false

theme:
  name: 'material'
  language: 'ja'
  palette:
    primary: 'cyan'
    accent: 'orange'
  font:
    text: 'BIZ UDPGothic'
    code: 'Consolas'
  features:
    - tabs

plugins:
  - search:
      lang: ja

markdown_extensions:
  - admonition
  - footnotes
  - codehilite:
      linenums: true

最近 PhpStorm をアップデートしたら JetBrains Mono というフォントに自動的に変更された。 このフォントが特に低解像度ディスプレイで異常に見やすくて気に入って使っている。 どういう仕組みになっているのか分からないが、デフォルトだと PhpStorm のような JetBrains 製の IDE 以外で使えないので改めてフォントをダウンロードする必要があった。 JetBrains Mono 公式サイトでダウンロードできる。 そしてこのフォントは Web フォントが同梱されており自分の Web サイトやアプリで自由に使用して良いと明記されているのが素晴らしい。 早速この Blog にも Web フォント (.woff2.woff) を埋め込んでみた。 プログラミングコードが出現した際に使用される。

ちなみに JetBrains Mono を Web フォントとして使うには以下のような CSS を書けば良い:

// @font-face として JetBrains Mono を定義する
@font-face {
    font-family: 'JetBrains Mono';
    src: local('JetBrains Mono'),
        url('fonts/JetBrainsMono-Regular.woff2') format('woff2'),
        url('fonts/JetBrainsMono-Regular.woff') format('woff');
}

// 使用したいところで普通に指定すればよい
div.post-content code {
    font-family: "JetBrains Mono", monospace;
}

src の指定の意味は local (ローカル PC) に JetBrains Mono が入っていたらそれを使い、無かったら woff2, woff フォーマットの順でダウンロードして使用するという意味である。 woff2 の方が新しいフォーマットで圧縮率が高いので先に指定している。

業務で触っているシステムがやっと Java 8 に対応したので Java 8 の文法で書いているが、前回 Java 8 を書いたのがもう相当昔なので記法を結構忘れていて焦った。 書いているうちに思い出してきたが、やはり定期的に触っていないと忘れる。 Android アプリ開発は完全に Kotlin に移行してしまっているので Java は使わないし (そもそも Android の JDK が未だに Java 7 相当なのが謎だが)、その他のプロダクトで Java に触る機会もほとんどなく PHP ばかりだったからだ。 Java 8 のラムダ式が => でなく -> なのがちょっと違うし、関数型プログラミング的な書き方をする時に Stream に変換して collect()List などの形式に戻すというのも今見ると冗長に思える。

ちょっと前から Google Chrome などのブラウザで従来の HTTP 接続だとこのサイトへの接続は保護されていませんと警告表示が出るようになった。 この Blog では特にセンシティブな内容を扱ってはいるわけではないのだが、何となく気になってはいた。 しかし従来は SSL 証明書は有料が当たり前だったので自分の趣味でやっている Blog の SSL 化にお金をかけるのも躊躇うところだったが、昨今では Let's Encrypt という 90 日単位での証明書の更新作業が必要だが無料の SSL 証明書が出てきたので重い腰を上げた。

Qiita の記事で【apache】conohaのUbuntu18.04にLet's EncryptでSSL設定するまでというピッタリなものがあったので記事通り作業したら難なく対応できた。 Laravel で既に用意されている .htaccess に HTTPS へのリダイレクト処理を入れるのを試してみたが、なぜか個別記事に HTTP でアクセスされた際に URL が消失してしまい index.php へのアクセスになってしまう問題を発見して、これが解決できない。 と思ったらただ単に RewriteCond - Rule の記述の最後に入れてしまっていたので index.php へのリダイレクト後に HTTPS へのリダイレクトが走ってしまっていたからだった。 HTTPS へのリダイレクト処理を一番上に書くようにしたらうまくいった。

<IfModule mod_rewrite.c>
    <IfModule mod_negotiation.c>
        Options -MultiViews -Indexes
    </IfModule>

    RewriteEngine On

    # HTTP を HTTPS にリダイレクト
    RewriteCond %{HTTPS} off
    RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]

    # Handle Authorization Header
    RewriteCond %{HTTP:Authorization} .
    RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

    # Redirect Trailing Slashes If Not A Folder...
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_URI} (.+)/$
    RewriteRule ^ %1 [L,R=301]

    # Handle Front Controller...
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^ index.php [L]
</IfModule>

私は今の今まで Windows の日本語フォントは UD デジタル教科書が最も良いが書体が教科書然として万人向けとは言い難いと思っていた。 しかし今日何の気なしに Windows 日本語フォント で検索して衝撃を受けた。 あのモリサワから Windows 10 向けに BIZ UD ゴシックというフォントが提供されている! しかも 1 年も前からという。 何故今の今まで気づかなかったのか、本当に悔やまれる。 プロポーショナルの BIZ UD P ゴシックと等幅の BIZ UD ゴシック、そして明朝体の BIZ UD (P) 明朝と用意されている。 早速 Google Chrome の環境設定でフォントをこれらで設定してみると、とても読みやすい。 一番気に入ったのは游ゴシック体などだと線が細くて小さい文字が見にくかったのがこのフォントだととても見やすいということだ。 今まで視認性の関係上仕方なくメイリオを使っていた場面でもこのフォントで代用できそうだ。

Web サイトに適用する為の CSS 表記が気になったのだが、Webページのテキストのフォントに BIZ UDゴシック BIZ UD明朝 を利用する (CSS Tips)によると以下のように書くようだ:

/** BIZ UD ゴシック */
font-family:"BIZ UDGothic";

/** BIZ UD P ゴシック */
font-family:"BIZ UDPGothic";

/** BIZ UD 明朝 */
font-family: "BIZ UDMincho";

/** BIZ UD P 明朝 */
font-family: "BIZ UDPMincho";

これで Windows だとフォントがイマイチという悩みがほぼなくなった。 一点、Web サイトによっては font-family: Meiryo が適用されており Chrome で BIZ UD ゴシックを指定していてもメイリオで表示されてしまう場合がある。 macOS だとそもそもメイリオが無いのでそういうサイトでも綺麗なヒラギノフォントで表示されるという利点はある。

Android Studio や PhpStorm を使っていて、昔は英語表記のまま使っていたので Pleiades で日本語化できると分かった後でもそのまま使っていた。 しかし一旦 Pleiades で日本語化して使ってしまうとそれはそれで慣れてしまって今度は Pleiades なしだと気になるようになってしまった。 今日も PhpStorm の更新があったのだが、マイナーアップデートがあった場合に設定項目がクリアされるのかその都度 Pleiades を適用し直す必要がある。 といっても以前 Mac や Ubuntu でやっていたような設定ファイルの手動書き換えでなく、インストーラで適用先のバイナリを選択するだけなので楽で助かっている。

Vue.js のコンポーネントファイル (いわゆる Hoge.vue) 内で最初から <style> タグが用意されているので疑いもせずに CSS で書いていたのだが、これが普通に SCSS (SASS) で書けるのを今日知った……。 <style lang="scss"> と書き換えると SCSS で記述できる。 この程度のことはまず疑問をもって調べるのが良いと身を持って感じた。

最近 PHP のフレームワークとして Laravel をよく使用している。 Laravel にバンドルされている ES6 や SASS を JavaScript や CSS にトランスパイルする仕組みがよくできていると感心していた。 この Laravel Mix という仕組みが実は Laravel を使用していないプロジェクトでも簡単に導入できたのでここに書き記しておく。

導入

  1. Laravel 公式を参考に Laravel の新規プロジェクトを適当に作る
  2. そのプロジェクト直下の package.jsonwebpack.mix.js を取り出して適用したいプロジェクトにコピー
  3. そのプロジェクト直下の resources/js/app.jsresources/js/bootstrap.jsresources/sass/app.scss を取り出して同じく適用したいプロジェクトにコピー
  4. package.json があるディレクトリで npm install を叩く

npm run dev を叩いて public/js/app.jspublic/css/app.css にビルドされるのを確認する。 ビルド先を変えたい場合は webpack.mix.js を適当に書き換える。

使い方

Laravel 公式参照。 基本的に npm run watch を叩くと変更を監視してくれるのでその状態のまま開発を進めると楽。

Pixel 4 が発表されたようだ。 モバイル Suica が 50 円の鉄道利用で 1 円ぶんのポイントが付くようになったので FeliCa の重要性が増している。 その点 SIM フリーとして販売されていて FeliCa が搭載されている Pixel シリーズは貴重な存在だ。 勿論今回も FeliCa 対応のようだ。 私が使用している Pixel 3 XL はまだまだ使えるので Pixel 5 か 6 あたりまで保たせたいが、今見ると Pixel 3 XL は画面が大きい割にバッテリー容量が少ないのが少し不満だ。 やけに電池の減りが早い気がする。 私のタブレットメインの利用パターンだと廉価版の Pixel 3a でも良かったところだ。 Pixel 3a でも防水対応されているので、バイクのナビとして使用する際にも安心なのがポイント。

MediaPad M5 8.4" LTE モデルに通話 SIM を挿してスマートフォンとして使用し始めて 3 週間が経過した。 工夫次第という条件付きではあるが、特に問題なく使用できている。 チェスなどのゲームアプリがいつでも快適に使用できるのはやはり良い。 スマートフォンだと Pixel 3 XL のような比較的大きい画面のモデルでも画面が気持ち小さくて少し我慢させられているような感じだった。

私のいう工夫というのは以下のようなものだ:

  • MediaPad M5 が入るサイズのレッグポーチを身につける習慣をつける
  • HUAWEI WATCH GT などの着信や LINE 通知などが受け取れるスマートウォッチを併用する

最初会社に行くときはショルダーバッグを持ち歩くから大丈夫だろうと思っていたが、それだと昼食や休憩時に困った。 一応ズボンの後ろポケットに入れることはできるが、あまり快適ではない。 そこでショルダーバッグの代わりに DEVICE のレッグポーチを身につけていくことにより解決できた。 入りきらない荷物を持ち歩かなければならない場合のみショルダーバッグを併用すればよい。 しかしレッグポーチと併用すると足のあたりで干渉しがちなので本当はショルダーバッグではなくボディバッグやリュックの方が良いのかもしれない。

HUAWEI WATCH GT の着信や通知を受け取る機能は本当に便利で、これがなかったらさすがに MediaPad M5 をスマートフォン代わりに使用しようとは思わなかったかもしれない。 HUAWEI WATCH GT 2 がもう出るような感じではあるが、もう少しモデルチェンジが進んでからまた買いたいところだ。

それにしても、MediaPad M6 のグローバル版も未だに出ないし Google 系のサービスがインストールされない事がほぼ確定してしまったのは本当に残念だ。 かといってファーウェイ以外のメーカーの 8 インチタブレットは期待できないし、最悪将来的に iPad mini に移ることも考えなければならない。

最終的に何をやっているのかよく分からない行動になってしまっているが、一度手放した MediaPad M5 8.4 インチ LTE モデルを再度購入した。 MediaPad M6 は本当に素晴らしいタブレットだと思うが、ファーウェイに関してどうにも不穏なニュースばかりで Mate 30 に Google Play がプリインストールされないという話だし MediaPad M6 もプリインストールされない可能性が高い。 予想だが Expansys などでグローバル版は手に入るようになるかもしれないが Google Play はインストールされず、日本版は販売されないのではないかと思う。

MediaPad M5 8.4 LTE の今のところの実売価格は 4 万強とあまり落ちてはいないのだが、今回の楽天お買い物マラソンで実質 3 万円程度で購入できた (SPU + 10 店舗買いまわり 9 倍 + ショップ 9 倍)。もしも事態がいい方向に進んで日本版が Google Play 入りで販売されることになったとしても普通に買い換えれば良いと考えた。 ヤフオクやラクマで処分すれば 2 万 5 千円では売れるだろうから金額的なロスは少ない。 ただ単に売るのが面倒くさいだけだ。

スマホはちょっと使うぶんにはいいのだが、自宅でゆったりした姿勢で長時間使うと画面が小さくてどうにも目が疲れる。 MacBook Pro でここをカバーできるかと思っていたのだが、動画を見だすとバッテリーがみるみるうちに減っていき 2 時間も保たない。 やはりこのあたりの用途はタブレットが最善だと思う。

Kirin 980 は強い

MediaPad M6 8.4" が欲しいと思うようになってきた。 以前 MediaPad M5 を使ってもう 8 インチタブレットを使うことはないだろうと過去に書いており、自分でも何という心変わりだろうと笑ってしまう。 しかし 8.4 インチタブレットに対する私の考え方が変わってきたのがその心変わりの理由だ。 以前 MediaPad M5 を使っていた時はメインとなるスマートフォンを使いながら LTE モデルでデータ SIM を契約して使っていた。 今回はそうではなく MediaPad M6 をスマートフォンとして (要するに音声 SIM を指して) 使い今使っているスマートフォンは売却するということを考えている。 私はバイクのナビとしてスマートフォンを使用するのでどうしてもスマートフォンを外すことはできないのだが、最近 Pixel3 XL をメインスマートフォンとして使い P20 をバイクのナビ用として Bluetooth テザリングで使うのに慣れてしまいメイン回線をタブレットにするのもアリなのではと考えるようになってきた。

MediaPad M6 はスペックだけ見るとものすごく魅力的だ。 何しろ P30 などに使われている最新 SoC である Kirin 980 が積まれている。 恐らく値段を抑えるためだろうが、今までファーウェイのハイエンドタブレット (MediaPad M3, MediaPad M5) には型落ちの SoC を積んできていた。 それを我慢して使っていたのに、今回はどうしたことかといい意味でびっくりさせられた。 これなら最新スマートフォンに引け目を感じずに十二分に快適に使えるだろう。 肝心の Google Play ストアに関してもネット上の情報を眺めているとどうやら問題なく使えるようになりそうで安心している (非公式ではあるが中国国内版でも Google Play ストアが使えるようにできるようになったらしい)。

廉価版とは思えない完成度

仕事で触る機会を得られたのでレビューを書き記しておく。 MediaPad M5 lite 8" は MediaPad M5 の廉価版であり、MediaPad M3 lite (8 インチ) の後継モデルである。 だから私はそういう気持ちでこのモデルに対峙したのだが、いい意味で期待を裏切られた。 ハッキリ言って MediaPad M3 lite とはぜんぜん違う。

指紋認証なしはデメリットではない

MediaPad M5 や MediaPad M3 lite についていた前面下部の指紋認証が取り除かれている。 これにより認証が面倒になったのかもしれないが、私としてはそんなに気にならなかった。 それどころか指紋認証のボタンがないので、ゲーム中に間違って触れてしまうようなことがなくなるだろう。 そして、指紋認証がないので上下のベゼル幅が同じくらいになり、特に横画面にした際にデザイン的にバランスが取れるようになった。 MediaPad M3 lite と並べてみると狭ベゼルになっており一回り小さいのも好印象だ。

背面のデザインが格好良い

MediaPad M5 の背面デザインは iPhone のように白いアンテナのラインが見えてあまりクールではなかったように思うが、MediaPad M5 lite 8" は見事にグレーに統一されていて実に渋い。 MediaPad M5 より高級感があるのではないかと思うほどだ。

十分キビキビ動く

MediaPad M5 lite 8" は廉価版だが Kirin 710 という nova lite 3 と同じ SoC を積んでおり、これがまずまずのパフォーマンスを見せる。 Antutu ベンチマーク的には Kirin 950 (MediaPad M3) 相当ということで全くストレスなく使える感じがした。 ちなみに兄弟機に MediaPad M5 lite という 10 インチモデルがあるが、こちらは Kirin 659 というスペックが低い SoC を積んでいるので性能が全く違う。 MediaPad M5 lite を買うなら 8 インチモデルのほうが絶対にいい。

不満点は microUSB のみ

一点大きな不満点があるとすれば今どき microUSB というところのみだろう。 何故こうしたのか全く理解出来ないが、これのせいで他の機器が USB-C に移行できていても microUSB も併用しなければならなくなってしまう。

ただ不満点といえばそれくらいで、このタブレットが Wi-Fi 版で実売 2 万円強、LTE 版でも 2 万 5 千円強で買えてしまうのは破格だと思う。 何しろ税込価格で計算すると、Wi-Fi 版は昨日書いたニンテンドースイッチライトより安い

本日 MediaPad M6 が中国で発表された。 細かい情報に関しては phablet.jp 様の記事が詳しい。 どうせ最新の Kirin 980 ではなく型落ちの Kirin 970 (しかも MediaPad M5 の Kirin 960 と大して変わらない) を載せてくるんだろうな、と思っていたら本当に Kirin 980 (Mate 20 Pro や P30 Pro 相当!) を載せてきた。 しかも 8.4 インチに関してはバッテリー容量が減るという不穏なリークがあったのだがそれとは逆に 5100 mAh から 6100 mAh に増えているとのこと。 そして MediaPad M5 は日本発売されたモデルは 32 GB のみという悲しさだったが、今回は 64 GB からなのでその心配もない。 スペックからすると本当に神がかっている

これで例のアメリカのファーウェイ制裁がなければ 8 インチクラスとしてはライバル不在の完全無欠最強タブレットだったはずなのだが……。 OS が Android 9 というのは良いが Google Play ストア (Google Play Services: 開発者サービス) はバンドルされた状態で提供されるのだろうか。 それが気になって仕方がない。 もしそれがされるのであれば欲しすぎる一品ということになりそうだ。

1 台の Mac に Boot Camp などで macOS と Windows のデュアルブートにしているものとする。

US キーボードはシンプルだが日本語入力は設定が必要

各種ショートカットで macOS は Command キー、Windows は Ctrl キーを使用するため「同じキーバインドを設定する」だと少し語弊があるかもしれない。 要するに以下の要件を満たしたい:

  • IME の状態を左 Command 押下で直接入力、右 Command 押下で日本語入力に変更
  • Caps Lock を Ctrl に変更

前者は macOS で日本語キーボードを使用する時になっている設定と同じにしたいという意図だ。 今どちらの状態になっているかを意識せず単打で IME の状態を切り替えられるのでとても便利だ。 後者は US キーボードの Mac の Ctrl の位置がとても押しにくいので、Happy Hacking Keyboard などと同様の押しやすい位置に変更したい。

IME の状態を左 Command 押下で直接入力、右 Command 押下で日本語入力に変更

macOS

これは有名な Karabiner を使う方法で可能だ。 これに関しては Mac用 Apple英字配列(US)キーボードにおける日本語入力切替のおすすめ:Commandキーのみで実現がとてもよくまとまっているので、この通りやれば実現できる。

Windows

私が月配列を使っていた時に使用していた DvorakJ というキーリマップ用のソフトで実現可能だ。 Mac のキーボードの Command キーは Windows だと Windows キーとして認識されるので、DvorakJ の設定の「キーボード」->「単一キー」->「Win」で「左 Win」を「直接入力にする」、「右 Win」を「日本語入力にする」にすればよい。

Caps Lock を Ctrl に変更

macOS

macOS の場合標準機能のみでできる。 システム環境設定の「キーボード」から「修飾キー」ボタンを押下すると Caps Lock や Ctrl キーなどを差し替える画面が表示されるので、そこで Caps Lock に Ctrl を割り当てれば良い。

Windows

DvorakJ でできるのかと思っていたが、DvorakJ のレファレンスマニュアルにも書いてある通り DvorakJ (というより AutoHotKey) では仕組み上実現できない。 これに関してはレファレンスマニュアルに書いてある通り Ctrl2cap というソフトを使用することで解決できた。

Mac で Boot Camp を使う場合当然 Windows 10 が必要だ。 ちょっと試すだけならば Windows 10 の評価版を使用すればよいのだが、在宅勤務で使用するというのもあってそれで済ますのも憚られるところだ。 ちゃんとした Windows 10 Pro が欲しいのだが普通に Amazon などでパッケージ版を検索するととても高い。 何とか安く済ませる方法はないものか、と検索して探していたらこちらのブログの記事を見つけた。 普通に手に入れるよりかなり安い。 しかも日本語のサイト (Yahoo! ショッピング) で買えるので問い合わせの時も安心できる。 パット見怪しいサイトに見えるので大丈夫かな、と思ってしまうが Yahoo! ショッピングならばクレジットカード番号が店舗に直接渡るようなこともないだろう。

実際その店で購入してから 1, 2 時間もしたらライセンスキーが書かれたメールが送られてきた。 Windows 10 Pro でそのライセンスキーを入力したらちゃんと認証された。 これは便利だ。

自宅では 4K ディスプレイを使用していて、そちらで UD デジタル教科書体や Consolas を表示すると Mac には敵わないがそれなりに綺麗に表示される。 しかし低解像度環境での Windows フォントはとにかくプログラミングを行う気力が失せるほど見づらく汚い。 以前は同様に Consolas を使用していたが Consolas は欧文フォントなので IDE にもよるが日本語の部分はメイリオで表示されたりする (IntelliJ の場合はフォールバックフォントとして日本語フォントを使用できるので Consolas + UD デジタル教科書体ということができる)。

いい加減この見づらいのが何とかならないか、と探していて見つけたのが Myrica というフォントで ASCII 文字部分は Inconsolata を使用しているらしい。 Eclipse で表示してみたが、すごく綺麗ではないが Consolas よりは見やすい気がする。 ただ 4K ディスプレイでも使用してみたら個人的には Consolas の方が綺麗な気がした。 このあたりは個人の好みかもしれない。

というわけで、仕事でやむなく低解像度ディスプレイ Windows で作業する時は Myrica を愛用しようと思った。

8 インチタブレットに代替機が無いのが非常に厳しい

Googleがファーウェイに対してAndroidのサポートを中止へにある通り、ファーウェイが今後発売するスマートフォンやタブレットでは Play ストアを含めて Google 系のサービスが一切使用できなくなるというニュースが出た。 私は Android アプリのプログラマの為このニュースを注意深く見ていたのだが、昨日になって ARM が取引を中止したりパナソニックも取引を中止したというニュースを見てこれは非常にまずい事態になったと思った。

例えばスマートフォンであれば Galaxy や Pixel に乗り換えることができるが、7 インチから 8 インチにかけてのタブレットに関して言うとファーウェイと同等、もしくはちょっと劣る程度であったとしても代替機が全くない。 仕事で取引先に業務用タブレットの購入を促す際に 7, 8 インチタブレットの推奨端末を聞かれた時、いくら既存ファーウェイ端末が変わらず使えるとはいってもこのニュースの中「MediaPad M5 Lite 8, 若しくは MediaPad M3 Lite 8 を推奨します」とは言い辛い。

「LTE モデルを有しているそれなりのスペック、コストパフォーマンスの 7, 8 インチタブレット」という条件でギリギリ使えそうなのが Lenovo TAB4 8 Plus だがこれも中国メーカーの上やはりファーウェイの製品に比べると見劣りする。 あとは ASUS の ZenPad 8 くらいだろうがスペックが低すぎる。 サムスンあたりが作ってくれればまた少しは違うのではないかと思うのだが、フォルダブルデバイスの方に注力するためもう 8 インチタブレットは作らないと思われる。

ファブレットで我慢していくしかないか

プライベートで 8 インチタブレットに通話 SIM を刺してそれ 1 台をスマートフォンのように持ち歩き、バイクのナビの時のみ SIM を抜いた小さめのスマートフォンに Bluetooth テザリングでナビをさせる、という運用方法をいつか実現しようと思い描いていた。 しかし今回の騒動でそれがかなり難しくなってしまった。 やるなら現行 MediaPad M5 を購入してそのままずっとそれでいくしかないが、このニュースの中ではそんな気になれない。 かといって iPad mini にはしたくない (そもそも iPad mini では通話 SIM を刺していても電話ができない)。 仕方なく Pixel 3 XL や Galaxy Note 10 のようなファブレットを使用していくしかないだろう。

本日注文していた HUAWEI WATCH GT が楽天から届いた。 早速軽く使ってみたのでファーストインプレッションを書く。

バンドの装着感は良好だが写真と色味が違う

新しく出た 46mm モデルのダークグリーンのバンドのものを購入したが、バンドの素材が Apple Watch スポーツモデルでも使用されているフルオロエストラマーというものらしくしなやかなシリコン風の素材でとてもつけ心地が良い。 若干重さは感じるが許容範囲内だと思う。 ただ、写真よりバンドの色味がくすんで見える。 写真だと深緑といった感じの色だが実際見てみるとほぼ黒寄りの緑といった感じだ。

設定はとても楽

スマートフォンの方にファーウェイのヘルスケアアプリをインストールしてあれば、そのアプリ上から Bluetooth のペアリングとファームウェアのアップデートまでやや時間はかかったが簡単にできた。 Bluetooth ペアリング後はヘルスケアアプリを見れば歩数、睡眠、消費カロリー、心拍数、活動記録などグラフで見れるのが便利だ。 いちいちスマートウォッチの小さい画面で見なければならないのだと億劫だからだ。

睡眠機能に驚いた

「心拍数」もなかなかすごいが、一番驚いたのは睡眠機能だ。 装着して寝るだけで自分がいつ寝ていつ起きたか、深い睡眠と浅い睡眠はいつ合計何時間とったか、夜中いつ覚醒したかなどすべて表示される。 腕時計をしながら寝るというのにはちょっと慣れないが、この機能があるならば腕時計をしながら寝ることを習慣づけたいと思った。

バッテリーが持つのが一番のウリらしいが

この時計はバッテリーが 1 週間単位で持つほど電池持ちがいいのがウリらしいが、まだ使い始めたばかりなのでよく分からない。 昨晩 94 % まで充電してから結構操作していて、朝に少しワークアウト実施 (GPS 起動) して今 12 時間経ったが 85 % になった。 この使い方だと 1 週間は持たない気がする。 これが Apple Watch だとこの使い方で既に 50 % 以下になっているのだろうか。 それはかなり厳しい……。 ともかくバッテリーに関してはもう少し様子見が必要だ。

通知は確かに使える

スマートウォッチに送りたい通知 (例えば LINE や Gmail) をあらかじめヘルスケアアプリで設定しておけば、通知が届いた時にスマートウォッチが振動して通知内容が表示される。 確かにこれは便利だ。 特にバッグにスマートフォンを入れている女性には嬉しい機能だと思われるし、私も将来的にスマートフォンでなく 8.4 インチタブレットをスマートフォン代わりに使おうという事になった時に使える。

総括

2 万 5 千円弱で購入できるスマートウォッチとしては費用対効果がかなり高いのではないかと思う。

割と普通に使えた

HUAWEI WATCH GT というスマートウォッチのニュースが流れていたのでその情報を見ていた。 Apple Watch が出た頃は 1 日 1 回バッテリー充電しなければならないような有様で全く不便なものだな、と思っていたのだがこのスマートウォッチに関しては 1 週間、下手すると 2 週間は持つらしい。 今はまだスマートフォンを使用しているので不要だが、将来的に 8.4 インチタブレットをスマホ代わりに使うことになった時に結構使えそうな感じがする。

そこで気になったのが、このスマートウォッチがスマートフォンの Huawei Health アプリを使用しているらしいことだ。 このアプリはファーウェイのスマートフォンにプリインストールされているが、タブレットにはプリインストールされていなかった。 気になって検索してみたが、タブレットでも LTE モデルならば普通に Google Play からダウンロードしてインストールできるようだった。 そして、別にファーウェイ製のスマートフォンでなくても Google Play からダウンロードして普通に使うことができた。 これを知らなかったので Pixel 3 XL でずっと歩数計なしでやっていたが、早速今日から使うことにした。

電池の最適化に引っかかって計測できていなかった

次の日の朝早速歩いてみたが全く計測されていなかった。 どうも昨今のスマートフォンの「電池消費の最適化」に引っかかっておりサービス停止されてしまっているようだった。 アプリ内に警告も何も表示されないので分かりにくい。 そして言うまでもないがファーウェイ製のスマートフォンを購入した場合プリインストールされているのでこの問題は起きない。 こちらのブログの情報にあるように、スマートフォンの「設定 -> アプリと通知 -> 詳細設定 -> 特別なアプリアクセス -> 電池の最適化」から「ヘルスケア」と「Huawei Mobile Services」アプリの最適化をしないように設定する。

アプリ自体は使いづらいが楽天との連携が魅力

楽天市場の SPU が今月から改悪され、先月までは楽天ブックス 2,000 円以上の購入で +1 倍だったものが楽天ブックス 1,000 円以上の購入で +0.5 倍となってしまった。 その代わり楽天 Kobo で 1,000 円以上購入で +0.5 倍という条件が追加されたが、今まで私は基本的に Kindle で購入しており Kobo は使ったことがない。 今回は洗濯機を購入するため 1,000 円購入してもそれ以上のポイントがバックされる予定なので、試しに Kobo アプリを使用してみた。

小説を読んでみたが、10 インチタブレット (MediaPad M5 Pro) だと何故か設定アイコンの表示が崩れてうまく設定できない。 スマートフォンや 8 インチタブレットならば大丈夫だったので 10 インチタブレットでの使用は想定されていないのかもしれない。 あと小説のページめくりを行った際に Kindle や Google Play ブックスは指に吸い付いてくるようなスライドアニメーションをするが Kobo は指で完全にスワイプした後でアニメーションが始まりページ送りが進むといった感じで正直劣る (文章だと書くのが難しいので実際にアプリで試してみるのがよい)。 Kindle や Play ブックスだと行間幅が設定できるが Kobo は設定できないのもつらい。 ただ Kobo の日本語フォント (Kobo 筑紫明朝) はかなり読みやすいので、その点だけはよい。

ただ今回 Kobo が SPU の条件に追加されたというのもあるし、楽天ポイントが使用できポイントも貯まるのが魅力ではある。 アプリの使い勝手が劣るのはちょっと困るが、それに目を瞑ってでも使う価値はあると思った。

「罪と罰」と「決断力 + 大局観」を購入

1,000 円以上買う必要があったのだが、初回 500 円オフクーポンがあったので合計 1,500 円以上にする必要があるということで罪と罰 (完全版) 決断力 + 大局観 (羽生善治)を購入してみた。 罪と罰は普通は 2, 3 冊に分かれている長編小説だが、前述の本は 1 冊にすべて入って 800 円となかなかお得だった。

罪と罰を読むのは高校生の時以来ではあるがなかなか面白い。 余談だが私のハンドルネーム「コジオン」は罪と罰の主人公の名前からとっている。 「決断力」はちょっと読んだところ既視感があったので、実は以前読んだことがある (20 代の頃?) のかもしれなかった。 いい本であるのは間違いないが、読んだことがあるのであればちょっと失敗したかもしれない。

恥ずかしながら今まで jQuery ばかり使用していたので document.querySelector の存在を知らなかったのでメモ。 Laravel の Blade テンプレート内でデフォルトで head 要素内に以下のように meta タグで CSRF トークンが出力されている:

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="csrf-token" content="{{ csrf_token() }}">
    ...
</head>

上記は Blade 内なので {{ csrf_token }} という Blade の記法で取得できるが、この値を Vue.js のコンポーネント内で取得するのに meta タグから取り出す必要があった。 そこで私は最初「jQuery なら一発で取れるのに」と思いながら以下のように書いてしまった:

/**
 * メタ要素から CSRF トークンを取得する.
 *
 * @returns {string} CSRF トークン
 */
csrfToken() {
    let children = document.head.children;
    for (const child of children) {
        if (child.getAttribute('name') === 'csrf-token') {
            return child.getAttribute('content');
        }
    }
    return null;
}

しかしこれを書き終えた後で「Laravel で使われている Axios でヘッダに CSRF トークンを付与しているはずだがそこはどうやっているのか」と思い bootstrap.js を参照してみたら以下のように書かれていた:

let token = document.head.querySelector('meta[name="csrf-token"]');
if (token) {
    window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
} else {
    console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token');
}

この querySelector というのを知らなかったのだが、どうも CSS の記法でセレクタを記述することで jQuery のように DOM 要素を取得できる DOM のメソッドらしい。 最近できたのかと思いきや IE8 から利用可能なようで意外とレガシーなものだった……。 最初の 1 件を取得したい場合 querySelector を使用し、複数件数取得したい場合は querySelectorAll を使用する。

これから Vue.js のようなモダンな JS フレームワークをメインとし脱 jQuery としていくにあたって必須知識だろう。 覚えておきたい。

Vue.js を使用していて Vuex を使用すべきか否かというのがよく分からなかった。 公式にも以下のように書いてある:

もし、あなたが大規模な SPA を構築することなく、Vuex を導入した場合、冗長で恐ろしいと感じるかもしれません。そう感じることは全く普通です。あなたのアプリがシンプルであれば、Vuex なしで問題ないでしょう。

シンプルとはどのような状態を指すのか、というところだがこの場合のシンプルというのは「単一のコンポーネントのみで構成されるようなもの」だということが作っているうちに分かってきた。 Vue.js は子コンポーネント同士、若しくは子コンポーネントから親コンポーネントに伝達させるのに this.$emit などというよく分からないものを使用し、これがバケツリレーのように冗長だし記法も分かりにくい。

今回後から Vuex を導入したが、後から変更すると既存をそのように修正しなければならず面倒だ。 コンポーネントで分割することが最初から分かっているならばはじめから Vuex を導入してしまったほうがいいと思う。 「大規模向け」とかそういうワードに惑わされると損だ。

仕事でバッチ処理を 24 時間実行する必要があってそうしていたが、Windows のスリープを無効にしていても何故かいつの間にかスリープになってしまって困っていた問題の対処。

MSeeeeN 様の Windows の電源設定で無効にしても勝手にスリープになる現象を回避するにはという記事を参考にした。 Windows にはスリープの設定以外にシステム無人スリープ タイムアウトという設定があって、電源ボタンを押さずにスリープ解除した時に指定時間でスリープに戻るという罠があるらしい。 私は確かに電源ボタンでなくキーボード押下でのスリープ解除を行っていたのでこれの可能性が高い。 参考サイトの通りレジストリを弄り設定を変更。

Android Studio で公開用の APK を作成する場合に Build -> Generate Signed APK から署名された APK を作成するが、署名ファイルを後から変更した場合に古い情報がキャッシュされてしまいメニュー上からいくら署名ファイルを新しいものに変更しても古い方で署名されてしまう不具合が起きる場合がある。 私はこの問題に 2 回遭遇したが解決に時間がかかってしまったのでメモ。

Generate Signed APK のキャッシュはモジュール直下の build/intermediates/signing_config/release/out/signing-config.json に存在する。 この中身の JSON を直接書き換えることで新しい署名を使用することができる。

スマートフォンを買う時は毎回 Spigen のスマートフォンケースを買うようにしている。 ゴツくて頑丈なので男性向けかもしれない。 今回 Pixel 3 XL 向けに購入したのはネオ・ハイブリッドというシリーズのバーガンディで、これが写真だとあまり良くなさそうに見えるが実際届いたものをみてみると Pixel 3 XL のノットピンクの筐体にもよく合っているし、レビューにも書いてあったが「ファミコンカラー」のような感じがして親近感を覚える。

早速 Pixel 3 XL のおサイフケータイ機能を使用してみた。 とりあえずモバイル Suica で通勤してみた。 全く違和感なく使えた。 ただ、これが便利かどうかは「人による」としか言えない。 Google Pay を使うことによってビューカードでなくても年会費を払わずにクレジットカードチャージができるようになったので、ビューカードを持っていない人は結構便利と言えそうだ。 モバイル Suica にするとオートチャージもできる (例えば 5,000 円を切ったら 3,000 円チャージするなどの設定ができる)。 このオートチャージは Suica 圏内でなくてもちゃんとチャージしてくれるので便利だ。 ただ、オートチャージはビューカードも機能として持っているので、モバイル Suica が特段優れているというわけではない。 ビューカードを持ち歩かなくていいというのが最大の長所ということかもしれない。 私に関して言うとビューカードと銀行キャッシュカードが一体になったものを使用しているので、モバイル Suica を使おうが使うまいが結局持ち歩かなくてはならないところが微妙ではあった……。 ただ、スマホをかざして改札を通過するのが何か懐かしくて楽しいというのはあった (昔キャリア経由のスマホを使っていた時はよくモバイル Suica で乗っていたものだった)。

他には Edy を考えてみたが Suica も Edy も今まで使っていたそれぞれのカードからの残高移行ができない。 ということは既存カードを使い切ってからでないと何となく使いにくい。 とはいえ、せっかく FeliCa 付きの端末を使っているのだから何とか Edy を使い切って移行しようと思った。

2/10 に注文した Pixel 3 XL がようやく Play ストアから発送され家に届いた。 早速弄り倒しているので、ファーストインプレッションを書いておく。 尚、はじめに結論を書いておくと非常に好印象で買ってよかったと思った。 ネット上では「値段とスペックが合ってない (高すぎる)」という意見も散見されるが、これは触ってみてはじめて良さがわかる端末のような気がした。

Not Pink は確かにピンクではない

Pixel 3 及び Pixel 3 XL のカラーバリエーションは白・黒・ピンクの 3 色だが、バレンタインセールの 25,000 円引きの対象がピンク (Not Pink) のみだった。 どうせカバーをつけて使うわけだし色など大して気にならないだろうとそのままポチったが、これは正解だった。 Not Pink の名前通り全然ピンクじゃない。 白にうっすらと淡くピンクがかっている感じで、ピンクと言われないとピンクと視認できない。 ベージュと言ったほうが近いだろうか。 ただ、筐体横の電源ボタンは確かにピンクだ。 これがアクセントになっているわけだが、裸で使う人は気になるのかもしれない。

横幅が広い有機 EL がとても美しい

Pixel 3 XL は最近の縦長液晶の流行りの中ではまずまず横幅が確保できている端末であることは以前散々調べた通りだが、更に ppi 値が 500 以上と非常に高い。 しかも有機 EL で発色がとても良く、至近距離で見ても見惚れるくらい画面が綺麗だ。

尚、私はノッチが大嫌いで Pixel 3 XL はデフォルトで大きいノッチが配置されているのだが、開発者オプションから隠すことができるのは事前に知っていたので早速隠してみた。 結果、全く違和感なく普通の大きいスマホといった雰囲気になった。 とてもいい感じだ。

スクロールや画面のトランジションなど異常に滑らか

これは前の端末である P20 (といってもまだ出て 1 年経っていないので十分現役スペックの端末である) と比べると顕著だったのだが Twitter やまとめサイトや Google Play ストアなどでのスクロールが異常に滑らかだ。 アプリ同士の画面の切り替え (トランジション) も美しく iPhone を触っているかのような感じがする。 Android 9 (Pie) の効果かもしれないが、とても驚いた。

余計なアプリが入っていないのはやはり良い

これは Nexus 時代からの利点だったが Google 謹製ということで最初は本当に最低限のアプリしか入っていないということで好印象だ。 最近だとデフォルトのアプリが比較的少ないファーウェイも Facebook アプリなどを同梱してくるので少し気になっていたのは事実。 本当に素の状態の端末が欲しいのであれば最重要候補になるだろう。 まぁ 1 点だけ文句をつけると「おサイフケータイ」アプリが異彩を放っているので FeliCa を使わない方は気になるかもしれない。

端末をノックしているかのようなバイブレーション

多くの端末では指紋認証してスクリーンロックを解除したり戻るボタンを押したときなどにバイブレーションを発生させることができる。 P20 もそうだ。 私はこれが好きなので必ず ON にしているのだが Pixel 3 XL のこのバイブレーションが他の端末とちょっと違う。 何か端末を誰かがノックしたかのような軽やかで短いバイブレーションになっている。 これが何か新しくて楽しい。

バイクのナビ用の端末は別に用意したい

Google 謹製スマホは 3 年前の Nexus 5X 以来なので期待して待っている。 今回の端末は長く使うために今使っている P20 を売らずに取っておこうと思う。 P20 を売却してしまうとバイクのナビ用にも Pixel 3 XL を使うことになるが、バイクのナビとして使うと端末が汚れやすくなるのが気になる。 それに万一転倒などで落下させてしまい定価 10 万超えの端末を破損させてしまったら精神的ダメージがかなり大きいし、ロードサービスなどに電話しようとしても電話できないという羽目になりかねない。

バイクのナビ用の端末としては以下の要件を満たしているのが望ましい:

  • GPS が搭載されていること
  • 防水であること (雨対策)

P20 はちょうどこれを満たしている。 そして P20 が駄目になってしまったら何を使えばいいのかというところだが、これはヤフオクで iPhone 7 (防水) が安く手に入るのでそれが良さそうだ。 とはいっても P20 から買い換える時はもっと世代が進んでいそうだが。 P20 を売却して iPhone 7 を調達するという手もあるが、昨今のファーウェイ騒動のせいか P20 の買取価格がかなり安くなってしまっている。

Pixel や iPhone のバッテリー交換はやりやすい

Pixel シリーズや iPhone, iPad のバッテリー交換に関しては iCracked という店が使用できる。

iCracked(アイクラックト)は、2010年に米カリフォルニア州・シリコンバレーで創業した世界最大級のスマートフォン・タブレット修理事業者です。

とあるが、サービスとして明記されているのは前述の機種なのでスマートフォンなら何でも、というわけではなさそうだ。 バッテリー交換したくなったらこの iCracked に持ち込めば即日対応してもらえる。 東京に近ければ間違いない (埼玉県は何故か嵐山町のみなのに驚き)。 気軽にバッテリー交換できるのであれば綺麗に長く使っていこうという気にもなるというものだ。 Pixel シリーズであれば最新の Android OS が 3 年は提供されるので、開発用としても長く使っていけそうだ。

本当に大事なのは横サイズ

今日のスマートフォンは軒並み縦長液晶でノッチがついているというデザインになっている。 以前は例外を除けばほぼ 16:9 で統一されていたが、今売られているものは大体がノッチ込みで 18.5:9 やそれ以上に縦が長い。 これがとても使いにくいと思っている。 縦が長いからというよりも、横が短すぎる。 CHESS HEROZ などのゲームアプリを起動すると縦が長いぶんのスペースは単純に死に領域となるので意味がない。 それよりも横が短すぎるのでゲーム画面が小さくなり、チェスの一マスが小さくなってタップし辛い。

以前 16:9 の Mate 9 から 18.5:9 の P20 に機種変更した。 画面のインチサイズ的にはそれほど変わらないのだが、横のサイズが全く違う。 昔最近の自分のガジェット事情という記事で各タブレットの液晶の縦横実寸サイズを掲載したのだが、今回は私が今回購入を検討したスマートフォンを並べてみようと思う。 計算には以前と同様に液晶ディスプレイの表示部実寸法を調べるサイトを使用する。

端末名画面サイズ (inch)解像度縦 (mm)横 (mm)ppi (参考)
Pixel 35.52160x1080125.2862.64439.082
P205.82244x1080132.39663.72429.374
Mate 95.91920x1080130.5673.44373.374
Mate 10 Pro6.02160x1080136.0868.04402.492
Pixel 3 XL6.32960x1440145.0470.56522.49
Mate 20 Pro6.43120x1440146.6467.68536.918
Find X6.42340x1080147.4268.04402.689
Mate 20 X7.22244x1080163.81278.84345.885

上記をみる限り、やはり Mate 9 は名機だったと思う。 そして P20 の横サイズは Mate 9 よりも 1 cm 近く短い。 この 1 cm がとても大きい。 スマートフォンというよりタブレットな Mate 20 X は別格として置いておくとしても、今回私が購入した Pixel 3 XL は Mate 9 には敵わないものの、まずまずの横幅を確保できている。 これならチェスも快適にプレイできそうだ。

チェスのようなゲームを快適にプレイしたいだけなのであれば、環境が許せば上記表の Mate 20 X もしくは MediaPad M5 のような 8.4 インチタブレットを購入するのが最善だ。 しかしタブレットを常に持ち歩くのは私のように相当なタブレット好きであってもなかなか難しい。 例えば仕事で昼食に出かけたり、バイクでツーリングに行ったり、ちょっと買い物に出かけたりといった時にさすがにタブレットも持っていったりはしない。 そういう時でもちょっとポケットから取り出してタクティクスを解きたいというのがチェスを趣味とする者の人情というものだ。

iOS はどうか

ついでなので iOS も調べてみた:

端末名画面サイズ (inch)解像度縦 (mm)横 (mm)ppi (参考)
iPhone 7/8 Plus5.51920x1080120.9668.04400.529
iPhone X5.82436x1125133.9861.875462.626
iPhone XR6.11792x828139.77664.584323.614
iPhone XS Max6.52688x1242150.52869.552455.549

ということで画面の横幅だけで判断するならば iPhone 7 Plus, 8 Plus もしくは iPhone XS Max が良さそうだ。 iPhone 7 Plus であればヤフオクで割と安く手に入れることができるので、お金をかけたくない人には有効な選択肢だと思う。

久々の Google 謹製のスマホ

今はファーウェイの P20 というスマートフォンを使用しているのだが、このスマホは画面サイズの表記だけ見ると 5.8 インチということで十分なサイズのように思えるが、今どきのノッチ込みの縦長液晶なので実際の液晶サイズはそれほど大きくなく、特に横幅がないのでチェスをプレイする際にマス目が小さくて操作し辛かったりする。 それに私も歳なのか小さい画面の端末をずっと見ているとどうにも目が疲れるというのもあり、もう一回り大きい端末に買い替えたいという欲求はあった。 同じくファーウェイの Mate 20 X というファブレットが 7.2 インチということでなかなか魅力的に見えたのだが、ここまで大きいサイズだとバイクのナビとしてマウントするには抵抗があるので、バイクに乗る時だけスマホ 2 台持ちのような感じになってしまいそうなのが少し気がかりだった。

Google 謹製の Pixel 3 XL にも心惹かれたのだが、何しろ値段が高すぎる。 iPhone 並に高い。 Expansys で買えばそれなりに安いのだが、こちらはグローバル版だ。 グローバル版には日本版についている Felica が無い。 別に Felica など使わなくてもいいのだが、せっかくなので使ってみたいし売却する時に差がつくかもしれないと考えた。 しかし今日何の気なしに Google Play ストアを見ていたらバレンタインセールということでなんと 25,000 円引きになっていた (Pixel 3 の方は 20,000 円引き) ! 色がピンクのみというのが少し気になったが、どうせカバーをつけて使うのでどの色にしようが変わらない。 ということで少し考えた上で購入した。 楽天でいつもの Spigen のスマホケースも注文した。

ダサいノッチは隠せることを確認済

Pixel 3 XL のノッチ (画面上の切り欠き) はとてつもなくダサいと思っていて、これが隠せないようであれば絶対に買いたくなかった。 幸いノッチを隠す方法は検索するとすぐに出てきた。 ただ、ファーウェイなどの端末のノッチを隠す内容とはちょっと違い、開発者オプションで設定する上にノッチを隠すとノッチ部分が黒くなり表示領域が少し下がるといった感じになるようだ。 ファーウェイの端末の場合はノッチ部分が黒くなりノッチ部分に通知が表示される (つまり表示領域は同じままでノッチが目立たなくなる) のでこちらの方がいいのではないかと思ったが、最近の縦長スマホの液晶の縦横比バランスがイマイチだと思っていたので別に Pixel 3 XL 方式でも問題ないのかもしれない。

Laravel 学習中だがモデルに関して感じたことを書く。

xxxRaw メソッド

O/R マッパーは確かに便利なのだがちょっと凝ったことをしようとすると SQL だと簡単に書けるのに O/R マッパーでどういう風に表現するのか分からずに実装できないという問題がある。 CakePHP などは顕著な問題で特にアソシエーションなどを使用すると正しいはずなのに何故か動かないというコードになりがちだ。 しかも COUNT(*)SUM(*) を書きたいだけでも $query->func()->xxx() などという訳のわからない記法を要求される。 これが覚えられない。

Laravel だと DB::raw(), selectRaw()whereRaw(), orderByRaw() などの低レベルに書けるタイプのメソッドが用意されており、これを使うと対象部分だけ SQL 直で書くことができる。 単純な機構だがとても便利だ。 例えば以下のような感じだ:

/**
 * 年月ごとの記事数を返却する.
 *
 * @return \Illuminate\Http\JsonResponse
 */
public function countYearMonth(): JsonResponse
{
    $posts = Post::query()
        ->groupBy(['year', 'month'])
        ->orderByDesc('year')
        ->orderByDesc('month')
        ->get(['year', 'month', DB::raw('COUNT(id) AS count')]); // DB:raw() で SQL 直書き
    return Response::json($posts);
}

これは覚えやすい。 「何故既存の O/R マッパーは使いにくいのか」がよく分かっている人が設計した感じがする。

何もしないと取得結果がすべて string になってしまう

クエリを投げて DB からモデルに取得した結果が INTEGER カラムであったとしてもすべて string になってしまう。 これだと毎回変換しなければならず使いにくい。 これに関しては【Laravel】DBから取得した値を$castsで型変換する【Attribute Casting】に記載がある通り、モデルに型変換したいカラムを明示的に指定する。

class Post extends Model
{
    use SoftDeletes;

    // このように定義しておけば year, month, day, count は取得時に integer にキャストされる
    protected $casts = ['year' => 'integer', 'month' => 'integer', 'day' => 'integer', 'count' => 'integer'];
}

上記の例で、例えば countposts テーブル内に存在しないカラムだったとしても問題ないし、DB::raw('COUNT(id) AS count') で取った結果がちゃんと integer で返ってくる。 この辺りは便利なのか不便なのかちょっとよく分からなかった。

ボタン連打されてしまい不正な動作をするのはよくある話

Android アプリ開発においてボタン、というより View に対しタップイベントを仕込む際に View.OnClickListener() を使用するというのは基本だと思うが、何も考えずに実装すると連打された時にイベントが 2 回、3 回発行されてしまう。 これが問題になってくるのが例えば登録処理などの部分で、データが 2 重に登録されてしまったり既に登録されている為不正なレスポンスが返却されてきてしまったりする。

対応策に関しては使い古された話題なので検索すればいくらでも出てくるが、例えばお手軽にAndroidのButtonの連打を防止してみたの記事のようにボタン押下直後にボタンの押下を無効にし Handler を使って 1 秒後に再度有効にする、といった処理を挟む。

ただ、毎回このようなお決まりの処理を書くのは辛いので Kotlin の Extension を使って共通化してみた、という記事である。

実装例

このような Extension を定義する:

/**
 * 短時間での連打による複数回実行を防ぐ setOnClickListener 実装.
  *
  * @param listener setOnClickListener
  */
 fun View.setOnSingleClickListener(listener: () -> Unit) {
     val delayMillis = 1000 // 二度押しを防止する時間
     var pushedAt = 0L
     setOnClickListener {
         if (System.currentTimeMillis() - pushedAt < delayMillis) return@setOnClickListener
         pushedAt = System.currentTimeMillis()
         listener()
     }
 }

私は Handler ではなく pushedAt という押下時の時間を持たせて押下時に現在時刻と比較することにより 1 秒以内の連打を防止する実装を行った。 勿論多くの記事がやっているように Handler でも良いが個人的には Handler インスタンスをその都度 new するより生成コストの低い Long を持たせる方が好きだ。

これを以下のように使用できる:

val register = view?.findViewById<AppCompatButton>(R.id.register)
register.setOnSingleClickListener {
    // 連打してはいけない登録処理...
}

私は仕事では CakePHP を使用しているので PHP 最強のフレームワークと名高い Laravel をなかなか触る機会に恵まれなかった。 Laravel は世界的には圧倒的なシェアを誇る PHP フレームワークである。 日本では依然として CakePHP が強いのだがこの 2 年の間にかなり追い上げてきたようなので、今後日本でも PHP での開発の主体が Laravel に変わっていくのではと思う。 今回個人的に勉強しようと思ったので、この記事に試行錯誤の過程を記録しておくことにする。

私が今使っているのは IntelliJ IDEA Ultimate だが PhpStorm でも同様と思われるので PhpStorm と書いておく。 事前条件として Composer はローカルにインストールしておくこと。 Composer 及び Vagrant に関してはここでは述べない (既に知っているものとする)。

Laravel インストール (PhpStorm 上から実行する場合)

これを行うにはローカルに Composer がインストールされている必要がある。 拙記事「Windows に Composer インストール」を参照。

PhpStorm で「新規プロジェクトの作成」を選択。 表示されるプロジェクトの種類から PHP -> Composer プロジェクト を選択する。 プロジェクト名などを適当に入力するがここでは test_laravel としておく。 一番下「パッケージ」のところで laravel/laravel を検索して選択。 するとインストールすることができるバージョンが一覧されるので最新のバージョンを選択する。 執筆時点だと v5.7.13 だった。 そのあと完了を押下すると composer install 相当の処理が走りすぐに開発ができるようになる。

PhpStorm を使わない場合やローカルに Composer などを入れたくない場合は以下に示すとおり Homestead 上から行う。

Laravel インストール (Homestead 上から実行する場合)

手順前後して恐縮だが、後述の Homestead インストールが済んでいるものとする。 Homestead には PHP 環境や Composer が最初からインストールされているので非常に簡単。 vagrant ssh で Homestead に接続する。 Homestead.yaml に書いてある folders: to: /home/vagrant/code がそのままの場合は以下で /home/vagrant/code に新規 Laravel プロジェクトを作成する:

composer create-project --prefer-dist laravel/laravel code

インストール完了後 http://192.168.10.10/ にアクセスし正しく Laravel のスタートページが表示されることを確認する。

Homestead インストール

Homestead は見た感じちょっと便利な Vagrant の Box という感じだった。 インストールは公式の手順に従い忠実に行えば苦もなく完了した。 そしてインストール後は Vagrant を使ったことがあれば何ら違和感なく使用することができた。 ちょっと違うのは Homestead.yaml という YAML で共有フォルダや nginx サイト設定などを簡単に行えるというところだった。 同じフォルダにある Vagrantfile 側に Homestead.yaml をインクルードするようなコードが書いてあったので、より簡単に設定が書けるようにという配慮なのだと思われる。

Vagrant の時はデフォルトで Vagrantfile があるフォルダがそのまま共有フォルダとしてマウントされるようになっているので Vagrantfile とプロジェクトのソースコードを同一フォルダに置くような運用をしたりするのだが Homestead の場合は最初から Homestead インストールディレクトリとプロジェクトのソースコードが切り離せる構造になっている上、複数プロジェクト・複数ホスト名が考慮された作りになっている。 ということで、

  • Homestead は C:\Users\hoge\Homestead にインストール
  • プロジェクトのソースは C:\Users\hoge\PhpStormProjects\test_laravel に置く
  • (今後増えた場合) プロジェクト 2 のソースを C:\Users\hoge\PhpStormProjects\fuga に置く

といったことが苦もなくできる。 つまり Homestead.yaml などの Homestead 関連のファイルは個人の環境で書き換えさせるものとして Git 管理下にしないほうが良さそうだ。 例えば上記の例の場合 (今後増えた場合は含まない) は Homestead.yaml を以下のように設定する:

ip: "192.168.10.10"

...

folders:  # これは複数定義できる
    - map: C:\Users\hoge\PhpStormProjects\test_laravel
      to: /home/vagrant/code

sites:  # これも複数定義できる
    - map: homestead.test
      to: /home/vagrant/code/public

本当は hosts ファイルに上記 homestead.test を定義すれば http://homestead.test で確認できるが、今回は 1 サイトしかないし面倒なので http://192.168.10.10 でアクセスする (2 サイト以上ある場合は名前をつけないと区別ができなくなる)。 そうすると Laravel のデフォルトページが表示されるのが確認できる。

注釈するが、結局のところ中身はただの Vagrant (Box の中身は Ubuntu Server だった) なので vagrant up してからアクセスすること。 Homestead.yaml を書き換えた場合は vagrant reload (再起動)。

まとめるとローカルに PHP をインストールして試行錯誤するより遥かに楽なので使わない手はない。

各 index.html へのリンクが不親切

MkDocs は以前記事で触れた通りとても便利な Markdown で文書を生成するツールなのだが、MkDocs で複数の Markdown ドキュメントをリンクする形 ([xxx](yyy) 形式での .md ファイル間のリンク) にするとリンクが意図通り作成されない。 具体的には <a href="hoge/index.html"> のようになってほしいところが <a href="hoge/"> のように生成されてしまう。 生成された HTML ドキュメントを Web サーバ上に配置した場合は hoge/ 形式でも正しく表示されるのだが、ローカルに置いた形でブラウザを起動してみる (つまり file:///C:/Users/fuga/hoge/... のような形式の URL) だと例えば hoge/ を開くとその直下の index.html が暗黙的に呼び出されるのではなく hoge/ ディレクトリ以下のファイル一覧が表示されてしまう。

この挙動に関しては MkDocs でマテリアルデザインな Markdown ドキュメントサイトを作ろうにも以下のように書いてある:

Webサーバに載せることが前提
>MkDocsで生成した静的サイト用ファイルは、ローカルで index.html を開く使い方は想定されていません。 開くことと表示することはできますが、リンクから他の記事に遷移することができません。 これはMarkdownファイルへのリンクが ./TheTitle/ のように張られており、ファイルプロトコル上では これが ./TheTitle/index.html と解釈されず、正しく表示できません。

理屈はわかるが、HTML ドキュメントを生成したからといって Web サーバを立てて使うとは限らない。 何とかローカルで見たい。

index.html を正しく補ってやる

というわけで対象となるリンクを正しく置き換える Python スクリプトを書いた:

import glob, os, re

# MkDocs がビルドした HTML が置かれているディレクトリ
SITE_PATH = 'C:/Users/fuga/hoge/site'

# ルート index.html と各フォルダに分かれている index.html を対象とする
files = glob.glob(os.path.join(SITE_PATH, 'index.html')) + glob.glob(os.path.join(SITE_PATH, '*', 'index.html'))

for file in files:
    # HTML 読込
    with open(file, 'r', encoding='utf-8') as fp:
        html = fp.read()

    # index.html を付与
    html2 = re.sub(r'href="(.*?)/"', 'href="\\1/index.html"', html)

    # . と .. というリンクもあるのでそれも index.html を付加する...
    html3 = html2.replace('href=".."', 'href="../index.html"').replace('href="."', 'html="./index.html"')

    # HTML 書込
    with open(file, 'w', encoding='utf-8') as fp2:
        fp2.write(html3)

これを適当な名前で保存 (例えば mkdocs_converter.py) し、ソース内の SITE_PATH を MkDocs の site ディレクトリに書き換え python3 mkdocs_converter のように実行すれば変換される。

2020/07/30 追記: 設定一発で解決できる

実は設定一発で解決できた。 拙記事を参照されたい。

以下 12.9 インチ iPad Pro のような大きすぎて持つのも辛いようなもの以外のサイズのタブレットにも通用する内容と思っている。

スタンド付きのケースでなくシンプルなカバーが良い

MediaPad M5 や MediaPad M5 Pro には最初からタブレットケースが付属しているのだが、それがこんな感じのいわゆる iPad のような風呂蓋のようなケースだ。 これの利点は本体を傷や衝撃から守るだけでなくそれ自体タブレットスタンドとして使用できるというところなのだが、私はこれがあまり好きではない。 タブレットスタンドとして立てて使う時以外、普通に縦持ちや横持ちで持っている時に蓋の部分がブラブラしたりちょっと束ねて持ったりすることになりあまり快適ではないからだ。

今回私が購入したものがこの普通のシリコン製の包み込むだけのカバーなのだが、これだと普通に縦持ちや横持ちで持って使用した時にとても快適だ。 私の場合は電車の中やベッドに入ったまま使うといった場合に快適ということになる。 欠点はスタンド機能がないのでそれだけでは立てて使えないということだが、別売のタブレットスタンドを使えば問題ない。 「タブレット スタンド」で検索するといろいろな種類のものが出てくるので自分の好みのものを選択できるし、何より多くのものは安価だ。

Bluetooth テザリングを使えば Wi-Fi 版でも問題ない

私はスマートフォンの方は楽天のポイントの関係で楽天モバイルを契約しているのだが、これがお世辞も良いとは言えない SIM で特に通勤電車や昼時に低速通信になりブチブチ切れたりしていた。 そのため以前のタブレットである MediaPad M5 の時は高速な LINE モバイルを使いからと LINE をタブレットでも見たいからという理由で LTE 版を使用していた。 これはいつでも好きなタイミングでネットに接続できてとても便利なのだが、格安 SIM を 1 回線契約するとどうしてもその分の費用が増えてしまう。 私の場合は基本 1 G のプランで足りないときだけ 3 G に変更していたのだがこの使い方でも年間 1 万円強といったところだ。

MediaPad M5 Pro は日本国内版だと Wi-Fi モデルしか無いのでそれにしたが、懸念点はテザリングがあまり快適ではないのでは、ということだった。 しかし今回 MediaPad M5 Pro に買い換える前にしばらく Bluetooth テザリングで運用してみて全く問題ないことを確認したので躊躇なく買い換えることができた。 ネット接続を共有するテザリングには 3 種類の方式 (USB, Wi-Fi, Bluetooth) があるのだが、PC と接続すること前提の USB は置いておくが Wi-Fi テザリングはずっとつけておくとスマートフォン側の電力を消費してしまうし常時 ON にしておくのが難しい。 そこをいくと Bluetooth テザリングに関してはスマートフォン側を一旦 ON にすればスマートフォンの電源を落とすまで有効の上消費電力も少ない。 通信速度に関しては仕様上 1 Mbps が上限というのが少し残念だが、私の使用用途に関していえば全く問題なかった。

問題は Bluetooth テザリングだとスマートフォンとタブレットの距離が離れてしまったり、タブレットを長時間スリープにしていたりするとテザリングが切断されてしまうことだが、これに関しては Bluetooth Auto Connect というアプリで「タブレットの画面が ON (スリープ解除) になったら自動的にスマートフォンに Bluetooth 接続」という設定をすることで解決できた。

8.4 インチタブレットである MediaPad M5 から 10.8 インチの MediaPad M5 Pro に買い換えてみた。 ちょっと調べればわかるがこの 2 つは SoC (CPU, RAM サイズ) が同じなので画面サイズが違うところ以外は大体同じような感じで使えることが予想できた。

8 インチか 10 インチかは人による

人によるのかもしれないが 8.4 インチタブレットを 5 ヶ月使ってみて思ったのは、軽くて持ち運びが便利なのだが縦持ちする場合でも片手でホールドするような場面は結局ほとんどなく小さい利点をあまり生かせなかった。 私は以前からずっと 10 インチ級のタブレットを使用していたので 10.8 インチの MediaPad M5 Pro が重いとは特に感じない。 むしろ昔の 10 インチタブレットは大抵 600 g 以上あったのを考えると MediaPad M5 Pro の 500 g は割と軽いように感じる。

私はゲームはほとんどしないし、プレイするゲーム (チェスやバックギャモンなどのテーブルゲームが主) が別に横向きでホールドしながらプレイする必要のないゲームばかりなので 10 インチタブレットでも快適にプレイできた。 あと Amazon プライムや YouTube などの動画はやはり大きい画面のほうが快適だ。 結局のところ使う人のスタイルによって最適な画面サイズは変わってくるのだろう。

早速電車でも使ってみたが、さすがに電車の中の取り回しは 8 インチタブレットに軍配が上がるのは間違いない。 縦向きならばそんなに気にならないが立ちながら横向きで持つのは結構疲れる。 このタブレットは 10.8 インチということで従来の 10 インチタブレットよりも更に少し大きいのが効いてくるようだ。 電車の中メインで使用する方は迷わず 8 インチタブレットがいいだろう。

32 GB と 64 GB の差はかなり大きい

MediaPad M5 の方は日本で販売されているモデルは残念ながらすべて 32 GB のものとなっている。 この 32 GB というのがキツく、OS やプリインストールされているアプリが含まれているので実質的には初期状態でも 20 GB 強程度の空き容量しかない。 これだと写真や音楽、動画を溜め込まない人であったとしても心もとない容量だ。

そこをいくとこの MediaPad M5 Pro は 64 GB ということで初期状態で 50 GB 強の空き容量がある。 空き容量的には MediaPad M5 の 2.5 倍ということになっており、普通に使うぶんには十分な容量だ。 更に写真や音楽、動画をいっぱい保存したい場合は microSD カードを挿せば問題ない。

スタイラスペンは一般人には不要か

MediaPad M5 Pro にはスタイラスペンが付属してくる。 これが Apple Pencil のように USB-C 端子で充電できるし筆圧感知して線を書き分けられる……らしいが個人的にそういう機能は使わないし、別に手書きをしたいとも思わなかった。 マウス代わりに使えるかと思ったが Web 上ではタップしているのと同じ挙動となるようだ。 というわけで個人的には不要だった。

デスクトップモードは割と面白い

MediaPad M5 にない機能として「デスクトップモード」があり、これを起動すると Windows のような画面になりアプリをウインドウで開いて複数起動することができる。 これがなかなか便利そうだ。 Bluetooth キーボードとマウスをつなげればちょっとしたノート PC 気分だ。

プライバシーポリシー

広告 ID の使用

広告を表示する際に固有の ID が利用されています。 アプリから送信されるデータという点で、利用することをここに明記しています。 この情報から個人が特定されることはありません。

インターネットアクセス

アプリ内への広告表示に利用されています。

バイブレーション

タイマー機能での通知で使用しています。

作った理由

最近はプライバシーポリシーを明記しないとストアからの掲載が削除されてしまうケースが多くなった。 今回私の作成している時報アプリであるコジ時計も広告を使用しているという理由で掲載から外されてしまった。 そのため急遽プライバシーポリシーのページを作成して対処することにした。

プライバシーポリシーは HTML リンクを Google Play の対象ページ及びアプリ内から張る必要があるが、プライバシーポリシーのみのサイトを新たに作成するのも手間なので Blog の 1 記事として載せてお茶を濁しておく。

この対応に関しては Cocoamix.jp 様の記事が大変参考になった。

Android アプリがどの SDK バージョンをターゲットにしているかの指定に targetSdkVersion があるが、この値は常に最新の SDK バージョンを指定することが推奨される。 今現在だと 28 (Android P) となっている。 厄介なのが targetSdkVersion をインクリメントすると既存のコードでコンパイルができなくなったり @Deprecated アノテーションが付与された API が増えていたりと修正を余儀なくされるということだ。 古いアプリに関してはこれが面倒なので無理に上げなくていいのでは、と構えていたがデベロッパに対しては既に何度かアナウンスされていた通り 2018 年 11 月以降既存アプリのアップデートでも targetSdkVersion >= 26 が必須となる。 26 というのは結構高い値で、これは困る。 ちょっとした修正をしてお茶を濁そうとした時でも targetSdkVersion を上げてコンパイルが通るように調整且つ既存のコードが正しく動作するかを確認しなければならないからだ。 「作ったアプリをずっとメンテナンスし続ける」というのは結構辛い。

よくあるクラッシュレポートツール

Firebase Crashlytics に概要の説明が書いてある:

Firebase Crashlytics は軽量なリアルタイムのクラッシュ レポート ツールで、アプリの品質を低下させる安定性の問題を追跡し、優先順位を付け、修正するのに役立ちます。Crashlytics を使用すると、クラッシュをインテリジェントにグループ化し、クラッシュにつながった状況をあぶり出すことによって、トラブルシューティングの時間を節約できます。

この手のツールというのは昔から何かしらあって Google Play Console にも同様にクラッシュレポートを表示する機能があるのだが、これを入れておくと開発中のアプリを誰かに試してもらっている時にクラッシュした場合にそのバグレポートを Web 上ですぐに確認できるので便利だ。 ちなみに昔は Firebase Crash Reporting という機能があったのだがこれは非推奨となり Firebase Crashlytics への移行が求められる。 というわけで私としても既存アプリの Crashlytics への移行を余儀なくされたわけだが、その過程で微妙にハマったのでメモ。

手順を正しく踏まないと収集開始されない

導入方法は 公式の Get started に詳しく書かれているのでこの通りにやれば簡単に導入できる。 はずだったのだがこの通り build.gradle に書いてアプリを実行、適当にクラッシュさせても全く反映されない。 具体的には Firebase Console から対象アプリの Crashlytics を開いても以下の未設定の画面のままとなる。

Crashlytics 未設定

どうも以下の手順を踏まないと駄目なようなので書いておく。

1. 「Crashlytics を設定」を押しておく

前述の Crashlytics の未設定を示す画面で「Crashlytics を設定」を押下し画面遷移、このアプリでの Crashlytics の利用は初めてですか?の質問にはいと答える。 そして次の SDK をインストールしますの項目で 「Crashlytics ドキュメントに移動」ボタンを押下する。 すると以下の「Crashlytics 設定待ち」の画面になる。 この画面を立ち上げたままにしておく。

Crashlytics 設定待ち

2. アプリを起動して一旦クラッシュさせる

前述の状態を満たした上でアプリをビルドし、何かしらのわざとクラッシュするコードを仕掛けてクラッシュさせる。 公式に書いてある通り Crashlytics.getInstance().crash() がいいだろう。 そして、公式に書いてある通りアプリを起動した瞬間にクラッシュするのではなく、何かしらのボタンを押下したりなどの際にクラッシュさせるのがいいと思われる。 何故ならどうも Crashlytics のレポートは再度アプリを起動した時に送信されるようだからだ。

3. アプリを再度起動する

Crashlytics のレポートを送信させるために再度アプリを起動する。

4. Crashlytics 設定待ちの画面を見ながら少し待つ

  1. で設定した設定待ち画面を見ながら少し待っていると、設定が完了したことを示す画面に遷移する。

サムスンの microSDXC カードである EVO Plus の 128 GB を先日購入し、今日届いたのでまた試しに A1 SD Bench で計測してみた。 このカードは公称では読み込み最大 100 MB/s に書き込み最大 90 MB/s なのだが、計測してみたところ 読み込み 57.80 MB/s に書き込み 49.77 MB/s という結果だった。 何度か計測して同じような値が出たので間違いないと思われる。 私が前使っていた microSD カードに対して書き込み速度は大幅に向上し MediaPad M5 の内部ストレージの速度とも遜色ない状態となったが、読み込み速度が思ったより振るわなかった。 まぁ折角買ったわけだし、まずまず満足な結果なのでこのまま大切に使おうと思う。

Android の環境設定で「ストレージの設定 -> デフォルトの保存場所」を SD カードにしても即座にすべてアプリのデータが移るわけではないようだ。 それどころか内部ストレージ側に本来不要なデータが残り続けてしまうような気がする。 端末を一旦リセットして最初から SD カード保存でやった方が良さそうだ。 今ちょうど土日で時間があるので一旦綺麗にしようと思った。

Composer の設定を同期してしまう

タイトルに PhpStorm と書いたが私は今 IntelliJ IDEA Ultimate を使用しているのでそちらで再現した問題である。 だが検索すると PhpStorm でも同様の問題に遭ったという投稿が見受けられるので同様と思われる。

PhpStorm (IntelliJ IDEA Ultimate) では PHP language level (PHP バージョン) を設定することができる。 これによって例えば PHP 7.1 などに設定して PHP 7.1 の文法 (nullable なタイプヒンティングなど) を使ってもエディタ上でエラーにならない。 だが、一旦 IDE を再起動してしまうと何故か元に戻ってしまい文法エラー表示されてしまう。 これは何なのだろう、と調べてみたがデフォルトで composer.json の設定と同期されてしまうからだった。

対処

以下のどちらかを実施する:

1. composer.json の言語レベルを見直す

composer.json の以下の箇所を使用したい PHP バージョンに書き換えればよい:

"require": {
    "php": ">=7.1",
}

2. composer.json と IDE 設定の同期を止める

これに関しては IntelliJ のサポートフォーラムに投稿があった のでこの通り実施する。

That looks like IDE is synchronising these settings with your composer.json. This happens on project opening or when that file change is detected. "Settings/Preferences | Languages & Frameworks | PHP | Composer" -- uncheck appropriate option there ("Synchronize IDE Settings with composer.json").

ということで Settings -> Preference -> Languages & Frameworks -> PHP -> Composer の「Synchronize IDE Settings with composer.json」という設定を外せばよい。

日本では出ないかと思っていた

ファーウェイのスマートフォンである P20 シリーズにはそれぞれハイエンド、ミドルレンジ、ローエンドとして P20 Pro, P20, P20 lite (Lite でなく lite なのは伝統のようだ) が用意されており P20 Pro に関してはドコモ専売、P20 lite は au から出るということで日本での SIM フリー端末として P20 が発売されないのでは、と思っていたが先日 11 日に発表があった通り 6/15 に発売するようだ。

私が愛用しているスマートフォン (というよりファブレット) である Mate 9 は素晴らしい端末であり、特にライカ製のカメラで撮影した写真はこの Blog で何度も掲載している通りとても美しい。 ただ、私はバイクのナビで使用しているという関係もあって防水モデルの方が望ましい。 また MediaPad M5 などのタブレットと併用する為、端末のサイズはできるだけコンパクトな方がいい。 ファーウェイはものすごくコンパクトなモデルというのは販売していないが、この P20 ないし前モデルの P10 などは昨今のスマートフォンにしては比較的サイズがコンパクトで控えめながらも防水 (防雨程度) 端末であり、且つしっかりとライカ製のデュアルカメラを搭載している。 現時点で私の理想に近いと言っていいスマートフォンとなっている。

P20 を見て思うこと

前情報で示されていた通り、この P20 (Pro と lite も) には iPhone X のようなノッチ (液晶カケ) が存在する。 私がこれがとても格好悪いと思っているのだが、ファーウェイはちゃんとノッチが隠せるようなオプションも用意している。 ノッチを隠す設定にするとノッチの部分は黒塗りになり Android のステータスバーとして機能するようだ。 この設定が無かったら P20 は見送っていただろう。

また P20 の価格がミドルレンジモデルにしては高価で 69,800 円 (税抜) となっている。 ただこれに関しては Mate 9 が当時にしてはとても挑戦的な価格だった (税抜 60,800 円) だけで、最近の価格設定を通常に戻してきただけのように見える。 SoC は Mate 10 Pro や P20 Pro と同じ Kirin 970 だし内部ストレージ容量は 128 GB もある。 正直 128 GB もあっても使い切れないくらいだ。 日本発売の MediaPad M5 の方にこのくらいの容量が欲しかった。

一つ気になるのが AppGallery というファーウェイ独自のアプリストアがプリインストールされているということだ。 こういう独自のアプリストアというのは何度も見てきたがどれもいい印象がないし正直邪魔だった。 MediaPad M5 などと同じく幾つかのプリインストールされているアプリは消せないだろうし、変な動きをしないか気になるところだ。 追記: 日本国内で販売されているものにはインストールされていない。 結局何だったのか。 Google Play が使えない中国国内用だろうか。

多分狙っているのだろうが、また都合よく楽天スーパーセールが始まるので買うのに丁度いい。

Redmine のインストールは面倒

基本的に Redmine のインストールは面倒なイメージがある。 特に apt を使わずにインストールした場合などとても煩雑だ。 ここではすべて apt を使用してパッケージからインストールするものとし、且つ MySQL は utf8mb4 問題 (MySQL はデフォルトの utf8 だと絵文字などの 4 バイト文字が入力できない) があるので避け、最も簡単な SQLite を使用することにする。 SQLite だとデータベースがただのファイルなのでバックアップがとても簡単だし、ユーザという概念もないのでユーザを作成したりパスワードを設定する必要もない。 既に MySQL / PostgreSQL で構築されている Redmine のデータを移行するのなら話は別だが、大体の場合 SQLite で事足りるはずだ。

インストール

sudo apt install redmine-sqlite

途中画面が切り替わり以下の質問をしてくるので「はい」を選択すると DB が作成され Redmine のマイグレーションファイルが走る:

redmine/instances/default パッケージを使用する前に、データベースをインストールして設定する必要があります。これは必要に応じて dbconfig-common で処理することもできます。 あなたが上級データベース管理者で、手動でこの設定を実行することがわかっている場合、あるいはデータベースのインストールと設定が完了している場合は、このオプションを拒否するべきです。何をすべきかの詳細は、/usr/share/doc/redmine/instances/default で提供されている可能性が最も高いです。 そうでなければ、おそらくこのオプションを選ぶべきです。 redmine/instances/default 用のデータベースを dbconfig-common で設定しますか?

更に以下も必要なので apt で入れる:

sudo apt install apache2 libapache2-mod-passenger bundler imagemagick libmagick++-dev

redmine-sqlite パッケージでインストールされた Redmine は /usr/share/redmine にある。 以下に示すのはサブドメインなしでデプロイする場合だがサブドメイン (たとえば http://redmine.hoge.com など) に配置する場合は適宜読み替えて欲しい:

sudo vi /etc/apache2/sites-available/000-default.conf

<VirtualHost *:80>
    DocumentRoot /usr/share/redmine/public
    PassengerHighPerformance on
    <Directory /usr/share/redmine/public>
            AllowOverride None
            Options None
    </Directory>
    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

また sudo vi /etc/apache2/mods-available/passenger.conf で以下のように書き換える必要がある:

<IfModule mod_passenger.c>
  PassengerRoot /usr/lib/ruby/vendor_ruby/phusion_passenger/locations.ini
  PassengerDefaultRuby /usr/bin/ruby
  PassengerDefaultUser www-data
  RailsBaseURI /redmine
</IfModule>

Apache を再起動する:

sudo service apache2 restart

http://localhost/ (または適切なドメイン) にアクセスし Redmine の画面が表示されることを確認する。

バックアップ

Redmine の公式ドキュメントに示されている通り Redmine のインストールディレクトリ直下の files ディレクトリと SQLite ファイル (/var/lib/dbconfig-common/sqlite3/redmine/instances/default/redmine_default) をコピーするだけなので楽だ。

ppi とは

ディスプレイの緻密さを表す単位として ppi がよく用いられる。 Wikipedia の記載を引用する:

ppi(ピーピーアイ)とは、pixel per inchの略で、ディスプレイやビットマップ画像における解像度を示す単位である。別名画素密度 (pixel density) とも呼ばれる。 解像度とは、すなわち、画像を表現する格子の細かさであり、一般に1インチあたりのピクセルの数を表す(1平方インチあたりではない)。ppiで表したピクセル密度のことを単にppiと呼ぶことがある。

つまり、例えば 220 ppi だと 1 インチ (2.54 cm) の直線の中に 220 点のドットが含まれているということになる。

久々に MediaPad M3 Lite 10 を使ってみた

MediaPad M5 (8.4 インチ) をしばらく使って慣れてきたので、久々に MediaPad M3 Lite 10 を引っ張り出してきて試してみた。 やはり自分にとっては画面が大きいほうがいいのかが気になったからだ。 これに関しては個人差も大きいだろうが、私にとっては 8.4 でも 10.1 でもどちらでもいい、という結論になった。 どちらのスクリーンサイズも利点・欠点あり甲乙つけがたい。 8.4 インチに最適なゲームをプレイすることがメインであれば 8.4 インチがいいと思うが、10.1 インチの大画面も魅力的だ。 そして 8.4 インチが持ち運びに優れているように見えるが、結局のところズボンのポケットに入らずバッグに入れる必要があるので感覚的に 10.1 インチとそんなに変わらない。

私が「もう MediaPad M3 Lite 10 には戻れないな」と思ったのは画面サイズではなく前述の ppi だ。 MediaPad M3 Lite 10 は 10.1 インチで 1920x1200 (224 ppi) とタブレットとしては標準的な ppi なのだが MediaPad M5 の 8.4 インチ 2560x1600 (359 ppi) と見比べてしまうととても荒く見えてしまって辛い。 iPad や MacBook Pro で初めて Retina ディスプレイを見た時に近いような感覚がある。

そういえば今販売されている iPad ないし iPad Pro の ppi が 300 ppi に届いていない (264 ppi) のにも少しびっくりしたが iPad のディスプレイは十分美しいように感じた。 MediaPad M5 Pro の 10.8 インチ 2560x1600 (280 ppi) でも iPad より高精細なので十分なのだろう。

FF5 のクラウドセーブがフォルダ一覧に出てこない

私は Android 端末でドラクエ 5 や FF5 などのゲームで遊んで、その結果をクラウドセーブしている。 スクウェア・エニックスが提供するアプリのクラウドセーブは Google Drive を利用しているものが大半と思われる。 例えばドラクエ 5 でクラウドセーブを実施した場合は Google Drive のルートフォルダの下に DQ5 というフォルダが作成されそこにセーブデータが格納される。だが FF5 でクラウドセーブを実施してもルートフォルダの下には何も作成されない。一体 FF5 のセーブデータはどこに行ったのだろうか。

Google Drive の保存領域は普通にアクセスして見えるフォルダツリー (ルートフォルダ) と個々のアプリケーションからしか見えない AppFolder の 2 種類が存在していることを Google Drive API の仕様書を眺めていて知った。

AppFolder の確認方法

Web 上の Google Drive から確認できる。 画面右上の歯車マーク (設定) を押下し「アプリの管理」を選択する。 そうすると Google Drive へのアクセス権を保持しているアプリの一覧が表示されるが、例えば今回の FF5 の場合は非表示のアプリデータ: 254 KB などと表示されている。 セーブデータの表示はできないが「非表示のアプリデータを削除する」を選択すればデータを削除することはできる。

この AppFolder は結局 Google Drive の容量 (現時点で無料枠 15 GB) を使用してしまうが、一般のフォルダツリーに表示されないので間違って削除してしまうようなことが起きないし、余計なバックアップ格納用フォルダがルートフォルダ直下に乱立しないということでノイズが増えないという利点がある。

コジごみカレンダーの調整をしていて、今はサーバを立てなくてもこのように Google Drive に気軽にクラウドセーブを行うことができるのでそういう機能を実装するのもアリだと思った。 しかも一度実装してしまえば他にも簡単に使い回せそうだ。

デフォルトだと -Xmx128m

この間この Blog システムを Tomcat (Spring Boot) 上で動作するように修正したのだが、それからどうも動作が緩慢になったように思っていた。 確かに Load Average を見ても高めの値を示している。 そして、画像をアップロードしたら OutOfMemoryError: Java heap space. でクラッシュした。 これはまずいと調査を開始した。

Tomcat のヒープサイズは Ubuntu Server のデフォルトの状態だと -Xmx128m (最大ヒープサイズ 128 M) になっている。 これは Tomcat 起動時に catalina.out にログ出力されているので確認する:

view /var/log/tomcat8/catalina.out

" CATALINA_BASE, CATALINA_HOME もログ出力されているので確認しておく
CATALINA_BASE:         /var/lib/tomcat8
CATALINA_HOME:         /usr/share/tomcat8
...(略)...
Command line argument: -Xmx128m

setenv.sh

Tomcat のヒープサイズを変更するために Tomcat 起動時に環境変数を渡すにはどうしたら良いのか調べてみたが setenv.sh を定義するのがいいらしい。 Ubuntu のデフォルトではこのファイルは存在しないので $CATALINA_HOME/bin 若しくは $CATALINA_BASE/bin に作成する必要がある。 Tomcat を起動するためにキックされる catalina.sh に以下のように書かれている:

if [ -r "$CATALINA_BASE/bin/setenv.sh" ]; then
  . "$CATALINA_BASE/bin/setenv.sh"
elif [ -r "$CATALINA_HOME/bin/setenv.sh" ]; then
  . "$CATALINA_HOME/bin/setenv.sh"
fi

そこで $CATALINA_HOME/bin/setenv.sh に以下のように作成する:

export CATALINA_OPTS='-Xms256m -Xmx512m -Xss1024k -XX:PermSize=256m -XX:MaxPermSize=512m -XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled'

変更を適用するために Tomcat を再起動する:

sudo service tomcat8 restart

catalina.out を確認すると正しくこの値が使用されているのがわかる:

情報: Command line argument: -Xms256m
情報: Command line argument: -Xmx512m

また ps aux | grep tomcat8 のようにプロセスを検索しても良い。

今日ようやく MediaPad M5 (8.4 インチ LTE) を受け取ることができた。 早速一通り設定し、軽くだがゲームもプレイしてみたところでファーストインプレッションを書き留めておくことにする。 尚、私の以前のタブレットは MediaPad M3 Lite 10 なのでそちらと比較する内容が含まれることに留意されたい。

プリインストールアプリが増えてしまっている

この MediaPad M5 は残念ながら以前のファーウェイの端末 (MediaPad M3 Lite 10 や Mate 9) に比べてプリインストールアプリが増えてしまっている。 具体的には Abema TV やウイルスバスターに Facebook, Instagram や「キッズモード」なるアプリも入っていた。 大体のアプリはアンインストールできるのだが Facebook App Manager というアプリが無効にできるだけでアンインストールできない。 正直気持ち悪いのでアンインストールさせてほしい。

ファーウェイ端末のいいところはプリインストールアプリが少なくファーウェイ製のアプリが Google の純正アプリかと思わせるほどのセンスのいいものが揃っていることだと思っていたのだが、少しそのアドバンテージが削られてしまった気がする。 1 回端末を初期化すれば綺麗になるのだろうか。

ゲームモードを実現する Game Suite

MediaPad M5 にはプリインストールアプリとして Game Suite が入っているのだが、このアプリを使うとゲームアプリと思われるものを選択してゲームモード (電池消費が激しいがパフォーマンスが向上する) で起動することができる。 私はあまり負荷の高いゲームはやらないので正直効果のほどはよく分からなかったが、ゲーム好きの方には刺さる機能だろう。 実際 MJ モバイルは MediaPad M3 Lite 10 だとモタツキ気味だったが MediaPad M5 だととても快適に動作する。

この Game Suite でいいなと思ったのはゲームモードに加えて以下の設定ができることだ:

ゲーム時の鳴動制限

有効にすると、通話、アラーム、電池残量低下の警告を除き、画面上のすべての通知が非表示になります。 また、通話やアラームに対しても鳴動しません。

スマホでリアルタイムに進行するゲーム (MJ モバイルなど) をやっていて突然電話がかかってきて中断させられて悔しい思いをしたことが何度かあった。 更に LINE の通知が重要な部分に被さってしまって見えないなどの不都合もあった。 この機能があればその心配はなくなるだろう。

ナビゲーションキーのロック

画面外ナビゲーションキーをロックすることで、ゲーム時の誤動作を防止できます。 操作を実行するには、キーを 3 回タップしてください。

ゲームに熱中する余りナビゲーションキーを誤って押してしまったことは確かに何度かあった。 私がプレイしているゲームは簡単に復帰できるのでそこまで必要性を感じないが、アプリが隠れてしまっては困る方にはとてもいい機能だと思った。

高解像度の恩恵はあまり感じない

MediaPad M3 Lite 10 が 10.1 インチで 1920x1200 だったのに対しこの端末は 8.4 インチで 2560x1600 という高解像度だが、使っていてそんなに違いを感じなかった。 Web ブラウジングや Kindle (漫画でなく活字) であれば多少は違って見えるが、ゲームや動画などは全く違いを感じない。 高解像度のせいで電池消費やパフォーマンスに影響が出てしまっているのだとしたら、このサイズの端末は 1920x1200 の方がいいのではないか。

パフォーマンスの差

MediaPad M3 Lite 10 との比較であるが、ゲーム (特に MJ モバイル) は動作が機敏になったし、タスクを切り替える時の処理が速い。 とはいっても感動するほどの差ではない。 よく言われていることだが YouTube 動画や Kindle を読む程度なら MediaPad M3 Lite で十分だと思う。

急速充電は便利

このタブレットは急速充電に対応しているのが便利だ。 Tronsmart のモバイルバッテリーを使うと Mate 9 でも急速充電できるのだが、試しに MediaPad M5 を繋いでみたら急速充電になった。 但し Mate 9 の純正 AC アダプタを繋いでも急速充電にはならない (Mate 9 は超急速充電になる)。

8.4 インチと 10.1 インチのサイズ感

今回 10.1 インチの端末から 8.4 インチの端末に変更して「ちょっと小さい」と思った。 これが面白いものでネット上の噂を見ると 8 インチでも大きすぎるから 7 インチのタブレットが欲しいという声も少なからずあるのを知っている。 このあたりは完全に好みの問題だろう。 勿論ゲームをプレイするにはこのサイズがベストだと思う。 10.1 インチだと指を動かす距離が長くて辛い時がある。 本当は 8.4 インチと 10.8 インチの MediaPad M5 を両方持って使い分ければ最高なのだが、そうするとどちらか使わなくなりそうなので致し方ないところだ。

決定的な対処法が今のところ存在しない様子

どうも数日前から一部のファーウェイ製スマートフォン・タブレットにおいて Uber のような Google Map を使用しているアプリで本来地図が表示される領域に「Google Play services are updating」という文言が表示されて地図が描画されないという不具合が出ているようだ。 一部のと書いたが、正常に動いているファーウェイ端末においても後述する「Google Play 開発者サービスのデータの削除」を行うと現象が再現する。

この問題は Google Play Help ForumIssue Tracker に上がっている。 今後問題が修正されることを期待したい。

尚、私が試した以下のような手法では現状ではどの方法をとっても端末の再起動を行うと元に戻ってしまう:

  • Google Play 開発者サービスのデータを削除
  • Google Play 開発者サービスをアンインストールし工場出荷状態に戻した上で更新
  • APKMirror を使用し過去のバージョンの Google Play 開発者サービスをインストールする (放っておくと結局 Google Play によって最新にされてしまいそのまま再起動すると同現象となる)

Google Play 開発者サービスのデータを削除

Google Play 開発者サービスのデータを削除

上記に紹介した対応方法の中で「Google Play 開発者サービスのデータを削除」が一番簡単なので、とりあえずバグが修正されるまではこの方法を使用して端末を再起動せずに使う形でお茶を濁すしかなさそうだ。 これは以下の方法で行うことができる:

  1. 端末の「設定」「アプリ」を開く
  2. 表示されているリストの中から Google Play 開発者サービスを選択 (表示されない場合はプルダウンメニューで「全てのアプリ: 有効」を選択)
  3. 「ストレージ」を押下するとメモリ使用量が表示されるので「容量を管理」を押下 (「キャッシュを消去」だと変化がないので注意)
  4. 「データをすべて消去」を押下しデータを削除する
  5. Uber のような Google Map を使用するアプリを開き Google Map が正しく表示されるのを確認する (再起動すると戻ってしまうので注意)

Google Play 開発者サービスが修正された (2018/05/19 追記)

この問題は 2018/05/19 に公開された Google Play 開発者サービス (バージョン 12.6.85) で修正された模様。 ただ私の環境だと再起動した直後は依然として Google Play services are updating が表示されるが、再起動してから 2, 3 分待ってから対象のアプリを開くと Google Map が正しく描画されるようになった。

Google Drive REST API

以前 Python で Google Drive にファイルアップロード という記事を書いたがそれの Kotlin (Java) 版である。 Java に関しては Google API Client が用意されているのでそれを導入して指定の手順を踏めば良い。 書くコードは多めなのだが手順としては分かりやすかった。

公式サイトの Quick Start を辿ればファイル一覧取得までは出来るのだが、ここでは「指定のディレクトリにファイルアップロード」までを書くものとする。

公式の Quick Start を Kotlin で書く

まず Google API Developer Console で対象プロジェクトに対し Google Drive API が有効になっていることを確認する。 有効になっていれば「API とサービス -> 認証情報」から「OAuth 2.0 クライアント ID」の認証情報を作成する (既にあればそれを使用する)。 作成したらそれを押下すると「クライアント ID」と「クライアントシークレット」が表示され、画面上部に「JSON をダウンロード」というメニューが表示されるのでそれを押下しダウンロードする。 ファイル名を crient_secret.json とリネームし Java (Gradle) プロジェクトの /src/main/resources 上に配置する。

build.gradle には公式の通り以下を追加する:

dependencies {
    compile 'com.google.api-client:google-api-client:1.23.0'
    compile 'com.google.oauth-client:google-oauth-client-jetty:1.23.0'
    compile 'com.google.apis:google-api-services-drive:v3-rev110-1.23.0'
}

公式の Quick Start 相当のコードは以下となる。 ファイル一覧を取るだけでもコード量は少し長いが、そのまま書けば動いた:

...(略)...
typealias DriveFile = com.google.api.services.drive.model.File  // java.io.File と被るので別名をつける

fun index() {
    val transport = GoogleNetHttpTransport.newTrustedTransport()
    val jsonFactory = JacksonFactory.getDefaultInstance()
    val input = javaClass.getResourceAsStream("/client_secret.json")  // クラスパス上ルートディレクトリにあるものとする
    val clientSecrets = GoogleClientSecrets.load(JacksonFactory.getDefaultInstance(), InputStreamReader(input))
    val scopes = Collections.singletonList(DriveScopes.DRIVE)  // Quick Start と異なり書き込み権限を与えておく
    val flow = GoogleAuthorizationCodeFlow.Builder(transport, jsonFactory, clientSecrets, scopes)
            .setDataStoreFactory(FileDataStoreFactory(File("credentials")))  // 認証情報を格納するディレクトリ
            .setAccessType("offline")
            .build()
    val credentials = AuthorizationCodeInstalledApp(flow, LocalServerReceiver()).authorize("user")
    val service = Drive.Builder(transport, jsonFactory, credentials)
            .setApplicationName("アプリ名")
            .build()
    val files = service.files().list()
            .setPageSize(10)
            .setFields("nextPageToken, files(id, name)")
            .execute()
            .files
    println("files: ${files.map { it.name }}")  // ファイル名の一覧が表示される
}

Collections.singletonList(DriveScopes.DRIVE) の箇所が公式 Quick Start では Collections.singletonList(DriveScopes.DRIVE_METADATA_READONLY) になっているが、これだと読み込みだけの限られた処理しかできない (ファイルを書き込もうとすると例外がスローされる)。 後の手順でファイルアップロードを行うために全権限を付与している。

java.io.File を使わなければならないのに Google Drive API の方のファイルオブジェクトが同名の com.google.api.services.drive.model.File になっているのが少し不便に感じるのでここでは typealias で別名をつけておくとコードがスッキリする。

尚、初回実行時に認証が入るのだが、コンソールに Please open the following address in your browser: (URL) といった出力がされるのでそのリンクを踏んで認証を完了させる。 この認証情報は setDataStoreFactory() で指定したディレクトリに入るので、また認証を行いたい場合はこの中のファイルを削除すればもう一度行うことができる。

指定ディレクトリにファイルアップロード

上記コードからの続きで以下を書く:

    val file = DriveFile()
    file.name = "hogefuga"  // ファイル名
    file.parents = arrayListOf("1b5-2LHlr1j0t6dRIsODNlMRXVJ5TGvos")  // 親ディレクトリ ID
    val content = FileContent("text/plain", File("C:\\Users\\hoge\\fuga.txt"))
    service.files().create(file, content).execute()  // execute() を忘れると実行されないので注意

指定ディレクトリの ID は Web 上の Google Drive にアクセスし対象ディレクトリに移動すると URL に表示されるのでそれを使用すれば良い。

数日間 Markdown を快適に書ける環境をいろいろ探していたが、結局 MarkdownエディタはTyporaとJotterPadで決まりだ! を見て同様に Typora と JotterPad を使用することにした。 Boostnote も良かったのだが Android 版の出来が悪いのが致命的だった。 現状 Dropbox としか連携できないし Dropbox 側の決まったディレクトリから移動することができない。 また保存フォーマットが Markdown テキストでなく JSON ライクな形式になっているのも微妙なところで、いくら Markdown 形式でエクスポートできるといっても気軽に見ようとした時に JotterPad が入っていないと見ることができない。 素の Markdown テキストならばどんな環境でも見ることができるので個人的にはそちらの方が嬉しいところだ。 また Boostnote はフォルダ分けに加えてタグ付けもできるが、個人的には管理が面倒なのでタグは不要だ。

Typora だとフォルダ分けしてそのアウトライン (エクスプローラのような階層化されたフォルダの状態) を左サイドバーで見ることができるし Markdown を書くとその場で整形されて表示される為プレビュー画面が分かれていないのが新しいと思った。 Redmine のように Textile で書かなければならない時に Textile 形式のエクスポートがとても便利だ。

歳のせいか分からないが、「あれ、これなんだっけ」といった事が増えてきたように感じる。 忘れそうなことは逐次 Markdown でメモ書きを残す癖をつけたいところだ。 この Blog も自分の備忘にとても役に立っている。

プログラマに特化した Markdown 記法のメモアプリ

昨日の記事で主に他人に対して Markdown でドキュメントを起こす手段は MkDocs がいいだろうという事になったわけだが、自分用のメモアプリでも Markdown を使いたいという欲求が出てきた。 Markdown でメモを残しておけば、もしアプリを乗り換えたい場合でも乗り換え先が Markdown 記法をサポートしていれば比較的簡単にデータを移すことが可能だ。

ちなみに今までは Google Keep を使用していた。 Evernote を使用していた時期もあったがゴチャゴチャしていて好みではなくすぐに止めた。 Google Keep はとてもシンプルなメモアプリで簡単に PC / Android 間でクラウド同期できるし、チェックボックスを使用して TODO リストを作ることも容易なので残作業の管理にとても役立っていた。 ただ、以下の点で少しだけ不満に感じていたのは事実だ:

  • 記法がプレーンテキストの為若干物足りない
  • フォルダ分けの機能がない (タグを使用して近い事はできるが)

MkDocs の情報を探すついでにクライアントアプリで代わりになるようなものを探していたら Boostnote がヒットした。

凄くいいなと思ったところ

  • Markdown 記法をサポートしているが表組とチェックリストに対応していること
  • Boostnote で生成される結果がただのファイルなので簡単に Dropbox などのクラウドストレージと連携できること
  • フォルダ分けに対応していること (タグにも対応している)
  • Vim キーバインドが使えること
  • Windows / Mac / Linux アプリだけでなく Android / iOS アプリが用意されていること

尚 Boostnote がサポートしている Markdown 記法に関しては公式の Blog 記事「仕事効率、学習効率を加速させるMarkdown記法の紹介」に詳しく書かれている。 テーブル記法もチェックリストも Qiita の Markdown 記法と同じとなっているのがありがたい (MkDocs も同じだが)。

このアプリを見ると真っ先に思い出すのは Qiita と連携できる同じく Markdown 記法のメモアプリ Kobito だが、今日見たら既に公開終了していた……。

現在 Android 版の出来がかなりイマイチ

Android 版も Google Play から落として使ってみたのだが、機能が全然足りていないし挙動が不安定でよく落ちる状態で正直イマイチだと思ってしまった。 記事を参照しようとすると編集画面になってしまってプレビューを表示したい場合は一旦保存しなければならない UI など全く練り込まれていない感じがする。 また Dropbox 連携ができる (Google Drive 非対応) 為試しに連携して使おうとしたところ記事を追加しようとすると 100 % クラッシュするという不具合に見舞われた。 にっちもさっちもいかないので Dropbox に作成された boostnote-mobile フォルダを Windows 側のアプリで Add Storage Location した。 そうしたら 1 つもフォルダが無い状態だったので 1 つ追加した上で Android アプリ側で記事を追加しようとしたら今度はうまくいった。

正直今のところ Android 側でちょっと見たいくらいの要件にしか使えない。 今後の更新に期待したいところだ。

久々に使い勝手の良さに感動した

ドキュメンテーションツールといえば Sphinx が定番だったし過去私もよく使用していたのだが Sphinx が標準でサポートしているマークアップ言語である reStructuredText がお世辞にも書きやすいとは言えず常に記法を調べたり別の箇所からコピペするなどして頑張って書いていた記憶がある。 最近では技術系に関わらず文書を起こす場合は Markdown がデファクトの地位を確立していると思う。 Sphinx でも設定すれば Markdown を使えるようだがこれも使い勝手がいいとはいえない。 設定が簡単で Markdown を書くことに集中できるようなツールを探していたのだが、今回見つけた MkDocs がとても素晴らしかったので自分用メモを兼ねてここに書き記しておく。

  • Markdown なのに最初から表組みの拡張が入っている (しかも書きやすい)
  • mkdocs serve でローカルサーバが起動するが Markdown ドキュメントを編集すると即時反映される (ビルド不要)
  • Sphinx の admonition だったり footnotes (注釈) が設定 1 行付け足すだけで簡単に使用できるようになるし他の機能も必要に応じて 1 行書くだけで即追加可能

導入・設定

導入に関してはカンタンにドキュメントが作れるmkdocsをはじめてみようがとても分かりやすかった。 基本は Python をインストールして pip からすべて導入できるので楽だ。 細かい設定に関しては MkDocsによるドキュメント作成が詳しい。

その他の設定は MkDocs 公式 (英語) を確認すればよい。 また使用するテーマに関してだが readthedocs か material のどちらかがいいということだが私は material にした。 CSS をいじらずに配色 (マテリアルデザインにおけるテーマ色とアクセント色) が簡単に変更できるし、日本語対応がされている。 後何故か readthedocs の方だとコードハイライト時にうまく表示されなかった。

私が導入した設定

site_name: 'サイトネーム'
site_description: 'サイト説明'
site_author: '所有者名'
site_url: 'サイト URL'
copyright: '著作表記'

theme:
  name: 'material'
  language: 'ja'
  palette:
    primary: 'light blue'
    accent: 'pink'
  font:
    text: 'UD デジタル 教科書体 NK-R'
    code: 'Consolas'
    
extra:
  search:
    language: 'jp'
    
markdown_extensions:
  - admonition
  - codehilite
  - footnotes
  - pymdownx.inlinehilite
  - pymdownx.tasklist:
      custom_checkbox: true

Material for MkDocs

Material for MkDocs 公式 (英語) にすべて網羅されているので設定はここを見れば良い。 言語ロケールの選択をすると全体的に日本語表記になる (検索も日本語対応されている) し、テーマ色やフォント (コードハイライトと別々に定義できる) の設定も可能だ。

Extensions

以下を導入した:

  • Admonition (Sphinx のような警告文)
  • CodeHilite (コードハイライト)
  • Footnotes (文末注釈)
  • pymdownx.inlinehilite (インラインコードのハイライト)
  • pymdownx.tasklist (チェックリスト)

アノテーションを定義する方法が面倒な時

Spring Boot でのバリデーション実装に関しては昨日の記事に書いた。 今回は @NotBlank@Size など標準で用意されているアノテーションだけではまかないきれないようなバリデーションを行いたい時にどうするかについて書く。

まず、独自のバリデーションを行いたい場合は自分でアノテーションクラスを作成して Form クラスの対象フィールドに定義するといった方法がある。 これに関しては「Spring バリデーション 独自」などで検索すればいくらでも出てくるのでここでは言及しない。 アノテーションクラスを定義すればアノテーションを付与するだけでどこにでも使えるようになるので便利なのだが、私は以下の理由で後述の方法をとることにした:

  • 普遍的に使うのではなくただ 1 箇所で使いたいだけなのにアノテーションクラスを定義して使うのは面倒
  • RepositoryService などのモデル層が絡むようなバリデーションを使いたい

尚、自分でガリガリ書けば (Spring Validation を使用しなければ) どのようなバリデーションも思いのままだが、ここではそういうことではなくあくまで BindingResult の対象のフィールドにエラーが入った状態で Thymeleaf テンプレートが表示され、対象のフィールドに th:errors="(Form フィールド名)" でエラー表示がされるといったように組み込みのバリデーションと同じ流れで表示されるようにしたい。

BindingResult.rejectValue()

最初 BindingResult.addError() というメソッドを見つけたので何度か試していたのだが、全くうまくいかないので諦めて検索したら正しくは BindingResult.rejectValue() というメソッドを使用するのだという情報を見つけた。 このメソッドは rejectValue({Form フィールド名}, {messages.properties キー}) のようにして任意のタイミングで messages.properties (設定を変更していない場合は ValidationMessages.properties) に定義されているバリデーションエラーメッセージを対象のフィールドに対し設定することができる。 また、バリデーションエラーにプレースホルダがある場合 (例えば「{0} には日付の形式で入力してください。」のような) に rejectValue({Form フィールド名}, {messages.properties キー}, {プレースホルダに渡したい引数の配列}, {デフォルトメッセージ}) のようにして引数を渡すこともできる。

実装例

ここでは例として「同一日付に対する記事は登録できない (但し更新時は自分自身を対象外とする)」というバリデーション実装を行う。 messaegs.properties に以下リソースが定義されているものとする:

validation.date-format={0} は日付形式で入力してください。
validation.date-already-registered=その日付の記事は既に登録されています。

コントローラ側に組み込みのバリデーションを実施した後今回の独自バリデーションを以下のように実装する:

/**
 * 記事を追加する.
 *
 * @param model モデル
 * @param form PostForm
 * @param result BindingResult
 * @param id postId
 * @return template 名
 */
@PostMapping("/posts/add", "/posts/{id}")
fun savePost(model: Model, @Validated @ModelAttribute("form") form: PostForm, result: BindingResult,
        @PathVariable("id") id: Int?): String {
    model.addAttribute("tags", tagService.findAll())

    // 組み込みバリデーションエラーに引っかかった場合
    if (result.hasErrors()) {
        return "/posts/add"
    }

    // 日付の形式が間違っている場合はエラー
    val date: LocalDate
    try {
        date = LocalDate.parse(form.date)
    } catch (e: DateTimeParseException) {
        e.printStackTrace()
        result.rejectValue("date", "validation.date-format", arrayOf("日付"), "")
        return "/posts/add"
    }

    // 既に登録されている年月日の場合はエラー (但し更新時は自分自身を対象にしない)
    val post2 = postService.findByDate(date)
    if (post2 != null && (id == null || id != post2.id)) {
        result.rejectValue("date", "validation.date-already-registered")
        return "/posts/add"
    }

    TODO("保存処理")
    return "redirect:/posts"
}

これで同一日付で記事を登録しようとした時に「その日付の記事は既に登録されています。」といったエラーメッセージが表示される。

導入と properties の統合及び UTF-8 化

まず build.gradle に以下を定義する:

dependencies {
    compile('org.springframework.boot:spring-boot-starter-validation')
}

これですぐに使えるわけだが、その前にバリデータの設定をする。 後述するがバリデーションメッセージに関してはデフォルトで ValidationMessages.properties に定義されているものを使用するのだが、これも後述するがバリデーションメッセージのプレースホルダに適用されるフィールド名は messages.properties のものが使用されるので 2 箇所に書くことになってしまう。 できれば messages.properties に両方書くようにしたい。 更にデフォルトで properties ファイルは UTF-8 エンコーディングになっていない。 これも UTF-8 に変更したい。

これを実現するには以下のような WebMvcConfigurer を実装したコンフィギュレーションファイルを定義する:

@Configuration
class Configuration : WebMvcConfigurer {

    /**
     * バリデータを返す.
     *
     * @return バリデータ
     */
    override fun getValidator(): Validator? {
        val source = ReloadableResourceBundleMessageSource().also {
            it.setBasename("classpath:messages")  // ValidationMessages.properties でなく messages.properties を使用する
            it.setDefaultEncoding("UTF-8")  // エンコーディングとして UTF-8 を使用する
        }
        return LocalValidatorFactoryBean().also { it.setValidationMessageSource(source) }
    }
}

ちなみに Spring 4 以前は WebMvcConfigurerAdapter というインターフェースを使用していたようだが Spring 5 (Spring Boot 2.0) では非推奨となり WebMvcConfigurer を使用するようになった。

messages.properties でなく messages.yaml などというファイルを置くとよろしくやってくれるのかどうか試したのだが駄目だった。 残念。

バリデーション例

例えば以下のように messages.properties に定義してあるものとする:

# フィールド名
date=日付
name=名前
tag=タグ
markdown=Markdown

# バリデーションメッセージ (デフォルトの差し替え)
javax.validation.constraints.NotBlank.message={0} を入力してください。

# カスタムバリデーションメッセージ
validation.max-length={0} は {1} 文字以下で入力してください。
validation.not-selected={0} を選択してください。

javax.validation.constraints.NotBlank.message に関してはバリデーションで使用する @NotBlank アノテーションでのエラー時に表示されるメッセージの差し替えである。 この場合 @NotBlank アノテーションのパッケージ名を含めたクラス名が javax.validation.constraints.NotBlank の為それに .message を加えたものを定義しておくとデフォルトメッセージの差し替えができる。 @NotEmpty@Size なども同様となる。

尚、こういう全体的に適用できるバリデーションメッセージだけでなく項目ごとに個別に指定したいバリデーションエラーメッセージがある。 例えば @Size によるバリデーションエラーは「0 文字以上 64 文字以下」のような表示になってしまうが、多くの場合は「0 文字」は不要で最大桁数のみ通知すればいいはずだ。 こういう場合に任意のプロパティ名でカスタムバリデーションメッセージを定義しておく。

フィールド名に関してはフォームの POST 時に使用する Form インスタンスのフィールド名と同じにしておく。

Form

class PostForm {

    // 必須
    @NotBlank
    var date: String = ""

    // 必須かつ 64 文字以内
    @NotBlank @Size(min = 0, max = 64, message = "{validation.max-length}")
    var name: String = ""

    // 選択必須
    @NotEmpty(message = "{validation.not-selected}")
    var tag: Array<Int> = arrayOf()

    // 必須
    @NotBlank
    var markdown: String = ""
}

上記の例のようにバリデーションエラー時に表示されるカスタムメッセージを messages.properties を使用して指定したい場合は {validation.max-length} のような記法で書く。 {} を付けずに書くと任意の文字列を指定できるが、折角 messages.properties が使えるのにハードコーディングすることもないだろう。

Controller

@Controller
class MyController {

    /**
     * 記事追加画面を表示する.
     *
     * @param model モデル
     * @param form PostForm
     * @return template 名
     */
    @GetMapping("/posts/add")
    fun addPost(model: Model, @ModelAttribute("form") form: PostForm): String = "posts/add"

    /**
     * 記事を追加する.
     *
     * @param model モデル
     * @param form PostForm
     * @param result BindingResult
     * @return template 名
     */
    @PostMapping("/posts/add")
    fun savePost(model: Model, @Validated @ModelAttribute("form") form: PostForm, result: BindingResult): String {
        if (result.hasErrors()) {
            return "/posts/add"
        }
        
        // TODO 登録処理
        return "redirect:/posts"
    }
}
  • 登録画面初期表示時の GET と POST を分ける (Form に対するバリデーション指定の為)
  • BindingResultForm の直後の引数として定義する (※位置が違うと正しく機能しないので注意) とメソッド内で result.hasErrors() でバリデーションエラーの有無を取得できる (BindingResult がないとメソッドの中まで処理が進まずに弾かれてしまう)
  • 登録画面初期表示時の GET のメソッドの方にも Form を含めたほうが良い (フォーム項目の初期データの表示時に Form に直接値をセットすれば良い)
  • FormModel は HTML 内の <form> によって POST される項目か否かで使い分けると良さそうに見える

Thymeleaf

<form th:action="@{/posts/add}" method="POST" th:object="${form}">
    <div>
        <input type="text" id="date" name="date" th:value="*{date}" th:classappend="${#fields.hasErrors('*{date}') ? 'is-invalid' : ''}">
        <div class="invalid-feedback" th:if="${#fields.hasErrors('*{date}')}" th:errors="*{date}"></div>
    </div>
    <div>
        <input type="text" id="name" name="name" th:value="*{name}" th:classappend="${#fields.hasErrors('*{name}') ? 'is-invalid' : ''}">
        <div class="invalid-feedback" th:if="${#fields.hasErrors('*{name}')}" th:errors="*{name}"></div>
    </div>
    <div>
        <select class="custom-select" id="tag" name="tag" multiple="multiple" th:classappend="${#fields.hasErrors('*{tag}') ? 'is-invalid' : ''}">
            <option th:each="t : ${tags}" th:value="${t.id}" th:text="${t.name}" th:selected="${#arrays.contains(form.tag, t.id)}"></option>
        </select>
        <div class="invalid-feedback" th:if="${#fields.hasErrors('*{tag}')}" th:errors="*{tag}"></div>
    </div>
    <div>
        <textarea id="markdown" name="markdown" class="form-control" rows="16" th:classappend="${#fields.hasErrors('*{markdown}') ? 'is-invalid' : ''}">[[*{markdown}]]</textarea>
        <div class="invalid-feedback" th:if="${#fields.hasErrors('*{markdown}')}" th:errors="*{markdown}"></div>
    </div>
    <div>
        <input type="submit" value="保存"/>
    </div>
</form>

<form> の内容を全部書いたので若干見づらいが重要なのは ${#fields.hasErrors('(Form フィールド名)')} でそのフィールドにエラーがあるかどうかが取得でき th:errors="(Form フィールド名)" で対応するエラーメッセージが要素内のテキストノードに格納されるということだ。 コードにも例示したがエラーの有無で入力フォームの見た目を変えたい場合は th:classappend を使用して class 属性を追加して CSS で見た目を変更すれば良い。

ここまで定義した内容で名前以外すべて空、名前は最大文字数をオーバーした状態で submit すると以下のようにエラーメッセージが表示される:

  • 日付 を入力してください。
  • 名前 は 64 文字以下で入力してください。
  • タグ を選択してください。
  • Markdown を入力してください。

Android 開発で未だに完全な Java 8 が使えないということもあり (Stream API でさえも最低ビルドターゲットを引き上げないと使用することができない)、どうしても Java 8 Time API への移行ができずにいにしえの DateCalendar などを駆使してイマイチな日付処理を行う癖がついてしまっていた。 Spring Boot ならばちゃんと Java 8 Time API が使用できるので古い API は捨て去りたい。

しかし Thymeleaf が標準で備えている #dates.format() などのユーティリティメソッドは Date を対象としておりそのままでは LocalDateTime などは使用することができない。 これでは困ってしまうので拡張機能を導入する。

解決法

build.gradle に以下を追加する:

dependencies {
    compile('org.thymeleaf.extras:thymeleaf-extras-java8time:3.0.1.RELEASE')
}

どこでもいいのだが component-scan 対象パッケージのどこかに以下のような @Bean 定義を置く:

@Configuration
class Configuration {

    /**
     * Java 8 Time Dialect を返却する.
     *
     * @return Java 8 Time Dialect
     */
    @Bean
    fun java8TimeDialect() = Java8TimeDialect()
}

これだけで Thymeleaf 上で LocalDateLocalDateTime を扱うことができるようになる。 例えば #date.format() にあたるのは #temporals.format() で以下のように書く:

<div th:text="${#temporals.format(firstDate, 'yyyy/M/d')}"></div>

他の使い方は Thymeleaf - Module for Java 8 Time API compatibility の GitHub に書いてあるので参照すればすぐ分かるだろう。

server.xml の場合

Tomcat はデフォルトで PUT と DELETE のリクエストボディが無効になっているらしく POST と同じような感じでフォームデータを PUT, DELETE してもすべてクリアされてしまう。 これを有効にするには server.xml に以下の様に設定を行う:

<Connector port="8080" protocol="HTTP/1.1" 
           connectionTimeout="20000"
           redirectPort="8443"
           parseBodyMethods="POST,PUT,DELETE"
           URIEncoding="UTF-8" />

Spring Boot 組み込み Tomcat の場合

Spring Boot で Application クラスを実行して組み込みの Tomcat が立ち上がった際もこの設定を有効にしたい。 この場合 application.yaml ではなく @Configuration アノテーションを付けたクラスに @Bean として TomcatServletWebServerFactory を返すメソッドを書く:

@Configuration
class Configuration {

    /**
     * 組み込み Tomcat のデフォルトで PUT, DELETE に Request Body が許可されていないので許可する.
     *
     * @return TomcatServletWebServerFactory
     */
    @Bean
    fun tomcatEmbeddedServletContainerFactory(): TomcatServletWebServerFactory = object : TomcatServletWebServerFactory() {
        override fun customizeConnector(connector: Connector?) {
            super.customizeConnector(connector)
            connector?.parseBodyMethods = "POST,PUT,DELETE"
        }
    }
}

Tomcat の他の設定をいじりたい場合も同じようにここに追記することができるようだ。

JPA の Entity では単一キーである id 列を持たせるような構造を定義することが出来るが SQLite の INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT な列に関して適切な定義がよく分からなかったのでメモ。 例えば以下のようなテーブルがあるとする:

create table comments (
    id integer not null primary key autoincrement,
    name varchar(16) not null,
    body text not null,
    created datetime not null,
    modified datetime not null,
);

このような AUTOINCREMENTPRIMARY KEY があると SQLite は内部的に sqlite_sequence というテーブルに各テーブルのシーケンスを格納するという挙動をする。 sqlite_sequence テーブルの DDL は以下のようになっている:

CREATE TABLE sqlite_sequence (name, seq);

name にはテーブル名、seq には現在のシーケンス値が格納される。 この sqlite_sequence テーブルを Hibernate 側に教えてやればよい。 この場合の Entity 定義は以下のようになる:

@Entity
@Table(name = "comments")
data class Comment(
        @Id
        @GeneratedValue(generator = "sqlite_comments")  // Generator 名 (何でもよい)
        @TableGenerator(
                name = "sqlite_comments",  // @GeneratedValue.generator と合わせる
                table = "sqlite_sequence",  // SQLite のシーケンステーブル名と合わせる
                pkColumnName = "name",  // sqlite_sequence のシーケンスカラム名 (name 固定)
                valueColumnName = "seq",  // sqlite_sequence のシーケンス値名 (seq 固定)
                pkColumnValue = "comments",  // sqlite_sequence.name に格納されている値 (テーブル名)
                initialValue = 1,  // シーケンス初期値. 多くの場合 1
                allocationSize = 1  // AUTO INCREMENT される場合の増減値. 何故かデフォルト 50 になっているので 1 を指定する
        )
        var id: Int? = null,
        var name: String = "",
        var body: String = "",
        @CreatedDate var created: Date = Date(),
        @LastModifiedDate var modified: Date = Date()
)

他のフレームワークには用意されていたりする

テンプレートにおける表示用の加工処理は大体 Thymeleaf が標準で備えているが、例えば CakePHP における Helper だったり Django における独自のテンプレートタグの作成のようにテンプレートの機能だけでは賄いきれない、多くはプレゼンテーション層における HTML への変換のためのロジックを使いたい時がある。 こういう時に Spring Boot における Thymeleaf 上ではどうすればいいのだろうか、というのが今回のテーマである。

勿論 ControllerModel (Form) に対応する HTML タグへの変換処理を書いたりすることはできるが MVC において本来コントローラやモデルでプレゼンテーション層の処理を書くのは好ましくないので避けたいところだ。 いろいろ試行錯誤してみた結果、プレゼンテーション層のヘルパクラスをコンポーネントとして登録して Thymeleaf 側から呼び出すのが一番シンプルな気がした。

ヘルパークラス定義

今回は Markdown で書かれたテキストを HTML に変換したいとする。 以下のようなヘルパークラスを任意のパッケージに定義する:

/**
 * Thymeleaf テンプレート上で使用するヘルパ.
 */
@Component
class Helper {

    /**
     * Markdown を HTML に変換して返す.
     *
     * @param markdown Markdown
     * @return HTML に変換された Markdown
     */
    fun toHtml(markdown: String): String {
        val (parser, renderer) = Parser.builder().build() to HtmlRenderer.builder().build()
        return renderer.render(parser.parse(markdown))
    }
}

@Component アノテーションを付与することにより Spring 管理下のコンポーネントとして機能する。 これを Thymeleaf 上で使用するには以下のように ${@helper.toHtml(xxx)} といった @ を頭につけた記法となる:

<!-- HTML エスケープされないように th:utext を使用する -->
<div class="post-content" th:utext="${@helper.toHtml(post.markdown)}"></div>

後はこういう要件が出てくる度にこの Helper クラスにメソッドを追加していけばよい。

定数の参照はどうする

同じような悩みとして Thymeleaf 上から Kotlin の定数を参照したいというのがある。 一応 Thymeleaf 上で ${T(パッケージ.クラス名).static フィールド名} という記法で任意の static フィールドやメソッドを呼び出すことはできる。 ただ、例えば Kotlin で object を使用して static を表現したとする:

object Consts {
    val DAYS = arrayListOf('月', '火', '水', '木', '金', '土', '日')
}

上記の定数を Thymeleaf 側で参照するには ${T(com.kojion.etc.Consts).INSTANCE.DAYS} のようにしなければならない。 Kotlin の object が Java コード側から見るとシングルトンな INSTANCE という static フィールドを介してアクセスするようになっているので INSTANCE といちいち付けなければならず、あまり綺麗とは言えない。

この場合あえて Kotlin でなく Java で書いてみる:

public class Consts {
    public static final List<String> DAYS = Arrays.asList("月", "火", "水", "木", "金", "土", "日");
}

これで ${T(com.kojion.etc.Consts).DAYS} とアクセスできるので少しシンプルになった。 Thymeleaf 側からは Kotlin でなく Java として見なければならないのが少し辛いところだ。

パッケージ名を書くのも気になる場合は、先程のヘルパークラスと同様にコンポーネントとして登録して定数定義するのがいいのかもしれない:

@Component
object Consts {
    val DAYS = arrayListOf("月", "火", "水", "木", "金", "土", "日")
}

これで Thymeleaf 側から ${@consts.DAYS} でアクセスできるようになった。 object で定義しているので Kotlin 側から定数としてアクセスしたい場合も自然だ。

手軽に入れ物を作る場合便利

Kotlin にはデータクラスという JavaBeans のようにデータを入れることに特化したクラスを簡単に作成するための仕組みがある。 詳しくは公式リファレンスを参照すればよいが、簡単に書くと以下を自動で用意してくれる:

  • equals() / hashCode()
  • "User(name=John, age=42)" 形式の toString()
  • 宣言した順番でプロパティに対応する componentN() 関数
  • copy() 関数

尚、上記の恩恵を受けられるのはプライマリコンストラクタに指定したプロパティのみということに注意が必要である。 クラスの本文に書いたフィールドに関しては一切データクラスの影響を受けない。 以下それを検証する。

以下のような Kotlin というデータクラスが定義されているものとする:

// a, b, c, d の 4 つのプロパティを受け取るプライマリコンストラクタ
data class Kotlin(var a: Int, var b: Int = 0, val c: Int, val d: Int = 0) {
    var e: Int = 0  // ただのフィールド (再代入可)
    val f: Int = 0  // ただのフィールド (再代入不可)
}

この Kotlin クラスを Java 側から使ってみる:

public class Java {
    public static void main(String...args) {
        final Kotlin kotlin = new Kotlin(0, 0, 0, 0);
        kotlin.setA(1);
        kotlin.setB(1);
        kotlin.setE(1);

        // プライマリコンストラクタに指定してあるフィールドのみ toString() の対象になる
        System.out.println(kotlin);  // Kotlin(a=1, b=1, c=0, d=0)

        // プライマリコンストラクタに指定してあるフィールドのインスタンスの equals() がすべて true ならば true
        final Kotlin kotlin2 = new Kotlin(1, 1, 0, 0);
        kotlin2.setE(2);  // 関係ない値を違う値にする
        System.out.println(kotlin2);  // Kotlin(a=1, b=1, c=0, d=0)
        System.out.println(kotlin.equals(kotlin2));

        kotlin2.setA(2);  // 関係ある値を違う値にしてみる
        System.out.println(kotlin.equals(kotlin2));  // false

        // コピーしても関係ない値はコピーされない
        System.out.println(kotlin2.getE());  // 先ほど変更したので 2
        final Kotlin kotlin3 = kotlin2.copy(3, 3, 3, 3);  // Kotlin で呼ぶと任意のプロパティのみ変更可
        System.out.println(kotlin3.getE());  // 2 ではなく初期値の 0
    }
}

デフォルトコンストラクタの作成条件

前述の Kotlin クラスでは引数なしのコンストラクタ、いわゆるデフォルトコンストラクタが作成されない。 データクラスにおけるデフォルトコンストラクタの作成条件はプライマリコンストラクタのすべてのプロパティに初期値が存在することとなっている。 つまり前述の Kotlin クラスを以下のように書き換える:

// a, b, c, d すべてに初期値を設定
data class Kotlin(var a: Int = 0, var b: Int = 0, val c: Int = 0, val d: Int = 0) {
    var e: Int = 0
    val f: Int = 0
}

するとコンストラクタがデフォルトコンストラクタとプライマリコンストラクタの 2 種に増えている:

public class Java {
    public static void main(String...args) {
        final Kotlin kotlin = new Kotlin();  // デフォルトコンストラクタを呼ぶ
        final Kotlin kotlin2 = new Kotlin(1, 2, 3, 4);  // 従来のすべての引数ありコンストラクタも作成される
    }
}

尚、以下のようにデフォルトコンストラクタを明示的に宣言しても良い:

data class Kotlin(var a: Int, var b: Int = 0, val c: Int, val d: Int = 0) {
    constructor(): this(0, 0, 0, 0)  // 明示的なデフォルトコンストラクタ
    var e: Int = 0
    val f: Int = 0
}

JPA Entity ではデフォルトコンストラクタの定義が必要

ここからは Spring Boot での JPA の話である。 例えば以下のようにデータクラスで Entity を定義する:

@Entity
@Table(name = "posts")
data class Post(
        @Id @GeneratedValue
        var id: Int,
        var date: String,
        var name: String,
        var body: String,
        var enabled: Boolean,
        var created: Date,
        var modified: Date
)

これを PostRepository から DB アクセスを行うと以下のエラーが表示される:

org.hibernate.InstantiationException: No default constructor for entity:  : com.kojion.entity.Post

先ほどの教訓から解決法は明らかだ。 以下のようにすべてデフォルト値を指定してやれば良い:

@Entity
@Table(name = "posts")
data class Post(
        @Id @GeneratedValue
        var id: Int = 0,
        var date: String = "",
        var name: String = "",
        var body: String = "",
        var enabled: Boolean = false,
        var created: Date = Date(),
        var modified: Date = Date()
)

toString() が循環呼び出しされてしまう場合データクラスの対象外にする

例えば以下のように相互にアソシエーションを張っている場合に起きる:

// Post は複数の Tag を持つ
@Entity
@Table(name = "posts")
data class Post(
        @Id @GeneratedValue
        var id: Int = 0,
        @ManyToMany
        @JoinTable(name = "posts_tags", joinColumns = [JoinColumn(name="post_id")], inverseJoinColumns = [JoinColumn(name="tag_id")])
        var tags: List<Tag> = arrayListOf()
)

// Tag も複数の Post を持つ
@Entity
@Table(name = "tags")
data class Tag(
        @Id
        @GeneratedValue
        var id: Int = 0,
        @ManyToMany(mappedBy = "tags")
        var posts: List<Post> = arrayListOf()
)

この例だと PostTag が中間テーブル posts_tags を通して多対多のアソシエーションが張られている。 ここで同じように PostRepository から取得した Entity を出力しようとすると java.lang.StackOverflowError: null とクラッシュする。 PosttoString() しようとしてフィールドの List<Tag> に対しても toString() を試み、更に Tag にも子の List<Post> があり……というわけである。

この例の場合 Post が何の Tag を持つかは見たいが Tag が何の Post を持っているかはそこまで見たくない (必要ならば別途取ってくれば良い)。 そこで Tag 側の @ManyToMany 定義されているプロパティをプライマリコンストラクタの範囲から出すことで TagtoString() しようとした時に子の List<Post> を見に行かなくなる:

@Entity
@Table(name = "tags")
data class Tag(
        @Id
        @GeneratedValue
        var id: Int = 0
) {
    @ManyToMany(mappedBy = "tags")
    var posts: List<Post> = arrayListOf()
}

Flyway とは

DB の状態をバージョン管理する為のツールで Spring Boot で簡単に使用することができる。 IntelliJ IDEA で新規プロジェクトを作成する時に依存関係に Flyway を含めると以下が build.gradle に追加される:

dependencies {
    compile('org.flywaydb:flyway-core')
}

Spring Boot の場合 application.yaml に DB 設定を行うことが必須だ。 SQLite の場合以下のように定義しておく:

spring:
  datasource:
    url: jdbc:sqlite:./db.sqlite3
    driverClassName: org.sqlite.JDBC

Flyway のマイグレーションファイルは (クラスパス)/db/migration 以下に V(バージョン番号)__(説明).sql の形式で置く。 バージョン番号は公式的には単調増加自然数 (1, 2, ...) のようだが V1_0_2__(説明).sql のようにするといわゆるマイナーバージョンとリファクタリング番号を記録する (v1.0.2 的な) ことができるようだ。

Spring Boot の場合アプリケーションを実行すると (クラスパス)/db/migration 以下のマイグレーションが自動で走り、今までのマイグレーションの状態は DB の中に flyway_schema_history テーブルが作成されて管理される。 この flyway_schema_history テーブルの中を見てみるとどこまでマイグレーションが適用されているか簡単に確認できる。

Flyway の詳しい解説は Flyway使い方メモが分かりやすかった。 SQL ファイルだけでなく Java コードでマイグレーションを記述することができるらしい。 以下、上記ページで解説されていなかった「既存 DB に対するマイグレーション定義」に関して書く。

既存の DB を元にマイグレーションを行う場合

Flyway のマイグレーションはデフォルトでは DB 設計を一から行って新規で作成するアプリケーションの場合が想定されている。 つまり初めは空のデータベースが用意されており V1__Initial.sql などというファイルを用意し初期テーブルを定義し、その後 V2__Add_delete_flag などといった感じで初期テーブルに変更を加えていくイメージだ。

だが「既にテーブルが定義されている DB に対しマイグレーションを行った場合」 (まだ flyway_schema_history が定義されておらず初期投入扱い) は以下の様にエラーが表示される:

Caused by: org.flywaydb.core.api.FlywayException: Found non-empty schema(s) "main" without schema history table! Use baseline() or set baselineOnMigrate to true to initialize the schema history table.

これを回避するためにベースライン (初期状態のバージョン番号) を Flyway に教える必要がある。 application.yaml に以下を定義する:

spring:
  flyway:
    baseline-on-migrate: true
    baseline-version: 1
    baseline-description: Initial

spring.flyway.baseline-on-migratetrue の場合既存 DB にマイグレーションを行った場合に spring.flyway.baseline-version まで適用済みとみなしてくれる。 つまり前述の例の場合 V1__Initial.sql は実行されずに V2__Add_delete_flag のみが適用される。 spring.flyway.baseline-description に定義した文字列はマイグレーション実行後に flyway_schema_history.description のベースラインまで適用されたことを示す文字列として挿入される。 初期値 (未定義の場合) は << Flyway Baseline >> のようなのでこのままでも問題ないと思われる。

このあたりの設定の解説は Flyway 公式に書いてある (英語)。

最初に注意として JDK は今のところ必ず 1.8 (Java 8) を使用すること。 JDK 9.0 を使用してしまうと以下の手順の中で原因不明のエラーが表示されてしまう。 Kotlin 側のプラグインもまだ 1.8 までしか用意されていない。

まず PostgreSQL の例

Spring Boot やその他 Java EE プロジェクトでデータベースを扱う際は JPA を使用することが多いと思うが、まずここでは IntelliJ IDEA を使用し Spring Boot で PostgreSQL を使用できる環境を作る手順を示す。 新規プロジェクトの Spring Initializr の依存関係で以下を選択する:

  • Web (Spring MVC)
  • Thymeleaf
  • JPA
  • PostgreSQL

この状態でアプリケーションクラスを実行すると以下のエラーとなる:

Failed to auto-configure a DataSource: 'spring.datasource.url' is not specified and no embedded datasource could be auto-configured.

エラーメッセージの通り application.yaml に以下を定義する:

spring:
  datasource:
    url: jdbc:postgresql://localhost/postgres
    driverClassName: org.postgresql.Driver
    username: postgres
    password: postgres

これで実行するとまたエラーとなる:

java.lang.reflect.InvocationTargetException: null
...(中略)...
Caused by: java.sql.SQLFeatureNotSupportedException: org.postgresql.jdbc.PgConnection.createClob() メソッドはまだ実装されていません。

これでは何だか分からないので Stack Overflow に聞いてみると application.yaml に以下の設定を追加すれば良いようだ:

spring:
  jpa:
    properties:
      hibernate:
        temp:
          use_jdbc_metadata_defaults: false
        dialect: org.hibernate.dialect.PostgreSQLDialect

この状態で再度アプリケーションクラスを実行し、先ほどのエラーが表示されずに正しく Tomcat が起動する事を確認する。

SQLite の場合

SQLite の場合 PostgreSQLDialect に値する SQLiteDialect といった実装が最初から用意されていない。 ただ、これに関しては既に作成して Maven リポジトリに上げている方がいらっしゃるのでありがたく使用させていただくことにする。 それと SQLite の JDBC ドライバも必要なので build.gradle に以下を追加する:

dependencies {
    compile('com.enigmabridge:hibernate4-sqlite-dialect:0.1.2')
    runtime('org.xerial:sqlite-jdbc')
}

これに従い application.yaml を以下に書き換える:

spring:
  datasource:
    # SQLite のファイル位置を指定
    url: jdbc:sqlite:./db.sqlite3
    driverClassName: org.sqlite.JDBC
  jpa:
    properties:
      hibernate:
        temp:
          use_jdbc_metadata_defaults: false
        dialect: com.enigmabridge.hibernate.dialect.SQLiteDialect

アプリケーションクラスを実行し、正しく Tomcat が立ち上がることを確認する。

簡単にテストしてみる

JPA の詳細に関しては検索すればいくらでも出てくるのでここには記載しない。 まず該当するテーブルが SQLite 内にあるものとして以下のような感じで Entity クラスを用意する:

@Entity
@Table(name = "post")
data class Post(
        @Id @GeneratedValue(strategy = GenerationType.AUTO) var id: Int = 0,
        var date: String = "",
        var name: String = "",
        var text: String = "",
        var enabled: Boolean = false,
        var created: Date = Date(),
        var modified: Date = Date()
)

Kotlin だと data classEntity が用意できるのでとても便利だ。 toString() を実装しなくてもいい感じにクラス内のデータを出力してくれる。 var にして意味のない初期値を与えなければならないところがちょっと格好悪いが仕方がないところだろうか。

対応する PostRepository クラスを以下のように定義する:

interface PostRepository : JpaRepository<Post, String> {
    fun findById(id: Int): Post
}

定義したリポジトリを使用してコントローラから実行してみる (本来は Service から実行するのが筋だがここでは例のため簡単にする):

@Controller
class SampleController {

    @Autowired
    lateinit var postRepository: PostRepository

    @GetMapping("/")
    fun sample(model: Model): String {
        val post = postRepository.findById(1)  // ID をキーにして 1 件取得
        System.out.println(post)  // Post の中身が出力される
        return "sample"
    }
}

特に意味はないがテンプレートが必要なので /resources/templates/sample.html として以下を用意:

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Sample</title>
</head>
<body>
<p>Hello World!!</p>
</body>
</html>

実行して http://localhost:8080 にアクセスし、標準出力に正しく DB 取得結果が表示されることを確認する。

Spring Web の例は前回行ったので、今回は Spring Batch での Hello World を実施する。

要件とやりたいこと

  • IntelliJ IDEA + Kotlin + Spring Boot 2.0.0 + Gradle
  • 複数の Job を作成しコマンドライン引数で実行を分ける
  • ユニットテストでの実行を行えるようにする

初期設定

IntelliJ IDEA の新規プロジェクトから Spring Initializr を選択。 Gradle Project で言語を Kotlin にしパッケージングは jar を選択。

依存関係のところでは勿論 Batch を入れるのだが Spring Batch はデータベースが使用できるようになっていないと実行できない。 その為対応する JDBC ドライバが必要なのでこれも導入する。 ここでは PostgreSQL がローカルに既にインストールされているものとする。 生成されたプロジェクトの build.gradle の依存関係は以下のようになる:

dependencies {
    compile('org.springframework.boot:spring-boot-starter-batch')
    compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    compile("org.jetbrains.kotlin:kotlin-reflect")
    runtime('org.postgresql:postgresql')
    testCompile('org.springframework.boot:spring-boot-starter-test')
    testCompile('org.springframework.batch:spring-batch-test')
}

この時点で Application クラスを実行すると以下のようにエラーが出力される:

Failed to auto-configure a DataSource: 'spring.datasource.url' is not specified and no embedded datasource could be auto-configured.

DB の URL 設定がなされていないというわけで、プロパティファイルより YAML で書いたほうが便宜がいいということですでにある application.properties を削除し application.yaml として以下を定義する:

spring:
  datasource:
  url: jdbc:postgresql://localhost/postgres
  username: postgres
  password: postgres
  driverClassName: org.postgresql.Driver

この状態で同様に実行し、まだバッチの実装をしていないので何も起きないがとりあえず上記のエラーが出ずに正常にアプリケーションの実行が終了することを確認する。

Tasklet の実装

ネットの情報を見てみると CommandLineRunner で実装する方法と Tasklet 若しくは Reader, Processor, Writer で逐次処理をする方法があるようだが、ここでは Spring Batch 公式の Quick Start に記載してある通り Tasklet を使用する。 以下のような BatchConfiguration クラスを定義する:

@Configuration
@EnableBatchProcessing
class BatchConfiguration(val jobBuilderFactory: JobBuilderFactory, val stepBuilderFactory: StepBuilderFactory) {

    @Component
    class Tasklet1 : Tasklet {
        override fun execute(contribution: StepContribution?, chunkContext: ChunkContext?): RepeatStatus {
            System.out.println("Hello World!")
            return RepeatStatus.FINISHED
        }
    }

    @Component
    class Tasklet2 : Tasklet {
        override fun execute(contribution: StepContribution?, chunkContext: ChunkContext?): RepeatStatus {
            System.out.println("Hello World2!")
            return RepeatStatus.FINISHED
        }
    }

    @Bean
    fun step1(): Step? = stepBuilderFactory.get("step1")?.tasklet(Tasklet1())?.build()

    @Bean
    fun step2(): Step? = stepBuilderFactory.get("step2")?.tasklet(Tasklet2())?.build()

    @Bean
    fun job1(): Job? = jobBuilderFactory.get("job1")?.start(step1())?.build()

    @Bean
    fun job2(): Job? = jobBuilderFactory.get("job2")?.start(step2())?.build()
}

従来の Spring だと、この場合の JobBuilderFactoryStepBuilderFactory を DI する為に @Autowired アノテーションを付けていたようだが、最近のバージョンだと付けなくても注入してくれるようだ。 勿論 @Autowired を付けても良い。

この例だと job1()job2() という 2 つのジョブが定義されていることになる。 ジョブは更にステップという単位に分割され順々に実行することができるようだが、ここでは 1 ジョブ 1 ステップ構成ということでそれぞれ step1()step2() を定義している。 また Tasklet の定義はラムダ式でも良いのだが、後述するが Tasklet 単位でのユニットテストを行いたいのであえてクラスとして定義している。

この状態で実行してみると以下のエラーが出る:

java.lang.IllegalStateException: Failed to execute CommandLineRunner
...(中略)...
Caused by: org.postgresql.util.PSQLException: ERROR: relation "batch_job_instance" does not exist

この batch_job_instance というテーブルは何なのだろうか。 調べてみると、どうも Spring Batch がジョブの実行状態を管理するために内部的に生成するメタデータらしい。 このメタデータのテーブルがまだ DB に無いため does not exist と言われてしまうわけだ。

このメタデータを生成したい場合は application.yaml に以下のように書けば良い:

spring:
  batch:
    initialize-schema: always

これで先ほどのように実行すれば問題なく動作するのだが、このメタデータで管理されているのが逆に煩わしく感じる。 多くのプロジェクトの場合ジョブの管理など不要で、実行したい時に実行できればそれでいいはずだ。 以下、このメタデータを使わなくてもバッチを実行できるようにする。

メタデータを使用しないでいい方法

これに関しては Spring Batchのメタデータテーブルを作らせない/使わせないが大変参考になった。 記事を参考に同様の実装を Kotlin で行う。 以下のような MyBatchConfigurer を定義する:

@Component
class MyBatchConfigurer : BatchConfigurer {

    private val transactionManager = ResourcelessTransactionManager()

    private val mapJobRepositoryFactoryBean = MapJobRepositoryFactoryBean(transactionManager).also { it.afterPropertiesSet() }

    private val jobRepository = mapJobRepositoryFactoryBean.`object`!!

    private val jobExplorer = MapJobExplorerFactoryBean(mapJobRepositoryFactoryBean).also { it.afterPropertiesSet() }.`object`!!

    private val jobLauncher = SimpleJobLauncher().also {
        it.setJobRepository(jobRepository)
        it.afterPropertiesSet()
    }

    override fun getJobRepository(): JobRepository = jobRepository

    override fun getJobLauncher(): JobLauncher = jobLauncher

    override fun getJobExplorer(): JobExplorer = jobExplorer

    override fun getTransactionManager(): PlatformTransactionManager = transactionManager
}

この状態で再度実行すると Tasklet1Tasklet2 の実装が呼び出され Hello World! と Hello World2! が出力される。

Tasklet のユニットテストを行う

以下のように @Autowired を使用して注入した Tasklet インスタンスに対して普通に実行してみる:

@RunWith(SpringRunner::class)
@SpringBootTest
class TestApplicationTests() {

    @Autowired
    lateinit var tasklet1: BatchConfiguration.Tasklet1

    @Autowired
    lateinit var tasklet2: BatchConfiguration.Tasklet2

    @Test
    fun tasklet1() {
        tasklet1.execute(null, null)
    }

    @Test
    fun tasklet2() {
        tasklet2.execute(null, null)
    }
}

こうすると以下のように 2 重に実行されてしまう:

Hello World!
Hello World2!
Hello World!
Hello World2!

Spring Boot Batch のデフォルトの挙動としてアプリケーションの main() が実行された時点ですべてのジョブを実行するので、その実行の後にこのテストケースでの各 Tasklet が実行されてしまう。 やりたいのは各 Tasklet の実行だけであり、アプリケーション起動時の全ジョブの実行は不要だ。 この実行を無効化するには application.yaml に以下のように書く:

spring:
  batch:
    job:
      enabled: false  # main() での全ジョブ実行を行わない

再度ユニットテストを実行し Hello World! が 2 重に表示されないことを確認する。

JAR から実行するジョブを分けたい

このアプリを JAR ファイルにする。 IntelliJ IDEA のサイドメニューの Gradle から Tasks -> build -> bootJar を選択すると JAR ファイルがビルドされプロジェクトの build/libs 下に置かれる。 これをコマンドラインから job1() だけ実行したい場合は以下のように呼び出す:

java -jar (JAR ファイルパス) --spring.batch.job.names=job1 --spring.batch.job.enabled=true

コマンドライン引数で application.yaml の設定を一時的に上書きすることができるので、もし先ほどの設定で spring.batch.job.enabled=false が指定されている場合は JAR ファイルを実行してもバッチが実行されないので spring.batch.job.enabled=true を明示的に渡すことで実行できる。

Git for Windows で公開鍵必須になった?

今日新しい PC で Android Studio をセットアップしリモートの Git リポジトリに SSH 接続で git clone しようとしたら以下のエラーが表示された:

Could not read from remote repository.

これだけでは何だか分からない。 Android Studio 的には PATH に通っている git コマンドを発行しているだけなので Git for Windows 側が同様に失敗しているわけだ。 Windows PowerShell 上で同様に git clone してもエラーが表示されるが、今度はもう少し具体的なエラーが出た。

Permission denied (publickey)

これは何なのだろうか、ということで検索してみたら、どうも公開鍵が無いので接続できないということらしい。

公開鍵を生成しサーバに送信

Windows のユーザホームディレクトリは /Users/(ユーザ名) であり、その下の .ssh フォルダに移動する。 そこで公開鍵を生成する:

ssh-keygen -t rsa

出てくる質問は全部そのまま Enter でよい。 秘密鍵 (id_rsa) と公開鍵 (id_rsa.pub) が生成されるので、公開鍵の方を SSH 接続したいサーバに転送する:

scp id_rsa.pub (サーバ URL):~

最後にサーバ側 .ssh/authorized_keys に追記する形で公開鍵の登録完了する:

ssh (サーバ URL)
cat id_rsa.pub >> .ssh/authorized_keys

試しに ssh コマンドで接続し、パスワード認証無しで接続できることを確認する。

全 Kotlin 化されたコジごみカレンダー v3.0 公開に書いた通り 1 年半前にコジごみカレンダーを Java から Kotlin に全面刷新したのだが、コジ時計の方はずっと Android Studio でなく Eclipse ベースのコードのままで少し変更しようにもかなりの労力を傾けて修正作業をしなければならず、どうしても手が進まない状況になってしまっていた。 そこで今回コジ時計の方も少しずつ古いコードを見ながら Kotlin で移植するという作業を進めており、今日やっとアプリ内に表示している広告の承認が下りたので Google Play に公開することができた。

1 年半前と違って Kotlin を業務で使用するのにも慣れている状態になっているので、古い Java コードを移植すると機能によっては半分以下のステップ数になった。 Kotlin の記述力はとにかく強力で、もう Java に戻りたいとは全く思わないほどだ。

また、デザインのトレンドもマテリアルデザイン (フラットデザイン) に完全に移っているので、古いグラデーションを多用するようなデザインは排除し、マテリアルデザインを意識した「単色基調でのっぺりとしたデザイン」に調整した。

フェードインやフェードアウト

点滅の前に一番よくあるアニメーションであるフェードインやフェードアウトについて言及しておく。 Android 2.x 系の時は Animation クラスを使用した古いやり方をしていたと思うが Android 4.x 以上では ObjectAnimator を使う:

// ビューの alpha 値を 1 から 0 に変化させるアニメーション
ObjectAnimator.ofFloat(view, "alpha", 1f, 0f).apply {
    addListener(object : Animator.AnimatorListener {
        override fun onAnimationEnd(animation: Animator?) {
            view.visibility = View.GONE  // アニメーション終了時にビュー自体を非表示にする
        }
        override fun onAnimationCancel(animation: Animator?) = Unit
        override fun onAnimationStart(animation: Animator?) = Unit
        override fun onAnimationRepeat(animation: Animator?) = Unit
    })
    duration = 1000  // 1,000 ミリ秒で実行
    start()
}

この ObjectAnimator.ofFloat() を使うと任意の時間内に任意のプロパティを滑らかに変更させることができるので、ほとんどの場面はこれでいけるはずだ。 この例ではフェードアウトさせるアニメーションなのだが、フェードアウトさせた後にビュー自体の存在判定を消さないと当たり判定として残り続けてしまう (alpha = 0 では透明度が 0 になっているだけでタップした際のイベントは走ってしまう)。 その為アニメーション終了時に visibility に対し GONE をセットしている。 これ自体は Android 2.x 時代からの使い古された手法だ。

点滅

ビューを点滅 (blink) させるというのは marquee と共に古いホームページの技法のように思えるが、今回実装する機会があったのでメモ。 数百ミリ秒ごとに visibility を切り替えてもいいのだが、それだとチカチカとした感じの古いタイプの点滅になってしまう。 フェードイン・フェードアウトを繰り返すタイプの点滅にしたい。

ObjectAnimator に逆方向にリピートする為の設定があったので、これを使えば短いコード量で実現できた:

ObjectAnimator.ofFloat(view, "alpha", 1.0f, 0.0f).apply {
    repeatCount = ObjectAnimator.INFINITE  // 無限に繰り返す
    repeatMode = ObjectAnimator.REVERSE  // 逆方向に繰り返す
    duration = 500
    start()
}

repeatModeObjectAnimator.RESTART という定数も入れることができるが、こちらだとただ単に 1.0 から 0.0 へのアニメーションを繰り返すような動きになる。 ObjectAnimator.REVERSE にすると alpha に対し 1.0 から 0.0 へのアニメーションが終わった後 0.0 から 1.0 へ再度アニメーション、その後そのパターンが永遠に繰り返されることになる。

Ubuntu 16.04 LTS を想定。 例えば hoge.kojion.com と fuga.kojion.com にそれぞれアプリをデプロイする場合を考える。 昨日の記事までの Tomcat 及び Apache 連携の設定は既に済んでいるものとする。

Tomcat 側の設定

sudo vi /etc/tomcat8/server.xml

以下のように <Engine> 内に必要な分だけの <Host> を用意する:

<Engine>
    ...
    <Host name="hoge.kojion.com"  appBase="hogeapps"
        unpackWARs="true" autoDeploy="true">
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
            prefix="hoge_access_log" suffix=".log"
            pattern="%h %l %u %t &quot;%r&quot; %s %b" />
    </Host>
    <Host name="fuga.kojion.com"  appBase="fugaapps"
        unpackWARs="true" autoDeploy="true">
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
            prefix="fuga_access_log" suffix=".log"
            pattern="%h %l %u %t &quot;%r&quot; %s %b" />
    </Host>
</Engine>

上記の設定でログが /var/log/tomcat8 に指定した prefix, suffix の形式で吐かれることになる。 この後 hogeapps ディレクトリと fugaapps ディレクトリを用意する (以下 fugaapps の操作は hogeapps と同様とする):

sudo cd /var/lib/tomcat8
sudo mkdir hogeapps
sudo chmod 777 hogeapps
sudo chown tomcat8.tomcat8 hogeapps

上記で作成した appBase のディレクトリにはこの後置く war ファイルが展開されて置かれる。 次に各ホストに対する設定ファイルを置く:

sudo cd /var/lib/tomcat8/conf/Catalina
sudo mkdir hoge.kojion.com
sudo vi hoge.kojion.com/ROOT.xml

以下に指定する docBase のディレクトリは任意の場所で良い (一般的に /home が置きやすいだろうということでそうしている):

<Context docBase="/home/user/hoge/ROOT.war"/>

後はこの docBase に指定したディレクトリに war ファイルを置けば、対象 URL に初回アクセスした際に appBase に war ファイルを展開してくれる。 ROOT というコンテキスト名にするとルートディレクトリへのアクセスで対象アプリが駆動する。 例えば http://hoge.kojion.com/hage で hage アプリが動作してほしい場合は war ファイル名を変更すれば良い:

sudo vi hoge.kojion.com/hage.xml
<Context docBase="/home/user/hoge/hage.war"/>

Apache 側の設定

sudo vi /etc/apache2/sites-available/hoge.conf

バーチャルホスト毎に設定ファイルを用意すれば良い:

<VirtualHost *:80>
    ServerName hoge.kojion.com

    ...

    <Location />
            ProxyPass ajp://localhost:8009/
            Order allow,deny
            Allow from all
    </Location>
</VirtualHost>

最後に sudo service tomcat8 restartsudo service apache2 restart し、試しに war ファイルを置いて http://hoge.kojion.com にアクセスして正しくアプリが稼働するのを確認する。

パッケージングを War にした場合

昨日の記事である Spring Boot を Kotlin で Hello Worldパッケージングを Jar でなく War でプロジェクトを新規作成した状態とする。 build.gradle の記述が少し変わるのだが、具体的には以下の部分だ:

apply plugin: 'war'

configurations {
    providedRuntime
}

dependencies {
    // ここをコメントアウトして Application を実行すると組み込みの Tomcat で実行できる
    providedRuntime('org.springframework.boot:spring-boot-starter-tomcat')
}

上記に書いた通り providedRuntime('org.springframework.boot:spring-boot-starter-tomcat') をコメントアウトすれば普通に Jar でビルドし組み込みの Tomcat で実行できる。 開発時はコメントアウトしておくのがいいだろう。 尚 War でプロジェクト作成した場合 ServletInitializer というクラスが増えているのが分かると思うが、これが無いと Tomcat で war ファイルをデプロイしても実行できない。

war ファイル作成

上記の状態で普通に Gradle のビルドを行うと build/libs 直下に war ファイルが作成される。 尚ファイル名は (プロジェクト名)-(Gradle に指定している version).war となるようだ。

Ubuntu 側の用意

今回はローカルの仮想環境に Ubuntu Server 16.04.3 LTS をセットアップしてあるものとする。 後今回は便宜上 Oracle の Java ではなく OpenJDK とする。

sudo apt install apache2 openjdk-8-jdk tomcat8 tomcat8-admin

これで Tomcat がインストールできたので http://(仮想環境の IP):8080/ にアクセスし Tomcat の稼働を意味するスタートページが表示されることを確認する。 仮想環境の場合ネットワークの設定を「ブリッジアダプタ」などホスト PC から IP が見える状態にしておかないといけないので注意する。

tomcat8-admin をインストールしたので http://(仮想環境の IP):8080/manager にアクセスすると Tomcat 管理画面に遷移することができるが、最初の認証の為のユーザを設定しなければならない。

sudo vi /etc/tomcat8/tomcat-users.xml

以下を追記する。 ユーザ名とパスワードは適宜強固なものに変更する。

<tomcat-users>
    ...
    <user username="admin" password="admin" roles="manager-gui,admin-gui"/>
</tomcat-users>

Tomcat を再起動し設定を反映する:

sudo service tomcat8 restart

これで http://(仮想環境の IP):8080/manager にアクセスし設定したユーザ名とパスワードを入力し認証、正しく管理画面が表示されるのを確認する。 更に管理画面上から前述の手順で作成した war ファイルをデプロイし、正しくアプリケーションのページが表示されるのを確認する。

Apache との連携

これは UbuntuでTomcat Apache連携がとても参考になった。 ほぼ記事のオウム返しになって恐縮であるが、まず以下で Tomcat の接続設定を変更する:

sudo vi /etc/tomcat8/server.xml

8080 ポートを閉じて proxy_ajp 連携用の 8009 版ポートを開く:

<!-- コメントアウト
<Connector port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000"
           URIEncoding="UTF-8"
           redirectPort="8443" />
-->

<!-- コメントアウトを外す -->
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />

次に sudo vi /etc/apache2/sites-available/000-default.conf し以下追記する:

<Location />
    ProxyPass ajp://localhost:8009/
    Order allow,deny
    Allow from all
</Location>

最後に proxy_ajp を有効にしそれぞれ再起動:

sudo a2enmod proxy_ajp
sudo service tomcat8 restart
sudo service apache2 restart

http://(仮想環境の IP)/(コンテキスト名) にアクセスし、正しく Spring Boot アプリケーションが表示されるのを確認する。 /etc/apache2/conf-available の方に設定を書いて sudo a2enconf xxx する方法もあるが、個人的にはバーチャルホストで設定を分ける時に今回記載した手順のほうが便利だと思った。

Spring Boot は Pleiades 適用済の IntelliJ IDEA でビルドするものとする。

IntelliJ IDEA で「新規 -> プロジェクト」を選択。 プロジェクトの種類は Spring Initializr (typo ではない) を選択。 プロジェクト JDK は今のところ 1.8 で行ったほうがいいようだ。 まだリリースされたばかりの Java 9 だと問題が出ることが多かった。 グループ、成果物は適当に入力。 型として Gradle Project を選択 (Maven でやりたい人は Maven Project を選択)、言語は Kotlin を選択。 パッケージングはこの時点では Jar を選択しておく。 依存関係だがとりあえずは以下の 3 点を入れておけば良い:

  • Core -> DevTools
  • Web -> Web
  • Template Engines -> Thymeleaf

尚、この画面の上部で Spring Boot のバージョンを選択できるのだが 1.5.7 ではなく 2.0.0.BUILD-SNAPSHOT を選択しておくのが良い。 Spring のバージョンが 5.0.x となり Kotlin に対応している。

プロジェクトを作成すると何やら Gradle の設定画面が出るがそのまま「次へ」を選択。 Gradle の依存関係ライブラリの取得が始まるので終わるまで待つ。 終わったらサンプルページを表示するためのコントローラとテンプレートを用意する。 Kotlin ディレクトリのどこかのディレクトリに以下のように SampleController を用意する:

@Controller
class SampleController {
    
    @GetMapping("/")
    fun sample(model: Model): String = "sample"
}

そして resources/templates 下に以下のように sample.html を用意する:

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Sample</title>
</head>
<body>
    <p>Hello World!!</p>
</body>
</html>

この状態で TestApplication.kt を右クリックして実行すると組み込みの Tomcat が起動して http://localhost:8080/ にアクセスすると Hello World!! が表示される。 尚、この時点だとテンプレートファイルを書き換えても TestApplication.kt を実行し直さないとブラウザ上の HTML が書きかわらない。 ホットデプロイに関しては過去記事 Spring Boot + IntelliJ IDEA でホットデプロイを参考に設定してほしい。

IntelliJ で Spring Boot を動作させる場合にホットデプロイの設定でハマる事例が後を絶たないらしく、情報を調べていても Spring Boot のバージョンもまちまちでなかなかこれといった解決策に辿り着かない。 休日に試行錯誤していたがとても苦戦したので備忘。 尚ビルドシステムとしては Gradle を使用することとする。

ちなみに私は Spring Boot は初めてなので理解が怪しいところがあるかもしれない。

新規 Spring Boot プロジェクト

Spring は 10 年以上前から存在する歴史ある Java のフレームワークだが、昔は Spring ですべて賄うというよりは Struts と Spring を組み合わせて構築するといったパターンが多かったように思う。 その後途中あたりから Spring MVC という Web アプリケーションを作成する為のパッケージが出てきていわゆる「XML 地獄」から開放され、その後に更に Spring の各パッケージを適切に組み合わせて簡単に使えるようにしたオールインワンパッケージのようなものが Spring Boot ……だと思う。

IntelliJ IDEA Ultimate で新規 Spring Boot プロジェクトを作成する時は「新規プロジェクトを作成する」から Spring Initializr を選ぶ。 「Spring」の方ではない。 こちらを選択すると Spring Boot ではなく通常の Spring になってしまうと思われる。 また「Initializr」は typo ではなく公式にこのスペルで書かれている。

まず最初の知として現時点では Java 9 は避けるのが望ましい。 どうも Gradle や Spring のバージョンによって Java 9 だと正しく動かないらしく原因不明のエラーに悩まされた。 どうしても使いたいと言うのでない限り Java 8 にしておくのが無難だ。 モダンなコードを書きたいのであれば Kotlin を採用する方法もある。 私は未検証だが Web 上に使用例が幾つかあるし IDE 上でも Kotlin の選択肢が示されている。

依存関係を選ぶ箇所ではとりあえず最小構成として「Web」と「Thymeleaf」を選択しておく。 Thymeleaf (タイムリーフ) は Spring で推奨されているテンプレートエンジンらしい。 JSP を書かされるよりは遥かにいい。

ホットデプロイのハマり箇所

さて、ホットデプロイである。 これが出来ないとソースコードを書き換える度に組み込みの Tomcat を再起動しなければ変更が反映されず非効率な開発を強いられることになる。 下記に自分がハマったことを列挙する。

Spring Loaded でなく Spring Boot DevTools を使う

「Spring Boot ホットデプロイ」で情報を探していると「Spring Loaded」という単語がヒットする。 これは Spring Boot v1.2 以前の 2 年前まで有効だった情報だ。 今は Spring Boot DevTools の方を使用する。 導入は build.gradle に以下を追加すればよい:

dependencies {
    compile('org.springframework.boot:spring-boot-devtools')
}

IntelliJ IDEA 管理下でなければこれだけでも動くのかもしれないが IntelliJ だとまだ足りない。 以下に進む。

自動的にビルドする設定とレジストリ

How to Use Spring Boot Live Reload with IntelliJ を参照 (英語)。 IntelliJ で以下の 2 点を設定する:

  1. Settings -> Build, Execution, Deployment -> Compiler -> Make project automatically にチェックを入れる。これをしていないとソースコードを変更しても自動ビルドされない
  2. SHIFT + CTRL + A で出てくるウィンドウで Registry を検索し compiler.automake.allow.when.app.running にチェックを入れる。尚 CTRL + SHIFT + ALT + / でも Registry を選択できた。

さて、ここまで実行して gradlew.bat bootRun を実行 (もしくは IntelliJ IDEA 上で Gradle タスク実行) してみる。 やはりソースコードもテンプレートも変更が即反映されない。 そこで以下の設定を行う。

IntelliJ 用に自動ビルド時の出力先ディレクトリを調整

デフォルトで Spring Boot DevTools によって /build/classes/main//build/resources/main/ 以下の変更が監視され即反映されるのだが IntelliJ が自動ビルドした時のクラスファイルの保存先がこのディレクトリになっていない。 よって build.gradle に以下の記述を追加する:

apply plugin: 'idea'

idea {
    module {
        inheritOutputDirs = false
        outputDir = file("$buildDir/classes/main/")
    }
}

上記は Gradle のバージョンが 3.x.x の例なのだが Gradle のバージョンを 4.2.1 などに上げるとディレクトリ構造が異なってしまいうまく動かなくなってしまうので注意。 $buildDir/classes/main/ でなく $buildDir/classes/java/main/ などといったように /java サブディレクトリが増えている。 恐らく Kotlin 対応のためだろう。 この先またディレクトリが変更されたとしても /build/classes 以下の構造をよく見て同じように指定してやれば良い。

これでソースコードのホットデプロイはできるようになった。 ホットデプロイに成功した場合はコンソールのログに再デプロイのログが流れるので分かりやすい。

Thymeleaf テンプレートの反映に失敗する場合

特にこれがハマった。 application.propertiesspring.thymeleaf.cache=false を指定するという情報もあるが、そもそも DevTools ではこの設定がデフォルトになっているようで不要のようだ。

結論から言うとコンソールから gradlew.bat bootRun 実行 (または IntelliJ 上からの同様の操作) を行った場合はテンプレートファイルを変更しても全く反映されない。 @SpringBootApplication アノテーションが付与された main() を実行 (つまりクラスファイルを右クリックして実行など) すると同様にビルトインされた Tomcat サーバが起動するが、こちらだと何故かテンプレートファイルが即時反映される。 よく分からないがこういうもののようだ。

再度設定

今年 9 月に「かえうち」が届いた時に月配列 K を見据えて一通りの設定を行い Android タブレットで便利に利用していたわけだが、少し改良をしようと思い今日会社から持ち帰ってきて設定を行うことにした。 改めてかえうちの公式サイトを見てみると何やらいろいろとソフトが変わっている。 以前はかえうちの設定ファイルを Web アプリ上で行い、ファイルを吐き出してクライアントサイドのアプリ (かえうちライター) で書き込むという流れで、いろいろと罠があってよく調べないとハマるような感じだった。 しかし今日見てみると「かえうちカスタマイズ ソフトウェア版」というのができており、これを使うと設定のカスタマイズを行ってそのまま「書き込む」ボタンを押すと「カスタマイズモード」にしなくてもアプリが自動的にかえうちをカスタマイズモードにして書き込んでくれるという便利仕様になっていた。 これなら「設定→書き込み→試し打ち」という一連の流れがとてもスムーズにできる。 素晴らしい改良だと思った。

配列面切り替えと同時のマクロを IME 切り替えではなく入力モード切り替えにする

私は PC で月配列 K を使う時にキーボードのスペースキーの両隣にある変換キーと無変換キーをそれぞれ「入力モードかな」「入力モード英数」キーとして設定している。 Mac の JIS キーボードではスペースキーの隣に英数キーとかなキーがあるのだが、これを押下するとそれぞれ入力モードを「英数」「かな」に切り替えられるという挙動を踏襲している。 この使い心地を Android 端末でも実現できれば最高なのだが、残念ながら Android の場合入力モードを「英数」「かな」にするような独立したキーはない。 ただトグルであれば行う方法はある。 トグルだと間違えて 2 回押してしまった場合はかえうちの認識とシステムの IME 状態がズレてしまうのだが、それはもう仕方がない。

以前は IME の切り替え (例えば Google 日本語入力と Gboard のトグル) でこれを実現していた。 Android 端末の場合 Win + Space でこれを行うことができる。 ただ、これは想定通り動作はするのだが IME の切り替えに 1, 2 秒かかりその間に入力を始めてしまうと意図しない文字が入力されてしまい快適ではない。 あとから気づいたのだが 半角/全角 で Google 日本語入力のままで入力モードを英語と日本語に切り替えることができる。 なので変換キーと無変換キーを押下した際のマクロに 半角/全角 を含めることで対応できた。

ちなみに Android 端末の方に繋いでいる物理キーボードが英語配列であると認識されていると正しく 半角/全角が発行されず、英語キーボードの「`」が発行されてしまう。 Android 端末側の物理キーボード配列の定義はかえうちの方の設定ではなく「設定→詳細設定→言語と文字入力」の「物理キーボード」で Google 日本語入力をインストールしている場合は「Google 日本語入力」を押下するとキーボードレイアウトを選択することができる。 ここで英語配列のものではなく「日本語 109A 」などの日本語配列を選択する。

Android で月配列 K でイータイピングができるか

表題の通り Android 端末でイータイピングをすることができるかという話なのだが、実はローマ字や英語では普通にイータイピングをプレイすることができる。 これは Bluetooth キーボードなどを使用してもいいし、とても打ちにくいがイータイピングで表示されるキーボード上で打ってもいい。 うまくマッピングすれば月配列 K を使えるのではないか……と思い試してみたが駄目だった。 かえうちを使わずに普通に JIS かなでイータイピングのかなをプレイしてもうまくいかないのだから出来るわけがない。 濁点、半濁点や「む」などのキーを押下した時に発行されるキーコードが異なるのだろう。 このあたりがよく分からない。

やりたいこと

この Blog システムでは毎日データベースのダンプファイルを出力してバックアップを取っているのだが、あくまでサーバー内へのバックアップ出力なのである日突然サーバーがダウンして再起動できなくなってしまった場合サルベージできなくなってしまう。 バックアップファイルは外部ストレージに保存したいところだ。 今までは適当な期間をおいて Google Drive に手動でバックアップを移していたのだが、私は毎日 Blog に日記をつけているのでこれだとバックアップデータが十分に古い場合が出てくる。 毎日バックアップファイルを出力した時点で Google Drive に自動で転送したい。

PyDrive

Qiita に素晴らしい記事があったので大体の部分はこれでできた。 一部ハマった部分があったのでここに注釈として記しておく。

settings.yaml

公式サイトにも書いてあるが settings.yaml という設定ファイルを Python 実行ファイルと同じディレクトリに置く必要がある。 Google Developers Console から出力した client_secrets.json でも良いが、これだと認証情報が保存されない。毎回ブラウザベースでの認証が必要になってしまう。 具体的には YAML 内の以下の部分である:

save_credentials: True  # 認証情報を保存する
save_credentials_backend: file  # 何に保存するか. 今のところ 'file' しか指定できないらしい
save_credentials_file: credentials.json  # 認証情報保存ファイル名

上記の設定がされていることで初回アクセス時 (credentials.json がまだない時) はブラウザベースの認証画面がキックされ、認証が成功すると credentials.json として保存される。

また settings.yaml 内に OAuth の対象を絞り込むための設定があり、サンプルで以下のようになっている:

oauth_scope:
  - https://www.googleapis.com/auth/drive.file
  - https://www.googleapis.com/auth/drive.install

これだとファイルと作成のみの許可となっておりディレクトリなどの検索ができずにハマる (特にエラーなど出力されずヒットしないだけとなる)。 公開するアプリなどではない場合、特に権限を絞り込む必要がない場合は以下のように全権限にしておけば問題ない:

oauth_scope:
  - https://www.googleapis.com/auth/drive

ディレクトリに対し操作を行う場合必ず ID が必要

例えばローカルディレクトリ /path/to に対し操作を行う場合はシステムに対し一意となっているディレクトリパス /path/to の部分が分かっていれば良いのだが Google Drive の場合はそれだと駄目で必ずディレクトリに対する ID を指定する必要がある。 この ID はどうやって調べるのかというところだが、Web 版の Google Drive を使用している場合に対象となるディレクトリに移動して URL を見てみると https://drive.google.com/drive/folders/0B3GHspmhxAvDOXRCGTlSWjhrRWs のようになっている。 この 0B3GHspmhxAvDOXRCGTlSWjhrRWs がそのディレクトリの ID であり、これを使用して以下のように操作できる:

google_auth = GoogleAuth()
google_auth.CommandLineAuth()
drive = GoogleDrive(google_auth)

# 対象の ID のディレクトリ配下のファイル (ゴミ箱に入っていないもの) を全取得
file_list = drive.ListFile({'q': "'{}' in parents and trashed=false".format('0B3GHspmhxAvDOXRCGTlSWjhrRWs')}).GetList()

# Google Drive 上の対象 ID のディレクトリ内に hoge.txt を作成
file = drive.CreateFile({'title': 'hoge.txt', 'parents': [{'id': '0B3GHspmhxAvDOXRCGTlSWjhrRWs'}]})

この 'q': '(ID) in parents and trashed=false' というクエリが奇妙に映るが、これは Google Drive APIs (REST) の記法なので仕方ないところのようだ。

対象ディレクトリのファイルを全削除した上でローカルからコピーするサンプル

ということで Django のコマンドラインで動作するように以下のように実装してみた:

class Command(BaseCommand):
    help = 'Google Drive にバックアップファイルを転送する.'

    def add_arguments(self, parser):
        parser.add_argument('path', type=str, help='バックアップ元ファイルパス')
        parser.add_argument('parent_id', type=str, help='Google Drive 転送先ファイルパス')

    def handle(self, *args, **options):
        self.stdout.write(self.style.SUCCESS("Google Drive 転送処理を開始します."))

        # Google Drive 認証を行う
        google_auth = GoogleAuth()
        google_auth.CommandLineAuth()
        drive = GoogleDrive(google_auth)

        # まず旧ファイルを削除
        file_list = drive.ListFile({'q': "'{}' in parents and trashed=false".format(options['parent_id'])}).GetList()
        for file in file_list:
            file.Delete()
        self.stdout.write(self.style.SUCCESS("{} 内の旧ファイルを削除しました.".format(options['parent_id'])))

        # backup フォルダ配下のファイルを軒並み転送
        for directory_path, directory_names, file_names in os.walk(options['path']):
            for file_name in file_names:
                file = drive.CreateFile({'title': file_name, 'parents': [{'id': options['parent_id']}]})
                file.SetContentFile(os.path.join(options['path'], file_name))
                file.Upload()
                self.stdout.write(self.style.SUCCESS("{} を転送しました.".format(file_name)))

        self.stdout.write(self.style.SUCCESS("Google Drive 転送処理が完了しました."))

第一引数に転送元ローカルディレクトリを指定、第二引数に Google Drive の対象ディレクトリ ID を指定して以下のように実行できる:

manage.py sendtodrive backup/ 0B3GHFpmhx9vdUFFLVPE4EG9YNnM

Fall Creators Update で入ったフォント

Windows 10 の大型アップデートである Fall Creators Update が 10 月にあったわけだが、新しい日本語フォントである UD デジタル教科書体が入っていたのを忘れていた。 このフォントに関しては窓の杜に以下のように説明があった:

「UDデジタル教科書体」は、教育現場の要望に応えるため、ヒアリングや検証をもとに改良を重ねながら10年にわたり開発されてきた書体。筆運びの向きや点、ハライ、画数、筆順などは学習指導要領に準拠しつつも、太さの強弱を抑えてロービジョン(弱視)、ディスレクシア(読み書き障害)などにも配慮しているのが特徴。電子黒板やタブレット端末といったICT教育の現場で効果を発揮する可読性・視認性に優れるユニバーサルデザイン対応の書体となっている。

試そうと思っていたのを今日思い出したので早速 Full HD と 4K の液晶両方で見てみた。 Full HD でもまずまず綺麗に見えるのだが 4K 液晶で見ると確かにかなり綺麗だ。 ただ「教科書体」という名前からイメージする通り、いかにも教育用といった字体で macOS のポップなヒラギノ角ゴと比べるとちょっと好みが分かれるような気がした。 どちらかというと堅い文章を書く人に合っているかもしれない。 ということは私向きだろうか。

N, NP, NK の違い

  • UD デジタル教科書体 N-R
  • UD デジタル教科書体 NP-R
  • UD デジタル教科書体 NK-R

末尾の R はレギュラーということであり標準の太さという意味で分かるのだが N, NP, NK の違いが最初よく分からなかった。 調べてみるとどうも N は等幅フォント (但し半角英数は半幅) で NP は英数字がプロポーショナルになっており、NK は更にかなもプロポーショナルになっているフォントのようだ。 ということで普段指定する際は NK-R でいいだろう。

Blog をフォントに合わせてデザイン変更

気に入ったので、早速この Blog で指定しているフォントのプライマリをこの UD デジタル教科書体にしてみた。 合わせてフォントサイズも適切に変更してみた。 とても見やすくて満足だ。

今までは以下のように ASCII フォントを先頭に出して「英数字は Arial だが漢字とかなは sans-serif (多くは游ゴシック体やメイリオ)」という技法を使用していた:

body { font-family: Arial, sans-serif; }

今回も最初これでやってみたのだが、どうも UD デジタル教科書体のデザインと一般的な ASCII フォント (例えば Arial) の字体の雰囲気が合わない。 そこで今回思い切って全て UD デジタル教科書体に任せることにした。 UD デジタル教科書体の CSS の指定は以下のようにそのままの名前で指定するようだ (半角・全角に注意):

body { font-family: "UD デジタル 教科書体 NK-R", "Roboto", "Lucida Grande", Verdana, Arial, sans-serif; }

どうも半角英数字と全角英数字の見分けが付きにくいフォントのように思えるが、慣れかもしれない。 ともかく Windows でこれだけ綺麗なフォントで記述・閲覧できるのであれば、もうヒラギノ角ゴ目的で Mac を選ぶようなこともなくなるだろう。 勿論 Mac を選ぶ理由はフォントだけではない (技術者としては主に OS 自体が正当な UNIX であることに起因する) のだが、それは別の話である。

昔対応した問題だったのだが、最近再度同じようなアプリを作成した時に忘れていてハマったので備忘を残しておく。

Google Maps Android API v2 で長時間同じ地図画面で待ち受けるようなアプリを作成する場合、地図を長時間触っていると StackOverflowError で落ちる場合がある。 これが地図を拡大したりちょっとスクロールした際に突然落ちるのでプロダクションでこれが起きてしまうと「このアプリは地図をちょっと触っているだけで落ちる」という散々な評価をいただくことになるし、長時間触っていないと再現しないという関係上開発時に気づかないことが多い。 例えば以下のようなスタックトレースが吐かれる:

java.lang.StackOverflowError: at com.google.maps.api.android.lib6.gmm6.util.e.b(:com.google.android.gms.DynamiteModulesB@11746036:22)
at com.google.maps.api.android.lib6.gmm6.store.cache.s.a(:com.google.android.gms.DynamiteModulesB@11746036:8)
at com.google.maps.api.android.lib6.gmm6.store.n.a(:com.google.android.gms.DynamiteModulesB@11746036:49)
at com.google.maps.api.android.lib6.gmm6.indoor.o.a(:com.google.android.gms.DynamiteModulesB@11746036:80)
at com.google.maps.api.android.lib6.gmm6.indoor.o.a(:com.google.android.gms.DynamiteModulesB@11746036:70)
...

これは Google Maps Android API v2 の「インドア表示」で起こる。 ほとんどの要件ではインドア表示など不要だと思われるので、以下のように設定で OFF にすれば起きなくなるので地図を使用する画面では機械的に以下を入れてしまっていいのではないかと思う:

mapFragment.getMapAsync { it.isIndoorEnabled = false }  // インドア表示 OFF
mapFragment.getMapAsync { it.uiSettings.isTiltGesturesEnabled = false }  // チルト操作 (傾けて地図を 3D 表示) OFF

2 番目の「チルト操作 OFF」も可能であれば設定した方がいい。 確かこれも ON にしていると OutOfMemoryError で落ちることがあった気がした (確証とれず)。

ちなみに OutOfMemoryError で落ちる件に関しては Stack Overflow に書かれている通り AndroidManifest.xmllargeHeap = true を宣言するなり適切なタイミングで System.gc()GoogleMap.clear() を呼ぶ必要が出てくるだろう。

先日書いた通り 10 インチタブレットである MediaPad M3 Lite 10 のカバーを風呂蓋型のものから普通のシリコン型に変えた。 やはりこちらの方が電車内での取り扱い時に身軽で便利だ。 ただ、風呂蓋型のカバーは蓋を三角に折りたたんでスタンドとして使えるという利点があった。 そこでシリコン型のカバーであったとしても外付けのタブレット用のスタンドを使用して立てて使用することにした。

Anker コンパクトマルチアングルスタンドを購入。 このスタンドは何段階かの角度調整をすることができる。 シンプルな作りながら機能的に必要十分で全く問題ない。 むしろ風呂蓋型のカバーに対して明らかに勝っていると思った部分は横だけでなく縦向きでスタンドに立てて使うことができることだ。 これは将棋ウォーズやドラクエなどの縦画面固定のアプリをスタンドに立てて遊びたい時に重宝しそうだ。

自宅と会社で持ち運んで使ってもいいのだが、価格も安いので 2 台持ちしてそれぞれに常備してもいいだろう。 次の楽天でもう 1 台買い足すつもりだ。

妻が Amazon お急ぎ便で HUAWEI のスマートフォンである P10 lite を注文し、昨日の夕方に届いた。 妻は Android は使ったことがあるとはいえ長らく iOS だったということもあり「初期設定だけして欲しい」ということでその手伝いをしていた。 システムのフォントサイズを調整したり、ランチャーをドロワー型にしたり、スリープ時間を調整したり、Google 日本語入力をインストールして使いやすく設定したり、といった自分がよく行っていることを同様に行った。

P10 lite を触ってみた感想としては「事前の想像とほぼ違わなかった」といったところだ。 ミドルレンジモデルに関しては会社で何度か触っているのもあって同様なのだろうなと思っていたが、やはり想像通り所々もたつく場面はあるがまあ普通に使えるなといった印象だった。 大抵の人は今のハイエンドモデル (P10 や Mate 9) を触らずにこれだけ使っていればそう不満は感じないだろう。 実際 Antutu ベンチマークのスコアから言っても 2, 3 年前のハイエンドモデルと遜色なく、そう考えるとコストパフォーマンスは異常に良い。

一点だけ想定外だったのは端子が USB Type-C でなく未だに microUSB だったことだ。 自分としては USB Type-C の方が前後区別なく挿せるし端子の統一といった意味でも有利なので気になった。

風呂蓋型カバーが故障

今私は MediaPad M3 Lite 10 という 10 インチタブレットを便利に使用している。 そのタブレットでは AVIDET 製 (中国?) の iPad でよくある風呂蓋型のカバーを使用していたのだが、つい先日蓋を留める磁石を固定しているプラスチックの部分が割れて磁石が飛び出してしまい、しかも磁石をそのまま紛失してしまった。 2 カ月くらいしか保たなかったことになる。 1,000 円強しかしないカバーだったのでそこまで悲観はしていないのだが、これを機に風呂蓋型でなく普通のシリコン製の透明で覆うだけのタイプのものに変更しようと考えた。 10 インチタブレットは結構重量もあるのだが、それでも机の上に置いて使うような場面はそう多くない。 それに電車で手に持って使っていると、風呂蓋型だとどうしてもフタの部分が後ろに垂れ下がっている状態であまりスマートではない。

シリコン製のカバーだと更に安く 1,000 円以下で余裕で購入できてしまう。 メーカーはまた AVIDET 製とした。 よく分からないメーカーだが安いのはありがたい。

タブレットスタンド

机の上で使うシーンは多くはないとはいえ、それでもちょっと立てかけて使えると便利だ。 今はいろいろなメーカーからタブレット用のスタンドが出ているのでその中からチョイス。 安心の Anker 製のものを注文。 100 g 程度なので常にカバンに忍ばせておくのもアリだろうか。

届いたらまずは試しに使ってみて、使い勝手がよかったらもう 1 台買って会社に据え置くと便利そうだ。

タブレットは 10.1 インチか 8.4 インチか

今私は HUAWEI のタブレットである MediaPad M3 Lite 10 という 10 インチタブレットを使っているのだが、概ね満足であり感想は以前レビューで書いた通りなのだが最近もう少し不満点が出てきた。 私は同様に HUAWEI のハイエンド大型スマートフォンである Mate 9 を使用しているのだが、それより明らかに Wi-Fi の掴みが悪いのである。 私の自室は Wi-Fi ルータから結構距離が離れているのだが、そこだと Mate 9 はほぼ確実に電波を掴めるのに対し M3 Lite 10 の方は一旦スリープに入れて復帰した時など掴めないことが多く Wi-Fi の ON/OFF を繰り返しても結局接続できないことがあり諦めて Mate 9 のテザリングを入れる (Mate 9 側の LTE の残量が結構あるので) などしてしまっている。 多分これは 8.4 インチのタブレットである MediaPad M3 の方を使用すればこちらは廉価版という位置付けではないので Mate 9 と同様に Wi-Fi 電波を掴むのだろう (未検証)。 仕方がないのかもしれないが、こういう風に見えないが必須の部分で差を付けてくるところが少し残念に思った。 HUAWEI が 10.1 インチの方の MediaPad M3 にあたる上位版を出してくれればいいのだが、需要の関係上望みは薄いだろう。

私は以前 Nexus 7 を使用していたことがあり職場にもあるので今でも触ることがあるのだが、私の感覚で恐縮であるが明らかにタブレットとして常用するには苦痛を感じるほど画面が小さい。 今時のスクリーンが大型化したファブレットと比べると「一回り大型化したファブレット」といった印象しか受けず、これならスマートフォン 1 台のみでいいのではと思ってしまう。 そして、私は以前 Nexus 9 という 8.9 インチのタブレットを使用していたが、これは世の中の人からはいろいろと酷評されたようだが自分としてはサイズを含めて割と満足に使用していた。 何より 4:3 の画面比率が Kindle や PDF を読むのに都合が良かった。 ただ PDF を読むのには画面は大きければ大きいほど良い。 10.1 インチタブレットでも「少し文字が小さいかな」と思ってしまう場面があるほどなので 8.4 インチタブレットだと横画面にしてスクロールしないと厳しいだろう。

常々「大は小を兼ねる」と思っているので迷わず 10.1 インチにしたのだが、今後 HUAWEI が M3 と同様に 8.4 インチでのみハイエンド版を出すのであれば次 (M4) はそちらにしようと思った。 よくよく考えるとファブレットである Mate 9 で明らかに守備範囲外なのが PDF 閲覧、次点 Kindle であと動画鑑賞とゲーム、ブラウジングが続くぐらいだが別に 8.4 インチであったとしても少し快適でなくなるのが PDF 閲覧くらいで Kindle 以降は問題ないと思われる。

そもそもスクリーンサイズが一回り小さいと思っていた Nexus 9 (8.9 インチ) より 0.5 インチ下がるのがどうなのか、Nexus 7 より「1.4 インチしか大きくなっていない」のがどうなのかと思っているのだが、ここはスクリーンサイズを調べてみることにする。 こういうのはディスプレイの実寸法を計算するサイトがあるのでそこを利用する。

端末名 画面サイズ (inch) 解像度 横 (mm) 縦 (mm) ppi (参考)
Mate 9 5.9 1920x1080 130.56 73.44 373.374
Nexus 7 (2013) 7.0 1920x1200 151.68 94.8 323.451
MediaPad M3 8.4 2560x1600 181.76 113.6 359.39
Nexus 9 8.9 2048x1536 180.224 135.168 287.64
iPad 2017 9.7 2048x1536 196.608 147.456 263.918
MediaPad M3 Lite 10 10.1 1920x1200 216.96 135.6 224.174

こう見てみるとやはり Nexus 9 の画面比率 4:3 が効いていて縦 (短辺) の長さが 10 インチタブレットと同じくらいの長さだが横 (長辺) の長さが 8.4 インチタブレットと同じくらいという結果になった。 MediaPad M3 は Nexus 9 よりも縦が 2.15 cm くらい短い。 これがちょっと気になるところな気がするのだがどうなのだろうか……。 ただ Nexus 7 よりは 1.88 cm ほど長い。 Nexus 7 は MediaPad M3 Lite 10 の中間くらいでちょっと Nexus 7 寄り。 ……表にすれば分かりやすいかと思ったが却って迷う結果になってしまった。 やはり使ってみないと分からない。

MediaPad M4 がどうなるのか分からないが、少しサイズが大きくなってくれればありがたいと思うばかりだ。 逆にサイズが小さくなってしまった場合は更に困ってしまうことになるが……。

ファブレットやタブレット 1 台持ちよりスマートフォン + タブレット

以前私は「タブレットとスマートフォンを別々に持つよりもファブレットを 1 台だけ持って使ったほうが便利なのでは」と思って Mate 9 のみを使用していた時があった。 人によってはそれが最適解なのだろうし、大きい端末で通話することが気にならないのであれば MediaPad M3 にも通話アプリが付いているのでタブレットのみの運用というのも可能ではある。 だが、私はバイクのナビとしても使用しているのでタブレットのみという選択肢はあり得ない。 タブレットをバイクにマウントする製品も無いことはないが、やはり脱落の危険性が大幅に増えるので常用するには厳しい。 ナビに使用するのにはそんなに大きいスクリーンサイズは必要としないというのもある。 4.5 インチ程度でも十分だ。

ファブレットでも明らかに使う気になれないのは前述の通り PDF と Kindle だ。 ここが必要ない方はスマートフォンのみでも良いのだろう。 ゲームに関してはファブレットでも十分快適であり、私が今使っている MediaPad M3 Lite 10 だと逆に大きすぎて指の可動範囲が広いのがそんなに快適でないように感じる。 そう考えるとやはり 8.4 インチはなかなか考えられたサイズなのかもしれない。 難しい。

ともかく、そういうわけで次買い換えるとしたら出るであろう MediaPad M4 と Mate 9 より少し小さいスマートフォン、できれば防水端末が良い。 ナビとして使う場合に防水が付いていると安心感がまるで違う。

イヤホンジャックが消える日が来るのだろうか

昨今の iPhone では極端な薄型化の為かイヤホンジャックさえも廃止されている。 これは賛否両論あるところだが、もし HUAWEI の端末もこの時流に乗ってきたとしたら私もまた Bluetooth イヤホンに戻すことを考えなくてはならなくなる。 Bluetooth イヤホンは確かにコードが絡まりにくくなるのは長所なのだが、それよりも電池の心配をしなければならないのとペアリングされた複数のデバイスを切り替える時に失敗する場合があるのが気になるところではある。 イヤホンを使うシーンというのは要するに音を漏らすと迷惑である場合がほとんどだと思われるので、例えば電車内などで切り替えたと思ったつもりだったのが切り替わってなくて音を出してしまった……という失敗も何度かあった。 あと「まだ大丈夫だろう」と思って使っていたら突然電池が切れて同様に音を漏らしてしまったケースも何度もあった。 Apple の AirPods は使ったことがないが、値段が高すぎるのでなかなか厳しいところがある。 そもそも iPhone ではないので連携部分の優位性がないので微妙だろうか。