デザイナーが今年買った本を振り返る

こんにちは!SaaS・DX事業部デザイナーの鍜治本です!
エキサイトホールディングス Advent Calendar 2023 シリーズ2の20日目の記事です!

qiita.com

新卒で入社してから早三年。仕事も慣れた部分や生活環境が変わったこともあり、例年に比べて本を読む機会が多くありました。
ペースが遅く1冊を読みきれないことも多々あるので、しっかりとした紹介はできませんが、購入した本をいくつかご紹介します。

今年購入した本一覧

購入した本のうち、仕事柄関連していそうな本をリストアップしてみました。リンクも記載したので、もし興味がある本があればぜひ読んでみてください🙆

2023年に購入した本たち

読み終えた本・読みきれなかった本

読み終えて二週三週と繰り返し読んでいる本もあれば、冒頭に記載した通り、読みきれなかった本も入っています。
詳しい内容までは触れませんが、読み終えた本とそうでなかった本の特徴に触れ、それぞれ一言程度でご紹介します。

読み終えた本

こちらの4冊は、読み終えたり読み返している本です。

左から、「銀行とデザイン」「ちいさくはじめるデザインシステム」「決算書が読めるようになる!」「会計の地図」

左の2冊はデザインに関連しており、実務レベルの事柄や事例を紹介されています。「銀行とデザイン」は、大手金融業界に変革をもたらしたデザインと、デザインを浸透させた過程・課題解決についてふんだんに盛り込まれています。業界は違えど、デザイナーがプロダクト・サービスに対して何ができるか?を改めて認識できた一冊です。
「ちいさくはじめるデザインシステム」は、デザインシステムについて具体的な事柄から抽象的な事柄まで広く描かれており、自分が現在進行形で制作しているUIの参考にもしました。実務と重ねて考えられ、読み進める上でも納得感を持ちながら読めた2冊です。

一方、右の2冊は会計や会社の数値にまつわる書籍です。担当しているKUROTENは大きな括りで言えば会計分野、お金周りの知識を増やすべく読み始めました。
馴染みのない用語や実体験のない仕組みにすぐには理解できない部分もあり、前の2冊と比べても読むのに時間を要した覚えがあります。ただ、書籍自体はかなり易しく書かれていたこともあり、周回して少しずつ知識化しています。

読みきれなかった本

反対に、読んでみたものの途中で挫折した本や、完読できなかった本はこちらの3冊。

左から、「Googleアナリティクス4」「デジタル・ブランディング」「思考技術」
左の「Googleアナリティクス4」は、GA4の勉強のために購入したものの、基礎知識がない状態で読むには全くわからずだったため挫折しました。UXをより良くするべく分析方法を学ぶ為だったのですが、まずはGA4自体を触りながら慣れていった方が良いと判断し、現在は積読しています。
中央の「デジタル・ブランディング」は、ブランド構築におけるメソッドやヒントを得るべく読み始めました。書籍の内容は、事例紹介や著書の経験談などで少し冗長だったため、すぐ読む温度感ではないとし、優先度を下げて保留しています。
右の「思考技術」については、考え方・アウトプットの仕方が言語化されており分かりやすいものの、ワークアウト的に進めた方が良い構成だったため積読中です。

業務と関係なく今年買った本

ここからは業務・デザインが関係ない今年購入した本です。おまけ程度でご紹介します。

一目惚れで買った本

左から、「イラストで見るゴーストの歴史」「47㎡、2人暮らし」
イラストで見るゴーストの歴史」はSNSで見かけ、イラストのかわいらしさに一目惚れして購入。世界各国伝承されたゴーストたちや、それらにまつわる事例など、見た目とは裏腹にかなり濃密にまとめられた書籍です。
47㎡、2人暮らし」もSNSで見かけて衝動買いした本で、部屋を作る上で意識すべきポイントを簡単なルールにしてまとめられています。生活環境が変わったので、年末の大掃除で取り入れておしゃれ部屋を目指したい所存です。

完全に趣味で買った本

左から、「未踏の蒼穹」「ガニメデの優しい巨人」「巨人たちの星」「内なる宇宙 上下」
全てSF小説です。学生時代に読んだ「星を継ぐもの」の新版が出るニュースに触発され、ジェイムズ・P・ホーガンの本を一気買い。とんでもなく面白いのですが、私の語彙では説明しきれないので、興味を持った方はぜひ読んでみてください。

おわりに

蛇足が長くなってしまいましたが、デザイナーとして業務で活かせるインプットとして本を読んできました。インプットとして読書を選んでいるのは、知識として蓄積できるもの・今はまだ自分にふさわしくないもの、これらを実際に読まねば分からないワクワク感が好きなのだと感じています。
ちょうど大きい本棚を購入したので、知識を蓄えられるようにたくさんインプット・アウトプットして、本棚をぎゅうぎゅうに埋めたいです。

最後までご覧いただきありがとうございました🙇

初めてのオフライン展示会からデザイナーが学んだこと②

こんにちは!SaaS・DX事業部デザイナーの鍜治本です! エキサイトホールディングス Advent Calendar 2023 シリーズ1の20日目の記事です! qiita.com

2023年11月に出展したBOXIL EXPOについて、展示に向けて準備したことについても記事化していきます〜!
今回はイベント期間中に着用していたスタッフウェアについて、どのようなステップで作成されたかをお伝えします。

スタッフウェアができるまで

今回制作したウェアはエキサイトのロゴかプリントされた黒地のTシャツと、KUROTENのロゴが印刷された白地のTシャツの2種類。

発注前に作成したTシャツのモックアップ
文化祭で定番の『クラスT』を作成したことがある方は分かるかもしれませんが、オリジナルのTシャツを作成できるサービスがあります。
Tシャツのデザインはあらかた決まっており、デザインデータも用意できることから、印刷業者にデザインデータを渡し印刷してもらうプロセスで制作を進めました。

着用するウェアの扱い

これまで、社内カンファレンスやサマーインターンといったイベントでも、オリジナルでTシャツを作成することがありました。エキサイトで開催される『イベントの一部を体感するもの』としてTシャツが求められていたことから、イベントのモチーフをあしらったり、既存のイメージを広げるようなTシャツのデザインを作成していました。

過去に社内カンファレンス(TechCon)で試作していたもの
今回のEXPOでは「エキサイトという企業」として出展しているものの、来場者は「サービス・プロダクト」を見にきているイベントです。そのため、『イベントの一部を体感するもの』ではなく『企業・サービス・プロダクトとしての姿』を示すウェアが求められると考え、サービスロゴやコーポレートロゴのみの質素なスタイルにしました。

デザインデータの作成

作成する目的が決まったら、デザインデータを作成していきます。今回印刷を依頼したクラTジャパンさんはテンプレートを配布しているので、テンプレートの指示に従いデザインを作成します。

テンプレートにKUROTENロゴを配置しています
ロゴをプリントする位置を確認するため、手持ちのTシャツを使って襟ぐりから距離を測ってみたり、着用時のシワが響かない位置を探りながら入稿用デザインを作成しました。

入稿確認と発注

作成したデザインを印刷業者に渡し、プリント前の最終チェックします。
中でもエキサイトロゴの印刷において業者とやり取りを挟んだ箇所が2つあり、それぞれの原因と解決方法について記載します。

エキサイトロゴの色

エキサイトロゴは赤色と白(または黒)の組み合わせで構成されています。この赤色はRGBで指定されている赤色であり、印刷などCMYKで再現される色味とは一致しません。*1

RGBをCMYKにしても、鮮やかさが再現できるわけではない
ロゴの赤色部分はR255 G0 B0のディスプレイで見る鮮やかな赤色です。このまま印刷するとくすんだ赤色になってしまったり、下手に調整すれば朱色のようなオレンジがかった色になりかねません。
今回は時間との兼ね合いもあったため、印刷業者の方とすり合わせた上で、印刷可能なインクからロゴに近しい赤色を選びました。

エキサイトロゴの輪郭

エキサイトのロゴの「x」部分は、筆で書き上げたような筆のかすれが再現された形をしています。勢いがあって個人的に好きな要素ですが、デザインデータにおいてはそうもいきません。
今回の印刷では、ロゴが印刷されたシートをTシャツに貼り付ける手法が取られています。輪郭の形状がこまかくなる程、シートの縁も複雑になり、剥がれやすくなってしまう懸念がありました。
そこで印刷業者の方から、ロゴの縁に沿って輪郭線をつける提案をいただきました。

データに追加された黒い輪郭線
輪郭線を追加し細かな部分を1シートに収めたことで、複雑なシルエットでも印刷しやすい状態にしています。

これらの修正を加えた上でそれぞれのTシャツを発注しています。単にロゴを乗せただけで完成ではなく、目的や細部まで目を配った上で制作できました。

