sourceタグとpictureタグでレスポンシブに画像要素を切り替える

こんにちは。エキサイトでデザイナーをしている齋藤です。

エキサイトホールディングス Advent Calendar 2024 シリーズ1の2日目を担当します。

qiita.com

今回は、HTMLのsourceタグとpictureタグを使用してレスポンシブに表示する画像を切替える方法をご紹介します。

はじめに

冒頭、sourceタグとpictureタグについてざっくり整理します。

sourceタグとは

sourceタグは画像や動画などのメディアファイルを格納する要素です。

srcset属性にファイルを指定します。

media属性を使用するとCSSのようにメディアクエリーを指定できます。

<source srcset="..." media="(max-width: 480px)" />

※終了タグは記述しません

pictureタグとは

pictureタグは画像要素の一つで、子要素にはimgタグ(必須)とsourceタグ(任意)を持ちます。

ページが読み込まれた際に、子要素のsourceタグを検証してメディアクエリーなどの条件に合致する画像を表示します。

どのsourceタグの条件に合致しない場合は、imgタグの内容が使用されます。

どうやって実装するか

例えば、スクリーンサイズが480px以下の場合はmobile.pngを、それ以外の場合はdesktop.pngを表示したいとします。

その場合は以下のようになります。

<picture>
  <source srcset="mobile.png" media="(max-width: 480px)" />
  <img src="desktop.png" alt="hello world" />
</picture>

sourceタグには、「スクリーンサイズが480px以下の場合」という条件となるメディアクエリーをmedia属性に、条件に合致した場合に表示させたいmobile.pngsrcset属性に指定します。

「スクリーンサイズが480px以下の場合」以外はdesktop.pngを表示させたいため、imgタグにはdesktop.pngを指定します。

これで完成です。

注意点

sourceタグとpictureタグを用いる場合はいくつか注意すべきことがあります。

sourceタグはimgタグより前に置く

WHATWGが発行しているsourceタグの仕様書を見ると以下のような記述があります。

As a child of a picture element, before the img element.

これは、ブラウザがHTMLを上から順に解読するため、条件を持つsourceタグを先に検証して、どれにも合致しない場合はimgタグの内容を用いる仕組みのためです。

sourceタグに指定した画像のaltはimgタグの内容になる

sourceタグに指定したmobile.pngの代替テキスト(alt)は、desktop.pngalt属性の内容が用いられます。

<picture>
  <!-- mobile.pngのaltは「hello world」になる -->
  <source srcset="mobile.png" media="(max-width: 480px)" />

  <img src="desktop.png" alt="hello world" />
</picture>

さいごに

今回は、HTMLのsourceタグとpictureタグを使用してレスポンシブに表示する画像を切替える方法をご紹介しました。

画像周りのレスポンシブ対応をされる方の一助となれば幸いです。

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

Tailwind CSSで擬似クラス「:focus-within」をスタイリングする方法

こんにちは。エキサイトでデザイナーをしている齋藤です。

エキサイトホールディングス Advent Calendar 2024 シリーズ2の2日目を担当します。

qiita.com

今回は、Tailwind CSSCSSの擬似クラス:focus-withinをスタイリングする方法をご紹介します。

「:focus-within」とは

冒頭、CSSの擬似クラス:focus-withinについて整理します。

:focus-withinは、子要素や孫要素にフォーカスが当たっている場合に一致する擬似クラスです。

:focus-withinを使用すると以下のような体験を実現できます。Codepenに触れて、実感してみてください。

See the Pen Untitled by AyumuSaito (@ayumusaito-excite) on CodePen.

<!-- inputタグがフォーカスされている場合にdivタグの背景色が赤くなる -->
<div>
  <label for="...">氏名</label>
  <input type="text" id="..." name="..." />
</div>
div {
  background: #FFFFFF;
  
  /* 子要素のinputタグがフォーカスされている場合に一致して適用される */
  &:focus-within {
    background: #FEE2E2;
  }
}

Tailwind CSSでスタイリングするには

Tailwind CSSでは擬似クラスは、class名に接頭辞をつけることでスタイリングします。

:focus-withinの場合は、接頭辞focus-within:を使用します。

先の例の場合は以下のようになります。

<div class="bg-white focus-within:bg-red-100">
  ...
</div>

さいごに

今回は、Tailwind CSSCSSの擬似クラス:focus-withinをスタイリングする方法をご紹介しました。

Tailwind CSSを使用される方の一助となれば幸いです。

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

Tailwind CSSでアニメーション付きのスケルトンスクリーンをスタイリングする方法

こんにちは。エキサイトでデザイナーをしている齋藤です。

エキサイトホールディングス Advent Calendar 2024 シリーズ2の1日目を担当します。

qiita.com

今回は、Tailwind CSSでスケルトンスクリーンをスタイリングする方法をご紹介します。

ケルトンスクリーンとは

冒頭、スケルトンスクリーンとは何かを整理します。

ケルトンスクリーンは、読み込み中にコンテンツの代替となる図形を表示して、処理が実行中であることなどをユーザーに示すUXテクニックの一つです。

この記事では、以下の画像のようなフィードUIでのスケルトンスクリーンのスタイリング方法をご紹介します。

読み込み中はコンテンツの代替となる図形を表示する

Tailwind CSSでのスタイリング方法

Tailwind CSSでは、スケルトンスクリーンに有用なアニメーション用のUtility classが標準で搭載されています。

スタイリング方法を順を追って説明します。

骨組みをつくる

まずは骨組みをつくります。

この時点ではアニメーションはありません。

See the Pen Tailwind CSSでスケルトンスクリーン by AyumuSaito (@ayumusaito-excite) on CodePen.

<div class="p-4 rounded bg-white border border-slate-300">
    <div class="flex gap-3">
        <!-- プロフィール画像のプレースホルダー -->
        <div class="w-12 h-12 aspect-square rounded-full bg-slate-300"></div>

        <div class="flex flex-col gap-2">
            <!-- 文字のプレースホルダー -->
            <div class="h-6 w-24 rounded bg-slate-300"></div>
            <div class="h-6 w-40 rounded bg-slate-300"></div>
            <div class="h-6 w-80 rounded bg-slate-300"></div>
        </div>
    </div>
</div>

アニメーションを追加する

