torikatsu.dev

Flutterとかプログラミングとかガジェットとか書きます

AG03MK2をデスク下にマウントしたら視界がスッキリした

AG03MK2をデスク下にマウントしたら、デスクのスッキリ感が向上しました。 この記事ではAG03をデスクしたにマウントする方法について紹介します。

課題感

最近、オーディオインターフェースが必要になりAG03MK2を購入しました

AG03MK2はAG03の後継に当たるオーディオインターフェースで、

  • 一通り機能が揃っている
  • 品質がいい
  • 1クリックでマイクをミュートできる
  • マットな黒色でデスクに統一感が出せる

の理由で選びました。

AMAHA AG03MK2 ライブストリーミングミキサー 3チャンネル ブラック

Amazonで見る 楽天市場で見る

使い心地に問題はとてもいいのですが、存在感の強さが気がかりでした。
実際にデスクに置くとこのような感じです。

デスクスッキリ教の信者としてこの存在感は許せないため
なんとかデスク下にマウントしようと思いました

オーディオインターフェースのデスク下マウント事情

デスク下マウントを調べると以下のように天板にぴったりマウントするパターンをよく見かけます。 https://insnote.net/wp-content/uploads/2023/03/DSC00023-2048x1365.webp

これはmotu m2のように上下が平らなオーディオインターフェースで可能なマウント方法です。 ですが、AG03MK2はコネクタが上部にあるためこのようなマウント方法は選択できません。

MOTU USB-Cオーディオ・インターフェース (M2) [並行輸入品]

Amazonで見る 楽天市場で見る

調べていくとオーディオインターフェースをデスク下にマウントするパーツの多くはこのようなタイプを想定して作られていることがわかりました。
ゆえにAG03MK2のような上側にコネクタがあるタイプをマウントするためには、

  1. マウント用のパーツを自作する
  2. 本来の用途とは違うパーツを流用する
  3. 諦めて机の上に置く

のどれかで対応するしかありません。 1は素人にはレベルが高すぎますし、3はもはや敗北したも同然です。 私は消去法で2の「本来の用途とは違うパーツを流用する」を選択しました

AG03MK2にぴったりなパーツを発見

ぴったりなパーツを探すため、1週間ほどインターネットの世界を旅した結果、KANADEMONOさんの「ティッシュボックストレイ」が最適であることがわかりました

ティッシュボックス トレイ #2(下向き)kanademono.design

私がパーツに求めた要件は以下の4つでしたが、このティッシュボックストレイは全ての要件を満たす最高な商品でした。

  • マットな黒色
  • オーディオインターフェースが落下しづらいこと
  • マウントした状態でも操作が可能なこと
  • 頑丈であること
  • 大きさに過不足がないこと

KANADEMONOさんのティッシュボックストレイはちょうど鼻セレブと同じ寸法ですが、
実はAG03MK2も鼻セレブとほぼ同じ寸法です。(L字のXLRケーブルの高さ込み)

1週間ほど探し回っていたため、この事実に気づいた時は飛び上がるほど嬉しかったです。

早速購入しデスク裏にマウントしました。

デスク裏にマウントしてみる

届いたティッシュトレイはこのような形をしています。

上部にm6-8のネジを取り付けられる穴が4つあり、この穴をつかってデスク裏にマウントします。

取り付けが若干大変だったのでその点だけ注意が必要です。
以下は真横から見た図なのですが、トレイがコの字になっているためインパクトドライバーが直接入りません。 そのため、小さいドライバーなどでネジを締める必要があり大変でした。

そのままXLRケーブルを指すと天板と干渉してしまうため、このL字アダプタを使用しました。 商品画像からはわかりづらいですが、好きな方向に向きを変えられて融通が効くためおすすめです。

LyxPro XLR アングルアダプター

Amazonで見る 楽天市場で見る

実際にAG03MK2を乗せてケーブルを繋ぐとこのようになります。

しっかり固定されていて且つ操作もできるようになっています。 ゲインやファンタム電源などの操作はしづらいですが、頻繁にいじるミュートは触りやすいため不満はないです。

デスクの全体はこのようになりました。 デスク下にマウントすることでAG03MK2とXLRケーブルのノイズがなくなり集中しやすい環境になりました。

おわりに

AG03MK2のデスク裏マウントにはKANADEMONOさんのティッシュボックストレイが最強です。

Top level ShellRoute というパターンの紹介

Flutterでgo_routerを使う中で「Top Level ShellRoute」というパターンが意外とイケると気づいたため紹介します。

pub.dev

Top Level Shell Routeとは

go_routerのルート定義のトップレベルにShellRouteを配置する以下のような実装パターンです。 (勝手に名付けました)

Top Level ShellRoute pattern

上記のShellRouteはchildを返すだけで追加要素の表示を行わないため一見無駄なShellRouteに見えますが、
以下のように多くのメリットがあることがわかりました

  • go_router_builderのルート定義の保守性が上がる
  • riverpodのProviderのスコープを作成できる
  • main.dartから初期化処理を分離できる
  • 自前の通知Widgetなど、オーバーレイ要素の定義をまとめられる

go_router_builderのルート定義の保守性が上がる

これが最大のメリットです。

先ほどのルート定義をgo_router_builderで書き直スト次のようになります。

メンテを楽にするための自動生成なのに、自動生成する前と比べて以下の問題があります

  • コード量が増えた
  • ルートの構造を把握しづらい
  • TypedGoRouteとGoRouteDataの定義を同じファイルに書く必要がある

ここでTop Level ShellRouteパターンを用いると問題を解消することができます。

余談: pathを / とした TypedGoRouteをトップレベルに置くパターンは?

先述の問題を回避するために、/をpathとしたTypedGoRouteをトップレベルに置く方法が考えられます。

一見良さそうですが構築されるページのスタックに問題があります。 /login, /todo, /doneはスタック一番下の要素にしたいですが、/がスタックの一番下の要素になってしまいます。

(構築されるスタックのイメージ)
2. login (LoginScreen)
1. /         (RootScreen)

Androidの戻るボタンで/loginから/へ戻れるなど意図しない挙動を引き起こすため、ShellRouteを使用する方が適していると考えています (アプリの要件によるが)

riverpodのProviderのkeepAliveより若干短い生存期間のproviderを作成できる

Top Level ShellRoteのbuild内でref.watchすることにより、Providerの生存期間をアプリ全体に広げることができます。 現状keepAliveで事足りるため、役に立つかもしれないテクニック程度に考えています。

riverpod.dev

これはriverpod公式で紹介されているEager initialization of providersというパターンに近いです。 (追加要素の表示をしたい場合、watchするウィジェットとUI構築するウィジェットを分離する必要がある)

main.dartから初期化処理を分離できる

初期化処理をかける場所がmain()ShellRoute$buildになるため、初期化処理を分離できるようになります。 MaterialApp.router$builderもあるので具体的なユースケースはあまり思いついていないです。 (強いて言えば初期化処理でcontext経由でgo_routerへアクセスしたいなど?)

自前の通知Widgetなど、オーバーレイ要素の定義をまとめられる

元々ShellRouteは追加要素の表示に使えるため、ShellRoute$buildで画面全体を覆うインジケータやカスタム通知のようなウィジェットを定義することもできます。 1点注意が必要で、Navigator.pushなどによる画面遷移はGoRouterよりも全面に表示されるため、Top Level ShellRouteに定義したオーバーレイ要素が隠れてしまうことがあります。 多くの場合、GoRouterとNavigatorを組み合わせた画面遷移を行うと思うため、使用する場合はこのことに留意する必要があります。

まとめ

Top Level ShellRouteというパターンを紹介しました go_router_builder採用時のメリットは絶大ですが、それ以外のメリットは代替方法あるためプロジェクトに合った方法を採用していくといいなと思いました。

初期化処理をフックできる箇所が増えて将来の実装の選択肢が増やせるため、とりあえずやっておいて損はないと考えてます。

HHKB Studioが最高すぎてもうType-Sに戻れない

10/25日に新型のHHKB、「HHKB Studio」が発表されました。

happyhackingkb.com

私は発売後すぐに購入し約3週間使用しているのですが、最高すぎて既にHHKB Professional HYBRID Type-Sに戻れない体になりつつあります。

HHKB Studioと同梱品。メッセージカードなども入っており非常に高級感があります

HHKB StudioはHHKB Professionalから多くの点が変更されています。 本記事では以下3点を中心にHHKB Studioのレビューを紹介したいと思います。

  • キースイッチ
  • トラックポインタ
  • ジェスチャパッド

キースイッチ

結論、究極のサイレントリニアスイッチです。

HHKB Studioでは、メカニカル方式のキースイッチが採用されています。 静電容量無接点方式は今までのHHKBの代名詞でもあったため、伝統を捨てたメカニカルスイッチの採用に対して懸念の声がありましたが、実際に使用してみると最高のキースイッチであることがわかりました。

www.itmedia.co.jp

打鍵感/タイプ音について

Studioは独自の静音リニア軸(HHKB軸)が採用されています。押し始めは反発があり、その後は滑らかに沈み込んでくれました。底打ち時はコトコトという感覚があり、静音赤軸によくあるぐにゃっと感はありませんでした。

タイプ音ですが、HHKB Type-Sと比べて圧倒的に静かです。従来のHHKBは指をキートップにおいた時や押した時、底打ちした時それぞれカチャカチャという高めの音域のノイズがあったのですが、Stuidoでそのポイントは大きく改善されていると感じました。(一説によると、打鍵音の大きさ自体は変わらず高周波数帯が抑えられたことにより静かに感じると言われています)

またメカニカルスイッチのためルブをするべきか迷うと思うのですが、HHKB Studioはルブなしで満足できるクオリティでした。(一説によるとルブなしで使うほうがいいらしい)よくあるスペースキー周りのスタビライザーの引っかかりも特に感じませんでした。

