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 で配信予定で、アーカイブもあるので当日参加できない方もあとからご視聴いただけます。

マルチプロセスに対応した DataStore とプロセス間の排他制御

androidx.datastore の DataStore はデータを非同期にストレージへ読み書きするためのフレームワークです。

DataStore は 1 つのファイルに対しデータの読み書きを実行するので、同一のプロセス内では必ず DataStore のインスタンスは 1 つ(シングルトン)に限らなければなりません。同一プロセス内で複数のおなじ DataStore インスタンスを使おうとすると例外が発生します。この制限は複数のプロセスで同じ DataStore を使う場合にも適用されますが、Java/Kotlin が提供している Mutex や Semaphore などではプロセスを超える排他制御ができないため、DataStore の作り方がすこし異なります。

複数のプロセスで同一の DataStore を使う場合、DataStore は MultiProcessDataStoreFactory をつかって初期化します。このとき、どのプロセスでも必ず MultiProcessDataStoreFactory を使って初期化しなければなりません。 MultiProcessDataStoreFactory を使うと、内部で FileLock を使ったファイルロックの仕組みで排他制御されます。

ファイルロックによる排他制御は MultiProcessCoordinator が実装していて、実はこの MultiProcessCoordinatorcreateMultiProcessCoordinator を呼び出すことで DataStore 以外の場所でも使えます。 次のコード例では DataStore は使わず Mutex を使うのと同じ感覚でクリティカルセクションを定義できます。 Kotlin では Mutex#withLock でブロックを抜けたときにロックを解除しますが、MultiProcessCoordinatorlock のブロックを抜けたときにファイルロックを解除します。

class Sample(
  coroutineScope: CoroutineScope,
) {
  private val lockFile = File(context.filesDir, "my_critical_section_lock")
  private val coordinator = createMultiProcessCoordinator(
    coroutineScope.coroutineContext,
    lockFile
  )

  suspend fun somethingImportant() {
    coordinator.lock {
      // critical section
      // do some work!
    }
  }
}

サウナにハマるきっかけになった温泉:箱根湯寮

この記事は「サウナAdvent Calendar 2023」の12日目の記事です。

adventar.org

箱根湯寮

箱根湯寮とは箱根湯本にある温泉です。

www.hakoneyuryo.jp

箱根湯本駅からも割りと近いのですが、激坂を登った先にあるため駅からシャトルバスで行くことをおすすめします。

温泉自体はアルカリ性の単純温泉のため肌の弱い自分にも優しく入りやすい温泉です。

一通り温泉を楽しんだらあとはサウナですね。私は以前、せまく暑苦しい空間でじっとしていないといけないことがよく理解できず、その後キンキンに冷えた水風呂に浸かるのもヒートショックになるじゃんと思っていたためサウナ自体を避けていましたが、箱根湯寮のサウナに入ってその思い込みが見事に崩れ去りサウナにハマりました。

ロウリュウサービス

箱根湯寮のサウナにはロウリュウサービスがあります。 日替わりで楽しめるアロマ水をサウナ石にかけ、一気に温度が上がったところで熱波師が大きな団扇で我々を仰ぎ始めます。

自分がロウリュウを受けた時は梅のフレーバーのアロマ水をかけていましたが、すぅっと心地よいアロマを感じた直後に浴びる熱風が素晴らしいですね。

一度にかけるアロマ水の量がそもそも多いのですが「アロマ水をかける→うちわで扇ぐ」を1セットとして何度もこのセットを繰り返すので、どんどん仕上がっていきます。 希望すればおかわりも可能です(が限界が来たらいつでもすぐに飛び出しましょう)。

水風呂

アツアツのサウナのあとの水風呂も気持ちよく、沢の水を引いてきているため季節によって水温は前後しますが、だいたい16度~18度くらい。 正直13度とか14度とかのキンキンに冷えた水風呂は未だに慣れないので、箱根湯寮くらいの水温がサウナにハマるにはちょうどよかったのかもしれません。

水風呂のあと更にサウナに行くと、ロウリュウサービスのアロマがまだこってり残っているので引き続き仕上がりを感じられます。

初めてサウナで整う感覚を得られたような気がして、以後温泉に行ったらサウナにも行くようになりました。

Jetpack Compose TextField の横幅を n 文字分だけ確保したい

View の仕組みでは、 TextView のプロパティとして ems が定義してあり、これを利用するとTextView の横幅を ems で指定した文字数分の横幅にしてくれます。

R.attr  |  Android Developers

Jetpack Compose では Text や TextField にそのものズバリな引数や Modifier はないので、自分で計算します。

TextMeasurer で横幅を計算する

TextMeasurer はその名の通り文字を描画するのに必要な領域を計算するクラスです。テキストスタイルや親コンポーネントの縦横の大きさを制約として与えることで、テキストが領域内に収まり切るかどうかも計算可能です。

developer.android.com

今回は TextField の横幅を常に一定に保つため、あらかじめダミーの文字列を用意して TextMeasurer に食わせて横幅を計算します。これでダミーの文字列の長さの分だけ横幅を確保することになります。 次の例では数字6文字分の横幅を計算します。

// 数字6文字分を TextField の横幅にするときの、数字6文字分の横幅を計算
val measurer = rememberTextMeasurer()
val measureResult = measurer.measure(
  text = "000000",
  maxLines = 1,
)

TextField の PaddingValues を求める

TextField は内部で PaddingValues を設定しており、自動でスペースをつけています。 このスペースの大きさは TextField のスタイルによって異なります。 OutlinedTextField ならば outlinedTextFieldPadding() で、TextField の場合は label があるときは textFieldWithLabelPadding()、label がないときは textFieldWithoutLabelPadding() が標準の PaddingValues です。

TextMeasurer で計算した横幅と PaddingValues の水平方向の padding を足し合わせる

TextMeasurer で計算した横幅と PaddingValue の水平方向の padding を足し合わせると、TextField の横幅が計算できます。 TextMeasurer で計算した横幅は px なのに対し、PaddingValues が持つスペースは dp を単位にしているので、TextMeasurer で計算した横幅を dp になおす必要があります。

val measurer = rememberTextMeasurer()
val measureResult = measurer.measure(
  text = "000000",
  maxLines = 1,
)
val padding = textFieldWithoutLabelPadding()

TextField(
  modifier = Modifier.width(
    with(LocalDensity.current) {
      measureResult.size.width.toDp() + padding.calculateStartPadding(LocalLayoutDirection.current) + padding.calculateEndPadding(LocalLayoutDirection.current)
    }
  )
)

注意点

今回は数字のみで文字列の横幅を計算しましたが、全角文字を入れたりするとまた計算が変わってきそうです。 TextMeasure#measure はフォントの種類や文字の大きさなども考慮して横幅を計算できますが、今回のように与えられた文字数に必要な横幅を計算するために予め決め打ちした文字で計算すると、実際に入力した文字によってはどうしてもズレが出ます。

近況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 らしいフルーティーさが合わさった感じですいすい飲めます。 最高です。