ExciteロゴTシャツの印刷部分をアップで撮影
実際のTシャツを見ても、ディスプレイと大差なく鮮やかさが確保できており、ロゴに追加した縁も悪目立ちしていませんでした。何度も着用するウェアであるからこそ、基準となる数値を完璧に再現よりも、相対的にどうあるべきかを考慮して制作できたと感じています。

イベントで着用してみた結果

ブース内で来場者と話している様子
スタッフウェアを着用することで、企業やサービスを目に入れてもらうのはもちろん、ブースのスタッフとしても分かりやすくなります。
また、今回のようなイベントのみならず、ウェブセミナーやオンライン商談といった場面でも横展開できます。今後も、それぞれのウェアが活躍できる場が増えると嬉しいです。

おわりに

今回は展示会出展で作成したものとして、スタッフウェアにフォーカスした記事をまとめました!これからスタッフウェアやイベントTシャツを作ろうとしている方の参考になれれば嬉しいです~!
さらにエキサイトでは、デザイナーやエンジニア(もちろんビジネスも!)を盛り上げてくれるメンバーを、新卒・中途問わず募集しています! カジュアル面談も随時受付可能ですので、お気軽にお声かけください!🙆

recruit.jobcan.jp

*1:印刷業者によっては色味の再現をできる「特色」という方法で再現できるところもあります。

エキサイト株式会社の採用候補者向け会社説明資料を制作した話

はじめに

こんにちは、エキサイト株式会社3年目デザイナーの山﨑です。 エキサイトホールディングス Advent Calendar 202316日目は、山﨑が担当させていただきます。

qiita.com

今回はエキサイト株式会社の採用候補者向け会社説明資料をリニューアルした時のお話をしたいと思います。

リニューアル目的とは?

元々エキサイトには会社説明資料があったのですが、「採用したい人材に刺さる会社説明資料を作りたい」「Speaker Deckに投稿されているエキサイトの会社説明資料の文字のあしらいがおかしなことになっているので、直してほしい」というオーダーによってリニューアルすることになりました。

早速Speaker Deckのエキサイト会社説明資料を確認してみると…

文字が袋文字になっている…!?しかも要素から文字がはみ出している…!

袋文字:輪郭線だけがある文字。文字の強調する時によく使われる。

他のデザイナーさんに調べていただいたところ、Googleスライドで「日本語フォント + Bold」で発生するバグのようです。

zenn.dev

対処法としては、下記の方法で解決するようです。

  • Boldを使用しない
  • フォントに「M PLUS 1p」を使用する
  • PowerPointで作る

Googleスライドかつゴシック体を使用して検証していただいたところ、下記フォントで文字の掠れはみられませんでした。

  • Noto Sans JP
  • BIZ UDP Gothic
  • Zen Kaku Gothic New
  • M Plus 1p、M Plus 1p

採用したい人材とは?

Speaker Deckに投稿されているエキサイトの会社説明資料の文字のあしらいがおかしなことになっているので、直してほしい」というオーダーはフォントを変更すれば解決できることがわかりました。

次に「採用したい人材に刺さる説明資料」というオーダーですが…

例えば、X事業部は「高エネルギーでスピード感があり、新規事業に興味がある人を採用したい!」という要望がありY事業部は「堅実で実直に業務を行ってくれる、既存事業の収益を伸ばすことに興味がある人を採用したい!」という要望がある場合、それぞれの採用希望人材に刺さるアウトプットはコンテンツ、デザインともに全く異なります。

事業部ごとに採用資料を作ることはあまりないのですが、今回はかなり採用したい人材の属性が異なっています。

そのため、汎用的に使える、安心・堅実な印象を与える採用資料Aパターンと、採用を強化している事業部をピックアップしその人材に刺さるデザインで、挑戦的な印象を与える採用資料Bパターンを制作することになりました。

加えて、締切にそれほど余裕がなかったので一旦「Aパターン」を作りOKが出たらBパターンを制作しようという方針になりました。

制作ツールは何にする?

制作ツールは、色々な方が編集しやすいようGoogleスライドを採用しました。

ただ、Googleスライドは収録フォントが少なくデザイン性に凝ることが難しいため、会社のミッションなど編集する機会が少ない部分はこぶりなゴシックで打ち込んだものを画像で書き出して貼り付けを行うなど工夫を行いました。

ラフデザインについて

Aパターンを制作するにあたり、採用候補者に「堅実さ、わかりやすさ、安心感」が伝わるよう意識しました。

まず参考企業の採用資料を集めて、どの部分に堅実さや安心感につながるエッセンスが散りばめられているのか模索しながらデザインラフを作っていきました。

以上のような雰囲気が伝わるラフデザインを7ページ×2パターンで計14ページ制作し、役員にデザイン案を決めていただきました。

最後に

最後に、エキサイトではデザイナー、フロントエンジニア、バックエンドエンジニア、アプリエンジニアを絶賛募集しております!

興味があればぜひぜひ連絡ください!🙇

www.wantedly.com

デザインシステムを活かすためのドキュメンテーション

こんにちは。エキサイトで内定者アルバイトとしてデザイナーをしている齋藤です。

エキサイトホールディングス Advent Calendar 2023 シリーズ2の19日目は、私齋藤が担当させていただきます。

今回は、あるプロジェクトでデザインシステム構築を担当していた際に、コンポーネントライブラリのドキュメントを書く中で考えていたことやドキュメントに記していたことについてお話したいと思います。

はじめに

冒頭、私がやっていたことをざっくりとご紹介します。

とある新規サービスの立ち上げにジョインし、当初はUIデザインとフロントエンド(主にマークアップ)を担当していました。デザインはFigma、フロントエンドはNuxt.js(v3・Composition API・TypeScript)を用いた開発環境でした。

開発を進める中で、新規機能の追加などの将来的な拡張を見据えて、デザイン面ではデザイン要素のルール化、エンジニアリング面では汎用的なUI要素のコンポーネントライブラリ化をすることで開発スピードの向上や非属人化、一貫性の担保などができると考え、メンターやエンジニアの方々にご相談をした結果、デザインシステムの構築をさせていただくことになりました。

デザインシステムの構築では以下のことをやっていました。

今回は「ドキュメンテーション」に絞ってお話をすることになります。

なぜデザインシステムにドキュメントが必要なのか

なぜデザインシステムにドキュメントが必要なのかを言語化しておきたいと思います。

デザインシステムを構築すると、デザイナー側はFigma、エンジニア側にはコードとして残りますが、どちらも双方に共通するデータとなることは難しいと考えます。

デザインシステム自体、デザインとエンジニアリングをつなげる共通言語的なものを理念としていますので、その成果物が完全に二分されてしまうことは理念に反します。

そこで、デザイナー・エンジニア双方に共通するデータとして、ドキュメントが必要であると私は考えます。

ドキュメントに何を記すか

実際のButtonコンポーネントを例に、どんなことをドキュメントに記していたかをご紹介します。

デザインパターン

存在するデフォルト状態の表層的なデザインのパターンの名称や使用用途を記したセクションです。

実際のドキュメントの一部

使用用途を明示することにより、そのデザインパターンがどのような場合に適するのかについての共通認識を生み出し、デザインの属人化を防ぎます。

状態変化

Hover時やActive時、Disabled状態など、デフォルト状態以外の表層的なデザインの変化を記したセクションです。

実際のドキュメントの一部

Figmaでは状態変化はVariantsで表現することが多いため、その情報も書いておきます。

サイズバリエーション

コンポーネントにサイズのバリエーションがある場合、その詳細な値を記したセクションです。

実際のドキュメントの一部

こちらも、FigmaではVariantsで表現することが多いため、その情報も書いておきます。

コンテンツバリエーション

コンポーネント内の要素の有無(Buttonならアイコンの有無など)が選択できる場合、その旨を記したセクションです。

実際のドキュメントの一部

こちらも、FigmaではVariantsで表現することが多いため、その情報も書いておきます。

Components API

ここからはコーダー向けのセクションです。

Import

コンポーネントのImport文を記します。

実際のドキュメントの一部

Props

コンポーネントにあるPropsの情報を記します。

実際のドキュメントの一部

それぞれのtypePropType、デフォルトの値などを明記します。

加えて、Components APIより前のセクションとの関係や、どういった挙動をするのかなども書いておきます。

Slot

コンポーネントにSlotが存在する場合、どのUI要素にあたるのかを記します。

実際のドキュメントの一部

これで以上です。

ドキュメントを書いてみて

ドキュメントを書くことでデザインシステムを構築する中での頭の整理にも繋がりますし、実際に活用される場合にFigmaやコードを観察したり対照させたりする必要がほとんどなく理解することができるため、開発のスピードも向上するのではないかと実感しました。