値段について

HHKB軸は10個3000円で販売されています。 1個当たり300円という値段はキースイッチとしてはかなり強気な価格設定だと思います。 www.pfu.ricoh.com

例えばDropで人気のHoly Pandaは35個で29ドル、1個当たり約122円($0.82, $1=¥149.54)です。 drop.com

このように従来の高級キースイッチと比べても3倍近くするのでかなり割高なキースイッチです。 ですが、上記で述べた打鍵感やタイプ音のクオリティからその価値があると感じました。

トラックポインタ

Studioの最大の特徴とも言えるトラックポインタですが、実際に使用してみて非常に便利だと感じました。 操作感ですが、おそらくThinkPadトラックポイントよりも使いやすいのではと思います。 ポインタの感度はHHKB側で20段階の設定ができるため、自分の好みに合わせて調整できます。
私はウルトラワイドモニターを使っており、たびたび大きくカーソルを移動する必要があります。大きなモニターにおけるカーソル移動がやりづらくないか気になっていたのですが、感度MAXの状態で1ピッチで端から端までカーソル移動が可能でした。

トラックポインタの位置はG, H, Bの3つのキーの間にあり、タイピング中に干渉しづらいようになっています。 たまにBを押す時に誤ってトラックポインタに触れてしまうことがあるのですが、正しいタイピングをしていれば滅多に干渉することはないと思います。

トラックポインタは干渉しづらい位置にあります

キーマップ変更ツールからトラックポインタのボタン機能を有効にすることもできます。 感度がそこまで良くなく強めにタップする必要があり、こちらは若干使いづらさを感じました。今後のファームウェアアップデートに期待です。

ジェスチャパッド

Studioには左右と全面に計4つのジェスチャパッドがあります。このジェスチャパッドは20段階の感度調整が可能で、キーマップ変更ツールから機能割り当てを変更することができます。 私は次のように、左からズームイン/アウト、アプリ切り替え、水平スクロール、垂直スクロールを割り当てています。 特に左右のジェスチャパッドはホームポジションを崩さずに小指で操作できるため、痒い所に手が届くような便利さがあります。

各キー/ジェスチャーパッドの割り当て

便利な機能ではあるのですが、macトラックパッドと比較して操作感は劣ると思います。その理由として次のようなものがあります。

  • 初動にラグがある
  • 慣性が効かない
  • ピンチイン/アウトのようなスムーズな操作の割り当てができない

私は普段フロントエンドエンジニアとして働いており、Figmaのようなデザインツールを使う場面が多いです。 こういったツールは繊細な操作が求められるのですが、上記の理由からジェスチャパッドは補助的な使い方に留まっています。 一応ジェスチャパッドの感度を低めに設定することで操作感の不満を軽減できたのですが、今後のアップデートに期待したいと思います。

Studioを使う上でパームレスト選びには気をつける必要があります。
HHKBユーザの中にはFilcoパームレストバード電子のタイピングベッドを使っている方がいると思いますが、それらのパームレストパームレストの高さとジェスチャパッドの位置が干渉してしまいます。
そのためHHKB Studioを使う場合は、PFUのオリジナルパームレストを使う必要があります。 www.pfu.ricoh.com

馬の鞍哲学について

従来のHHKBは「馬の鞍哲学」という考え方で設計されており、生涯使えるインターフェースを謳っていました。 happyhackingkb.com

これを可能にしていたのが次の2つの要素です。

  • 静電容量無方式の採用による、理論上壊れないスイッチ
  • バッテリーではなく乾電池の採用

Studioでは静電容量無接点方式ではなくメカニカルキースイッチが採用されています。 静電容量無接点方式コンデンサの静電容量の変化を検知しているのに対し、メカニカルキースイッチは物理的な接点が存在しており、接点が摩耗していくことでいつかは壊れてしまいます。 故に私は、「Studioは生涯伝えるインターフェースになり得るのか?」という疑問をもっていました。 3週間ほど使った現在では、「生涯使えるインターフェースになり得る」という結論に至っています。 その理由としてホットスワップが採用されていることが挙げられます。ホットスワップはキーソケットというパーツを用いることで半田付けなしにキースイッチを交換できるようにするものです。これによりキースイットが交換可能な部品になったため、キースイッチを消耗品と捉えることができるようになりました。 さらに、Studioが採用したキースイッチの規格は自作キーボード界ではかなりポピュラーなため、好みのキースイッチに交換することもできます。 これによりカスタマイズの幅が広がり、従来のHHKBシリーズよりもさらにユーザの手に馴染むインターフェースになり得ると考えています。

誰におすすめなキーボードなのか

HHKB Studioは市販のメカニカルキーボードの中でかなり高価な部類に入りますが、自作キーボードと比べるとコスパよく高いクオリティの0キーボードが手に入ると言えます。(自作キーボードは4,5-∞万円に対し、HHKB Studioは4.4万円) また製品のクオリティが高く、長く使い続けられることを考慮すると、その価格は0円に収束していくためとてもお値打ちであると言えます。

よって次のような方にオススメなキーボードだと思います - 自作キーボード沼に堕ちた方 - 最高のタイピング体験を求める方 - キーボードの静音性を追求する方 - デスクにマウスすら置きたくない方

まとめ

この記事では書いてないですが、他にも素晴らしい点はたくさんあります。 この記事がHHKB Studioの購入を悩んでいる人の助けになれば幸いです。

ユニットテスト考察

はじめに

この記事はUnipos Advent Calendar 2022の記事です

こんにちは、とりかつ(@torikatsu923)です。

「なぜユニットテスト(UT)を書くのか」

ユニットテストを書こうとすると必ずと言っていいほど上の質問が飛んできます。
開発者の肌感で、「テストは書いたほうがいい」は当たり前に近い感覚だと思います。
ですが、その目的や効果を問われると言葉が詰まってしまいます。

今回の記事では、ユニットテストの目的や効果について整理し、「なぜUTを書くのか」という問いに対し説明責任を果たせられる状態を目指します。

(zennと全く同じ内容です。基本記事はこっちのブログで管理したいので、同じものを載せています。)

なぜ目的・効果の説明が難しいのか

UTの目的や効果を問われた時の回答はとても難しいですよね。
私は、この問題は3つの問題に分解できると考えています。

  • テストに絶対はない
  • カバレッジが全てではない
  • 関係者がさまざまである

テストに絶対はない

テストに絶対はありません。 テストが示せるのは欠陥が存在しないことではなく、欠陥が存在することです。
そもそも何かが存在しないことの証明は困難で、悪魔の証明なんて言われたりもします。
また、人間はバグを作り込む生き物です。そんな人間がテストを書くからにはテストに抜け漏れが発生するのはごく自然なことです。

ですが、UTを書き始めたいときなぜか「UTはバグをなくす最強のツールなんだ!」と思い込みバグの撲滅を目標に掲げがちです。

テストを書く目的を「バグの撲滅」としてしまうと、こんなコミュニケーションに陥ります。
(こんな意地悪してくるマネージャはいないと思いますが...)

エンジニア「バグを撲滅するため、UT書きたいです!」

マネージャ「完全にバグなくせるの?」

エンジニア「いや...それは...」

マネージャ「完全にバグなくせないなら、UTに工数割く意味あるの?」

エンジニア「あっ、はい...」

こういった理由から、UTを始める際は「バグの撲滅」を目標に据えないようにしましょう。

カバレッジが全てではない

UTを書き始める時テストカバレッジが気になると思います。

ある程度のカバレッジを叩き出すことには効果があります。
しかし、カバレッジが担保されていないとUTの恩恵を受けられないわけではありません。
また、カバレッジが高いからと言ってUTが十分とは限りません。

リファクタリングでも次のように述べられていました。

「どれだけテストをすれば十分なのか」(中略) カバレージによる分析が有効なのは、コード中のまだテストされていない箇所を突き止める場合のみであり、テストスイート自体の品質を保証するものではない (中略) 十分なテストスイートが揃っているかどうかは、主観で決めるのが最も良い

今までUTが存在しなかったプロジェクトだと、次のような意見が出ることがあります。

「全部に対してUT書いてカバレッジ高めないと、UTの効果ないんでしょ? 全部に対してUT書くとか現実的じゃない」

実際、どんなプロジェクトにおいてもUTの効果は書き始めてすぐから現れます。
カバレッジの高さとUTの効果には相関がある程度にとどめ、カバレッジが絶対ではないという価値観を作りましょう。

関係者によって興味・関心が異なる

UTを書く目的や効果に対する問いは経営者やマネージャー、チームの開発者など社内のさまざまな人から飛んできます。
同じ目標を追っているとはいえ、経営者などの上位レイヤと現場のエンジニアでは興味・関心に差があります。

ある会社では次のようになっているかもしれません。

関係者 興味・関心
上位レイヤ 費用対効果、プロダクトの品質
エンジニア コードの品質、開発者体験

上位レイヤに対して現場のエンジニアがコードの品質や開発者体験について熱弁したところで、お互いの期待値がズレたままなので議論は並行したままです。
こういった理由から、テストの効果・目的を説明する際はその人が何に興味・関心があるのかを踏まえた上で、その側面からUTの説明することが求められます。

UTの目的・効果

ここまで、UTの目的・効果を説明する難しさと、それぞれの対処法について述べてきました。
ではUTの目的とは一体なんでしょうか?

私は、「作成したクラスや関数のインターフェースに対し洞察を得られる」ことだと考えています。

達人プログラマーでも次のように述べられていました。

テストとはバグを見つけることではない

さらに次のような記述もありました。

テストの主な利点は、テストについて考え、テストを記述している時にあり、テストを実行している時ではないと我々は確信しています

UTを書く際、必ず書いたクラスや関数の呼び出しを行います。 つまりコードを書いた本人が、作成したクラスや関数の最初の使い手です。

