Infinito Nirone 7

白羽の矢を刺すスタイル

近況2023

この記事は「mhidakaが2つ目を建立したAdvent Calendar 2023」の10日目の記事です。近況を報告せよとのことだったので、なんとなく今年のハイライト的な記事を書いておきます。

adventar.org

今年たのしかったこと

桜のAACR

AACRとは長野県の安曇野市周辺で開催される自転車のロングライドイベントです。レースではなく、設定されたチェックポイントを各々のペースで巡るイベントです。 以前にも参加したことはありますが、コロナ禍が始まって以降は開催も久しぶりでした。

AACRは年に2度開催され、桜と銘打ったAACRは4月中旬、緑と銘打ったAACRは5月中旬に開催されます。 今回は桜のAACRということで4月の安曇野を駆け巡りましたが、当日は近年稀に見る極寒のAACRとなりました。4月も半ばだというのに朝は真冬の寒さで震えながらのスタートでした。

4月ならアルプスの山々もまだ雪をかぶっている。

Droidcon Vietnam

超絶弾丸日程のベトナム(ホーチミン)でしたが楽しめました。数ある Droidcon の中でも際立ってチケット代が安い Droidcon Vietnam ですが、参加者は APAC を中心にいろいろなところから来ていて盛り上がりを感じました。 ホーチミンは以前旅行できたことがあるのでなんとなく覚えている場所もありましたが、やはりGrabで気軽に移動できるのが強いですね。

ちなみに次の写真の真ん中らへんに写っているピンクのやつが見た目に反して異様な苦味と辛さを備えていて涙目になりました。結局今もこれが何なのかよくわかっていません。

北海道旅行

人生3度目の北海道旅行です。今回はどうしても運転してみたい車がレンタカーにあるということでそれを乗りつついままで行ったことのない地域に行くという旅行でした。 シルバーウィークにかぶせてDroidKaigi 2023が終わった翌日に北海道に行くという少々おかしな日程を組んでいましたが、北海道についてからも少々おかしな行程で旅行をしました。

初日は新千歳に着陸後すぐ車を確保し、そのまま苫小牧や新冠を経由(わざわざ遠回り)して帯広に行く行程。

1日目

2日目は帯広から周辺をうろつきつつ池田町のワイン城を見てから摩周湖や屈斜路湖に行き、北見に至る行程。

2日目

3日目は北見から一旦網走に出て、そこから上川に寄り道をしつつ札幌に至る行程。

3日目

最終日は札幌から洞爺湖、室蘭に抜けて新千歳に戻る行程。

最終日

節子、それ旅行やない!ただのドライブや!!

全部で1300kmほど移動していたらしいですが、元々長距離の運転に苦を感じない(+いい車が借りれたので楽だった)ので、また行きたいという気持ちです。 帯広のハゲ天で食べた天丼と朝イチから並んでいるパン屋さん(ますやパン)、北見のバルで食べたサガリ肉のステーキと余市ワイン、ちょうどいい時期にやっていたさっぽろオータムフェストと、食べ物も一通り楽しみました。

ちなみに借りた車はこちら。スバルのアウトバックです。

心残りがあるとすれば、北海道をウロウロしている間いろいろないい景色を見てきましたが、写真以外はほぼ脳内にしか記録がないこと。次は車載動画もやってみたいところです。

ジャパンカップサイクルロードレース撮影

自転車競技のシーズン終盤に行われる、日本では珍しい国際的な自転車レースです。欧州で活躍するプロがやってきて眼の前を駆け抜けるのでファンの人気も高い。 毎年ひとりで足を運んでカメラ小僧になっていますが、今年は e10dokup さんを誘って行きました。

当初はレースが始まる頃には雨が上がっている予報でしたが、実際にはレースが終わるまで見事に雨。機材を濡らさない強い気持ちはありましたが自分自身は守れませんでした。 でもいい写真が取れたのでOKです。

ジュリアン・アラフィリップ選手。アラフィリップ応援団がいるほど日本のファンもたくさん。

長年日本の自転車競技を引っ張り続ける新城幸也選手。

今年買ってよかったもの

デロンギの自動でコーヒーいれるやつ

最高

Pixel Fold

待望のやつ

上川大雪の日本酒

うまい

ツルヤのPB商品

安定

WorkManager Migration to 2.9.0

AndroidX WorkManager の 2.9.0 がリリースされました。以前のバージョンからこのバージョンに上げるにはマイグレーションが必要です。

https://developer.android.com/jetpack/androidx/releases/work?hl=en#2.9.0

Configuration.Provider の実装

2.8.x 以前は次のような定義でした。

interface Provider {
  fun getWorkManagerConfiguration(): Configuration
}

この定義が 2.9.0 では次のように変わりました。

interface Provider {
  val workManagerConfiguration: Configuration
}

このインタフェースは Application クラスで実装せよとあるので、2.8.x 以前であれば次のような実装になるはずです。

class MyApp : Application(), Configuration.Provider {
  override fun getWorkManagerConfiguration(): Configuration = Configuration.Builder()
    .setMinimumLoggingLevel(Log.VERBOSE)
    // ... do your configuration customization
    .build()
}

2.9.0 へのアップデートでは、次のように書き換わります。

class MyApp : Application(), Configuration.Provider {
  override val workManagerConfiguration: Configuration = Configuration.Builder()
    .setMinimumLoggingLevel(Log.VERBOSE)
    // ... do your configuration customization
    .build()
}

HiltWorkerFactory と組み合わせる

次の記事で紹介した方法で、WorkManager と Hilt を組み合わせることができます。

blog.keithyokoma.dev

Hilt は各種 Worker に依存を注入するために独自の WorkerFactory を使います。そのため、Application クラスで次のように WokerFactory を注入して Configuration に渡しているはずです(2.8.x 以前)。

class MyApp : Application(), Configuration.Provider {
  @Inject
  lateinit var workerFactory: HiltWorkerFactory

  override fun getWorkManagerConfiguration(): Configuration = Configuration.Builder()
    .setWorkerFactory(workerFactory)
    // ... do your configuration customization
    .build()
}

WorkManager 2.9.0 へアップデートするとき、Hilt と組み合わせている場合は先程の書き換え方をそのまま適用すると workerFactory が注入される前に Configuration を作ってしまうためアプリがクラッシュします。

class MyApp : Application(), Configuration.Provider {
  @Inject
  lateinit var workerFactory: HiltWorkerFactory

  override val workManagerConfiguration: Configuration = Configuration.Builder()
    .setWorkerFactory(workerFactory)
    // ... do your configuration customization
    .build()
}

正しく動作するよう書き換えるには次のように getter で Configuration を作成します。

class MyApp : Application(), Configuration.Provider {
  @Inject
  lateinit var workerFactory: HiltWorkerFactory

  override val workManagerConfiguration: Configuration
    get() = Configuration.Builder()
      .setWorkerFactory(workerFactory)
      // ... do your configuration customization
      .build()
}

DEEDS BREWING #BeerAdventCalendar2022

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

これがうめぇんだ!

DEEDS XPA

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

DEEDS DOUBLE TIME

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

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