AG03MK2をデスク下にマウントしたら視界がスッキリした
AG03MK2をデスク下にマウントしたら、デスクのスッキリ感が向上しました。 この記事ではAG03をデスクしたにマウントする方法について紹介します。
課題感
最近、オーディオインターフェースが必要になりAG03MK2を購入しました
AG03MK2はAG03の後継に当たるオーディオインターフェースで、
- 一通り機能が揃っている
- 品質がいい
- 1クリックでマイクをミュートできる
- マットな黒色でデスクに統一感が出せる
の理由で選びました。
使い心地に問題はとてもいいのですが、存在感の強さが気がかりでした。
実際にデスクに置くとこのような感じです。
デスクスッキリ教の信者としてこの存在感は許せないため
なんとかデスク下にマウントしようと思いました
オーディオインターフェースのデスク下マウント事情
デスク下マウントを調べると以下のように天板にぴったりマウントするパターンをよく見かけます。
これはmotu m2のように上下が平らなオーディオインターフェースで可能なマウント方法です。 ですが、AG03MK2はコネクタが上部にあるためこのようなマウント方法は選択できません。
調べていくとオーディオインターフェースをデスク下にマウントするパーツの多くはこのようなタイプを想定して作られていることがわかりました。
ゆえにAG03MK2のような上側にコネクタがあるタイプをマウントするためには、
- マウント用のパーツを自作する
- 本来の用途とは違うパーツを流用する
- 諦めて机の上に置く
のどれかで対応するしかありません。 1は素人にはレベルが高すぎますし、3はもはや敗北したも同然です。 私は消去法で2の「本来の用途とは違うパーツを流用する」を選択しました
AG03MK2にぴったりなパーツを発見
ぴったりなパーツを探すため、1週間ほどインターネットの世界を旅した結果、KANADEMONOさんの「ティッシュボックストレイ」が最適であることがわかりました
ティッシュボックス トレイ #2(下向き)kanademono.design
私がパーツに求めた要件は以下の4つでしたが、このティッシュボックストレイは全ての要件を満たす最高な商品でした。
- マットな黒色
- オーディオインターフェースが落下しづらいこと
- マウントした状態でも操作が可能なこと
- 頑丈であること
- 大きさに過不足がないこと
KANADEMONOさんのティッシュボックストレイはちょうど鼻セレブと同じ寸法ですが、
実はAG03MK2も鼻セレブとほぼ同じ寸法です。(L字のXLRケーブルの高さ込み)
1週間ほど探し回っていたため、この事実に気づいた時は飛び上がるほど嬉しかったです。
早速購入しデスク裏にマウントしました。
デスク裏にマウントしてみる
届いたティッシュトレイはこのような形をしています。
上部にm6-8のネジを取り付けられる穴が4つあり、この穴をつかってデスク裏にマウントします。
取り付けが若干大変だったのでその点だけ注意が必要です。
以下は真横から見た図なのですが、トレイがコの字になっているためインパクトドライバーが直接入りません。
そのため、小さいドライバーなどでネジを締める必要があり大変でした。
そのままXLRケーブルを指すと天板と干渉してしまうため、このL字アダプタを使用しました。 商品画像からはわかりづらいですが、好きな方向に向きを変えられて融通が効くためおすすめです。
実際にAG03MK2を乗せてケーブルを繋ぐとこのようになります。
しっかり固定されていて且つ操作もできるようになっています。 ゲインやファンタム電源などの操作はしづらいですが、頻繁にいじるミュートは触りやすいため不満はないです。
デスクの全体はこのようになりました。 デスク下にマウントすることでAG03MK2とXLRケーブルのノイズがなくなり集中しやすい環境になりました。
おわりに
AG03MK2のデスク裏マウントにはKANADEMONOさんのティッシュボックストレイが最強です。
Top level ShellRoute というパターンの紹介
Flutterでgo_routerを使う中で「Top Level ShellRoute」というパターンが意外とイケると気づいたため紹介します。
Top Level Shell Routeとは
go_routerのルート定義のトップレベルにShellRouteを配置する以下のような実装パターンです。 (勝手に名付けました)
上記の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公式で紹介されている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」が発表されました。
私は発売後すぐに購入し約3週間使用しているのですが、最高すぎて既にHHKB Professional HYBRID Type-Sに戻れない体になりつつあります。
HHKB StudioはHHKB Professionalから多くの点が変更されています。 本記事では以下3点を中心にHHKB Studioのレビューを紹介したいと思います。
- キースイッチ
- トラックポインタ
- ジェスチャパッド
キースイッチ
結論、究極のサイレントリニアスイッチです。
HHKB Studioでは、メカニカル方式のキースイッチが採用されています。 静電容量無接点方式は今までのHHKBの代名詞でもあったため、伝統を捨てたメカニカルスイッチの採用に対して懸念の声がありましたが、実際に使用してみると最高のキースイッチであることがわかりました。
打鍵感/タイプ音について
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をもとにインターフェースを改善し、より良いコードづくりの助けになります。
これらの活動の結果、次のような効果が発生します。
効果的な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を生成してくれるとても便利なツールです。
今まで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)です。
以前、私はこんな記事を書きました。
サインインのようにFirebaseの認証状態を変化させる関数を叩いてからFirebaseAuth.instance.authStateChanges()
が反映されるまでに若干のタイムラグがあります
これが原因で以下のような場合にサインイン直後にcontext.go
で認証ガードが行われているページへ遷移しようとすると、認証ガード内で最新の認証情報がとれず、画面遷移できないことがありました。
- FirebaseAuth.instance.authStateChanges()をStreamProviderで管理している
- GoRouterをriverpodのProviderで管理している
- GoRouteのredirectでref.read()でFirebaseAuthを取得して認証ガードを行なっている
上記の記事ではこの問題を回避するため、authStateChagnes()
の更新を待ってからcontext.go()
する方法を紹介していました。
あれからgo_routerを触っていたら、よりスマートな方法があることがわかりました。今回の記事ではその方法を紹介します。
認証ガードの方法
今回紹介するのはGoRouterのrefreshListenableを使う方法です。 authStateChangeGoRouterのrefreshListenableに渡すことで、context.go()を呼ばなくても認証状態が変化するたびにGoRouterが勝手にリダイレクトしてくれます。
return GoRouter( routes: [...], ... refreshListenable: // ここにわたす, );
こんな感じでcontext.go('/home')
をする必要がなくなります。
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を組み合わせることでスマートに認証ガードを作る方法を紹介しました。 かなり説明を省いている箇所があります。フルサンプルを以下のリポジトリに用意したので、興味のある方は覗いてみてください。
【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.
つまり、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 build
がiOSのビルド時間です。
高速化前は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のリポジトリを覗いてみると...
なんと、Firestore SDKのバージョンに対応するようにタグが打ってあるようでした!
今回は9.3.0を要求されているため、以下のようにtag
を9.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で行なっており、GoRoute
のredirect
に認証ガードを設定したい時があります。
// 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をウィジェットツリーから利用するために以下のようなコードを書く必要がありました。
このコードでやっていることは以下のとおりです。
- StateNotifier.family(
providerFamily
)を定義する (最終的にウィジェットツリーから利用したいStateNotifier) - 初期値を設定したproviderFamilyをprovideするめに利用するScopedProvider(
provider
)を定義する - 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.
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との通信まわりのオブジェクトを格納するディレクトリがこのようになってしまった経験がある方は少なくないと思います。
このままだと見通しが悪いため、生成ファイルは一箇所でまとめて管理したいですよね。
ファイル生成先を変更するための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
これで自動生成ファイルを一箇所にまとめることができました。
参考
生成先のパスの指定方法について github.com
freezedのbuild_extensionsの対応についてのIssue github.com
【Flutter】Dartの実行環境を作る
こんにちは、とりかつ(torikatsu923)です。
Flutter開発をしていると、Dartの実行環境が欲しい時があります。 例えばFreezedのようなflutter sdkに依存しないDart onlyなパッケージを試したい時などが考えられます。
今回はDartの実行環境の作り方を紹介しようと思います。
目次
とりあえずHello worldしてみる
とりあえずdart run
でHello world
できるようにしてみます。
main.dart
を用意します。
main.dart
void main() { print('Hello world'); }
- main.dartを実行する
ターミナルで以下を実行します
$ dart run main.dart
無事コンソールに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
無事に実行することができ、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
エラーっぽいものが出てきました。僕の環境ではdartは2.15.1
なのですが、dart2以降はpubspec.yamlにdartのバージョンを指定する必要があるみたいです。
~/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 ${パッケージ名}
めっちゃ楽ですね...
おわりに
今回は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のコード中の設定
サンプルコード
サンプルコードは以下のリポジトリです。切り替え対象を全て切り替えられるよう設定した状態になります。
目次
- はじめに
- 基本設定
- アプリアイコン
- アプリのID(bundleId, applicationId)
- アプリの表示名
- Firebaseの設定ファイル(GoogleService-Info.plist, google-services.json)
- Dartのコード中の設定
基本設定
この章ではネイティブ側でDartDefinesの値を利用できるようにする方法を解説します。この基本設定が終わると以下のようにflutter run
で環境を指定できるようになります。
$ flutter run --dart-defines=FLAVOR=${環境}
環境
切り替える環境は以下の3つです。
環境 | FLAVOR |
---|---|
開発 | dev |
ステージング | stg |
本番 | prd |
iOS
iOSでは環境ごとに.xcconfig
を用意しておき、ビルド時にDartDefinesの値から読み込む.xcconfig
を変更する方法をとります。
1. 環境に対応するxcconfigを作成する
以下の.xcconfig
をRunner > 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.xcconfig
とRelease.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
でアクセスできます。
また$SRCROOT
はios
ディレクトリのパスを指しています。
以下のように指定した場合、$DART_DEFINES
で得られる値は次のようになります。
--dart-defines=A=valueA,FLAVOR=dev,B=valueB
QT1hLEZMQVZPUj1kZXYsQj1i,RkxVVFRFUl9XRUJfQVVUT19ERVRFQ1Q9dHJ1ZQ==
どうやら実行時に指定した値以外にFLUTTER_WEB_AUTO_DETECT=true
をbase64エンコードした値がカンマ区切りで追加されるようです。
この文字列からFLAVORの値を取り出し、include対象を指定したxcconfigを吐き出すため、ワンライナーで以下の処理をしています。
- 文字列をカンマ区切りる
- カンマで区切った各文字列をbase64でデコードし、カンマで区切る
FLAVOR=xxx
が見つかったら=
より右側の値を取り出す- flavorに対応する
xcconfig
をinclude
する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-${環境名}.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
このコマンドによってandroidはandroid/app/src/${環境}/res
に、iOSはios/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 dartEnvironmentVariables
とandroid { ...
の間である必要があります。
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
まず各環境に対応するxcconfig
にAPP_ID_PREFIX
という変数を定義します。今回はdev, stg, prdと3つの環境に対応するために以下のようにしました。
dev.xcconfig
# ... APP_ID_PREFIX=dev.
stg.xcconfig
# ... APP_ID_PREFIX=stg.
prd.xcconfig
# ... APP_ID_PREFIX=
次にxcodeでTARGETS > Runner > Build Settings
を開き、Product Bundle Identifier
で
APP_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
まず各環境に対応するxcconfig
にAPP_ID_PREFIX
という変数を定義します。今回はdev, stg, prdと3つの環境に対応するために以下のようにしました。(アプリIDの手順を先に行っている場合はこの手順は飛ばしてください。)
dev.xcconfig
# ... APP_ID_PREFIX=dev.
stg.xcconfig
# ... APP_ID_PREFIX=stg.
prd.xcconfig
# ... APP_ID_PREFIX=
次にxcodeでTARGETS > Runner > Info
を開き、Bundle display name
でAPP_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/
に置きます。
xcodeでTARGETS/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
を削除します。削除後にxcodeでGoogleService-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つに分類することができます。
- 無をリダイレクト(
>
)する - touchコマンドを使用する
- 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 ❯ ls ✘ 1 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
色々な方法があると思います。
せっかくターミナルで作業をしているなら、ターミナルでメモを取れるようにしたいですよね
今回はzshでコマンドのインストールなしでメモを取るための簡単な設定を紹介したいと思います。
メモの設定のステップ
ターミナルでサクッとメモをとるためには、以下の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を使った線香花火!
— Torikatsu (@torikatsu923) August 19, 2021
夏を満喫できて最高! pic.twitter.com/Xyvo8fBqLt
今回の記事はFlutterのCanvasを使って線香花火を作る方法を紹介します。
リポジトリ
リポジトリはこちらになります。 github.com
作り方
作り方は大きく以下の3つのステップに分かれます。
- 火花の動きを再現する
- 被写界深度を考慮する
- パラメータの調整
火花の動きを再現する
線香花火という現象は無数の火花の動きによって起きています。 これより火花一個一個の動きをきちっと再現できればリアルな線香花火を作ることができると考えられます。 火花の動きはざっくり火花の移動と火花の分裂に分けて考えることができます。
火花の動き
秒後の火花の動きは運動方程式をで2階積分することで求められます。
積分した位置の式は以下のようになります。
- : 空気抵抗係数
- : 火花の速度
- : 火花の質量
- : 加速度 (鉛直方向以外にも式を使い回したいのであえて加速度で考えます)
- : 初期位置
実際に作るときは吹いている風を考えて初速度をとして扱います。
導出
またより
となります。
これをvについて解いて、
両辺を積分して、
より
両辺の指数をとって
ここでとすると
となります。のとき、火花の速度は初速度なので、
となりDの値がわかります。
よって秒後の火花の速度は
となります。
さらにで積分して
ここでのとき、初期位置をとすると
となりがわかります。
よって位置の式は以下のようになります。
火花の分裂
線香花火の火花の内側からは気泡が発生しています。その気泡が表面に達した際に破裂することあの火花の分裂が起きます。
分裂周りの計算は複雑になるので割愛します。詳細はこちらの論文を見てください。 非常にわかりやすくまとめてあります。
https://www.jstage.jst.go.jp/article/jcombsj/60/193/60_156/_pdf/-char/ja
この論文より、線香花火の主成分はカリウム化合物で、火花の分裂回数は最大8回ということがわかります。 またn回分裂したときの火花の半径、火花の寿命、分裂時の初速度は以下で求めることができます。
- : 分裂時の定数
- (m) 火の玉から飛び出す火花の半径(分裂0回目の火花の半径)
- (m2/s) カリウム化合物の熱拡散率
- (kg/m3) カリウム化合物の密度
- (N/m) カリウム化合物の表面張力
- 分裂回数
火花の半径、寿命、初速度は全てnによって決まり、nは1から8になることがわかっています。 そのためプログラム起動時に各分裂時の計算結果を定数として持つことができます。アニメーション中は膨大な数の計算をすることになるので、計算結果を使いまわせるのは非常に嬉しいですね!
分裂時の火花の初速度がわかったので、次は火花の初速の向きを考えます。 自然な分裂を表現するためには運動量保存を守る必要があります。
- 分裂前の火花の重さ
- 火花の初速度
- 火花1の初速度
- 火花2の初速度
とすると、運動量保存の式は以下になります。
これを解いて
これよりが決まればがわかります。
初速度はであることがわかっているため
となります。ベクトルの方向の成分を、方向の成分を、方向の成分をとすると、
となります。 これより、火花の初速度の2乗した値を適当に3分割し、平方根ととるとを求めることができます。
奥行きを表現する
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
の情報はほとんど引っ掛からなかったので地味にハマりポイントでした。
FlutterのCanvasでImageFilter以外にblur入れれるやつないかなーってPaintのAPI眺めてたらmaskFilterってやつ見つけた
— Torikatsu (@torikatsu923) August 17, 2021
ImageFilterに比べてかなり軽そう
パラメータの調整
最後に色々なパラメータを調整して全体を整えます。 具体的には表示倍率を変更したり、初速度を計算結果より若干早くしました。
ここの記事にある時間の流れを若干遅くする方法は、計算量を減らしつつ見た目もそれっぽくなるのでかなり有効でした。 zenn.dev
あと計算量が多くなりコマ落ちしやかったので、isolate
で火花の計算を別スレッドで行うようにしたりしました。
おわりに
実際に線香花火を再現することで、家でコードを書きながら夏を満喫するという最高の体験をすることができました! ぜひみなさんも夏っぽいものをコードで再現して、夏を満喫しましょう!