使い手の立場になることで、不便な点が明るみになったり、不吉な臭いを嗅ぎつけられます。 そのFBをもとにインターフェースを改善し、より良いコードづくりの助けになります。

これらの活動の結果、次のような効果が発生します。

  • バグの減少
  • 変更が容易な品質の高いコードづくり
  • プロダクトの品質向上
  • 自動回帰テストによるテスト工数削減
  • 開発体験の向上 ... etc

効果的なUTの書き方

UTを効果的なものにするため、「何に」対してUTを書くのかというUTの戦略は非常に重要です。

では、「何に」対してUTを書いたら良いのでしょうか?

クラスでしょうか、関数でしょうか、それともモジュールでしょうか?

私は、「振る舞い」だと考えています。

前項でUTの目的は「作成したクラスや関数のインターフェースに対し洞察を得られる」ことだと述べました。
UTを書くことで洞察を得てインターフェースを洗練させることが、非常に重要な活動です。

振る舞いに対しUTを記述することの意義はさまざまな場所で触れられています。 また、テスト駆動開発(TDD)が「振る舞い駆動開発(BDD)」と呼ばれることも、振る舞いに対しUTを書くことに意義があることの表れだと考えられます。

また、インターフェースは変化しやすく振る舞いは変化しづらいため、そのテストは変更に強く壊れづらいものとなります。

【余談】UTはTDDに乗っ取らなければならないのか

よく「コードを書くより先にUTを書かなければならない」という意見を聞きます。

私はUTを書くのはコードの後先どちらでも良いと考えています。 UTを書く目的が「作成したクラスや関数のインターフェースに対し洞察を得られる」ことである以上、どちらを先に書いてもその目的を達成することができるからです。

ですが、テストを先に書くことで効果的に洞察を得ることができるかもしれません。 どちらを先に書くか決まりはないため、個人の好みで良いと思います。

おわりに

今回の記事では、ユニットテストの目的や効果について整理し、「なぜUTを書くのか」という問いに対し説明責任を果たすために必要な情報をまとめました。 この記事がUT書きたいけどうまく目的・効果を説明できないという方の助けになれれば幸いです。

参考文献

riverpod_generatorを使ってみる

2022/11/20追記: スニペットに間違いがあったため修正しました

こんにちは、とりかつ@torikatsu923です。

riverpod関連のパッケージにriverpod_generatorというものがあります。
これはアノテーションをつけるだけで各providerを生成してくれるとても便利なツールです。

pub.dev

今までStateNotifierProviderを宣言する際は次のようにしていましたが、

final someNotifierProvider = StateNotifierProvider.autoDispose<SomeNotifier, String>((ref) => SomeNotifier());

class SomeNotifier extends StateNotifier<String> {
  // ...
}

これだけでよくなります。最高ですね!

@riverpod
class SomeNotifier extends StateNotifier<String> {
  // ...
}

今回の記事ではriverpod_generatorのセットアップと、各providerの生成方法を紹介します。

セットアップ

1. 依存関係の追加

次のようにriverpod, riverpod_annotationをdependenciesに、build_runnerとriverpod_generatorをdev_dependenciesへ追加し、flutter pub get します。

dependencies:
  riverpod:
  riverpod_annotation:

dev_dependencies:
  build_runner:
  riverpod_generator:

2. コードを自動生成

次のコマンドをターミナルで実行し、providerを自動生成します。

dart pub run build_runner build

watchを実行するとコードの変更を検知してその都度buildを走らせてくれます

dart pub run build_runner watch

tips

コマンドを実行すると、対象のファイルと同じディレクトリ階層に xxx.g.dart というファイルが生成されます。プロジェクトの規模が大きくなるにつれて、自動生成されたファイルの数が多くなりノイズになっていきます。

build.yamlへ以下の設定を記述しておくと、自動生成ファイルの生成先ディレクトリを指定することができる様になりファイルがスッキリします。

build.yaml