一方で、デザインシステムの構築自体、規模が大きければ大きくなるほど時間と労力が必要となるので、それぞれの事情やプロダクトの特性を見極めながら行う必要がありそうです。

さいごに

デザインシステム構築についての書籍や記事がまだまだ少ない中で、特にドキュメンテーションに関する情報はより少ないと言っても過言ではありません。 そんな中で、デザインシステムに関わる方の一助となればと思い、この記事を執筆してみました。

エキサイトホールディングス Advent Calendar 2023では、他にも弊社のエンジニアやデザイナーが執筆した記事が多く配信されていますので、ぜひチェックしてみてください。

ご精読ありがとうございました。

Javaで整数型やbyte配列と16進表記の文字列との間の変換を行なう

こんにちは、エキサイト株式会社の平石です。エキサイトホールディングス Advent Calendar 2023の19日目を担当いたします。

今回は、Javaで整数型やbyte配列と16進表記の文字列との間の変換を行なう方法をご紹介します。

整数 → 16進表記 の変換

まずは、通常の10進表記(として表示される)整数を16進表記に変換してみます。

標準ライブラリのみを使う場合には、主に2つの方法があります。

IntegerやLongクラスのstaticメソッド

IntegerやLongクラスには、intやlongを16進表記に変換するstaticメソッドが用意されています。

var hexFromInt = Integer.toHexString(30);
var hexFromLong = Long.toHexString(255L);

System.out.println("30の16進表記: " + hexFromInt);
System.out.println("255Lの16進表記: " + hexFromLong);
30の16進表記: 1e
255Lの16進表記: ff

HexFormatクラスを使う

Java 17からはHexFormatクラスというものを利用することができます。

byte byteNum = 100;
short shortNum = 10000;
int intNum = 1000000;
long longNum = 100000000;

String hexFromByte = HexFormat.of().toHexDigits(byteNum);
String hexFromShort = HexFormat.of().toHexDigits(shortNum);
String hexFromInt = HexFormat.of().toHexDigits(intNum);
String hexFromLong = HexFormat.of().toHexDigits(longNum);

System.out.println("byteNumの16進表記: " + hexFromByte);
System.out.println("shortNumの16進表記: " + hexFromShort);
System.out.println("intNumの16進表記: " + hexFromInt);
System.out.println("longNumの16進表記: " + hexFromLong);
byteNumの16進表記: 64
shortNumの16進表記: 2710
intNumの16進表記: 000f4240
longNumの16進表記: 0000000005f5e100

このように、toHexDigitsに渡す引数の型によって最終的に出力される16進表記の長さも変わります。

Long型の整数は8バイトなので、1Lのような値を渡しても1ではなく0000000000000001が返ります。
一方、IntegerLongのstaticメソッドを利用するときには、1が返ります。
これが、この2つの方法の大きな違いですので、状況によって使い分けると良いでしょう。

HexFormatクラスについては後に詳しく紹介します。

16進表記 → 整数の変換

こちらも標準ライブラリのみを利用するのであれば、主に以下の2つの方法があります。

IntegerやLongクラスのstaticメソッド

IntegerやLongクラスのstaticメソッドを利用するには以下のようにします。

byte byteFromHex = Byte.parseByte("f", 16);
short shortFromHex = Short.parseShort("fff", 16);
int intFromHex = Integer.parseInt("fffff", 16);
long longFromHex = Long.parseLong("fffffff", 16);

System.out.println("f のパース結果: " + byteFromHex);
System.out.println("fff のパース結果: " + shortFromHex);
System.out.println("fffff のパース結果: " + intFromHex);
System.out.println("fffffff のパース結果: " + longFromHex);
f のパース結果: 15
fff のパース結果: 4095
fffff のパース結果: 1048575
fffffff のパース結果: 268435455

HexFormatクラスを使う

Java 17以降は、HexFormatクラスのstaticメソッドを利用しても、16進表記の文字列を整数に変換することができます。

int intFromHex = HexFormat.fromHexDigits("fffff");
long longFromHex = HexFormat.fromHexDigitsToLong("fffffff");
fffff のパース結果: 1048575
fffffff のパース結果: 268435455

HexFormatクラスによるbyte配列と16進表記の変換

HexFormatクラスを利用すれば、byte配列と16進表記の変換を行うことができます。

byte配列 → 16進表記

byte配列から16進表記の文字列に変換するには、formatHexメソッドを利用します。

byte[] bytes = {1, 15, 120, 127};
String byteHex = HexFormat.of().formatHex(bytes);

System.out.println(byteHex);
010f787f

ちなみに、負の数ではff(-1)から順に80(-128)となります。
(2の補数表現で-1は11111111なので当然といえば当然ですね。)

byte[] bytes = {-1, -2, -127, -128};
String byteHex = HexFormat.of().formatHex(bytes);
fffe8180

このメソッドを使って、ランダムな16進表記の文字列を作成できます。

final SecureRandom secureRandom = new SecureRandom();

final int numString = 64; // 生成したいランダムな文字列の文字数

final byte[] randomByte = new byte[numString / 2];
secureRandom.nextBytes(randomByte);

final String randomHex = HexFormat.of().formatHex(randomByte);

System.out.println(randomHex);

byte 1つにつき2文字に変換されるので、byte配列の長さはnumString / 2としています。

16進表記 → byte配列

逆に16進表記の文字列からbyte配列に変換するには、parseHexメソッドを利用します。
先ほどの例の変換後の文字列を、byte配列に戻してみましょう。

byte[] bytesFromHex = HexFormat.of().parseHex(byteHex); // byteHex = 010f787f

System.out.println(Arrays.toString(bytesFromHex));
[1, 15, 120, 127]

HexFormatクラスの詳細

最後に、HexFormatクラスの詳細についてみてみます。

これまでの例ではHexFormat.of()と書いてHexFormatインスタンスを取得していましたが、HexFormatインスタンスの取得のためにはofDelimiter()というstaticメソッドもあります。

of()delimiterなしのフォーマッター、ofDelimiter()delimitterありのフォーマッターを返します。

delimiterは、基本的には byte配列を16進表記に変換する際に区切り文字として使われます。 (toHexDigitsを使って単一のbyteや、int、longを変換する際には、delimiterは使われません。)

例えば、以下のように使います。

byte[] bytes = {1, 15, 120, 127};
String byteHex = HexFormat.ofDelimiter(":").formatHex(bytes);

System.out.println(byteHex);

byte[] bytesFromHex = HexFormat.ofDelimiter(":").parseHex(byteHex);

System.out.println(Arrays.toString(bytesFromHex));
01:0f:78:7f
[1, 15, 120, 127]

その他、HexFormatクラスのインスタンスに対しては、フォーマットの際の詳細設定を行うことができます。 prefix, suffix, delimiterはformatHexparseHexのみで利用されます。

  • withDelimiter・・・・delimiterを改めて指定できる
  • withLowerCase・・・16進表記に小文字(a ~ f)を使用するようになる
  • withUpperCase・・・16進表記に大文字(A ~ F)を使用するようになる
  • withPrefix・・・・・prefixを指定
  • withSuffix・・・・・suffixを指定
HexFormat hexFormat = HexFormat.of()
                .withDelimiter(", ")
                .withPrefix("#")
                .withSuffix(";")
                .withUpperCase();

byte[] bytes = {1, 15, 120, 127};

String hex = hexFormat.formatHex(bytes);
System.out.println(hex);

System.out.println(hexFormat.toHexDigits(255)); 
#01;, #0F;, #78;, #7F; // prefix, suffix, delimiterが適用される
000000FF // 単一の値の変換ではprefix, suffix, delimiterは適用されない

終わりに

今回は、Javaで整数型やbyte配列と16進表記の文字列との間の変換方法をご紹介しました。

では、また次回。

参考文献

AWSと外部システムを「良い感じ」に連携する

こちらはエキサイトホールディングス Advent Calendar 2023の18日目の記事になります。

qiita.com

まず最初に、

「良い感じ」とは

この記事では、アプリケーション側の実装を最小限に抑えることを前提に話を進めます。

概要

AWSと外部システム(SaaSなど)間でのデータ連携方法ですが、AWSにはAppFlowやEventBridgeなどのサービスを利用する方法があります。
ユースケースに応じて適切な選択をするため、いくつかの事例を元に解説してみます。

ユーザの処理をトリガーにして非同期にデータを連携したい

EventBridgeはイベント駆動型アーキテクチャの実装をサポートするサービスです。
AWSのHealthイベントが発行されたらSlackに通知する、みたいなことをやっている方も多いと思われます。

