Infinito Nirone 7

白羽の矢を刺すスタイル

DEEDS BREWING #BeerAdventCalendar2022

この記事は Beer Advent Calendar 2022 - Adventar の 9 日目の記事です。

これがうめぇんだ!

DEEDS XPA

しっかりとした苦味がありつつ、後味スッキリですいすい飲めます。 最高です。

DEEDS DOUBLE TIME

さっきの XPA の苦味と後味にくわえ、HAZY PALE らしいフルーティーさが合わさった感じですいすい飲めます。 最高です。

多くのパラメータを持つ関数・コンストラクタの呼び出しに必要な引数を名前付きで自動生成したい

やりたいこと

次のように定義されたコンストラクタを名前付き引数を使って呼び出したい。

// 定義
data class Sample(
  val hoge: String,
  val fuga: String,
  val moga: String,
  val piyo: String,
  val foo: String,
  val bar: String,
  val baz: String,
  val qux: String,
  val quux: String,
)

// 次のコードを補完機能で自動生成したい
val sample = Sample(
  hoge = "",
  fuga = "",
  moga = "",
  piyo = "",
  foo = "",
  bar = "",
  baz = "",
  qux = "",
  quux = "",
)

問題

IntelliJ の標準のコード補完機能では、コンストラクタや関数の呼び出しについて名前付き引数のコード補完ができない。ヒントは表示されるが補完まではしてくれないので、すべて手入力しないといけない。

解決方法

プラグインを使いましょう。

plugins.jetbrains.com

このプラグインを使うと、コンストラクタや関数の呼び出し箇所で Fill Function という補完メニューが出てくるようになり、すべての名前付き引数を自動で作ってくれます。

YouTrack にも Issue がたっていますが、今のところは上記の Plugin を使うのが一番はやいです。

つけ麺 えん寺 #つけ麺AdventCalendar2022

この記事は つけ麺 Advent Calendar 2022 - Adventar の4日目の記事です

つけ麺 えん寺

ベジポタつけ麺肉増し味たま付き

これがうめぇんだ!

説明

  • ベジポタつけ麺をやっているお店です。
  • 吉祥寺・中野・池袋・東高円寺に「えん寺」がある他、野方に「花みずき」と渋谷に「マンモス」という系列店があります。
  • 基本的にどのお店もベジポタつけ麺とベジポタ辛つけ麺を提供していて、お店によってはエビのだしを強くした数量限定のつけ麺を提供しているところもあります。
  • 辛つけ麺はデフォルトで十分辛い(自分基準)のですが、お好みで更に2段階辛くすることが可能なのようです。
  • 麺の種類が選べますが、おすすめは胚芽麺です。

宅麺

宅麺の冷凍つけ麺

  • 宅麺というサービスをつかうと、えん寺のつけ麺を自宅で楽しめます。
  • ベジポタつけ麺ベジポタ辛つけ麺があります。
  • 人気が高く入荷してもすぐ売り切れてしまうので、入荷通知を ON にしておくとよいです。
  • お店と同じ具の入ったスープと胚芽麺が入っています。味玉はお好みで自分で用意しましょう。

BottomSheetScaffold の sheetContent を空にしてはいけない

BottomSheetScaffold を使って BottomSheet を作るとき、BottomSheet の中身は BottomSheetScaffold の引数にある sheetContent で作っていきます。

developer.android.com

BottomSheetScaffold(
  sheetContent = {
    /* BottomSheet の中身 */
  }
) { paddingValues ->
  /* 画面の中身 */
}

この画面において BottomSheet の表示に複数の種類がある場合を考えてみます。 種類に応じて sheetContent を切り替えたいので、次のような enum による分岐をしてみます。 今回は 2 種類の BottomSheet を一つの画面で表示する想定で BottomSheetType という enum を定義して sheetContent 内部で表示を切り替えます。 このとき、BottomSheet には開いた状態と閉じた状態の 2 種類があることを考慮に入れ、閉じた場合に sheetContent を表示しないようにするためなにもないことを示す値も合わせて定義してみます。

enum class BottomSheetType {
  NONE, FOO, BAR
}

var sheetType by remember { mutableStateOf(BottomSheetType.NONE) }
val bottomSheetState = rememberBottomSheetState(
  initialValue = BottomSheetValue.Collapsed,
)
val scaffoldState = rememberBottomSheetScaffoldState(
  bottomSheetState = bottomSheetState,
)

BottomSheetScaffold(
  scaffoldState = scaffoldState,
  sheetPeekHeight = 0.dp,
  sheetContent = {
    when (sheetType) {
      BottomSheetType.NONE -> {
        // 閉じた状態を表現したい。何も表示するものはないので空のまま。
      }
      BottomSheetType.FOO -> {
        Text(text = "foo")
      }
      BottomSheetType.BAR -> {
        Text(text = "bar")
      }
    }
  }
)