targets:
  $default:
    sources:
      - $package$
      - lib/**
      - stories/**
    builders:
      source_gen|combining_builder:
        options:
          build_extensions:
            '^lib/{{}}.dart': 'lib/generated/{{}}.g.dart' # コロンより右側が生成先を指定している。
      riverpod_generator:
        options:

上の例ではlib/generated配下にファイルが生成される様になっています。

↓↓参考↓↓
torikatsu923.hatenablog.com

各Providerの生成方法

// 普通のProvider
@riverpod
String someValue(_) => 'some value';

// 今まで通りscopedProvider用にUnimplementedErrorをthrowする方法も使えます
@riverpod
String someScopedValue(_) => throw UnimplementedError();

// FutureProvider
@riverpod
Future futureValue(_) async => 'future value';

// AsyncNotifierProvider.autoDispose
@riverpod
class SomeAutodisposeAsyncNotifier extends AutoDisposeAsyncNotifier<String> {
  @override
  FutureOr<String> build() {
    throw UnimplementedError();
  }
}

// AsyncNotifierProvider
@Riverpod(keepAlive: true)
class SomeKeepAliveAsyncNotifier extends AsyncNotifier {
  @override
  FutureOr build() {
    throw UnimplementedError();
  }
}

// StateNotifierProvider
@riverpod
class SomeAutodisposeStateNotifier extends AutoDisposeNotifier<int> {
  SomeAutodisposeStateNotifier();

  @override
  int build() {
    return 0;
  }
}

// StateNotifierProvider.autoDispose
@Riverpod(keepAlive: true)
class SomeKeepAliveStateNotifier extends Notifier<int> {
  SomeKeepAliveStateNotifier();

  @override
  int build() {
    return 0;
  }
}

おわりに

riverpod_generatorはriverpodの作者さんのリポジトリに属するパッケージのため、今後も安心して使えるパッケージとなっています。 providerの宣言が面倒と感じるようになったらぜひ試してみてください。

【Flutter】flutterfire x riverpod x go_routerで認証ガードをスマートに実装する

はじめに

こんにちは、とりかつ(@torikatsu923)です。

以前、私はこんな記事を書きました。

torikatsu923.hatenablog.com

サインインのようにFirebaseの認証状態を変化させる関数を叩いてからFirebaseAuth.instance.authStateChanges()が反映されるまでに若干のタイムラグがあります
これが原因で以下のような場合にサインイン直後にcontext.goで認証ガードが行われているページへ遷移しようとすると、認証ガード内で最新の認証情報がとれず、画面遷移できないことがありました。

bad
bad

  • FirebaseAuth.instance.authStateChanges()をStreamProviderで管理している
  • GoRouterをriverpodのProviderで管理している
  • GoRouteのredirectでref.read()でFirebaseAuthを取得して認証ガードを行なっている

上記の記事ではこの問題を回避するため、authStateChagnes()の更新を待ってからcontext.go()する方法を紹介していました。

wait for update of authStateChange

あれからgo_routerを触っていたら、よりスマートな方法があることがわかりました。今回の記事ではその方法を紹介します。

認証ガードの方法

今回紹介するのはGoRouterのrefreshListenableを使う方法です。 authStateChangeGoRouterのrefreshListenableに渡すことで、context.go()を呼ばなくても認証状態が変化するたびにGoRouterが勝手にリダイレクトしてくれます。

    return GoRouter(
      routes: [...],
      ...
      refreshListenable: // ここにわたす,
    );

use refreshListenable
use refreshListenable

こんな感じでcontext.go('/home')をする必要がなくなります。

gorouter.dev

refreshListenableへの渡し方

以下のように`authStateChangeをStreamProviderで管理することを想定します。

final authProvider = StreamProvider<User?>(
  (ref) => FirebaseAuth.instance.authStateChanges(),
);

refreshListenableはListenableを受け取ります。そのためStreamProviderをそのままrefreshListenableへ渡すことはできません。

なので、以下のようにlistenSelfを使ってValueNotifierへ変換します。

class _AuthStateNotifier extends ValueNotifier<User?> {
  _AuthStateNotifier() : super(null);
  void change(User? v) => value = v;
}

final authStateNotifier = _AuthStateNotifier();

final authProvider = StreamProvider<User?>(
  (ref) {
    ref.listenSelf((_, v) => authStateNotifier.change(v.value));
    return FirebaseAuth.instance.authStateChanges();
  },
);

// ...
    return GoRouter(
      routes: [...],
      ...
      refreshListenable: authStateNotifier // pass Listenable
    );

あとは以下のようなリダイレクト関数をホーム画面、サインイン画面のGoRouteに設定すれば、認証情報の変化に伴って正しい遷移先へ勝手にリダイレクトされるようになります。

// use home route
String? authGuard(Reader read, [RouteGuard? guard]) {
  if (read(authProvider).value == null) {
    return '/signin';
  } else if (guard != null) {
    return guard();
  } else {
    return null;
  }
}

// use signin route
String? noAuthGuard(Reader read, [RouteGuard? guard]) {
  if (read(authProvider).value != null) {
    return '/home';
  } else if (guard != null) {
    return guard();
  } else {
    return null;
  }
}

おわりに

今回はGoRouterのrefreshListenableとFirebaseのauthStateChangesを組み合わせることでスマートに認証ガードを作る方法を紹介しました。 かなり説明を省いている箇所があります。フルサンプルを以下のリポジトリに用意したので、興味のある方は覗いてみてください。

github.com

【Flutter】プリコンパイルされたcloud_firestoreでビルド時間を短縮する

こんにちは、とりかつ(@torikatsu)です。

Flutter x Firebaseの組み合わせで開発をすることがよくあり、DBにCloud Firestoreを選択することも多いのではないでしょうか。 FlutterからCloud Firestoreを利用する場合はcloud_firestoreを使うことになると思います。

もしあなたがcloud_firestoreを利用している場合、もしかしたらiOSのビルド時間を 5分短縮 することができるかもしれません。

ビルド時間が短くなることによって開発体験が向上するだけでなく、GitHub Actionsのようなサービスの過金額の抑制にも繋がります。

そこで今回の記事ではcloud_firestoreを使っている場合にビルド時間を短縮するワザを紹介します。

なぜビルドが長いのか

なぜビルドが長いのでしょうか。 旧公式には次のようにありました。

Currently, the Firestore iOS SDK depends on some 500k lines of mostly C++ code which can take upwards of 5 minutes to build in Xcode.

firebase.flutter.dev

つまり、cloud_firestoreはiOSのFirestore SDKに依存しており、iOSのFirestore SDKはコード量が膨大なためビルドに時間がかかっているというわけでした。

じゃあどうしたらいいんだと思ったら旧公式に次のようにありました。

To reduce build times significantly, you can use a pre-compiled version

どうやらFirestore SDKコンパイルしたものが既に用意されているようです。これを使用することでFirestore SDKコンパイルのステップをスキップし、ビルド時間を短縮することが可能なようでした。

ビルド時間を短くする手順

プリコンパイルされたFirestore SDKを利用するためには、Podfileに1行追記するだけでOKです。

<project_root>/ios/Podfileを開き、target 'Runner' do ... endの中に次の1行を追記します。

pod 'FirebaseFirestore', :git => 'https://github.com/invertase/firestore-ios-sdk-frameworks.git', :tag => '9.3.0'

これでビルドが早くなりました!

どれぐらい短くなったのか

たった1行書いただけじゃん。こんなんで本当に早くなるの?って疑問に思いますよね。

実際にビルドしてみました! (厳密なベンチマークは取っていないのであくまでもイメージとして)

高速化前

高速化後

ログのうちRunning Xcode buildiOSのビルド時間です。 高速化前は286.4sなのが、高速化後は11.7sと、4分57秒も速くなりました!!

こんなに速くなるならやらない手はないですね。

エラーが起きる場合

これを追記してflutter runしてみるとぶわぁぁぁってエラーが出ることがあります。

こんなエラー出たら焦りますよね。 でも冷静にエラーの内容を見てみます。 赤枠のあたりに注目すると...

    [!] CocoaPods could not find compatible versions for pod "FirebaseFirestore":
      In snapshot (Podfile.lock):
        FirebaseFirestore (= 9.3.0, ~> 9.3.0)

      In Podfile:
        FirebaseFirestore (from `https://github.com/invertase/firestore-ios-sdk-frameworks.git`, tag `8.14.0`)

    None of your spec sources contain a spec satisfying the dependencies: `FirebaseFirestore (from `https://github.com/invertase/firestore-ios-sdk-frameworks.git`, tag `8.14.0`),
    FirebaseFirestore (= 9.3.0, ~> 9.3.0)`.

これを要約すると「本当は9.3.0のバージョンが欲しかったのに、先ほどの手順でPodfileに追加したURLからは9.3.0のバージョンのFirestore SDKが見つからなかったよ」と言っています。

ちなみにさっきの手順で追記したやつは以下のようになっています。

gitにGitHubリポジトリのURL、tagにタグバージョンを指定することでそっからFirebaseFirestoreのプリコンパイルをダウンロードするように指定しています。

target 'Runner' do
  pod 'FirebaseFirestore', :git => 'gitのリポジトリのURL', :tag => 'gitのタグ'
end

先ほど指定していたGitHubリポジトリを覗いてみると...

github.com

なんと、Firestore SDKのバージョンに対応するようにタグが打ってあるようでした! 今回は9.3.0を要求されているため、以下のようにtag9.3.0に変更すればOKです!

target 'Runner' do
  pod 'FirebaseFirestore', :git => 'https://github.com/invertase/firestore-ios-sdk-frameworks.git', :tag => '9.3.0'
end

ちなみに9.3.0を要求された、dartのcloud_firestoreのバージョンは3.4.0でした。

おわりに

今回の記事ではプリコンパイルされたiOS Firestore SDKを使ってFlutterのビルド時間を短縮するテクニックを紹介しました。

ビルドが長いことはそれ自体が時間が勿体無い上に、ビルド中にtwitterを触りたくなってタスクに戻れなくなったりなど、さらなる時間の浪費につながります。 ちょっとした工夫で済むのであれば、積極的にビルド時間を短くしていきたいですよね。

【Flutter】flutterfire x riverpod x go_routerで認証ガードをつくるときの落とし穴

はじめに

お久しぶりです。とりかつ(torikatsu923)です。

Flutterで開発をしているとflutterfire x riverpod x go_router の構成をとることがよくあります。 この構成でGoRouterの認証ガードを作ってみたらハマリポイントがあることがわかりました。 今回はその落とし穴と回避方法について紹介します。

FlutterFire x riverpod x go_routerの落とし穴

FlutterFireでは認証状態はonAuthStateChanged()Stream<User?>として取り出すことができます。 ここにriveropdのStreamProviderを組み合わせると、認証状態の変更を監視するのが容易になります。

しかし、この方法には少しだけ問題があります。 例えばルーティングをgo_routerで行なっており、GoRouteredirectに認証ガードを設定したい時があります。

// router provider
// ...
GoRoute(
  path: '/home',
  redirect: (state) {
     // Stream<User?> to User?
    final user = ref.read(authProvider.select((v) => v.value);
    if (user == null) return '/signin';
    return null;
  }
)
// ...

この方法はうまくいきそうですが、認証状態の変更を伴う画面遷移で問題が起きます。 例えば以下のようなサインイン処理を考えます。

// sign in screen

// emit when user pressed sign in button.
Future<void> onSubmit() async {
   // ...
      await FirebaseAuth.instance
          .signInWithEmailAndPassword(email: email, password: password);
   context.go('/home');
}

これは一見うまくいきそうですがうまくいきません。 FirebaseAuthのauthStateChanged()は最新の状態が反映されるまでに若干タイムラグがあります。 それゆえ、サインインして/homeへ遷移した際のonAuthStateChangedの値は未認証のままとなります。 ここでGoRouterのredirectでuserが取れず認証されていないと判断され、/signinにリダイレクトされます。

このままではユーザは認証したのに画面遷移されない...となってしまいます。

回避方法

これを回避するためにはauthStateChagne()の更新を待ってから画面遷移する必要があります。

一般にstreamで次の値を待つには.firstを使用します。しかし、StreamProviderはBroadcastStreamのため、.firstで取得できる値は最新の値ではなく最新のキャッシュです。

そのため、次の値を待つためのextensionを準備します。

extension StreamExt<T> on Stream<T> {
  Future<T> next() {
    final _completer = Completer<T>();
    final sub = listen(null);
    sub.onData((e) {
      sub.cancel();
      _completer.complete(e);
    });
    return _completer.future;
  }
}

これで値の更新を待ってから画面遷移する準備ができました。

final user = _ref.read(authStreamProvider.stream).next();

authStateChagned同じ値が流れてくることがあります。 例えば現在の値がnullで次にまたnullが流れてきたり、現在がUserのときに次にUserが流れてくるということがあります。 これは値の更新を待って画面遷移したとしても、再度nullが渡ってくることがあると言うことです。

これを回避するため、以下のように値の変更があったときだけ流すようStreamを変換します。

final _prev = StateProvider<User?>((ref) => null);

final authStreamProvider = StreamProvider<User?>(
  (ref) => FirebaseAuth.instance.authStateChanges().transform(
    StreamTransformer.fromHandlers(
      handleData: ((data, sink) {
        if (data != ref.read(_prev)) {
          sink.add(data);
          ref.read(_prev.notifier).update((_) => data);
        }
      }),
    ),
  ),
);

やっていることは以下の通りです。 - Streamの直前の値をキャッシュする - Streamに値が流れてきた時に値が変更されているかチェックする - 値の変更があればsink.addする

以上で認証状態の更新を待って正しく画面遷移することができるようになりました。

// onSubmit in sign in screen
// ...
        final user = _ref.read(authStreamProvider.stream).next();
        await _ref.read(authenticatorProvider).signin(
              email: state.email.text,
              password: state.password.text,
            );
        if (await user == null) {
          throw AppError.unknown();
        } else {
          _ref.read(routerProvider.notifier).go('/home');
        }

おわりに

今回の記事ではFlutterで人気な構成での落とし穴とその回避方法を紹介しました。 この記事が誰かの助けになれば幸いです。

【Flutter】RivderpodのStateNotifierがoverrideWithProviderできるようになっていた

こんにちは、とりかつ(@torikatsu923)です。

RiverpodでScopedProviderを使わなくてもStateNotifierをoverrideできるようになっていました。 今回はStateNotifierをoverrideする方法について紹介します。

今までのStateNotifier.family

以前以下のような記事を書きました。 torikatsu923.hatenablog.com

この記事では StateNotifierFamily を用いてStateNotifierの初期値を設定していました。 そして、初期値を設定したStateNotifierをウィジェットツリーから利用するために以下のようなコードを書く必要がありました。

このコードでやっていることは以下のとおりです。

  1. StateNotifier.family( providerFamily )を定義する (最終的にウィジェットツリーから利用したいStateNotifier)
  2. 初期値を設定したproviderFamilyをprovideするめに利用するScopedProvider( provider )を定義する
  3. ProviderScopeで生成した providerFamily を、 provider をoverrideすることで子ウィジェットから利用可能にする

provider定義側

final provider =
    ScopedProvider<StateNotifierProvider<SomeStateNotifier, SomeState>>(
        (ref) => throw Error());

final providerFamily = 
    StateNotifierProvider.family<SomeStateNotifier, SomeState, String>(
        (ref, text) => SomeStateNotifier(text: text));

なんらかのウィジェット

      // ...
      return ProviderScope(
        overrides: [
          provider
              .overrideAs((watch) => providerFamily(text)),
        ],

ところがRiverpod 1.0.0で以下のような変更が入っていました。

All providers can now be scoped. Breaking: ScopedProvider is removed. To migrate, change ScopedProviders to Providers.

pub.dev

ScopedProviderが撤廃されて、全てのProviderが overrideWithProvider することが可能になりました。

これによって上と同じことを以下のコードで実現できるようになりました。

provider定義側

final provider = StateNotifierProvider<SomeStateNotifier, SomeState>(
        (ref) => throw Error());

final providerFamily = 
    StateNotifierProvider.family<SomeStateNotifier, SomeState, String>(
        (ref, text) => SomeStateNotifier(text: text));

なんらかのウィジェット

      // ...
      return ProviderScope(
        overrides: [
          provider.overrideWithProvider(providerFamily("some text")),
        ],

StateNotifierをoverrideできると嬉しいこと

Providerをoverrideするためのコードは大きく変化しているようには見えません。

では、この変更がどういう時に嬉しいのでしょうか。 この変更はWidgetRef経由でStateNotifierを利用する際にとても便利になります。

従来のScopedProviderを利用する場合は、StateNotifierへアクセスするために以下のようにする必要がありました。

ref.read(ref.read(provider))

ScopedProviderによってprovideされるproviderをread する必要があったため ref.read を二重にしています。大したことないように見えますが、実装を進めていくと毎回二重に ref.read をするのは結構面倒でした。

これがアップデートによって ref.read が一回で済むようになりました。

ref.read(provider)

二重に ref.read する手間を省ける上に、StateNotifier.familyを利用してもStateNotifierと同じように利用できる点がとても便利になりました。

【Flutter】freezed・json_serializableのファイル生成先を変更するためのbuild.yaml

こんにちは、とりかつ(@torikatsu923)です。 今回の記事ではfreezed・json_serializableのファイル生成先を変更する方法を紹介します。

課題感

freezed・json_serializableは、手動で作成するのが面倒なコードをサクッと生成できるため非常に便利です。しかし、生成対象のクラスが増えるにつれて生成ファイルの数がとても多くなります。

APIとの通信まわりのオブジェクトを格納するディレクトリがこのようになってしまった経験がある方は少なくないと思います。

f:id:torikatsu923:20220207061208p:plain:h300
生成ファイルだらけになってしまったディレクト

このままだと見通しが悪いため、生成ファイルは一箇所でまとめて管理したいですよね。

ファイル生成先を変更するためのbuild.yaml

以下のようにbuild.yamlで生成先を指定することで、ファイルの生成先を変更することができます。

build.yaml

targets:
  $default:
    builders:
      source_gen|combining_builder:
        options:
          build_extensions:
            '^lib/{{}}.dart': 'lib/generated/{{}}.g.dart'
      freezed:
        options:
          build_extensions:
            '^lib/{{}}.dart': 'lib/generated/{{}}.freezed.dart'

ここでfreezedとjson_serializable別で生成先を指定する必要がある点に注意です。

また、source_genをpubspec.yamlのdev_dependenciesに追加する必要がある点についても注意が必要です。

pubspec.yaml

dev_dependencies:
  # ...
  source_gen: ^1.2.1

これで自動生成ファイルを一箇所にまとめることができました。

f:id:torikatsu923:20220207235136p:plain:h300
ファイルの生成先を変更できた

参考

  • source_genのbuild.yamlの設定方法の紹介 pub.dev

  • 生成先のパスの指定方法について github.com

  • freezedのbuild_extensionsの対応についてのIssue github.com

【Flutter】Dartの実行環境を作る

こんにちは、とりかつ(torikatsu923)です。

Flutter開発をしていると、Dartの実行環境が欲しい時があります。 例えばFreezedのようなflutter sdkに依存しないDart onlyなパッケージを試したい時などが考えられます。

今回はDartの実行環境の作り方を紹介しようと思います。

目次

とりあえずHello worldしてみる

とりあえずdart runHello worldできるようにしてみます。

  1. main.dartを用意します。

main.dart

void main() {
  print('Hello world');
}
  1. main.dartを実行する

ターミナルで以下を実行します

$ dart run main.dart

f:id:torikatsu923:20220115191234p:plain

無事コンソールにHello Worldが表示されました。

しかしこのままだとパッケージを試したい時に不便です。 いつもならpub getとするところを、手でパッケージをダウンロードしてきてimportも頑張ってパスで指定する必要があります。

pub getできるようにする

pub getできるようにするためには手元のプロジェクトをパッケージにする必要があります。 Dartのプロジェクトの単位はパッケージで、最小構成は以下のようになります。

.
├── ./lib
│   └── ./lib/${パッケージ名}.dart
└── ./pubspec.yaml

ここでpubspec.yamlではnameでパッケージ名指定する必要があるため注意です。

今回はパッケージ名をplaygroundとするため、ファイルツリーとpubspec.yamlは以下のようになります。

.
├── ./lib
│   └── ./lib/playground.dart
└── ./pubspec.yaml

pubspec.yaml

name: playground

それでは実行してみます。

dart run ./lib/playground.dart

f:id:torikatsu923:20220115192829p:plain

無事に実行することができ、pub getもできるようになりました。

今のままでもいいですが、dart run ./lib/playground.dartのファイル指定が少し面倒です。

ファイル指定なしでdart runできるようにする

一旦dart runしてみます。

You should edit ~/Desktop/flutter_dev/playground/pubspec.yaml to contain an SDK constraint:

environment:
  sdk: '>=2.10.0 <3.0.0'

See https://dart.dev/go/sdk-constraint

エラーっぽいものが出てきました。僕の環境ではdart2.15.1なのですが、dart2以降はpubspec.yamldartのバージョンを指定する必要があるみたいです。

~/desktop/flutter_dev/playground
❯ dart --version    
Dart SDK version: 2.15.1 (stable) (Tue Dec 14 13:32:21 2021 +0100) on "macos_x64"

pubspec.yamlを次のようにして再度dart runしてみます。

pubspec.yaml

name: playground
environment:
  sdk: '>=2.10.0 <3.0.0'
~/desktop/flutter_dev/playground
❯ dart run   
Could not find `bin/playground.dart` in package `playground`.

ファイルがないと怒られました。ファイル無指定時のdart runのエントリーポイントはlib/playground.dartではなく、bin/playground.dartのようです。 なのでbin/playground.dartを作って再度dart runします。

bin/playground.dart

import 'package:playground/playground.dart' as playground;

void main() {
  playground.main();
}

ファイルツリー

.
├── ./bin
│   └── ./bin/playground.dart
├── ./lib
│   └── ./lib/playground.dart
└── ./pubspec.yaml

無事実行されました!

~/desktop/flutter_dev/playground
❯ dart run
Building package executable...
Built playground:playground.
Hello World

【必殺技】dart createに頼る

ここまで手でpubspec.yaml等を作成してきました。 実はここまでの手順は以下のコマンドで一発でいけます。

dart create ${パッケージ名}

f:id:torikatsu923:20220115195507p:plain

めっちゃ楽ですね...

おわりに

今回はDartの実行環境の作り方を紹介しました。 普段Flutter触ってるけどDartのプロジェクト作り方わからんって人の助けになればと思います。

【Flutter】dart-definesでアプリの環境を切り替える

はじめに

こんにちは、とりかつ(@torikatsu923)です。

アプリ開発をしていると本番、ステージング、開発と環境を分けたい場合があります。 Flutterでアプリの環境を切り替える方法として、今まではFlavorを使う方法がありました。

最近、DartDefinesという仕組みでも環境を切り替えることができるようになりました。 Flavorと比べ、DartDefinesを使った方が設定がシンプルになります。

今回の記事では目的、OS別(android, iOS)にDartDefinesを利用したアプリの環境切り替え方法を紹介します。

この記事の読み方

この記事では基本設定が終わっている前提で各環境の切り替えの解説をします。そのため、初めに基本設定を読むことを強くお勧めします。 基本設定の後はどの順番で読んでいただいても大丈夫です。

この記事の読み方
この記事の読み方

環境切り替えの対象

  • アプリアイコン
  • アプリのID(bundleId, applicationId)
  • アプリの表示名
  • Firebaseの設定ファイル(GoogleService-Info.plist, google-services.json)
  • Dartのコード中の設定

サンプルコード

サンプルコードは以下のリポジトリです。切り替え対象を全て切り替えられるよう設定した状態になります。

github.com

目次

基本設定

この章ではネイティブ側でDartDefinesの値を利用できるようにする方法を解説します。この基本設定が終わると以下のようにflutter runで環境を指定できるようになります。

$ flutter run --dart-defines=FLAVOR=${環境}

環境

切り替える環境は以下の3つです。

環境 FLAVOR
開発 dev
ステージング stg
本番 prd

iOS

iOSでは環境ごとに.xcconfigを用意しておき、ビルド時にDartDefinesの値から読み込む.xcconfigを変更する方法をとります。

1. 環境に対応するxcconfigを作成する

以下の.xcconfigRunner > Flutterに作成します。

  • dev.xcconfig
  • stg.xcconfig
  • prd.xcconfig

Runner > Flutterを右クリックし、New File...を選択します。 NewFileを選択するとモーダルが表示されるため、Configuration Settings Fileを選択します。

選択後Save Asを作成したいファイル名に変更し、createを押してファイルを作成します。

作成したxcconfigの中身は以下のようにします。

FLAVOR=devまたはstgまたはprd

dev.xcconfigの場合はこのようになります。

次に環境ごとのxcconfigをincludeできるようDebug.xcconfigRelease.xcconfigに以下を追記します。

#include "DartDefine.xcconfig"

この後、ビルド時にFlavorに対応するxcconfigをincludeするDartDefine.xcconfigというファイルを動的に生成するようにします。そのためここではDartDefine.xcconfigをincludeするようにしています。

2. DartDefinesに対応するxconfigをincludeするシェルスクリプトを作成する

以下の内容のファイルをios/scripts/dart_define.shに置きます。

#/bin/bash
echo $DART_DEFINE | tr ',' '\n' | while read line; 
do
  echo $line | base64 -d | tr ',' '\n' | xargs -I@ bash -c "echo @ | grep 'FLAVOR' | sed 's/.*=//'"
done | (
  read flavor
  echo "#include \"$flavor.xcconfig\"" > $SRCROOT/Flutter/DartDefine.xcconfig
)

DartDefinesの値は$DART_DEFINESでアクセスできます。
また$SRCROOTiosディレクトリのパスを指しています。

以下のように指定した場合、$DART_DEFINESで得られる値は次のようになります。

--dart-defines=A=valueA,FLAVOR=dev,B=valueB
QT1hLEZMQVZPUj1kZXYsQj1i,RkxVVFRFUl9XRUJfQVVUT19ERVRFQ1Q9dHJ1ZQ==

どうやら実行時に指定した値以外にFLUTTER_WEB_AUTO_DETECT=truebase64エンコードした値がカンマ区切りで追加されるようです。

この文字列からFLAVORの値を取り出し、include対象を指定したxcconfigを吐き出すため、ワンライナーで以下の処理をしています。

  1. 文字列をカンマ区切りる
  2. カンマで区切った各文字列をbase64でデコードし、カンマで区切る
  3. FLAVOR=xxxが見つかったら=より右側の値を取り出す
  4. flavorに対応するxcconfigincludeするDartDefine.xcconfigというファイルを生成する

3. ビルド前に2.のスクリプトが走るようPre-actionを設定する

Runner > edit schema...を選択しモーダルを表示します。

Build > Pre-actionを開き以下を設定します。

Provide build setting from

Runner

スクリプト

sh "$SRCROOT/scripts/dart_define.sh"

4. .gitignoreの編集

ビルドのたびに差分が発生しないよう.gitignoreに以下を追記します。

.gitignore

DartDefine.xcconfig

iOSでDartDefinesを受け取る基本設定は以上になります。

android

androidではbuild.gradle(app)でDartDefinesから値を取り出して変数に代入する方法をとります。

android/app/build.gradleに以下を追記します。追記する場所はどこでもいいですが他の環境切り替えの設定でandroid.defaultConfigでDartDefinesの値を利用することを考えると、android { ...よりも前にする必要があります。

def dartEnvironmentVariables = project
            .property('dart-defines')
            .split(',')
            .collectEntries {
                new String(it.decodeBase64(), 'UTF-8')
                    .split(',')
                    .collectEntries {
                        def pair = it.split('=')
                        [(pair.first()): pair.last()]
                    }
            }

DartDefinesの値はproject経由でアクセスすることができます。DartDefinesはbase64エンコードされた文字列がカンマ区切りになっているため、split(',')してさらに各値をsplit('=')することでDartDefinesに渡された値全てを含むmapを作成しています。 [(pair.first()):pir.last()]よりmapのキーはDartDefinesに指定した変数名になるため、FLAVORの値は以下のようにアクセスできるようになります。

dartEnvironmentVariables.FLAVOR

androidの基本設定は以上になります。

アプリアイコン

本節ではflutter_launcher_iconsで自動生成したアイコンを使用して環境ごとにアイコンを切り替える方法を紹介します。

アイコンの自動生成

初めにpubspec.yamlにflutter_laucher_icons`の依存関係を追加します。

...

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^1.0.0
  flutter_launcher_icons: ^0.9.2 # 依存関係を追記

...

依存関係を追加したら以下を実行します。

$ flutter pub get

次に環境ごとのアイコン画像を用意します。今回は以下の3つの画像を用意してassets/launcher_icons/に置いておきます。

画像が用意できたら環境ごとの設定ファイルを用意します。今回は以下の3つを用意しました。

  • flutter_launcher_icons-dev.yaml
  • flutter_launcher_icons-stg.yaml
  • flutter_launcher_icons-prd.yaml

ファイル名はflutter_launcher_icons-${環境名}.yamlとする必要があり、置く場所はpubspec.yamlと同じ階層にする必要があります。

それぞれのファイルの中身は以下のようにします。以下はdevの例で、アイコン画像のパス(image_path)は環境ごとに変えておく必要があります。

flutter_icons:
  android: true
  ios: true
  image_path: "assets/launcher_icons/dev.png"

さらにエラーが出てしまうことを防ぐため、pubspec.yamlの一番最後に以下を追記します。

flutter:
  uses-material-design: true

# ここから先を追記
flutter_icons:
  android: true
  ios: true

最後に以下を実行してアイコン画像を自動生成します。

$ flutter pub run flutter_launcher_icons:main

このコマンドによってandroidandroid/app/src/${環境}/resに、iOSios/Runner/Assets.xcassets/AppIcon-${環境}.appiconsetにアイコンが生成されました。

あとはネイティブ側ビルド時に使用するアイコンを切り替えればOKです。

iOS

iOSでは使用するアイコンを変数によって切り替えることでアイコン切り替えを実現します。

xcodeでTargetのRunner > Build Settingsを開き、Primary App Icon Set Nameを以下に書き換えます。

AppIcon-$(FLAVOR)

これでiOSのアイコンを切り替えることができるようになりました。

android

androidではビルド時に環境に対応するアイコンをandroid/app/src/main/res/にコピーすることでアイコンを切り替えます。

android/app/build.gradleを開き以下を追記します。追記する場所はdef dartEnvironmentVariablesandroid { ...の間である必要があります。

task copyIcons(type: Copy) {
    from "src/${dartEnvironmentVariables.FLAVOR}/res"
    into 'src/main/res'
}

tasks.whenTaskAdded {
    it.dependsOn copyIcons
}

copyIconsでは標準で用意されているcopyタスクを使用しています。fromにコピー元、intoにコピー先を指定するだけでファイルをコピーするタスクを簡単に定義することができます。 タスクを定義したらwhenTaskAddedでタスクの依存関係にcopyIconsを追加します。これによって何かしらのタスクが実行された際にアイコンをコピーするタスクを実行することができるようになります。

whenTaskAddedが既に定義されている場合、既存のwhenTaskAddedクロージャ内にit.dependsOn copyIconsを追記してください。

ビルドのたびに差分が発生しないよう.gitignoreに以下を追記します。

.gitignore

**/android/app/src/main/res/mipmap-*/ic_launcher.png