そういったAWSのサービスが生成するイベントに加え、ユーザが独自に定義したカスタムイベントの発行も可能となっています。 実装は超シンプルで、PutEvents APIをコールするだけです。PHPのコード例は以下の通り。

$result = $client->putEvents([
    'Entries' => [
        [
            'EventBusName' => 'arn:aws:events:ap-northeast-1:123456789012:event-bus/hoge',
            'Detail' => json_encode($detail),
            'DetailType' => 'Excite Hoge Event',
            'Source' => 'excite.hoge'
        ]
    ]
]);

たったこれだけです(もちろんエラーハンドリングなどは別途必要ですが)
これにより、アプリケーションはイベント発行のみを行うだけで済み、後続処理については考慮する必要がなくなり、処理が疎結合になることでアプリケーションコードの保守性を高めることができます。

決まったタイミングに大量のデータを送受信したい

SaaSAWS間でデータを安全にやり取りするには、フルマネージド統合サービスであるAppFlowが適切です。

AppFlowを使ったデータ連携の例として、SalesforceからRedshiftへの問合せデータの取り込みや、ZendeskからS3へのチケットデータの取り込み、といった事例がよく上げられます。
2023年12月現在、80近くのアプリケーションがサポートされており、日本でも広く採用されているアプリケーションもあります。

AppFlowの良さは、接続設定の容易さにあると思っています。とにかく簡単です。
当然、OAuth 2.0など認証フローもサポートされています。
データが散らばっていて分析が困難になっている、データレイク(の手前となる基盤)を構築したい、などの要望にもサクッと対応できるかと思います。

外部サービスからイベントを受け取って処理を実行したい

  • Salesforce
  • Zendesk
  • Shopify
  • Datadog
  • New Relic
  • Auth0
  • KARTE
  • Mackerel

などのSaaSパートナーからイベントを受け取れます。
例えば、Auth0でのログインイベントを受取り、LambdaをキックしてRDSにログイン履歴として保存する、などといった実装も可能となります。

Salesforceの例ではイベントリレーの設定が必要になることもありますが、(個人的な意見として)これは少し設定が煩雑です。
もう一つの方法として、AppFlowでデータを受け取り、EventBridgeにペイロードすることもできます。
AWS側の設定でほぼ完結できるので、IaCで統一管理できるといったメリットもあります。

https://aws.amazon.com/jp/blogs/compute/building-salesforce-integrations-with-amazon-eventbridge/

まとめ

定期的に「相手」を確認する処理では、リアルタイム性が損なわれがちです。
しかし、すべての処理を同期的に行うとなるとアプリケーションコードが肥大化し、保守性が低下します。

イベント駆動型アーキテクチャは非同期処理を実装する選択肢の1つとなりますので、何かしらの参考になれば幸いです。

複数回答チェックボックスで、1つ以上のチェックを必須にする方法

こんにちは。 エキサイト株式会社の三浦です。

こちらは、エキサイトホールディングス Advent Calendar 2023の18日目の記事になります。

qiita.com

良ければ他の記事もどうぞ!

さて、HTMLでフォームを作る際、「複数回答が用意されているチェックボックス式の設問に対して、最低1つ以上のチェックをしてもらいたい」という状況はよくあるでしょう。

今回はそういった場合に、コードにより1つ以上のチェックを必須にする方法を紹介します。

複数選択式チェックボックス

HTMLでフォームを作る場合、以下のような「複数回答が用意されているチェックボックス式の設問」を作ることはよくあるでしょう。

コードだと、以下のようになります。

なお簡単にするため、デザイン用のクラスやCSSは省略します。

<form method="POST" action="***">
    <div>どんな動物が好きですか?</div>

    <input id="animal-selection-dog"
           class="animal-selection"
           type="checkbox"
           name="animal-selection-dog"
           value="犬"
    />
    <label for="animal-selection-dog"></label>

    <input id="animal-selection-cat"
           class="animal-selection"
           type="checkbox"
           name="animal-selection-cat"
           value="猫"
    />
    <label for="animal-selection-cat"></label>

    <input id="animal-selection-crow"
           class="animal-selection"
           type="checkbox"
           name="animal-selection-crow"
           value="カラス"
    />
    <label for="animal-selection-crow">カラス</label>

    <button type="submit">回答する</button>
</form>

これで設問の作成はできましたが、現状だと1つもチェックを入れていなくても問題なく回答できてしまいます。

ですが、実際は「1つ以上は必ずチェックして欲しい」という要件もよくあることでしょう。

では、「1つ以上のチェックを必須とする」にはどうすれば良いでしょうか。

1つ以上のチェックを必須にする方法:考えがちなミス

inputタグには、 required という属性があります。

それを使って以下のようにすれば良いのでは、と思う方も多いのではないでしょうか。

<!-- すべてのinputタグに required を追加 -->

<form method="POST" action="***">
    <div>どんな動物が好きですか?</div>

    <input id="animal-selection-dog"
           class="animal-selection"
           type="checkbox"
           name="animal-selection-dog"
           value="犬"
           required
    />
    <label for="animal-selection-dog"></label>

    <input id="animal-selection-cat"
           class="animal-selection"
           type="checkbox"
           name="animal-selection-cat"
           value="猫"
           required
    />
    <label for="animal-selection-cat"></label>

    <input id="animal-selection-crow"
            class="animal-selection"
            type="checkbox"
            name="animal-selection-crow"
            value="カラス"
            required
    />
    <label for="animal-selection-crow">カラス</label>

    <button type="submit">回答する</button>
</form>

では実際にこれで回答をしてみましょう。

なんと、すでに1つ以上の回答にチェックを入れているのにもかかわらず「チェックボックスをオンにしてください」と出てしまいました。

実は、チェックボックスは1つ1つのinputに対して required を判定するものであり、「1つ以上がチェックされているか」という判定は自動ではしてくれないのです。

では、どうすれば良いのでしょうか。

1つ以上のチェックを必須にする方法:正しい方法

残念ながら「1つ以上がチェックされているか」をHTMLだけで判定する方法は現状ありません。

JavaScriptを使うと良いでしょう。

<form method="POST" action="***">
    <div>どんな動物が好きですか?</div>

    <input id="animal-selection-dog"
           class="animal-selection"
           type="checkbox"
           name="animal-selection-dog"
           value="犬"
           required
    />
    <label for="animal-selection-dog"></label>

    <input id="animal-selection-cat"
           class="animal-selection"
           type="checkbox"
           name="animal-selection-cat"
           value="猫"
           required
    />
    <label for="animal-selection-cat"></label>

    <input id="animal-selection-crow"
           class="animal-selection"
           type="checkbox"
           name="animal-selection-crow"
           value="カラス"
           required
    />
    <label for="animal-selection-crow">カラス</label>

    <button type="submit">回答する</button>
</form>

<!-- 判定のため、JavaScriptを追加 -->
<script>
    (() => {
        // チェックボックスのinputタグを取得
        const checkBoxElements = Array.from(document.getElementsByClassName("animal-selection"));

        const errorMessage = "1つ以上の選択肢を選択してください。";
        checkBoxElements
            .forEach(m => {
                // エラーメッセージを、カスタムなものに変更
                m.setCustomValidity(errorMessage);

                // 各チェックボックスのチェックのオン・オフ時に、以下の処理が実行されるようにする
                m.addEventListener("change", () => {
                    // 1つ以上チェックがされているかどうかを判定
                    const isCheckedAtLeastOne = document.querySelector(".animal-selection:checked") !== null;

                    // 1つもチェックがされていなかったら、すべてのチェックボックスを required にする
                    // 加えて、エラーメッセージも変更する
                    checkBoxElements.forEach(n => {
                        n.required = !isCheckedAtLeastOne
                        n.setCustomValidity(isCheckedAtLeastOne ? "" : errorMessage);
                    });
                });
            });
    })();
</script>

少々コードが複雑に見えるかもしれませんが、やっていることは単純です。

  • 1つでもチェックが付いていたら、チェックボックスのinputタグ全てからrequired属性を排除
  • 1つもチェックが付いていなかったら、チェックボックスのinputタグ全てにrequired属性を追加
  • 加えて、エラーメッセージをカスタムなものに変更

これだけです。

実際にやってみると、以下のようになります。

もちろん、1つ以上チェックすれば問題なく回答することができます。

最後に

HTMLだけで複数回答チェックボックスの必須化をすることができないのは少々面倒ですが、それでも少しJavaScriptを加えれば対応可能です。

フォームを作成する上で必要になってくる場面も少なくないと思いますので、参考にしてもらえると幸いです!

accent-colorでチェックボックスの色を指定する方法と注意したい落とし穴(CSS)

こんにちは。 エキサイトで内定者アルバイトとしてデザイナーをしている齋藤です。