このようなコードで sheetContent を作る画面を動かすと BottomSheetState のもつ状態がおかしくなってしまいます。具体的には、rememberBottomSheetState で初期値を Collapsed にしていても、Composition がおわると Expanded な状態であると判定されてしまいます。

またこのような sheetContent の作り方を実装した上で BottomSheet を展開しようと次のようなコードを呼び出すとアプリがクラッシュします。

// Jetpack Compose 1.1.0 でクラッシュするコード (1.2.0 ではクラッシュしない)
coroutineScope.launch {
  bottomSheetState.expand()
}

// Jetpack Compose 1.2.0 でクラッシュするコード (1.1.0 ではクラッシュしない)
coroutineScope.launch {
  bottomSheetState.animateTo(BottomSheetValue.Collapsed)
}

クラッシュ時の例外は次の通りで、展開時のアニメーションを実行しようとしたとき、アニメーションが終了する位置が指定できていないことでクラッシュしていることがわかります。

java.lang.IllegalArgumentException: The target value must have an associated anchor.

そもそも BottomSheet を展開していないのに BottomSheetStateExpanded になってしまうことが良くないのですが、これは sheetContent で何も Composable を呼ばないケースが不具合を引き起こしています。 このため、次のように何も表示しないケースで Spacer(modifier = Modifier.height(1.dp)) を差し込むか、when 全体を Box で囲むか、そもそも enum の定義から表示しないパターンの定義を消すかのいずれかで、常に sheetContent が何かしらの Composable を呼び出すようにする必要があります。

// 小さな Spacer を差し込むパターン
BottomSheetScaffold(
  scaffoldState = scaffoldState,
  sheetPeekHeight = 0.dp,
  sheetContent = {
    when (sheetType) {
      BottomSheetType.NONE -> {
        Spacer(modifier = Modifier.height(1.dp))
      }
      BottomSheetType.FOO -> {
        Text(text = "foo")
      }
      BottomSheetType.BAR -> {
        Text(text = "bar")
      }
    }
  }
)

// when 全体を Box で囲むパターン。BottomSheetType.NONE でも最低 1dp は確保する
BottomSheetScaffold(
  scaffoldState = scaffoldState,
  sheetPeekHeight = 0.dp,
  sheetContent = {
    // 最低 1dp の高さは確保
    Box(modifier = Modifier.requiredHeightIn(min = 1.dp)) {
      when (sheetType) {
        BottomSheetType.NONE -> {
          // 閉じた状態を表現したい。何も表示するものはないので空のまま。
        }
        BottomSheetType.FOO -> {
          Text(text = "foo")
        }
        BottomSheetType.BAR -> {
          Text(text = "bar")
        }
      }
    }
  }
)

// enum の定義を削除するパターン
BottomSheetScaffold(
  scaffoldState = scaffoldState,
  sheetPeekHeight = 0.dp,
  sheetContent = {
    when (sheetType) {
      BottomSheetType.FOO -> {
        Text(text = "foo")
      }
      BottomSheetType.BAR -> {
        Text(text = "bar")
      }
    }
  }
)

参考リンク

stackoverflow.com

Jetpack Compose 1.2.0 では Scaffold の content に PaddingValues を必ず設定する

Jetpack Compose 1.2.0 (正確には Jetpack Compose 1.2.0-alpha07 以後) から Scaffold の content で Scaffold から渡される PaddingValues を使わないと Lint エラーとなります。

developer.android.com

Lint で指摘される例

次の例は Lint のエラーとなります。Scaffold の content は content: @Composable (PaddingValues) -> Unit と定義してあり、Scaffold 側から PaddingValues を渡すように設計してあります。 この PaddingValues は Jetpack Compose 1.1.x までは使わなくても特に Lint に怒られることはありませんでしたが、Scaffold が content を呼び出す前に内部で PaddingValues を計算しているため、 使わないとレイアウトが崩れる原因となる場合がありました。特に PaddingValues の計算には bottomBar の高さが計算に入っているようです。 このため、Jetpack Compose 1.2.0 からは Lint のエラーが出るようになりました。

@Composable
fun SampleScreen {
  Scaffold(
    scaffoldState = rememberScaffoldState(),
    topBar = { /* compose TopAppBar... */ },
  ) {

    // このラムダに渡される PaddingValues を無視して画面を構成するとエラーになる

    ConstraintLayout {
      /* compose screen... */
    }
  }
}

正しい記述

Scaffold の content で使う最もトップレベルの Composable に PaddingValues を設定します。

@Composable
fun SampleScreen {
  Scaffold(
    scaffoldState = rememberScaffoldState(),
    topBar = { /* compose TopAppBar... */ },
  ) { innerPadding ->

    // Scaffold 直下の Composable に innerPadding を設定する

    ConstraintLayout(
      modifier = Modifier.padding(innerPadding),
    ) {
      /* compose screen... */
    }
  }
}