今回のようなフィードUIのスケルトンスクリーンのために、animate-pulseというアニメーション用のUtility classが用意されています。

animate-pulseプレースホルダー用の図形要素をラップする要素に指定します。

See the Pen Untitled by AyumuSaito (@ayumusaito-excite) on CodePen.

<div class="p-4 rounded bg-white border border-slate-300">
    <!-- 図形のラッパーにanimate-pulseを追加 -->
    <div class="animate-pulse flex gap-3">
        <!-- プロフィール画像のプレースホルダー -->
        <div class="w-12 h-12 aspect-square rounded-full bg-slate-300"></div>

        <div class="flex flex-col gap-2">
            <!-- 文字のプレースホルダー -->
            <div class="h-6 w-24 rounded bg-slate-300"></div>
            <div class="h-6 w-40 rounded bg-slate-300"></div>
            <div class="h-6 w-80 rounded bg-slate-300"></div>
        </div>
    </div>
</div>

さいごに

今回は、Tailwind CSSでスケルトンスクリーンをスタイリングする方法をご紹介しました。

Tailwind CSSを使用されている方の一助となれば幸いです。

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

@RestControllerAdviceを複数定義するときに注意すること

エキサイト株式会社エンジニアの佐々木です。エキサイトHDアドベントカレンダー1日目を担当させていただきます。

SpringBootのRestControllerAdviceが便利で多用しているのですが、複数定義したときにハマりましたので共有になります。

コード

事象が発生した当時は下記のような実装がされていました。

  • ExceptionController.java
public class ExceptionRestController {

    @ExceptionHandler(RuntimeException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Map<String, String> lineTokenInvalidException(RuntimeException ex) {
        log.error("error: {}", "RuntimeException");
        return Map.of("error", "RuntimeException");
    }
}
  • ExceptionRest2Controller.java
public class ExceptionRestController {

    @ExceptionHandler(RequestForbiddenException.class)
    @ResponseStatus(HttpStatus.FORBIDDEN)
    public Map<String, String> lineTokenInvalidException(RequestForbiddenException ex) {
        log.error("error: {}", "RequestForbiddenException");
        return Map.of("error", "RequestForbiddenException");
    }
}

現象

上記のコードのときに、疑似的にRequestForbiddenExceptionを発火させてみます。

@RequestMapping("sample")
public class SampleController {

    @GetMapping("request_forbidden")
    public String index(){
          throw new RequestForbiddenException();
    }
}
curl localhost:8080/sample/request_forbidden


`error: RuntimeException`

RequestForbiddenExceptionをthrowしましたのに、error: RuntimeExceptionが出力されました。RequestForbiddenExceptionを期待したのですが、1つめのExceptionRestController.javaが実行されてしまいました。

別々のクラスにすると、このような現象が起きるみたいです。

解決方法

解決方法は、1つのクラスにまとめると適切にハンドリングをしてくれます。

public class ExceptionRestController {

    @ExceptionHandler(RequestForbiddenException.class)
    @ResponseStatus(HttpStatus.FORBIDDEN)
    public Map<String, String> requestForbiddenException(RequestForbiddenException ex) {
        log.error("error: {}", "RequestForbiddenException");
        return Map.of("error", "RequestForbiddenException");
    }

    @ExceptionHandler(RuntimeException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Map<String, String> runtimeException(RuntimeException ex) {
        log.error("error: {}", "RuntimeException");
        return Map.of("error", "RuntimeException");
    }
}

先程のコマンドを実行してみます。

curl localhost:8080/sample/request_forbidden


`error: RequestForbiddenException`

まとめ

ControllerAdviceは、AOPを使用した便利な実装ですが、エラー等もでないのでちょっとこわいとおもいました。濫用はせずに同じ用途のクラスでまとめておくことが大事かもしれません。

Notion初心者による、スプリント風タスク管理の作り方

こんにちは!SaaS・DX事業部デザイナーの鍜治本です!

皆さん、タスク管理はどのようにしていますか?
SaaS事業部では元々Jiraを使っていましたが、エンジニアと同じプロジェクトで管理するのが難しくなったこともあり、Notionへ移行しました。ただ、使い始めてみると運用面で少し物足りなさを感じることも。そこで、数式やオートメーションを活用して工夫を加えた仕組みを構築しました。

この記事では、実際に作成したアウトプットや手法をご紹介します。Notionのデータベース設定や、アラートの作り方を知りたい方のヒントになれれば嬉しいです🙌

背景:JiraからNotionへの移行と課題

もともとJiraを使ってエンジニアと同じスプリント管理をしていましたが、以下の課題がありました。

  1. デザイナーのタスクはエンジニアのスプリントと異なり、期限や進行ペースが不安定。
  2. その結果、関連性を維持しつつも運用が煩雑になり、生産性が低下してしまいました。

そこでNotionに移行し、デザイナー専用のタスクデータベースを作成。しかし、最初は可視化を重視したシンプルな構造だったため、次のような課題が浮き彫りに。

  • タスクが進まないまま停滞する(特に私自身が…反省)。
  • 飛び込みタスクや長期的なタスクが混在し、優先順位の管理が曖昧に。
  • タスクの進捗がスプリント単位で可視化されず、目標達成が難しい状態。

スプリントの仕組みを取り入れる試み

今年の8月、タスク過多に対応するため、デザイナー間でスプリント風の運用を始めました。毎週月曜日に「ウィークリーシンク」を開催し、その週のタスクを確認・分担するルールを設定。

しかし、何度か実施するうちに「タスクの停滞問題」が再発…。スプリントに挙げたはずのタスクが忘れ去られ、再び進捗が止まる現象が起こりました。

これを改善するために考えたのが、Notionで1週間を基準にタスクアラートを作る仕組みです。

Notionにタスクアラートを追加してみる

今回の取り組みでは、既存の「ステータス管理プロパティ」に加え、タスクの進捗を監視・通知するための4つの新しいプロパティを作成しました。それぞれの役割は以下の通りです:

  1. ステ変更日:ステータスが変更された日付を記録するプロパティ。

  2. 週末日:「ステ変更日」の週末にあたる日付を算出する数式プロパティ。

  3. 週末月曜日:「週末日」の翌日(次の週の月曜日)を特定する数式プロパティ。