これでandroidでもアイコンを切り替えることができるようになりました。

アプリのID(bundleId, applicationId)

本節では変数を利用してアプリのIDを切り替える方法を紹介します。

iOS

まず各環境に対応するxcconfigAPP_ID_PREFIXという変数を定義します。今回はdev, stg, prdと3つの環境に対応するために以下のようにしました。

dev.xcconfig

# ...
APP_ID_PREFIX=dev.

stg.xcconfig

# ...
APP_ID_PREFIX=stg.

prd.xcconfig

# ...
APP_ID_PREFIX=

次にxcodeTARGETS > Runner > Build Settingsを開き、Product Bundle IdentifierAPP_ID_PREFIXを参照するようにします。サンプルのアプリIDはcom.example.flutterenvのため、以下のようになります。

$(APP_ID_PREFIX)com.example.flutterenv

これでiOSのアプリIDを切り替えられるようになりました。

android

androidにはapplicationIdSuffixが用意されています。ここに値を設定することでapplicationIdにサフィックスを追加することができます。

android/app/build.gradleを開いて、defaultConfigの最後に以下を追記します。

android {

    // ....

    defaultConfig {
     
        // ...

        if(dartEnvironmentVariables.FLAVOR == 'dev') {
            applicationIdSuffix ".dev"
        } else if(dartEnvironmentVariables.FLAVOR == 'stg') {
            applicationIdSuffix ".stg"
        } else if(dartEnvironmentVariables.FLAVOR == 'prd') {
            applicationIdSuffix ""
        } else {
            throw new GradleException("unknown flavor" + dartEnvironmentVariables.FLAVOR + "has passed")
        }
    }

これでandroidでアプリIDを切り替えることができるようになりました。

アプリの表示名

本節では変数を参照することでアプリの表示名を切り替える方法を紹介します。それぞれのOSで以下のようにアプリの表示名が切り替わるようにします。

  • dev.${アプリ名}
  • stg.${アプリ名}
  • ${アプリ名}

iOS

まず各環境に対応するxcconfigAPP_ID_PREFIXという変数を定義します。今回はdev, stg, prdと3つの環境に対応するために以下のようにしました。(アプリIDの手順を先に行っている場合はこの手順は飛ばしてください。)

dev.xcconfig

# ...
APP_ID_PREFIX=dev.

stg.xcconfig

# ...
APP_ID_PREFIX=stg.

prd.xcconfig

# ...
APP_ID_PREFIX=

次にxcodeTARGETS > Runner > Infoを開き、Bundle display nameAPP_ID_PREFIXを参照するようにします。サンプルのアプリの名前はflutterenvのため、以下のようになります。

$(APP_ID_PREFIX)flutterenv

これでiOSで環境ごとに表示名を切り替えることができるようになりました。

android

初めにandroid/app/build.gradleを開いて、defaultConfigの最後に以下を追記します。

android {

    // ....

    defaultConfig {
     
        // ...

        if(dartEnvironmentVariables.FLAVOR == 'dev') {
            manifestPlaceholders += [appNamePrefix:"dev."]
        } else if(dartEnvironmentVariables.FLAVOR == 'stg') {
            manifestPlaceholders += [appNamePrefix:"stg."]
        } else if(dartEnvironmentVariables.FLAVOR == 'prd') {
            manifestPlaceholders += [appNamePrefix:""]
        } else {
            throw new GradleException("unknown flavor" + dartEnvironmentVariables.FLAVOR + "has passed")
        }
    }

manifestPlaceholdersに環境ごとのappNamePrefixを追加します。こうすることでAndroidManifest.xmlからbuild.gradleで追加した変数を利用することができます。

以下のように代入してしまうとflutterのプラグイン中で追加された変数を上書きしてしまうためビルドに失敗します。

manifestPlaceholders = [appNamePrefix:""]

次にandroid/app/src/main/AndroidManifest.xmlを開き、android:labelでgradleで追加した変数を使用するようにします。

<manifest
    ...
   <application
         android:label="${appNamePrefix}flutterenv" // この行を編集
         android:name="${applicationName}"
         ...

これでandroidのアプリ表示名を切り替えることができるようになりました。

Firebaseの設定ファイル(GoogleService-Info.plist, google-services.json)

本節ではFirebaseの設定ファイルの切り替え方を紹介します。iOS, androidどちらもファイルをコピーする方法をとります。

iOS

以下のファイル名で各環境のGoogleService-Info.plistを用意します。

  • devGoogleService-Info.plist
  • stgGoogleService-Info.plist
  • prdGoogleService-Info.plist

ファイルが用意できたらios/Firebase/に置きます。

xcodeTARGETS/Runner/Build Phasesを開き、右上の+ボタンからNew Run Script Phaseを選択します。

追加できたら以下のスクリプトを設定します。

cp -f ${SRCROOT}/Firebase/${FLAVOR}GoogleService-Info.plist ${SRCROOT}/GoogleService-Info.plist

Phaseの名前はわかりやすいようにCopy GoogleServiceinfo.plistに変更しています。

最後にGoogleService-Info.plistの参照をプロジェクトに追加します。

まずios直下にGoogleService-Info.plistを置きます。このファイルは後で削除するためファイル名さえあっていれば中身はなんでもOKです。

次にfinderからxcodeのRunner直下にGoogleService-Info.plistドラッグ&ドロップします。

ダイアログが表示されたらFinishを押します。これでファイルの参照を作ることができました。

finderからGoogleService-Info.plistを削除します。削除後にxcodeGoogleService-Info.plistが赤くなっていれば成功です。

ビルドのたびに差分が発生するのを避けるために.gitignoreに以下を追記します。

.gitignore

**/ios/GoogleService-Info.plist

以上でビルド時にiOSのFirebaseの設定ファイル切り替えられるようになりました。

android

以下のファイル名で各環境のgoogle-services.jsonを用意します。

ファイルが用意できたらandroid/app/src/firebaseに置きます。

次にandroid/app/build.gradleを開き以下のタスクを定義します。

build.gradle

task copyFirebaseSource(type: Copy) {
    from "src/firebase/${dartEnvironmentVariables.FLAVOR}-google-services.json"
    into './'
    rename { String fileName ->
        fileName = "google-services.json"
    }
}


tasks.whenTaskAdded {
    // ...
    it.dependsOn copyFirebaseSource
}

ここでもcopyタスクを利用します。コピーする際にファイル名を変更する必要があるため、renameにファイル名変更の処理を指定しています。

すでにwhenTaskAddedが定義されている場合、既存のwhenTaskAddedの最後にit.dependsOn copyFirebaseSourceを追記します。

ビルドのたびに差分が発生するのを避けるために.gitignoreに以下を追記します。

.gitignore

**/android/app/google-services.json

以上でビルド時にandroidのFirebaseの設定ファイル切り替えられるようになりました。

Dartのコード中の設定

DartコードからFlavorの値を利用したい場合があります。その際は以下のようにしてアクセスすることができます。

String.fromEnvironment('FLAVOR')

dotenv等を利用している場合、この値を使うことで読み込むファイルを切り替えることができます。

【zsh】空ファイルを作る3つの方法

この記事はUnipos Advent Calender 2021、11日目の記事です。

お久しぶりです。とりかつ(@torikatsu923)です。

開発作業をしていると空ファイルを作成したい時がしばしばあります。

新しいファイルにプログラムを書きたい、とりあえずメモを取りたい、、、

エンジニアの皆さんはターミナルから空ファイルを作成することが多いと思います。 ターミナルを使用した空ファイルの作成方法には複数あります。 今回の記事は空ファイルの作成方法とそれぞれの特徴について紹介していこうと思います。

空ファイル作成コマンド一覧

空ファイルを作成するコマンドをざっと5つぐらい上げてみました。

  • touch foo
  • cp /dev/null foo
  • cat /dev/null > foo
  • echo -n > foo
  • :> foo

空ファイルの作成方法はいろいろな記事で紹介されています。それらを大きく以下の3つに分類することができます。

  1. 無をリダイレクト(>)する
  2. touchコマンドを使用する
  3. 0byteのファイルをコピーする

無をリダイレクト(>)する

この方法はなんらかの方法で0byteの出力を得て、それをリダイレクトすることで空ファイルを作成する方法です。この方法の強みはタイプ数が最も少なくなることです。

0byteの出力を得る方法はいろいろあります。

echoで改行を表示しない方法や、、

$ echo -n

無をcatする方法、、、

$ cat /dev/null

その中でも最もタイプ数が短いのは:を使う方法です。

$ :

これだけで無を得ることができます。:という演算子がありこれは何もしません。 実際にバイト数を測ってみると0でした。

$ : | wc -c
       0

空ファイルを作成する際はこれをリダイレクトするだけなのでファイル名を除けば2キーストロークで完結します。

:>ファイル名

短くて最高ですね!!

touchコマンドを使用する

touchコマンドによってファイルを作成することができます。この方法は馴染みが深い方法だと思います。 touchの強みは以下のように複数のファイルを一気に作成できることです。

$ touch foo.txt bar.txt

$ ls
bar.txt foo.txt

また、touchはファイルが存在しなければファイルを作成し、ファイルが存在すれば更新日時を最新にしてくれます。リダイレクトはファイルが存在する場合は失敗するだけなので使い分けができそうですね。

~/Desktop/touch_test
❯ ls1 
bar.txt foo.txt

~/Desktop/touch_test
❯ :>bar.txt
zsh: file exists: bar.txt

touchを激推ししましたが、実はteeを使うことで複数のファイルを一気に作ることもできます...

$ : | tee a b c 

touchよりこっちのがタイプ数すくなくてええやん

0byteのファイルをコピーする

以下のようにあらかじめ0byteのファイルを用意しておいてコピーする方法です。

$ cp 0バイトのファイル foo

0byteのファイルを常に準備する手間がかかってしまうため、/dev/nullを使う方法がいいかなと思います

$ cp /dev/null foo

cpには強制的に上書きする-fオプションがあります。これを組み合わせると初期化処理で活躍しそうです。

~/Desktop/touch_test
❯ seq 5 >> bar.txt

~/Desktop/touch_test
❯ cat bar.txt
1
2
3
4
5

~/Desktop/touch_test
❯ yes | cp -f /dev/null bar.txt
overwrite bar.txt? (y/n [n]) 

~/Desktop/touch_test
❯ cat bar.txt

おわりに

今回は様々な空ファイル作成方法について紹介しました。 特に:>はめちゃめちゃ便利で、記号のみで構成されているところに非常に強い趣を感じることができます()

今回紹介した方法以外にも空ファイルを作成する方法は様々あると思うので、もしアイデアがあればコメント等いただけるととても嬉しいです!

【zsh】ターミナルでサクッとメモを取るための設定

いきなりですが、あなたは今ターミナルで作業をしています。 巧みなキーボードさばきで様々なコマンドをバシバシと打ち込みタスクを進めています。 そんな作業中ふとgitのコミットIDを控えたくなりました。

そんな時あなたはどうしますか?

  • OSデフォルトのメモアプリを開く
  • evernoteを開く
  • 手元のメモ帳にペンでメモ書きする

...etc

色々な方法があると思います。
せっかくターミナルで作業をしているなら、ターミナルでメモを取れるようにしたいですよね

f:id:torikatsu923:20210911232556p:plain

今回はzshでコマンドのインストールなしでメモを取るための簡単な設定を紹介したいと思います。

メモの設定のステップ

ターミナルでサクッとメモをとるためには、以下の3ステップが必要になります。

  1. メモのファイルを置くディレクトリを作成する
  2. メモのファイルを作成する
  3. ターミナル上で動作するエディタでファイルを開く

例えば以下のコマンドで上のステップを実行することができます。

$ mkdir ~/memo
$ touch ~/memo/foo.md
$ vim ~/memo/foo.md

でも毎回コマンドを叩くのは面倒だと思います。 なので、この一連のステップを関数にして、.zshrcに関数を呼び出すためのエイリアスを登録します。

.zshrcの設定

以下を.zsrhcに追記します。

# memo alias
function writeMemo() {
  MEMO_HOME=$HOME/memo
  MEMO_FILE=$MEMO_HOME/$(date +%Y_%m_%d).md
  if [[ -d $MEMO_HOME ]] then
    mkdir $MEMO_HOME -P
  fi
  if [[ -f $MEMO_FILE ]] then
    touch $MEMO_FILE
  fi
  vim $MEMO_HOME/`date +%Y_%m_%d`.md
}

alias memo=writeMemo

.zshrcを更新したら.zshrcの再読み込みをします。

$ source ~/.zshrc

これで以下のコマンドでメモを取れるようになりました!

$ memo

この設定の嬉しいこと

今回紹介した設定で嬉しいことはたくさんあります!
例えば...

  • キーボードのみで操作可能

    vim, vim, neovim, emacs, nano, vimといったターミナルで動くエディタを使用するのでキーボードのみで操作が可能です

  • シームレスにメモを取ることができる

    メモを取るためにアプリを移動する必要がないため、非常に高速にメモを取り始めることができます。  エディタにvimを選択している場合<C-z>でメモをバックグランドにし、ターミナルの様子を確認したあと$ fgでメモを再開することができるのも大きなメリットです。
    特にtmuxとのコンビネーションは最強なのでおすすめです。

  • メモ全体をgrepできる

    以下のようにメモ全体に対してgrepすることができます。
    cat ~/memo/* | grep '検索したい文字列'

  • バックアップが簡単

    以下のコマンドで簡単にバックアップを取ることができます。
    tar ~/memo | gzip > backup.tar.gz
    githubのプライベートリポジトリと連携し定期的にメモ同期することで、あらゆるPCでメモを共有することができるかもしれません

  • 任意の形式でメモを取ることができる

    生成するメモのファイル名を自由に指定することができるため、任意の拡張子のファイルを生成することができます。これにより、任意の形式でメモを取ることができます。
    今回紹介した設定では.mdを指定しているためマークダウン形式です。ここに.yaml.json.htmlを指定することもできます()
    またエディタ側の設定次第でシンタックスハイライトや入力補完を有効にすることができます。

おわりに

今回はターミナルでメモを取るための簡単な設定を紹介しました。
もし今回紹介した設定が誰かの生産効率UPにつながれば幸いです!

【Flutter】Canvasで線香花火を作る

はじめに

こんにちは、とりかつ(@torikatsu)です。

夏も終わりに近づいてきました。みなさん夏休みはどこかいかれましたか? 私はうっかり夏休みを取り損ねたためずっと仕事でした()

今年はコロナの影響もあって花火大会や夏祭りもなかったため、夏を感じる機会が少なかったように思われます。 なんだかこのまま夏を終えるのは少し寂しいですよね...

ということでFlutterのCanvasで線香花火を作りました!
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

今回の記事はFlutterのCanvasを使って線香花火を作る方法を紹介します。

リポジトリ

リポジトリはこちらになります。 github.com

作り方

作り方は大きく以下の3つのステップに分かれます。

  • 火花の動きを再現する
  • 被写界深度を考慮する
  • パラメータの調整

火花の動きを再現する

線香花火という現象は無数の火花の動きによって起きています。 これより火花一個一個の動きをきちっと再現できればリアルな線香花火を作ることができると考えられます。 火花の動きはざっくり火花の移動と火花の分裂に分けて考えることができます。

火花の動き

 t秒後の火花の動きは運動方程式 F=ma tで2階積分することで求められます。

積分した位置の式は以下のようになります。

  •  k: 空気抵抗係数
  •  v: 火花の速度
  •  m: 火花の質量
  •  a: 加速度 (鉛直方向以外にも式を使い回したいのであえて加速度で考えます)
  •  x_0: 初期位置

 x(t) = -\dfrac{m}{k} \lbrace (v_0 - \dfrac{ma}{k})e^{-\frac{k}{m}t} + at + v_0 - \dfrac{ma}{k} \rbrace + x_0

実際に作るときは吹いている風を考えて初速度を v_0 = v_0 + v_{wind}として扱います。


導出

運動方程式

 F = ma-kv

また F = m \dfrac{dv}{dt}より

 m\dfrac{dv}{dt} = ma-kv

となります。

これをvについて解いて、

 \dfrac{dv}{dt} = a - \dfrac{kv}{m}

 \dfrac{dv}{dt} = -\dfrac{k}{m}(v - \dfrac{ma}{k})

 \dfrac{dv}{v - \dfrac{ma}{k}} = -\dfrac{k}{m}dt

両辺を積分して、

 \int \dfrac{1}{v - \dfrac{ma}{k}}dv = \int -\dfrac{k}{m}dt

 \int \dfrac{1}{x + a}dx = \log | x + a | + C より

 \log | v - \dfrac{ma}{k} | = -\dfrac{k}{m}t + C (Cは積分定数)

両辺の指数をとって

 v - \dfrac{ma}{k} = e^{c}e^{-\frac{k}{m}t}

ここで e^{C} = Dとすると

 v - \dfrac{ma}{k} = De^{-\frac{k}{m}t}

 v = De^{-\frac{k}{m}t}+\dfrac{ma}{k}

となります。 t=0のとき、火花の速度は初速度 v_0なので、

 v0 = D + \dfrac{ma}{k}

 D = v_0 - \dfrac{ma}{k}

となりDの値がわかります。

よって t秒後の火花の速度は

 v(t) = (v0 - \dfrac{ma}{k}) e^{-\frac{k}{m}t} + \dfrac{ma}{k}

となります。

さらに t積分して

 x(t) = \int \lbrace (v0 - \dfrac{ma}{k}) e^{-\frac{k}{m}t} + \dfrac{ma}{k} \rbrace dt

 x(t) = -\dfrac{m}{k}De^{-\dfrac{k}{m}t}+\dfrac{ma}{k}t+C_2 (C_2は積分定数)

ここで t = 0のとき、初期位置を x_0とすると

 x_0 = -\dfrac{m}{k}D + C_2

 C_2 = x_0 + \dfrac{m}{k}D

となり C_2がわかります。

よって位置の式は以下のようになります。

 x(t) = -\dfrac{m}{k}De^{-\frac{m}{k}t}+\dfrac{ma}{k}t+\dfrac{mD}{k} + x_0

 x(t) = -\dfrac{m}{k} \lbrace (v_0 - \dfrac{ma}{k})e^{-\frac{k}{m}t} + at + v_0 - \dfrac{ma}{k} \rbrace + x_0

火花の分裂

線香花火の火花の内側からは気泡が発生しています。その気泡が表面に達した際に破裂することあの火花の分裂が起きます。

分裂周りの計算は複雑になるので割愛します。詳細はこちらの論文を見てください。 非常にわかりやすくまとめてあります。

https://www.jstage.jst.go.jp/article/jcombsj/60/193/60_156/_pdf/-char/ja

この論文より、線香花火の主成分はカリウム化合物で、火花の分裂回数は最大8回ということがわかります。 またn回分裂したときの火花の半径 r_n、火花の寿命 t_n、分裂時の初速度 u_nは以下で求めることができます。

 r_n = {β}^{n}R_0

 t_n = \dfrac{ {R_0}^{2} }{k} {β}^{2n}

 u_n = \sqrt{ \dfrac{σ}{ρR_0} } {β}^{ -\frac{n-1}{2} }

  •  β = 0.5: 分裂時の定数
  •  R_0 = 10^{-4} (m) 火の玉から飛び出す火花の半径(分裂0回目の火花の半径)
  •  k = {10}^{-1} (m2/s) カリウム化合物の熱拡散率
  •  ρ = {10}^{3} (kg/m3) カリウム化合物の密度
  •  σ = {10}^{-1} (N/m) カリウム化合物の表面張力
  •  n (1 \leq n \leq 8) 分裂回数

火花の半径、寿命、初速度は全てnによって決まり、nは1から8になることがわかっています。 そのためプログラム起動時に各分裂時の計算結果を定数として持つことができます。アニメーション中は膨大な数の計算をすることになるので、計算結果を使いまわせるのは非常に嬉しいですね!

分裂時の火花の初速度がわかったので、次は火花の初速の向きを考えます。 自然な分裂を表現するためには運動量保存を守る必要があります。

  •  m 分裂前の火花の重さ
  •  v_0 火花の初速度
  •  v_1 火花1の初速度
  •  v_2 火花2の初速度

とすると、運動量保存の式は以下になります。

 mv_0 = \dfrac{1}{2}mv_1 + \dfrac{1}{2}mv_2

これを解いて

 2v_0 = v_1 + v_2

 v_2 = 2v_0 - v_1

これより v_1が決まれば v_2がわかります。

初速度は v_nであることがわかっているため

 {v_{n}}^2 = {v_1}^2

となります。ベクトルの x方向の成分を a y方向の成分を b z方向の成分を cとすると、

 {v_{n}}^2 = a^{2} + b^{2} + c^{2}

となります。 これより、火花の初速度の2乗した値を適当に3分割し、平方根ととると v_1を求めることができます。

奥行きを表現する

2次元のCanvasで立体的な線香花火を表現するためには奥行きを表現する必要があります。 今回はピンボケを使って奥行きを表現します。

ピンボケは計算式があるようです。 keisan.casio.jp この式に則って計算をすると正確な値が出せますが、その分計算コストがかかります。 今回は少しでも計算コストを減らすために、原点と火花の距離を適当な範囲で区切ってボケに使う値を出すことにしました。

  late final double sigma = () {
    final z = position.z.abs();
    if (z < 0.035) return 0.00005;
    if (z < 0.040) return 0.0003;
    if (z < 0.045) return 0.0005;
    if (z < 0.050) return 0.0008;
    if (z < 0.055) return 0.001;
    return 0.002;
  }();

Canvasでボケを表現するためにMaskFilter.blur()Paint()に渡します。

...
        Paint()
          ..color = e.color
          ..maskFilter = MaskFilter.blur(BlurStyle.normal, e.sigma)
          ..strokeWidth = max(e.deameter, 0.001));
...

Canvasでぼかしをする方法を調べるとImageFilterばかり出てきて、MaskFilterの情報はほとんど引っ掛からなかったので地味にハマりポイントでした。

パラメータの調整

最後に色々なパラメータを調整して全体を整えます。 具体的には表示倍率を変更したり、初速度を計算結果より若干早くしました。

ここの記事にある時間の流れを若干遅くする方法は、計算量を減らしつつ見た目もそれっぽくなるのでかなり有効でした。 zenn.dev

あと計算量が多くなりコマ落ちしやかったので、isolateで火花の計算を別スレッドで行うようにしたりしました。

おわりに

実際に線香花火を再現することで、家でコードを書きながら夏を満喫するという最高の体験をすることができました! ぜひみなさんも夏っぽいものをコードで再現して、夏を満喫しましょう!