DroidKaigi: Weekend Chat の配信を支える構成

Mac を利用した Discord と OBS による YouTube Live 配信環境

DroidKaigi: Weekend Chat は Mac 上で Discord をつないで @mhidaka さんと話しているのを OBS に流して YouTube Live 配信にのせています。

Loopback

Windows であれば音声キャプチャはそれほど難しくありませんが、Mac でやるとなるとソフトウェアないしハードウェアを揃えていかないと配信環境が整いません。 特に Discord からの音声を配信にのせたり、BGM として使う音楽を配信にのせたりする場合はこれらのソフトからのオーディオ出力をキャプチャする手段が Mac OS 標準にも OBS にもないため、別途キャプチャ用のものが必要です。

そこで DroidKaigi: Weekend Chat 開始当初から使っているソフトとして Loopback が役立ちます。 Loopback は仮想オーディオ出力デバイスを作成し、その出力に対してどのソフトからのオーディオを入力するかをビジュアルで分かりやすく設定できます。

自分の場合、初回から長らくは Discord の音・Chrome の音(BGM 用・YouTube の BGM 素材動画から音を取り込んでいた)をソースとして OBS に流す仮想オーディオ出力デバイスを作成し、Discord から聞こえてくる @mhidaka さんの音と BGM を OBS に流しつつ、自分のマイク入力はそのまま Discord と OBS に流すようにしていました。

ただしこの設定だと、話し相手の mhidaka さんには自分の声しか聞こえないので BGM が入らず、mhidaka さん側にも BGM を流すためにわざわざ mhidaka さんに同じ BGM の再生ページを開いてもらって再生タイミングをせーので合わせる、という超絶運用でカバーをすることになってしまいました。

Loopback では、仮想オーディオ出力デバイスは複数作成可能です。 そこで、次のステップとして、Discord に流すための仮想オーディオ出力デバイスと OBS に流すための仮想オーディオ出力デバイスの 2 つを作成し、BGM の再生タイミングをせーので合わせることなく mhidaka さん側にも流せるようにしました。

まず OBS 用の仮想オーディオ出力デバイスは、Discord の音、BGM の音、追加で SE を鳴らすソフトの音を出力するように設定します。

次に Discord 用の仮想オーディオ出力デバイスでは、Discord のオーディオ入力をこの仮想オーディオ出力デバイスとするため、マイクの入力、BGM の音、SE を鳴らすソフトの音を出力するようにします。

これで OBS と Discord の両方に BGM を出力できるようになります。 ここで注意すべき点としては、Discord は独自にノイズキャンセルする仕組みを持っていて、ラジオ配信などで使う BGM の音量が小さいとノイズ判定されて mhidaka さん側では消えてしまうことに気をつけます。 ノイズ判定を回避して BGM を届けるには Discord の設定で Input Sensitivity を手動で設定し、閾値を下げておく必要があります。この閾値を下げても YouTube の配信には影響はありません。

このあとさらに YAMAHA AG06MK2 を購入しましたがその話はまた別記事で書こうと思います。

CircleCI で Android アプリプロジェクトのビルドに利用する Docker Image が変わったので更新する

次のような記述で Android 用の Docker Image を利用すると、ジョブのステータスを表示する画面で You’re using a deprecated Docker convenience image. Upgrade to a next-gen Docker convenience image. といったメッセージが表示されます。

executors:
  android:
    docker:
      - image: circleci/android:api-30

メッセージのリンク部分をたどると、新しい next-gen Docker convenience image へ置き換えるための手順が示されていますが、Android に関してはどの Docker Image を使うべきか明示されていません。

discuss.circleci.com

代わりに、この手順を示した記事のコメントにて新しい Docker Image の情報を記載したページのリンクが示されています。

Legacy Convenience Image Deprecation - #6 by zmarkan - Announcements - CircleCI Discuss

DockerHub で CircleCI が公開している Docker Image は api-30 を最後に更新が止まっていて、api-31 以上を含むイメージは提供されていません。 代わりに、DockerHub で cimg という別の organization (実体は CircleCI)が提供している android のイメージを利用します。

circleci.com

cimg で公開しているイメージはこれまでと Docker Image のタグの付け方が異なります。 これまでは API Level ごとにタグを付けていましたが、今後はビルドした日付ごとにタグが付与されます。 また Docker Image に含まれる Android SDK の内容にも違いがあり、最新の API Level から数えて直近 4 つ分の API Level のものが Docker Image に含まれています。

この他、node や ndk などを含むイメージもあり、それぞれタグの日付を示す部分の後ろに variant 名を付けています。

この記事をかいた時点では 2022.04 が最新イメージとなっているので、上記 YAML は次のように書き換えることになります。

executors:
  android:
    docker:
      - image: cimg/android:2022.04