  4. 超過サイレン:ステータス変更後、翌週になってもタスクに動きがない場合にアラートを表示する数式プロパティ。

さらに、オートメーション機能を活用して、ステータスに変更があった際、自動的に「ステ変更日」にその日付が入力される仕組みを作りました。

可視化するためのプロパティ構成

Notionのデータベースの数式やオートメーション機能を活用し、以下のプロパティを追加しました。

「ステ変更日」のオートメーション

データベースに対して、日付が自動的に入力されるオートメーションの機能をつけています。

今回は、タスクにつけている進行度合いを管理するプロパティ「ステータス」の変更をトリガーに、「ステ変更日」に変更したタイミングの日時を記録させています。

ステータスをトリガーに、変更があった場合の日付を「ステ変更日」に入れる仕組み

「週末日」プロパティの数式

このプロパティでは、「ステ変更日」を基に、その週の週末(例:日曜日)の日時を計算します。

週末日プロパティに記述した数式

dateAdd(
    dateStart(prop("ステ変更日")),
    7 - toNumber(format(formatDate(prop("ステ変更日"),"e"))),
    "days"
    )

「週末月曜日」プロパティの数式

週末日の翌日である、次の週の月曜日を特定するプロパティです。これにより、タスクの停滞を翌週ベースで確認できます。

週末月曜日プロパティに記述した数式

dateAdd(
    prop("ステ変更日"),
    8 - toNumber(formatDate(prop("ステ変更日"),"e")),
    "days"
)

「超過サイレン」プロパティの数式

タスクが翌週月曜日以降も動いていない場合、アラートを表示します。タスクの停滞を視覚的に把握するためのプロパティです。

超過サイレンプロパティに記述した数式

if(
    now() >= prop("翌週月曜日"),
    "🚨", 
    ""
    )

実際に動かしているもの

「ステータス」を変更すると「ステ変更日」に日付が入る様子
作成したタスクのステータスを変更すると、「ステ変更日」プロパティに自動的に今日の日付が入りましたね。

(流石に1週間待つのは大変なので)擬似的に週が変わった挙動をさせるため、「ステ変更日」を26日に変更します。28日と同じ週なので、「超過サイレン」が空のままになっています。

Notionの「ステ変更日」の日付を変え、「超過サイレン」が空になっていることを確認

再び「ステ変更日」のプロパティを開き、今度は19日に変更してみます。28日を基準に見た時、19日は前の週になるので「超過サイレン」にサイレン🚨絵文字が表示されるようになりました!

Notionの「ステ変更日」を19日に変え、「超過サイレン」に絵文字が表示された

まとめ

今回は、既にNotionでタスク管理をしている状態で、スプリント風の使い方を再現のため実践した事について記事にしてみました。 大したことはやっていないものの、運用中のものに機能を載せ込む難しさを感じつつ、理想通りに動いたのでよかったなと感じています。

また、数式プロパティへの記述はChatGPTに相談しながら進めたものです。数式の正しさは実際に当て込まないとわからず、何度か嘘つかれました…😂

まだ設定したばかりですが、今後どこまで効果があったかも追いかけていこうと思います。最後まで読んでくださり、ありがとうございました!

既存コンポーネントの改善・修正に取り組みました

こんにちは!現在、エキサイト株式会社 SaaS・DX事業部でデザイナーとして内定者アルバイトをしているやのふきです。

この記事では、KUROTENのUI改善業務で取り組んだ内容について紹介します。

業務内容

Menu with filterのコンポーネント修正

今回修正を行なったMenu with filterは組織・部門や科目を選択するためのコンポーネントです。組織などの階層構造を持つ情報をファイルツリーのように表示します。

既存のデザインではMenu with filter内にトグルによる折りたたみ機能が実装されていたものの、折り畳まれる仕組みが気づいてもらいにくかったため修正・改善を行いました。

実際の使用シーン(修正後のUI)

ステップ

  1. 既存UIの構造整理
  2. 似ているUIの調査
  3. KUROTENでのUIパターン作成

1. 既存UIの構造整理

まずは実際に現在リリースされているデザインを触ってみて自分なりに感じたことを整理していきました。左が組織・部門、右側が科目を選択するためのコンポーネントです。

今回難しいと感じた点は、Menu with filterの機能として親要素も選択できる要素であることでした。トグル機能を実装するにあたって、トグルとしての機能と要素選択の機能が干渉しないように工夫する必要がありました。

実際に操作してみて感じたことを整理

2. 似ているUIの調査

上記で整理したことを踏まえて、KUROTENと同じような使い方をする他のサービスのUIを集めました。自分がよく使うサービスで同じような機能がなかったか思い出したり、インターネット上で階層構造を持つUIを検索したりして参考事例を探しました。

他のサービスのUIを集めて整理

各サービスのUIの共通点・相違点から、KUROTENのUIを改善するにあたって取り入れられそうな要素をまとめました。

  • 親要素のフォントをBoldに変更する

  • 子要素の先頭に・を追加する

  • 親要素の内包範囲にラインを追加する

  • 親要素についても子要素と同じようにホバー時にハイライトを付けることで選択できる要素であることをわかりやすくする

  • アイコンをホバーした時にトグルアイコンに変更することでトグル機能があることを強調する

3. KUROTENでのUIパターン作成

ステップ2までを通して、改善案を洗い出すことができたので実際にKUROTENのデザインに落とし込んでいきます。取り入れられそうな要素を組み合わせ、5パターンを作成しました。

作成したものを事業部の先輩デザイナーやエンジニアの方にレビューいただき検討を進めました。使用するアイコンなど、どのような人がサービスを利用しているのかを考えながら取捨選択していきました。今回はWindowsを使っているユーザーが多いことを考慮し、フォルダの開閉アイコンがデフォルト状態で、ホバーした時にトグルアイコンに変わるUIを採用することになりました。

改善要素を組み合わせて出した5パターンのUI

まとめ

今回のコンポーネントの改善・修正は、以下の3ステップで進めました。

