Infinito Nirone 7

白羽の矢を刺すスタイル

チームで育てるAndroidアプリ設計の一般販売が始まりました。

告知記事です。

Peaks のクラウドファンディングで執筆し先日電子版を先行リリースした「チームで育てるAndroidアプリ設計」の一般販売が始まりました。 これまではクラウドファンディングで出資いただいた方にのみ電子版・書籍の配送手続きをしていましたが、本日からクラウドファンディングに参加していない一般の方々にも電子版・物理本の購入をいただけるようになりました。

peaks.cc

チームで育てるAndroidアプリ設計とは

改めてこの書籍が何なのかを紹介すると、大小様々な規模のチームで継続的にAndroidアプリの開発をすすめていく中で直面するアーキテクチャの成長痛を乗り越えるためのノウハウを詰め込んだ本です。

アーキテクチャは一度整えればそれで終わるものではなく、プロダクトの成長やチームの成長とともに少しずつ形を変えていくものであるという考えのもと、最初の一歩としてどのようにアーキテクチャを選定しチームに根付かせていくか、またアプリの成長にともなって徐々に現れるひずみをどのように解消していくのか、実際の方法論を交えつつも根本にある思想や考え方、行動指針を示すことで、特定のチームにおける実例を他のチームにも活かせるプラクティスとしてまとめています。

新規事業の立ち上げから運用にいたるまでの比較的小規模なチームにおける事例を @kgmyshin さんが担当し、すでに成長を続けてきたサービスをさらに拡大していく比較的大規模なチームにおける事例を自分が担当しました。それぞれ4章分の内容があります。そして最後の章では大小それぞれの事例を振り返り、規模によらない共通点や規模によって異なるポイントをまとめています。

クラウドファンディング開始当初の意気込みなんかは次の記事に書いてありますのであわせてどうぞ。

blog.keithyokoma.dev

オンライン輪読会

実はこのプロジェクトはまだ続いていて、出版後にオンラインで輪読会を開催します。

techbookfest.connpass.com

全9回、各回で書籍の1章分の内容を輪読します。初回は早速明日4/27の22時からで、1週間ごとに読みすすめる予定です。 YouTube で配信予定で、アーカイブもあるので当日参加できない方もあとからご視聴いただけます。

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

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 を購入しましたがその話はまた別記事で書こうと思います。