エキサイトホールディングス Advent Calendar 2023シリーズ2の12日目は、私齋藤が担当させていただきます。

今回はチェックボックスなどの強調表示色を指定できるCSSプロパティのaccent-colorの使用方法とその注意点についてご紹介したいと思います。

accent-colorとは

accent-colorCSSプロパティの一種で、以下の要素の強調表示色を指定することができます。

  • input要素(Type: checkboxradiorange
  • progress要素

以前はこれらの要素の色を指定したい場合、疑似要素を使用して無理やり表現していましたが、accent-colorの登場で一発で解決することができるようになりました。

対応ブラウザ

2023年12月18日時点の各ブラウザの対応状況は以下の通りです。

ブラウザ バージョン
Google Chrome 93以降
Microsoft Edge 93以降
Safari 15.4以降
Firefox 92以降
Opera 79以降
iOSSafari 15.4以降
AndroidGoogle Chrome 93以降
AndroidFirefox 92以降
AndroidOpera 79以降
Android WebView 93以降

なお、対応状況は変化する可能性があります。使用前にCan I use...などで確認をしてください。

実際に使ってみる

例はそれぞれ、<input type="radio" /> <input type="checkbox" /> <input type="range" />です。

通常時

通常はユーザーエージェントが指定した色(Chromeなら青色)が当てられます。

通常時の描写

この際、accent-colorは初期値のautoになっています。

accent-color適用時

accent-colorredを指定した場合、ユーザーエージェントが指定した色から任意の色に変更することができます。

accent-colorにredを指定した場合の描写

input {
    accent-color: red;
}

注意したい落とし穴

accent-colorは非常に便利なのですが、ひとつだけ注意したいことがあります。

例えば、エメラルドグリーン(#6ee7b7)を指定したとします。すると、以下のようにブラウザでは描写されます。

accent-colorに#6ee7b7を指定した場合の描写

あれ・・・?なにかがおかしい・・・

実は、accent-colorを指定して強調表示色を指定した場合、一定のコントラスト比を担保していない色ですと、ブラウザ側がaccent-colorを適用した要素にのみ自動的にダークモードにするため、指定する色のコントラスト比には十分注意する必要があります。

「一定のコントラスト比」の境界はブラウザのアルゴリズムによって異なるようですが、https://accent-color.glitch.me/で挙動を確認することができます。

先程指定した#6ee7b7は白に対して1:1.5コントラスト比でしたが、1:3.8#059666にすると通常のライトモードに戻りました。

accent-colorに#059666を指定した場合の描写

ブラウザによってラインが異なりますが、白に対して1:3以上の色を指定することをおすすめします。

※実験で使用したブラウザはGoogle Chrome(120.0.6099.109)です

さいごに

今回はaccent-colorを用いてチェックボックスの色を変える方法と、注意したい落とし穴についてご紹介しました。

エキサイトホールディングス Advent Calendar 2023では他にもエンジニアやデザイナーによる記事が多数投稿されています。ぜひチェックしてくださいね!

ご精読ありがとうございました。

参考文献

Spring Bootでのキャッシュのアノテーションの使い方

はじめに

こんにちは、新卒1年目の岡崎です。エキサイトホールディングス Advent Calendar 2023の14日目を担当します。

今回は、Spring Bootで使うことができるキャッシュのアノテーションの機能を紹介していきたいと思います。

環境

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.6.6)
## Java

openjdk version "17.0.2" 2022-01-18
OpenJDK Runtime Environment Temurin-17.0.2+8 (build 17.0.2+8)
OpenJDK 64-Bit Server VM Temurin-17.0.2+8 (build 17.0.2+8, mixed mode)
------------------------------------------------------------
Gradle 7.4.1
------------------------------------------------------------

Build time:   2022-03-09 15:04:47 UTC
Revision:     36dc52588e09b4b72f2010bc07599e0ee0434e2e

Kotlin:       1.5.31
Groovy:       3.0.9
Ant:          Apache Ant(TM) version 1.10.11 compiled on July 10 2021
JVM:          17.0.2 (Eclipse Adoptium 17.0.2+8)
OS:           Mac OS X 12.3 aarch64

設定

キャッシュを使うために、以下の設定を行う必要があります。

  • build.gradleの設定
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-cache'
    implementation 'org.springframework.boot:spring-boot-starter-data-redis-reactive'
}
  • application.ymlの設定
spring:
  redis:
    host: localhost
    port: 6379
  • キャッシュをする時に、返り値として使用するモデルにSerializableを継承させる
@Data
@Accessors(chain = true)
public class SampleModel implements Serializable {
    // 要素を記述    
}

どのようにSpring Bootでキャッシュのアノテーションをつけるのか

Spring Bootでは、アノテーションをつけることで、キャッシュをすることができます。
今回は、Spring Bootで用意されているキャッシュに関するアノテーションを三つ紹介します。

@Cacheable

@Cacheableをメソッドにつけることで、返り値をキャッシュすることができます。

@Cacheable(cacheNames = "GET_BOOK_ID_LIST")
public List<String> getBookIdList(String type, Integer size, Boolean useCache) {
    // 処理の記述を行う
}

デフォルトでは、キャッシュの名前とメソッドの引数をキーとして、返り値を保存しています。

@CachePut

@CachePutでは、キーが同じキャッシュの返り値を更新することができます。キーはキャッシュ名とメソッドの引数を一致させる必要がありますが、アノテーションで用いるkeyというアトリビュートに値を入れることで、メソッドの引数の代わりに使用することができます。

この時に新しく更新される値は、@CachePutのアノテーションがついているメソッドの返り値になります。

@Cacheable(cacheNames = "GET_BOOK")
public Book getBook(Integer bookId) {
    // IDから本の情報を取得する
}

@CachePut(cacheNames = "GET_BOOK", key = "#book.getBookID()")
public Book updateBook(Book book) {
    // 本の情報を更新する
} 

@CacheEvict

@CacheEvictでは、キーが同じキャッシュを削除することができます。

@CacheEvict(cacheNames = "GET_BOOK", key = "#book.getBookID()")
public void updateBook(Book book) {
    // 本の情報を更新する
} 

オプション

@Cacheable、@CachePut、@CacheEvictでは、それぞれオプションが用意されています。ここでは、オプションを三つ紹介します。

key

keyというアトリビュートに任意の変数を入れることで、キャッシュのキーに使うメソッドの引数を自由に設定することができます。
以下は、キャッシュをする時に、メソッドの引数の中でもtypeとsizeだけ使用したい場合の一例の実装です。

@Cacheable(cacheNames = "GET_BOOK_ID_LIST", key = "{#type, #size}")
public List<String> getBookIdList(String type, Integer size, Boolean useCache) {
    // 処理の記述を行う
}

condition

conditionというアトリビュートに条件式を書くことで、特定の値であれば、キャッシュをするように動作することが可能です。
以下は、useCacheがtrueの時だけキャッシュをしたい場合の実装例です。

@Cacheable(cacheNames = "GET_BOOK_ID_LIST", key = "{#type, #size}", condition = "#useCache")
public List<String> getBookIdList(String type, Integer size, Boolean useCache) {
    // 処理の記述を行う
}

unless

unlessというアトリビュートに条件式を書くことで、特定の値であれば、キャッシュの動作をしないようにすることが可能です。
以下は、返ってきた値が空だった場合、キャッシュしないようにする場合の実装例です。

@Cacheable(cacheNames = "GET_BOOK_ID_LIST", key = "{#type, #size}", unless = "#result.isEmpty()")
public List<String> getBookIdList(String type, Integer size, Boolean useCache) {
    // 処理の記述を行う
}

補足: キャッシュはいつつけるのか?

初心者の場合、キャッシュをつければ良いのか、それともつけなくて良いのか、よく分からない場合があります。(少なくとも私はそうでした。)キャッシュの実装の有無を検討する時の一例を、補足として残しておきます。

結論から言うと、キャッシュはヒット率が高い時に有効です。

例えば、1分間に10人のユーザーが「ジャンルごとの本の一覧」を見ていて、その中でもAジャンルを7人が見ているとします。 この場合、同じクエリーパラメーターのアクセスが7割も来ているので、ヒット率は高いと言えそうです。

しかし、1分間に20人のユーザーが「検索」機能を使っていても、この中で誰一人として同じキーワードを使っていなかった場合、ヒット率は低いでしょう。

このように、アクセスが多くても、ユーザーが同じキーワードを使用しないような検索の場合は、キャッシュをするように実装を行っても、DBのアクセス量がほとんど変わらない可能性があります。 このような場合、キャッシュをする必要がないかもしれません。

同じクエリーパラメーターを持つアクセスがどれくらいあるかを検討し、その結果に基づいてキャッシュをするように実装を行うかどうか決めるといいかもしれません。