  1. 既存UIの構造整理
  2. 似ているUIの調査
  3. KUROTENでのUIパターン作成

いきなりUIの調査を始めるのではなく既存UIの構造整理をしたことで、KUROTENで参考になるUIを調査段階で選びながら進めることができました。また、似ているUIの調査を行うことで改善案のパターン出しをスムーズに行うことができました。

修正前のUIと修正後のUI

〜学び〜 より良いアウトプットをするためには整理(理解)・調査を怠らないこと!

最後まで読んでいただきありがとうございました。

Androidの実機ビルド方法(Android Studio)

こんにちは。エキサイトでアプリエンジニアをしている岡島です。今回は、開発したアプリをAndroidの実機でテストする方法について解説していこうと思います。実機ビルドは、エミュレーターでは確認しづらいピンチイン、ピンチアウト、マルチタップでの操作や、端末のセンサー類を用いた動作の検証時に必要になってくると思うので、ぜひご参考にしてください。

環境について

Android Studio: Ladybug | 2024.2.1
Android バージョン: 15
デバイス: Pixel 6

設定について

Androidの実機ビルドをするには2つの方法があります。

  • USBケーブルを用いて接続する方法
  • Wi-Fiを用いて接続する方法

このどちらも、デバイスでのセットアップが必要となるので 順に説明していきます。

バイスでの設定

1.開発者モードを有効にする

設定アプリを開き、ビルド番号を7回タップ

Google Pixelの場合

バイスによってビルド番号の表示場所が異なるので、詳しくはこちらのURLを参照ください。

デバイスの開発者向けオプションを設定する  |  Android Studio  |  Android Developers

2.設定アプリを開き、システム開発者向けオプションをオンにする

3.必要に応じて、USBデバッグ、ワイヤレスデバッグをオンにする

USBケーブルを用いて接続する方法

USBケーブルを使った方法は簡単です。

1. ケーブルを用いて接続する

2. Android Studioで実機デバイスを選択してビルドする

以上です!

Wi-Fiを用いて接続する方法

こちらはAndroid 11 以降の機能となりますのでご注意ください。

1.実機デバイスとPCが同じWi-Fiを接続する

2.Android StudioのDevice Managerから[Pair Devices Using Wi-Fi]を選択

3.QRコードやペア設定コードを用いてペアリング設定を行う

ペアリング設定が完了するとAndroid Studioから端末が選択できるようになります。

まとめ

今回はAndroid Studioを用いて、Androidの実機ビルドをする方法をご紹介しました。やってみると意外に設定項目が少なく、すぐにビルドができました。 この記事がお役に立てれば幸いです。 最後まで読んでいただきありがとうございました。

【Flutter/Dart】定期的に関数を呼び出す方法:Timerを用いた実装

こんにちは、エキサイトでアプリエンジニアをしている岡島です。

今回はDartを用いて、定期的に関数を実行したい時にどうすればいいか、Timerを用いた実装を紹介していきます。

はじめに

開発をしていると、定期的にAPIからデータを取得したい場合のように、特定の関数を一定間隔おきに実行したいケースがあります。そのような時に、DartのTimerクラスを活用することができます。

Timerの基本的な使い方

一度だけ実行する場合

Duration(seconds: 〇〇)というように実行したいタイミングを指定することができます。 以下の例では2秒後に実行されますが、dayshoursminutesなども設定できます。

詳しくは下記ドキュメントを参照ください。

Duration class - dart:core library - Dart API

Timer(Duration(seconds: 2), () {
 print('2秒後に実行されます');
});

一定間隔ごとに実行する場合

Timer.periodicを利用して、インターバルの時間を設定することができます。

以下の例では5秒ごとに実行されます。

Timer.periodic(Duration(seconds: 5), (timer) {
  print('5秒ごとに実行されます');
});

Timerを停止する

Timer.periodicを使用していると、アプリがバックグラウンドに移行した時など、特定の条件でTimerを停止したくなる場合があります。

そのような時にはtimer.cancel();を使用します。

以下に1秒ごとにカウントをインクリメントし、5回になったらタイマーを止めるサンプルプログラムを書いてみました。

import 'dart:async';

int counter = 0;

void fetchData() {
  print('データを取得しました');
  counter++;
}

void main() {
  Timer.periodic(Duration(seconds: 1), (timer) {
    fetchData();

    if (counter == 5) {
      timer.cancel();
      print('Timerが停止しました');
    }
  });
}

まとめ

このように、定期的に処理を実行したい場合、DartのTimerクラスを活用することで実装ができます。Riverpodなどの状態管理パッケージとTimerを組み合わせることで、複雑なアプリケーションでもタイマー処理を適切に管理することができます。

読んでいただき、ありがとうございました。

scroll-behaviorでページ内遷移のスクロールを滑らかにする方法

こんにちは。エキサイトでデザイナーをしている齋藤です。

今回はCSSプロパティの一つであるscroll-behaviorを使用して、ページ内遷移のスクロールを滑らかにする方法をご紹介します。

実現したい挙動

冒頭、ページ内遷移のスクロールについて、既定の挙動と実現したい挙動をお示しします。

Codepenに触れて体験の違いを感じてみてください。

既定の挙動

既定の挙動は以下の通りです。スクロールされるというよりかジャンプするに近い挙動です。

See the Pen ページ内遷移:既定のスクロール挙動 by AyumuSaito (@ayumusaito-excite) on CodePen.

実現したい挙動

実現したい挙動は以下の通りです。遷移先に対して滑らかにスクロールします。

See the Pen ページ内遷移:滑らかなスクロール by AyumuSaito (@ayumusaito-excite) on CodePen.

どうやって実現するか

ページ内遷移によって発生するスクロールの挙動は、scroll-behaviorで制御することができます。

既定の値はautoとなっており、スクロールされるというよりかジャンプするに近い挙動です。

一方で値をsmoothに指定すると、遷移先に対して滑らかにスクロールされるようになります。

See the Pen ページ内遷移:滑らかなスクロール by AyumuSaito (@ayumusaito-excite) on CodePen.

* {
  scroll-behavior: smooth;
}

Tailwind CSSでの指定方法

Tailwind CSSではscroll-behaviorに対するUtility classが用意されています。

Utility class CSS
scroll-auto scroll-behavior: auto;
scroll-smooth scroll-behavior: smooth;

さいごに

今回はCSSプロパティの一つであるscroll-behaviorを使用して、ページ内遷移のスクロールを滑らかにする方法をご紹介しました。

スタイリングされる方の一助となれば幸いです。

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

【Dart/Flutter】ListとBuiltListの違いとBuilt Collectionについて

こんにちは、エキサイトでアプリアンジニアをしている岡島です。 今回は、BuiltListを使う機会があったので、調べたことについて共有したいと思います。

BuiltListとBuilt Collectionについて

BuiltListは、DartBuilt Collection Libraryに用意されている型です。

Built Collectionで用意されているコレクションは、Dartのcoreライブラリで用意されているコレクションとは異なり、次のような特徴を持ちます。(built_collection library - Dart APIより和訳)