最後に

今回は、Spring Bootで使うことができるキャッシュのアノテーションの使い方を紹介しました。改めてキャッシュを適切に実装することが大切だと感じました。ここまで読んでいただきありがとうございました。

【Android】内部アプリ共有と、URLからインストールするための設定方法について

はじめに

エキサイト株式会社で内定者アルバイトをしている岡島です。 エキサイトホールディングス 2023 Advent Calendar シリーズ2の15日目を担当させていただきます。

今回はアプリ開発時に内部アプリ共有を試したので、 内部アプリ共有と詰まったことについて共有したいと思います。

内部アプリ共有とは

内部アプリ共有とはAndroidアプリ開発者向けの機能のことで、リンクを使ってAndroid App BundleやAPKをチームに共有できます。 アプリをテストするときなどに、素早く任意のユーザーが利用できるようになります。

  • 権限がある場合のみ、App Bundleや APK を内部共有用としてアップロードが可能
  • バージョンコードの再利用が可能
  • リンクを使ってアプリをダウンロードできるユーザーは100 人まで
  • リンクの有効期限はアップロード日の60日後

詳しくはドキュメントをご覧ください。

support.google.com

内部アプリ共有を有効に設定する

内部アプリ共有を使用してアプリをダウンロードする際には、設定で内部アプリ共有を有効にする必要があります。 私はこの部分で詰まったので、ドキュメントの補足説明をしようと思います。

(1) Google Playストアを開く。

(2) [アイコン] > [設定] に進む。

(3) [概要(About)] > [Playストアのバージョン] を7回タップ

(4) 設定に戻り、[一般(General)] > [開発者オプション] > [内部アプリ共有] をオンにする

以上で内部アプリ共有有効化の設定が完了です!

私は[Playストアのバージョン] を7回タップといった操作や[内部アプリ共有]ボタンの場所がわからず戸惑ってしまいました。

これらの操作を行うことでAndroid端末で内部アプリ共有が可能になります。

最後に

この記事では、Androidアプリ開発者向けの機能である「内部アプリ共有」についてご紹介しました。 実際に試してみる中で、設定の一部で少し戸惑うことがありましたが、アプリのテストが簡単に行えて便利でした。

ご覧いただき、ありがとうございました。

参考文献

内部アプリ共有 | Google Play Console

Quick share / internal app sharing / 内部アプリ共有でaab、apkをURLで共有する #Android - Qiita

エンジニア母の復職前の不安と実際の経験

こんにちは。エキサイト株式会社のあはれんです。

エキサイトホールディングス2023アドベントカレンダー14日目をお届けします。

qiita.com

今年の4月に時短勤務で復帰して、1歳児の母をやりながらエンジニアとして働いています。

エンジニアの女性は周りに少なく、復職前は自分が働きながら育児ができるのか不安でした。 漠然とした不安に直面していましたが、実際に復職してみてどうだったのか、その経験を共有したいと思います。

仕事と育児の両立は、エンジニアという職種や性別に関係ないかもしれませんが、同じ悩むを持つ方々にとって、少しでも参考になれば幸いです。

Q. 仕事と育児を両立しながら生活できるのか?

A: できてます

子供が生まれる前は、通常時は10:00-18:00で勤務し、繁忙期には21時頃まで働くこともありました。 仕事で体力や集中力を使い切って、退勤後は仕事の疲れを癒すことで精一杯だったので、これに育児というタスクが追加されて、体力が持つのか、生活ができるのか不安でした。

そんな不安もあり、私は時短勤務で復職し、9:00-16:00 で勤務しています。 またリモートワークをしているので、通勤時間が大幅に短縮できています。

退勤後は、保育園に子供を迎えに行き、その後は家事をこなしながら子供と遊んで過ごしています。 子供が寝た後に、明日の準備をしつつ、個人の時間として、勉強したり趣味を楽しんだりしています。 個人の時間は大幅に減りましたが、それでも無理なく日々を過ごすことができています。

育児と仕事を両立することができているのは、 時短勤務による仕事量の調整や、リモートワークを受け入れてくれているチームメンバーの協力のおかげです。 この協力に本当に感謝しています。

Q. 子供の体調不良で休みが続くことで仕事に支障はないのでしょうか?

A. チームメンバーの協力のおかげで、できてます

保育園から帰宅後は、手と足を念入りに洗い、夜はたっぷり寝かせ、出掛ける際は人混みを避けて…とできる限りの対策をしますが、 子供の風邪を防ぐことは難しいです。

1週間に2日しか出勤しないときもありましたが、 仕事の量やスケジュール調整していただき、乗り越えることができました。

本当にチームメンバーには感謝の気持ちでいっぱいです。

Q. 子供を見ながら仕事はできますか?

A. 基本は無理ですが、ミーティングならギリギリできる時があります

テレビを見てもらったり、一人で遊んでもらいますが、やはり親が居ると一緒に遊びたいので近寄ってきます。 モニターやキーボードを触ったり、遊びに誘ってくることもしばしばあり、それを断りながら仕事を進めるのは非常に難しい状況です。

ミーティングなどの人が話している状況だと、 子供も話に興味を持って静かに聞いてくれたりすることもあるので、 ギリギリ参加できることがあります。

ただし、基本的には難しい状況です。

Q. スキルアップのための時間は確保できます?

A. 業務中や夜に少しずつコツコツやっています

以前は、休日に数時間集中して開発したりすることがありましたが、そのような時間の確保は難しくなりました。 なので、業務中や夜に毎日少しずつコツコツと学習に励んでいます。

子供が生まれてから、子供に尊敬されると人間になりたいという気持ちが芽生え、以前よりスキルアップに対するモチベーションが高まった気がします。

1日10分でも時間を確保することで、先日、AWS DVAに合格することができました。 ガッツリ開発できるほどの時間はありませんが、毎日少しずつ継続することで、自身の技術力を向上させるように励んでいます。

まとめ

家庭の状況によって変わると思いますが、一例として紹介させていただきました。

弊社の制度(フレックスタイム/リモートワーク等)と、チームメンバーの手厚い協力のおかげで、育児しながらで働くことができています。本当に感謝しかありません..!!!

弊社ではエンジニア募集していますので、ぜひ興味ある方はチェックしてみてください。

www.wantedly.com

JavaのシリアライザであるKryoで圧縮設定を追加する

エキサイト株式会社メディア事業部エンジニアの佐々木です。以前Kryoを用いてJava内のデータをシリアライズする記事を書きました。

tech.excite.co.jp

その後、弊社内でもポツポツKryoを使っていますが、Kryo公式にシリアライズデータをさらに圧縮する方法があるのでご紹介します。

前提

前回のものと同じ条件にします。

$ java --version
openjdk 17.0.2 2022-01-18
OpenJDK Runtime Environment Temurin-17.0.2+8 (build 17.0.2+8)
OpenJDK 64-Bit Server VM Temurin-17.0.2+8 (build 17.0.2+8, mixed mode)

$ ./gradlew --version

------------------------------------------------------------
Gradle 7.6.1
------------------------------------------------------------

Build time:   2023-02-24 13:54:42 UTC
Revision:     3905fe8ac072bbd925c70ddbddddf4463341f4b4

Kotlin:       1.7.10
Groovy:       3.0.13
Ant:          Apache Ant(TM) version 1.10.11 compiled on July 10 2021
JVM:          17.0.2 (Eclipse Adoptium 17.0.2+8)
OS:           Mac OS X 12.5 aarch64

前回のコード

前回のコードは下記になります。(多少差分が見やすいように変更を加えています。)

サンプルで用意したデータモデルをKryoを通してシリアライズとデシリアライズするというものになります。

public class KryoSample {

    public void main(){

        Kryo kryo = new Kryo();
        kryo.setRegistrationRequired(false);
        kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

        DataModel dataModel = new DataModel();
        byte[] bytes = serialize(kryo, outputStream, dataModel);

        DataModel deserializeDataModel = deserialize(kryo, bytes);
        System.out.println("Kryo Serialize and Deserialize consistency: " + dataModel.equals(deserializeDataModel));

    }

    private DataModel deserialize(Kryo kryo, byte[] bytes) {
        try (Input input = new Input(new ByteArrayInputStream(bytes))) {
            DataModel dataModel = kryo.readObject(input, DataModel.class);
            return dataModel;
        }
    }

    private byte[] serialize(Kryo kryo, ByteArrayOutputStream outputStream, DataModel dataModel) {
        try (Output output = new Output(outputStream)) {
            kryo.writeObject(output, dataModel);
            output.flush();
        }
        byte[] bytes = outputStream.toByteArray();
        return bytes;
    }

}