  • 変更不可(Immutable)
  • 比較可能(Comparable)
  • ハッシュ可能(Hashable)
  • nullを拒否(Reject nulls)
  • ジェネリクス型パラメータが必要(Require generic type parameters)
  • 間違った型の要素を拒否(Reject wrong-type elements)
  • 不要なコピーを避けるためCopy-on-Writeを使用(Copy-on-write to avoid copying unnecessarily)

Dartで注意が必要な参照渡し

Dartのcollections(List、Map、Set)は参照渡しとなるので注意が必要です。 以下のように、リストを別の変数に代入した場合、同じメモリを参照するため、一方のリストを変更すると両方のリストに影響が出てしまいます。

List<int> originData = [1, 2, 3];
final copiedData = originData;

copiedData[0] = 99;

print(originData); // [99, 2, 3]
print(copiedData); // [99, 2, 3]

BuiltListで参照渡しの問題を防ぐ

BuiltListは変更不可なため、このようなバグを防ぐことができます。

BuiltList<int> originData = BuiltList<int>([1, 2, 3]);

// originDataをコピーしても、内容を直接変更することはできない
BuiltList<int> copiedData = originData;

// copiedData[0] = 99; // これはエラーになる

// rebuildを使って新しいリストを作成
BuiltList<int> modifiedData = originData.rebuild((b) => b[0] = 99);

print(originData); // [1, 2, 3] - 元のリストは変更されない
print(modifiedData); // [99, 2, 3] - 新しいリストが作成される

Built Collectionの特徴

BuiltListとBuilt Collectionについての章で述べたBuilt Collectionの特徴についてもう少し詳しく見ていきます。

変更不可(Immutable)

Built Collectionは変更不可であり、一度作成した後に変更することができません。データの変更は、rebuildメソッドを使って新しいインスタンスを作成することで行います。

比較可能(Comparable)

List<int> list1 = [1, 2, 3];
List<int> list2 = [1, 2, 3];

print(list1 == list2); // false

普通のListの場合、メモリの参照を比較するので、list1とlist2は異なるインスタンスとして認識されます。

BuiltList<int> list1 = BuiltList<int>([1, 2, 3]);
BuiltList<int> list2 = BuiltList<int>([1, 2, 3]);

print(list1 == list2); // true

Built Collectionは、deep comparisonを行うため、内容が同じ場合は等しいと判定されます。

ハッシュ可能(Hashable)

deep hashCodeが計算されてキャッシュされるようです。

プログラムでみると分かりやすいと思うのでサンプルコードを書いてみました。 含まれているデータの内容を見て、ハッシュコードが計算されるので、 データが同じであればハッシュコードも同じです。

普通のList:

List<List<int>> coreList1 = [
  [1, 2, 3]
];
List<List<int>> coreList2 = [
  [1, 2, 3]
];
// ハッシュコードが異なる
print(coreList1.hashCode == coreList2.hashCode); // false

BuiltList:

BuiltList<BuiltList<int>> builtList1 = BuiltList<BuiltList<int>>([
  BuiltList<int>([1, 2, 3])
]);
BuiltList<BuiltList<int>> builtList2 = BuiltList<BuiltList<int>>([
  BuiltList<int>([1, 2, 3])
]);
// ハッシュコードが同じ
print(builtList1.hashCode == builtList2.hashCode); // true

nullを拒否(Reject nulls)

nullの要素が入っていると、エラーが発生します。

try {
  BuiltList<int> listWithNull = BuiltList<int>([1, null, 3]);
} catch (e) {
  print(e); // エラー
}

ジェネリクス型パラメータが必要

Built Collectionでは型パラメータを指定する必要があります。

Listでは型を指定せずに宣言すると警告が出ず、任意のタイプのListになるため、バグが起こりやすくなります。

List mixedList = [1, "two", 3.0]; // List<dynamic>となり、特定の操作をする時にエラーが発生する可能性がある。

しかし、BuiltListなどのBuilt Collectionでは、型を指定しないとエラーになるため、このような問題は発生しません。

BuiltList mixedList = [1, "two", 3.0]; // エラー

間違った型の要素を拒否(Reject wrong-type elements)

違う型の要素を追加しようとするとエラーが発生します。

BuiltList<int> numbers = BuiltList<int>([1, 2, 3]);
numbers.rebuild((b) => b..add("string")); // エラー

不要なコピーを避けるためCopy-on-Writeを使用(Copy-on-write to avoid copying unnecessarily)

「Copy-on-Write」の原則を使用しており、データが変更されるときのみ新しいインスタンスを作成します。これにより、メモリ使用量が最適化されます。

以下の例で示すように、identicalで同一オブジェクトであるかを判定すると、値が同じものは同一オブジェクトになっています。

BuiltList<int> originalList = BuiltList<int>([1, 2, 3]);
BuiltList<int> sameList = originalList.rebuild((b) => b);

print(identical(originalList, sameList)); // true (同じインスタンス) 

BuiltList<int> modifiedList = originalList.rebuild((b) => b..add(4));

print(identical(originalList, modifiedList)); // false (新しいインスタンスが作成される)

最後に

今回はBuilt Collectionのライブラリについて詳しく見てみました。 この記事がBuilt CollectionやBuiltListの理解を深める助けになれば幸いです。

【Dart】カスケード記法(..)についてと注意が必要なmapの操作

こんにちは。エキサイトの岡島です。今回はカスケード記法(Cascade notation)についてご紹介していこうと思います。

業務中に誤ってカスケード記法を用いてmapの操作を行なっており、期待した動きにならなかったので自戒の念をこめてこの記事を書いています。ドット(.)1つの違いで思わぬ事態が起きてしまったので気をつけたいです。

カスケード記法(..)とは

カスケード記法(..)を使用すると、同じオブジェクトに対して複数の操作を連続して行うことができます。

簡単な例を以下に記します。

class Person {
  String name = '';
  int age = 0;

  void introduce() {
    print('My name is $name and I am $age years old.');
  }
}

void main() {
  final person = Person()
    ..name = '太郎'
    ..age = 5
    ..introduce();

  print(person.name);
  print(person.age);
}
  1. Personクラスのインスタンスを生成
  2. nameプロパティに太郎を設定
  3. ageプロパティに5を設定
  4. introduceメソッドを呼び出す

このステップをカスケード記法を使って記述しています。 このようにカスケード記法を使用することで、 初期化など一連の操作をまとめて書くことができるようになるという利点があります。

ちなみにカスケード記法を使わない場合は以下の様になります。

  final person = Person();
  person.name = 'Alice';
  person.age = 30;
  person.introduce();

カスケード記法でmapの操作を行うときには注意

カスケード記法でmapの処理を書くと、mapの操作が反映されないので注意が必要です。

操作が反映されない具体例

具体的にサンプルコードを用いて説明していきます。

  // 問題となるmapの例
  final list = [1, 2, 3]
    ..map((e) => e * 2)  // この操作は反映されない
    ..add(4);
  print(list); // [1, 2, 3, 4] - 2倍の操作が反映されていない

この例では、リストの要素をmapの操作で2倍にしたあとにリストに4を追加しています(処理後の結果は[2,4,6,4]になることを期待)。 しかし、処理後の中身を確認してみると[1,2,3,4]となっています。

つまり ..map((e) => e * 2)の部分は反映されておらず ..add(4)の部分だけが反映されているコードになります。

なぜこの様なことが起こるのか

この問題が発生する理由は、同じオブジェクトを操作するカスケード記法に対して、map メソッドが元のリスト (List) を直接変更せず、新しい Iterable を返す仕様にあります。

詳しくmapの操作を見てみると以下の様になっています。

  @override
  Iterable<T> map<T>(T Function(E) f) => _list.map(f);

これはmap()は元のリストを変更するのではなく、 代わりに新しいIterableを返していることを示します。

mapは新しいオブジェクトを返しているのですが、 その返り値に対してカスケード記法の操作を行うことができないということでした。

まとめ

  • カスケード記法は、常に元のオブジェクトを操作する。
  • 元のオブジェクトに対して行う処理(初期化、リスト要素の追加など)は反映される。
  • 新しいオブジェクト返す操作(map, whereなど)は、カスケード記法では無視される。

今回はカスケード記法について詳しくなることができました。 メソッドチェーンを使う箇所が誤ってカスケード記法になっており、 期待した動作にならず困っていましたが、この謎が解決できてよかったです。

詳細な仕様を知らずに使っていたことが予期せぬ動作になっていたので、基礎の重要さを思い知らされる出来事でした。

最後まで読んでいただきありがとうございました。

scroll-snap-typeを使用して子要素単位にスクロールさせる方法

こんにちは。エキサイトでデザイナーをしている齋藤です。

今回はCSSscroll-snap-typeプロパティを使用して、子要素単位にスクロールさせる方法をご紹介します。

実現したいこと

冒頭、「子要素単位にスクロール」とはどんな状態かをご説明します。

CodePenに触れて、操作感の違いを感じてみてください。

通常のスクロール

まずは通常のスクロールです。

See the Pen Untitled by AyumuSaito (@ayumusaito-excite) on CodePen.

子要素単位のスクロール

次に、今回実現したい子要素単位のスクロールです。

通常のスクロールを異なるのは、スクロールの停止位置が子要素の上辺に強制される点です。

See the Pen CSSで子要素単位にスクロールさせる方法:子要素単位のスクロール by AyumuSaito (@ayumusaito-excite) on CodePen.

どうやって実現するか

子要素単位にスクロールさせるのはCSSだけで実現が可能です。

使用するのは scroll-snap-typeです。

scroll-snap-typeは、スクロール停止位置を子要素のスナップ点(スクロール停止位置)の指定に強制させるプロパティです。

先の例の場合で見てみましょう。

HTMLは以下のような構造とします。ul要素にはoverflow: auto;の指定があり、溢れた子要素はスクロールで省略されるようになっています。

<ul>
  <li>子要素1</li>
  <li>子要素2</li>
  <li>子要素3</li>
</ul>

親要素

親要素CSSは以下のようにします。

ul {
  overflow: auto;
  
  /* 中略 */

  scroll-snap-type: y mandatory;
}

scroll-snap-typeですが、今回は縦スクロールですのでY軸を指定します。

加えて、スクロール途中の場合は停止位置をスナップ点に合わせるmandatoryを指定します。

子要素

子要素ではスナップ点はどこかを明示する必要があります。

スナップ点の指定にはscroll-snap-alignを使用します。

li {
  scroll-snap-align: start;
}

今回は上辺、すなわち要素の先頭位置をスナップ点にしたいため、startを指定します。

これで完成です。

さいごに

今回はCSSscroll-snap-typeプロパティを使用して、子要素単位にスクロールさせる方法をご紹介しました。

スタイリングでお悩みの方の一助となれば幸いです。

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

デザインチームで取り組んだ業務フロー改善について

こんにちは、エキサイト株式会社メディア事業部デザイナー 小野寺です。 今日は、最近デザイナーチームで取り組んだ、デザイン業務ワークフロー改善についてシェアしたいと思います。

これまでも取り組んでいたこと

今回の取り組み以前にも、デザイン予算を持っていない(デザイナー不在)チームからのスポット的なデザイン依頼の受け皿問題を起点として、

  • Googleフォームからの依頼フロー作成&周知(口コミ)
  • 簡単な告知画像作成などはcanva等のデザインツールを使って制作出来るよう、ツール利用の啓蒙等を行ってきました。

また、canvaを含め、FigmaAdobeモリサワ等のデザインツールのアカウント管理は社内のインフラ部で最終管理をしてもらいつつも、主体で使うのはデザイナー職のため、アクティブユーザーの管理や新規利用開始の相談等はデザインチームで受けていました。

見直すに至ったきっかけ

今回このワークフローを見直してアップデートするきっかけとなったのは、非デザイン職からのFigmaAdobeツールの利用相談への対応についてのお悩みからでした。

よくよくヒアリングをすると、利用目的に対して希望するツールがアンマッチ(高機能すぎる)していたり、費用対効果に見合わないだろうな、というケースが度々あり、どうしよう?といった趣旨の内容でデザイナーMTGの時間を消費していました。

みんないろいろ「知らない」「知る機会がない」ので起こる→周知に工夫をしよう

そのほかにも、