圧縮処理を追加したコード

圧縮処理を追加する場合は下記のようなコードになります。

public class KryoSample {

    public void main(){

        Kryo kryo = new Kryo();
        kryo.setRegistrationRequired(false);
        kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

        DataModel dataModel = new DataModel();
        byte[] bytes = serialize(kryo, outputStream, dataModel);

        DataModel deserializeDataModel = deserialize(kryo, bytes);
        System.out.println("Kryo deflate Serialize and Deserialize consistency: " + dataModel.equals(deserializeDataModel));


    }

    private DataModel deserialize(Kryo kryo, byte[] bytes) {
        try (InflaterInputStream inflaterInputStream = new InflaterInputStream(new ByteArrayInputStream(bytes));  // 圧縮データを解凍するためのInflaterInputStreamを通す
             Input input = new Input(inflaterInputStream)) {
            DataModel dataModel = kryo.readObject(input, DataModel.class);
            return dataModel;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private byte[] serialize(Kryo kryo, ByteArrayOutputStream outputStream, DataModel dataModel) {
        try (
                DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(outputStream);  // データを圧縮するためのDeflaterInputStreamを通す
                Output output = new Output(deflaterOutputStream)) {
            kryo.writeObject(output, dataModel);
            output.flush();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        byte[] bytes = outputStream.toByteArray();
        return bytes;
    }

}

Kryoが用意してくれている圧縮・解凍用のInputStream/OutputStreamを通すだけになります。チェック例外が投げられるので、try-catchをする必要があります。

テスト実行

上記を実行します。

Kryo Serialize and Deserialize consistency: true
Kryo deflate Serialize and Deserialize consistency: true

上記の通り、圧縮・解凍処理の方も正常に実行されています。

サイズの比較

圧縮率等を比較するために約1000文字のテキストを使用して比較します。

original size:2974
deflate size:1207

約60%サイズダウンになっています。データにもよりますけど、ネットワーク転送量などがこのくらい下がるとお財布にも優しそうです。

まとめ

今回は、Kryoで簡単にデータ圧縮を行う方法をご紹介させていただきました。弊社でよく利用しているSpringBootですとJacksonでシリアライズすることが多いですが、JSONデータにして保存するのでサイズはKryoの通常とあまり変わらない印象です。Kryoだと少ない手間で圧縮・解凍ができ、CPU使用率もほとんど増えないので、弊社での導入も継続的に行おうと思います。

最後に

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しております。長期インターンも歓迎していますので、興味があれば連絡いただければと思います。

募集職種一覧はこちらになります!(カジュアルからもOK) www.wantedly.com

ボタンにまつわるエトセトラ(HTMLとアクセシビリティ)

こんにちは。エキサイトで内定者アルバイトとしてデザイナーをしている齋藤です。 エキサイトホールディングス Advent Calendar 202313日目は、私齋藤が担当させていただきます。

今回は『ボタンにまつわるエトセトラ』と称し、UIデザインで最も頻用される要素と言っても過言ではない「ボタン」について、HTMLやアクセシビリティの観点からお話したいと思います。

そもそも「ボタン」とは?

改めて言語化する機会が少ないのでおさらいをしておきたいと思います。

一言で言えば「ユーザーと対話するための要素」であると表現することができると考えています。

ボタンはユーザーに対して何らかの機能やアクションを提示し、ユーザーはボタンを通じて応答することでアプリケーション内で行動をすることができます。まさに、対話のような双方向の要素と言うことができますね。

UIでは単純にリンクとして画面遷移させる場合からCRUDのような操作を促す場合まで、ありとあらゆる操作のためにボタンを実装することになります。

それってaタグ?buttonタグ?

Webにおいてボタンを表現する(マークアップする)際には、ユーザーがクリックできる要素であるaタグとbuttonタグの2つのどちらを使用することが多いと思います。

しかし、どちらを使えば良いのか迷ってしまい、とにかく全部どちらか一方にしてしまうのが初心者がやりがちなことかと思います。(私もHTML触れたてのときはとにかくaタグにしてしまった経験があります・・・)

結論

結論から言うと、

  • 内外問わずに他ページへ遷移させる場合 = aタグ
  • 何らかのアクション(ボタンを通じてJavaScriptのイベントを発動させるなど)をトリガーさせる場合 = buttonタグ

の使い分けで良いのではないかと考えています。理由を含めてそれぞれのタグについて少し詳しく見てみましょう。

aタグ

MDNでは以下のように説明されています。

<a> は HTML の要素(アンカー要素)で、href 属性を用いて、別のウェブページ、ファイル、メールアドレス、同一ページ内の場所、または他の URL へのハイパーリンクを作成します。

aタグは本質的にハイパーリンクをユーザーに対して表し、ページ遷移などに用いられることが期待されている要素であることがわかります。

よって、内外問わずに他ページへ遷移させる場合にはaタグを用いるということが言えると考えます。

buttonタグ

一方でbuttonタグは以下のように説明されています。

<button> は HTML の要素で、マウス、キーボード、指、音声コマンド、その他の支援技術で起動することができる操作可能要素です。起動すると、フォームを送信したりダイアログを開いたりといった操作を実行します。

このことから、フォームの送信など、何らかのアクションをトリガーさせる場合にはbuttonタグを用いると言えると考えます。

アクセシビリティを考えてみる

最後に、UIデザインでボタンを考える上で、アクセシビリティや体験の向上のために考えておきたいことを少しだけ紹介します。

ボタンの状態変化

ボタンは状況によって表層的に異なる状態にすることで、ユーザーの体験を向上させることができますので、通常状態だけではなく状態変化も考慮してデザインを考えることが大切だと考えます。

通常状態

通常状態の例
通常状態はなにもされていない状態の外観です。

Hover時

Hover時の例
Hover時はカーソルがボタンの上に位置するときの状態です。

Hover時にデザインに変化を加えることにより、ユーザーに自分の操作が反映されていることを感知させることができます。

Active時

Active時の例
Active時はボタンが押された瞬間の状態です。

こちらもデザインに変化を加えることにより、ユーザーのアクションを可視化し、自分の操作が正しく認識されていることを示すことができます。

Focus時

Focus時の例
Focus時はボタンがキーボードフォーカスを受けているときの状態です。

何らかの不自由があり、マウスではなくキーボードを用いて操作しているユーザーに対して、どの要素にフォーカスしているしているのかを明示することで操作性を向上させることができます。

デジタル庁の公式Webサイトでもtabキーで操作するユーザーを考慮して、Focus時のデザインが設定されているようです。

デジタル庁Webサイトより

また、:focusクラスにoutline: none;があるとtabキーでの操作時にフォーカスのスタイルが当たらなくなり操作が困難になってしまうため、記載しないことを強く推奨します。(リセットCSSなどにも注意が必要かもしれません)

その他

その他にもエラー時や無効時などの状態変化も場合によっては考慮する必要があります。

Figmaでデザインしている場合

余談にはなりますが、Figmaでデザインをしている場合はVariantsを使用することで状態変化をわかりやすく定義することができます。

通常状態をデフォルトとして、「isHover」や「isActive」などのプロパティを作り状態変化を再現できるようにすることがエンジニアさんとのFigmaを介したコミュニケーションでも円滑になると考えます。

また、その際に、各プロパティの値を真偽値にすることで、トグルスイッチで操作することができるのでおすすめです。

FigmaのVariants

余談の余談ですが、実装するされることを見越して、実際にも真偽値で管理されるべきプロパティには「is」を冠しておくとよりわかりやすいかと思われます。(Nuxt.jsを用いたプロジェクトにいた際は、ボタン内に表示されるテキストなどslotを使用するプロパティには「↪」を冠してわかりやすくしていました)

他にもbuttonタグ要素にARIA属性を付すことや、role属性、tabindex属性などHTML特有のアクセシビリティ対策がありますので、気になる方はMDNなどのドキュメントを参照することをおすすめします。

さいごに

今回は『ボタンにまつわるエトセトラ』として、マークアップやデザインをする上で考えておきたいボタンのあれこれをご紹介してみました。 UIに関わるすべての方の一助となれば幸いです。

エキサイトホールディングス Advent Calendar 2023では、他にもエンジニアやデザイナーが執筆した記事が公開されていますので、ぜひご覧ください!

ご精読ありがとうございました。

参考文献

【Flutter】wakelock_plus パッケージを使って画面スリープを無効にする

エキサイト株式会社の@mthiroshiです。 エキサイトホールディングス Advent Calendar 2023の13日目を担当します。

Flutter で画面スリープを無効にする(画面を常時点灯にする)wakelock_plus パッケージについて紹介します。

wakelock_plus とは

wakelock_plus は、画面スリープを無効にするためのパッケージです。 pub.dev

wakelock_plus には、前身となる wakelock というパッケージがあります。 pub.dev

wakelock はメンテナンスされていない様子なので、現在は wakelock_plus の利用が好ましいと思います。 いくつかの issue でも、wakelock_plus の利用が推奨されています。 github.com

Flutter の画面スリープの無効化について検索すると、 2023年12月時点では wakelock の方が検索結果の上位に出てくるので注意が必要です。

使い方

下記のバージョンで動作検証しました。

wakelock_plus: ^1.1.2

公式のサンプルコードが下記です。

import 'package:wakelock_plus/wakelock_plus.dart';
// ...

// The following line will enable the Android and iOS wakelock.
WakelockPlus.enable();

// The next line disables the wakelock again.
WakelockPlus.disable();

WakelockPlus を static に呼び出して、有効 / 無効を切り替えられます。

特定の画面でのみ画面スリープを無効にしたい場合は、flutter_hooks の useEffect を使って実装できます。

import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:wakelock_plus/wakelock_plus.dart';

class DemoScreen extends HookWidget {
  const DemoScreen({
    super.key,
  });

  @override
  Widget build(BuildContext context) {

    useEffect(
      () {
        WakelockPlus.enable();
        return WakelockPlus.disable;
      },
      [],
    );

    return Scaffold(
    /// 画面の実装
    );

終わりに

Flutter で画面のスリープを無効にする wakelock_plus パッケージについて紹介しました。 実装のコード数が少なく、簡単に実現できます。 参考になれば幸いです。

採用アナウンス

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しております。長期インターンも歓迎していますので、興味があれば連絡いただければと思います。

募集職種一覧はこちらになります!(カジュアルからもOK) www.wantedly.com

参考

wakelock_plus | Flutter Package

useEffect function - flutter_hooks library - Dart API

FCMで直接トークンを指定してマルチキャストでプッシュメッセージを送信する

こんにちは、エキサイト株式会社の平石です。エキサイトホールディングス Advent Calendar 2023の12日目を担当いたします。

今回は、FCMで直接トークンを指定してマルチキャストでプッシュメッセージを送信する方法をご紹介します。

はじめに

FCM (Firebase Cloud Messaging)は、メッセージを送信するための手段です。
主にiOSAndroid向けのプッシュ通知やブラウザで動作するWeb Pushを送信する際に利用できます。

通常は、メッセージの送信には、トピックを利用することが多いです。
トピックとは、ざっくりいうと「メッセージを受信するユーザーのグループ」のようなもので、ユーザーごとに生成されるトークンをトピックに紐付けて、そのトピックを対象にメッセージを送信します。 (パブリッシュ / サブスクライブ モデル)

しかし、稀になんらかの理由でトピックが作成されておらず、送信対象のユーザーのトークンだけが手元にあり、そのユーザーに対してメッセージを送信したいという場面があります。
また、なんらかの理由でトピックを利用せずに、自前でプッシュ対象を制御したい場面もあるかもしれません。

今回は、そのような場面に使える方法をご紹介します。

プッシュメッセージのマルチキャスト

その方法は、メッセージのマルチキャストです。
マルチキャストは、「複数の端末(ホスト)に対して同一データを一斉に送信する方法」です。

FCMでも、Firebase Admin SDKマルチキャストのためのメソッドが用意されており、これを利用することで指定したトークンを持つユーザーにメッセージを送信することができます。

早速ソースコードを見てみましょう。
今回はJavaで記述しますが、他の言語でも可能ですので公式ドキュメントも参考にしてください。

public void sendSeriesFollowPush() {
    final List<String> tokenList = /*トークンのリストを渡す*/;
    final String body = "プッシュメッセージ本文";
    final Map<String, String> data = Map.of(
            "article_id", "A123456789"
    );

    final Notification notification = Notification.builder()
            .setBody(body)
            .build();

    final MulticastMessage message = MulticastMessage.builder()
            .addAllTokens(tokenList)
            .putAllData(data)
            .setNotification(notification)
            .build();

    try {
        final BatchResponse batchResponse = firebaseMessaging.sendMulticast(message);
    } catch (FirebaseMessagingException e) {
        throw new InternalServerErrorException("プッシュの送信に失敗しました。");
    }
}

※ firebaseMessagingは、別途Beanを作成し、DIしたものを利用。Bean定義は付録を参照。

基本的な構成は、トピックを利用する方法と変わりませんが、MulticastMessageクラスのインスタンスに対して各種の設定をセットしている点と、sendMulticastメソッドを利用している点が大きな違いです。

通常は、Messageクラスを利用して以下のように記述します。

final Message message = Message.builder()
            .setTopic("topic")
            .putAllData(data)
            .setNotification(notification)
            .build();

これを見ると、setTopicでトピックをセットする代わりに、.addAllTokensトークンのリストを渡していることがわかります。

たったこれだけでトークンに対して直接メッセージを送信することが可能なのです。

なお、一度に指定できるトークンの数は最大500なので、それ以上のトークンを対象にメッセージを送信する必要がある場合には、この処理を繰り返す必要があります。

メッセージ送信時のレスポンス

sendMulticastメソッドを利用してメッセージを送信すると、BatchResponse型の戻り値が返ってきます。

これの中身を見てみましょう。

https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/messaging/BatchResponse

Publicメソッドとして、getFailureCount()getResponses()getSuccessCount()が用意されています。
getFailureCount()getSuccessCount()はその名の通り指定したトークンのうち、送信に失敗した数と成功した数を取得できます。

では、getResponses()はといいますと、各トークンごとの送信結果を持つSendResponse型のリストを取得することができるメソッドです。

https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/messaging/SendResponse

SendResponseの中身を見てみると、getException()getMessageId()isSuccessful()というメソッドがあり、それぞれ以下のようなものを取得できます。

  • getException() ・・・そのトークンに対する送信が失敗した場合に、発生した例外を取得できる
  • getMessageId()・・・送信処理が成功した場合のメッセージID
  • isSuccessful()・・・そのトークンに対する送信の成否

ここで、「そのトークン」に該当する情報がこのSendResponseクラスのインスタンスにはないことに注意してください。

公式ドキュメントによると、

The responses list obtained by calling getResponses() on the return value corresponds to the order of tokens in the MulticastMessage.

とあり、どうやら「送信時にaddAllTokensで指定したトークンリストの順番と、getResponses()で取得できる送信結果のリストの順番は一致しているから、その2つを照らし合わせてくれ」という仕様なようです。

どのトークンに対する送信が失敗したのかを得るためには、2つのリストを突合させる必要があるため、少し面倒ですね......。

そのため、「送信が失敗したトークンに対しては今後送信しないようにするために削除したい」といった理由で、そのようなトークンを取得するためには、以下のようにする必要があります。

final List<Boolean> isSuccessfulList = batchResponse.getResponses().stream()
                .map(response -> response.isSuccessful())
                .toList();

final List<String> failureTokenList = IntStream.range(0, tokenList.size())
                .filter(index -> ! isSuccessfulList.get(index))
                .mapToObj(index -> tokenList.get(index))
                .toList();

おわりに

今回は、FCMでプッシュメッセージをマルチキャストで送信する方法を紹介しました。

基本的にはトピックで管理するのが便利ですが、このような送信方法が必要になった場合には、ぜひ活用してみてください。

では、また次回。

参考文献

付録

@Configuration
@RequiredArgsConstructor
public class FirebaseConfig {
    private static final String MESSAGING_SCOPE = "https://www.googleapis.com/auth/firebase.messaging";

    private final FirebaseProperty firebaseProperty;

    /**
     * Firebase用のプロパティ
     *
     * @param serviceAccountKey Firebaseのサービスアカウントキー
     */
    @ConfigurationProperties(prefix = "firebase")
    public record FirebaseProperty(String serviceAccountKey) {}

    /**
     * FirebaseMessagingのDIのためのBean登録
     *
     * @return FirebaseMessaging
     */
    @Bean
    public FirebaseMessaging firebaseMessaging() throws IOException {
        final String serviceAccountKeyString = firebaseProperty.serviceAccountKey();
        final InputStream serviceAccountKey = new ByteArrayInputStream(serviceAccountKeyString.getBytes(StandardCharsets.UTF_8));

        final GoogleCredentials googleCredentials = GoogleCredentials
                .fromStream(serviceAccountKey)
                .createScoped(List.of(MESSAGING_SCOPE));

        final FirebaseOptions options = FirebaseOptions
                .builder()
                .setCredentials(googleCredentials)
                .build();

        final FirebaseApp app = FirebaseApp.initializeApp(options);

        return FirebaseMessaging.getInstance(app);
    }
}

あくまで、一例ですので公式ドキュメントなども参考にしてください。