  • Adobe Stockうっかりクレジット枯渇して全員使えなくなる問題
  • Canva導入したけど、いまいち使いこなせていない問題

などうっすら課題に感じていたことはありましたが、大体の問題に共通しているのは「周知方法」と「周知のタイミング」なのではないかと考えました。

例えば、Figmaならば、デザイナ職にとっては

  • デザインシステム
  • Autolayout
  • Variables
  • Variants

などを使いこなしてUI制作と制作コミュニケーションを効率的に行えるツール、月額6,750円(ビジネスプラン/2024年現在)という認識がありますが、

非デザイン職がFigmaを使いたい動機としては

  • どんなツールか使ってみたい
  • 既存のデザインファイルをちょっとだけいじって改修指示を出したい
  • デザインを少し勉強したい

など少し動機がふわっとしていて、まず何が出来るツールなのか自体を知らなかったりしました。

こうした、 利用申請者がシンプルに「知らない」「知る機会がない」→こちらとあちらの前提に差分がありすぎてコミュニケーションコストがかかる ということが起きていました。

ツール申請時に各ツールの特徴、用途などを情報提供、確認事項を収集できるようにslackで自動化してコミュニケーションコストを削減

このような問題を解消するためにチームでもろもろ検討した結果、以下の方法にたどり着きました。

Designヘルプチャンネルをslackに開設

既存でインフラチームが運営していた、社内のPCインフラ周りの相談チャンネルを参考に、デザイン周り全般の相談ができるようなチャンネルを作りました。 デザイナーに個別で個人レベルで判断しづらい相談があった際にも、まずこちらのチャンネルへの誘導を行い、個々人の判断工数を減らすという狙いもあります。

チャンネルジョイン時の自動ウェルカムメッセージ

基本的には何か目的がある社員がジョインしてくるチャンネルなので、簡易的にできる事の選択肢に誘導する自動案内を設定しました。

目的に合わせたワークフローの追加作成&チャンネルの一本化

各種ツールの申請ワークフロー、デザイナーへの相談ワークフロー、仕事依頼フォームへの誘導 など、これまで別々のチャンネルを窓口にしていた既存のワークフローもこちらのチャンネルでアクセスできるように設計しました。

Figmaのメンバー追加ワークフロー

仕組み上、Figmaは自覚なく編集権限が付与されて課金アカウント対象になってしまうことがあり、手動での削除対応などが度々ありました。

裏返せば、Figmaは管理者によるユーザー追加作業がないと利用開始できないわけではないのですが、あえて利用開始時には申請を通過してもらう流れにしました。

このフローの中で、ツールに関する説明を添えて、Figmaが何に向いているツールなのか、費用がどれくらいかかるのか、などについて「知る機会」を作り、熟考・再検討してもらえるようにしました。

MTGに自発的にこういうフロー図をさっと書いてくれる&もりもりワークフロー作ってくれるメンバーばかりですごい

最初から完璧を目指さず、小声で一旦スタートしてみる

大体チームで思いついたものは実装したところで、全社アナウンスをかけてしまった時のインパクトが測れないので、一旦既存でcanvaを使っている社員向けに運営していたチャンネルを改造してチャンネルをスタートしてみました。

新しくちょっとした相談を持ちかけてきた社員に実際使ってみてどうだったか?といったインタビューなどを行い、問題点と感じた部分に関してはデザイナー定例で対話を行い、少しずつ改善する、という流れが生まれていて良い感じです👍

まだ試運転中だけど、デザイナーは少し楽になった

チャンネルリリースから1ヶ月経っていないこともあり、まだ相談件数などは少ないですが、相談される側のデザインチームも目線がバラバラ・ふわふわな状態からのスタートだったので一度情報とフローを一緒に整理したことで少し手放し運転が出来るようになったような、そんな安心感を感じています。

また、これまで個別に対応し、誰かに教えたTIPSなどデザイナー目線では気づかない困り事の解決方法なども都度可視化して共有(ナレッジシェア)することでじわじわと全社のクリエイティブの質と効率の向上に寄与できるようにブラッシュアップを続けたいと考えています。

みんな自覚なくDesignOps領域の業務をしている

弊社デザインチームでは、ここ数年で最初の依頼フォーム・フローの整備や、新卒採用プロジェクトなどを、拙いながらもチームで話し合い、模索しながら進めてきました。

今回のようにきっかけが問題提起ベースであったり、上長からの提案であったり、そこに課題があるので一生懸命なんとかしようという理由で動いていたのですが、一般的にはDesignOpsと呼ばれる業務の一部だったなと最近気づきました。

今やっていることが、一般的にはなんと呼ばれるスキルと経験に繋がるのか、という自覚は割と大事なのかなと個人的には考えているので、今後もう少しDesignOpsに関する知見も増やしていきたいなと思いました。

就業型インターンシップBooost!!!を通して

はじめに

初めまして、エキサイト株式会社のインフラストラクチャー部で就業型インターンシップ「Booost!!!」に参加させていたただいております、藤井です。 1ヶ月のインターンの内容や学んだことを紹介します。

自己紹介

私はネットワークを専攻している修士1年で、ネットワークとセキュリティの研究室に所属しています。 大学院では、広域分散環境におけるリクエスト分散制御について研究しています。 趣味で自宅サーバーをかじる程度にやっています。AWSアプリ開発をした経験もあり、操作には慣れています。

インターンについて

働き方

  • フルリモート

  • 期間:10/7〜10/31

  • 勤務時間:9:30〜18:00

1ヶ月間、フルリモートでの勤務でした。インターンの開始期間については融通が効き、10/1からの勤務が可能でしたが、他の用事があった為、10/7開始にさせて頂きました。

勤務時間についても、半日だけ・授業や用事での中抜けが可能で、自分のスケジュールに合わせて働けるところが魅力的でした。

フルリモートだと社内全体の雰囲気を知ることができないと思い不安でしたが、インターンシップ開始前の9月に参加したBeer Bashというイベントでメンターの方に対面でお会いでき、様々な社員さんと交流することができました。

インターンの業務内容

業務では、全社導入予定のCloudflare Zero Trustの機能検証に携わりました。

  • sandbox環境のEC2にCloudflare Zero Trustで設定したポリシーで制限し、許可された方法でのみアクセスできるようにした

  • Cloudflare Logpushという機能でsandbox環境のプライベートサブネットにあるS3にネットワークのログを送る検証

  • Firewall policiesのHTTPのインスペクション設定で指定する項目のDo Not Inspectの有無の調査

  • 他事業部のVPNアクセス環境移行に際して、限定したアクセスのポリシー検証

  • Local Domain Fallback機能の検証

  • Cloudflare DLP機能の調査

  • VPN機能「WARP」の検証・問い合わせ対応

  • Cloudflare APIトークンを用いたポリシー検証

  • VPCピアリングとCloudflare Tunnel環境の検証

インターンを通して学んだこと・感じたこと

これまで、実際に業務に携わる形でのインフラに関する仕事の経験はありませんでした。自分の知識が検証を通して、業務に役立つ事を経験できて良かったです。 社員の方も答えを把握していない範囲を検証して、一緒に解決していくというアプローチは私に合っており、解決する度喜びを分かち合ったことは忘れられない思い出となりました。

今回、Cloudflare Zero Trustの機能を沢山知ることができました。既にCloudflareのDNS機能は使用経験がありますが、無料でも多くの機能が使えるので、自宅にもZero Trustを導入したいと感じました。

また、検証中に長時間ドキュメントと向き合いました。インターンシップ参加前はドキュメントを読み込むことが苦手でしたが、自然とドキュメントの読み方が身につきました。エラーが出たらどう対処すべきか、原因を切り分ける力も身につきました。

インフラストラクチャー部では小部屋という常設Google Meetがあります。そこで朝会、夕会、それ以外の時間も長くサポートして頂きました。皆さん褒めて伸ばすスタイルで、とても和やかな雰囲気で仕事ができて、最高でした。

まとめ

1ヶ月のインターンシップの中で自分の経験を活かして、新たに全社導入する機能の検証をするという貴重な機会を頂きました。インターンを始める前よりも、自分で考えてアプローチする技術が上がり、ドキュメントと向き合えるようになりました。また、インターンシップやイベントを通して社内の雰囲気を知ることができました。

最後に

楽しく働くことができ、インターンシップをやり切れました。 支えてくださったメンターの方をはじめとする多くの方に、この場を借りて感謝申し上げます。 培った経験を今後のキャリアに活かします。本当にありがとうございました!

Excite就業型インターンに参加して

はじめに

2024年10月の1ヶ月間エキサイト株式会社が開催した「Booost!!! Excite Internship 2024」での経験や感想をお伝えします。

自己紹介

私はアメリカのオレゴン州立大学に在籍している大学4年生です。大学ではコンピュータサイエンスを専攻しています。 これまで大学の授業や個人プロジェクトを通じてソフトウェア開発を学んできましたが、今回は実際のサービスに関わる開発に携わることができました。

インターンについて

参加部署

私は「BBエキサイト」や「エキサイトモバイル」などのインターネット接続サービスを展開しているブロードバンド事業部に参加しました。

業務内容

BBエキサイトの光回線には、セットでお得になるオプションが多く、その中の一つがセキュリティ対策ソフト「esetファミリーセキュリティ」です。 私の業務は、主にこのサービスの解約を管理画面から行えるように解約ページを作成し、実際に解約処理を行うためのAPIを実装することでした。

今回作成した解約ページ

使用技術

開発では、バックエンドにPHPフレームワーク「Laravel」、フロントエンドには「Vue.js」を使用しました。

開発の流れ

開発は、まずフロントエンドの仮組みを作成し、その後APIを叩いて解約処理を実装しました。最後に、テストコードを書いて実装した処理が期待通りに動作するか確認しました。

学んだこと

開発の流れについて詳しくお伝えします。フロントエンドの仮組みが完成した後、実際のリクエストに基づいてAPIを叩き、レスポンスを整形して返す関数の実装に取り組みました。LaravelのMVCアーキテクチャにおいて、これらの処理はコントローラが担当します。

API呼び出しに必要なパラメータを確認した後、PostmanやLaravelのdd関数(Rubyのbinding.pryに相当)を使用して実際のレスポンスを確認しました。ここまでは正しい入力値に基づいてリクエストを投げて確認できましたが、実際の運用ではユーザーの入力値を必ず検証する必要があります。

入力値検証について

入力値の妥当性検証は、今回担当する「esetファミリーセキュリティ」の仕様に基づきます。 具体的には、解約処理をするにあたってユーザーの方には、「ユーザーID」と「退会日」入力していただきます。

この際に、仕様として、入会日以前の解約、先月よりも前の日付での解約、未来の日付での解約は設定できなくする必要があります。ユーザーIDにも正しい形式(16文字の英数字と数字)があるため、そちらも検証する必要があります。

今回、この検証処理を書いてみて、メンターの方から教えていただいたこと、調べていく過程で学んだことは以下のとおりです。

  1. 入力値検証はフロントエンドとバックエンドの両方で実装し、ユーザー側のエラーに対しては再操作を促し、サーバー側のエラーに対しては適切な例外処理を行う。
  2. フロントに送るエラーメッセージとは別に、開発者向けのログを残すと良い。
  3. コントローラ内の処理はリクエストを受け取りレスポンスを返すことに留め、入力値の検証は別のヘルパー関数に分けると良い。

LaravelはFormRequestやデフォルトでのミドルウェア処理など、入力値の検証やセキュリティに関する機能が豊富であることも実感しました。

ユニットテストについて

最後に、ユニットテストを書く機会がありました。PHPUnitとMockeryというツールを初めて使用しましたが、テストのしづらいコードがあることに気づき、そうしたコードの設計が良くないことを体感しました。また、これまで個人開発ではテストを書いてこなかったため、様々なテストコードを参考にすることで、テストコードの書き方を大まかにですが理解することができました。

最後に

1ヶ月という短い期間でしたが、多くのことを学び、貴重な経験をすることができました。メンターの方や人事の方々のサポートに心から感謝しています。至らない点も多々ありましたが、このような機会をいただき、本当にありがとうございました。