本記事は mhidaka が建立した
Advent Calendar 2024 Vol.1 の 9日目です(Vol. 2 は
こちら )。
2024年のとある時期にとある病院で骨髄採取の手術をしました(非血縁者間骨髄移植です)(時期とか病院とかをはっきり書いてはいけないのです)。
なかなか馴染みのないことだと思うので、時系列で起こったことを紹介します。
2022年12月
秋葉原の献血ルームに献血しに行った時に、そこに骨髄バンクの人がいて「今日採血した血で登録できます」って言われたのでじゃあ登録するかって登録しました。
それまで何度も献血してるけど、その時初めて骨髄バンクの人に会ったのでもっといろんなところでやればいいのにって思いました。
手術約4ヶ月前
日本骨髄バンクから「あなたと患者さんのHLA型が一致し、ドナー候補者に選ばれました。」という SMS が届きました。ちょうど旅行中だったのでよく覚えています。
SMS の中の URL から問診票に回答します。その際面談希望の病院を選択するんですけど、一覧に病院の名前しか載ってなくて一個一個 google map で検索してどこが行きやすいか調べないといけなかったので、ぜひマップで一覧できるようにして欲しいです。
SMS から約2週間後
日本骨髄バンクから担当のコーディネーターさんのお知らせが郵送で届き、その後コーディネーターさんから SMS で連絡が来ました。ここからは担当のコーディネーターさんとずっとやりとりします。
まず確認検査の日程調整をします。候補の病院は複数あげてたのですが病院側の都合とこちらの日程が合わなかったりで決まるまで1週間くらいかかってました。
手術10週間前 : 確認検査&面談
ドナーのためのハンドブックに沿って一通り説明を受けました。注射器で骨髄を採取する方法しかないと思ってたんですけど、今は末梢血幹細胞採取という方法もあるということを知りました。
ドナーになるための健康状態を満たしているかの血液検査が必要なので、そのための採血もしました。全部で2時間くらいです。
1週間後くらいの血液検査の結果が郵送されてきました。もしここで健康状態が基準を満たしていない場合コーディネート終了になります。
手術約3週間前 : 最終同意面談
家族が同席して、改めてドナーのためのハンドブックに沿って一通り説明を受け、同意書を作成します。このとき第三者の立会人もいます。採血がないので1時間半くらいです。
骨髄採取と末梢血幹細胞採取で違いがいろいろあり、こちらの方法は承諾できないと言えばその採取方法にはなりません。私は説明を聞いた上でどっちにもそれなりのメリデメがありどっちでもいいなとなったので両方OKと伝えました。ドナーがどちらでもいいよとなった場合患者側がどちらの採取方法にするか決めます。私の場合は骨髄採取になりました。
手術約3週間前 : 健康診断
私の都合で最終同意面談から手術まで最短日程で行くことになり、最終同意面談の次の日に健康診断を受けました。
健康診断は骨髄採取手術を受ける病院で受けます。
採血
検尿
レントゲン
心電図
肺機能(一気に息を勢いよく吐くのが難しかった)
手術約2週間前 : 自己血採取1回目
手術のときに骨髄を900ml(私の場合)採取するので貧血になってしまいます。そこで自分の血をあらかじめ取っておいて(自己血貯血)、手術時に体に戻します。
そのための自己血を2回に分けて採取します。最初にヘモグロビン濃度をチェックする採血をして、400ml貯血しました。
このとき麻酔科にも行って全身麻酔の説明を受けました。あと、入院の手続きもしました。
手術約1週間前 : 自己血採取2回目
ヘモグロビン濃度をチェックする採血をして、200ml貯血しました。
手術前日 : 入院
10時に入院(病院に入院するの初めてです)。まずベッドのところで4本採血。昼担当の看護師さんが挨拶にきて体温と血圧を測っていきました。
その後、血液内科の先生とか、麻酔科の先生が挨拶にきました。11時半くらいに昼ごはんがきて、15時半からシャワーを浴びました。健康体なので暇です。PCで仕事してました。
17時すぎに晩御飯が来ました。病院の晩ごはんは早いということがわかりました。夜担当の看護師さんが挨拶にきて体温と血圧を測っていきました。
21時すぎに消灯しました。健康的です。
次の日の9時から手術なので、21時まで飲食可能でそれ以降は食事NGでした。
手術当日
6時半までに500mlくらい水分を摂取しろと言われたので、5時半に目覚ましをかけてうとうとしながら6時半までに500mlの水を飲みました。
6時半から9時の手術まで水分摂取も禁止で、これがつらかったです。
6時あたりに2本採血。
8時55分くらいに看護師がきて手術室に移動します。乗って帰ってくるストレッチャーも一緒に連れて行きます。
キャップをかぶって手術エリアに入りました。手術エリアすごい広かったです。手術室がたくさんあって、自分の手術室に行くまでまぁまぁ歩きました。
手術台の横のストレッチャーに仰向けに寝て、心電図のシール貼ったり、点滴の注射したり、血圧計つけたり、いろいろしている間に酸素マスクつけられて「大きく深呼吸してください。だんだん眠くなります」っていわれて3回目くらいで寝ました。
名前を呼ばれてはっと気づいたら終わってました。
目覚めたのは手術室なんですが、そこからストレッチャーで病室まで移動したところはまた寝てたっぽくて覚えてません。
病室のベットによっこいせと移されたあたりでまた起きました。
そこから3時間安静にしていないといけないのですが、腰が痛いのでなかなか寝られず、寝れば一瞬なのになーと思ってまぁまぁつらかったです。
術後の感じとしては採取した部位が痛むのと、喉がガラガラになった(全身麻酔なので気管挿管するからです)くらいで、気持ち悪いとかなくてよかったです。
微熱(37.5くらい)と偏頭痛(これは多分貧血由来)があったけど、腰の痛みも含めて痛み止めの薬で全然我慢できる程度でした。
3時間後は普通に歩けるけど、血が足りない感じなので大人しくごろごろしてました。
朝からなにも食べてないので早く晩御飯こないかなーと思ってました。
術後2日目
今日も2本採血。朝ごはんのあと院内をちょっと散歩しました。
歩くのは全然問題なかったのですが、ちょっとくらっとしたので早めに戻りました。
担当医が来て傷口のチェックをして絆創膏を張り替えました。
シャワー入れるか聞いたら、絆創膏の上からサランラップみたいなぴっちりしたシートを貼ってくれました。
担当医が来てヘモグロビンが10切ってるから鉄剤出しますね、って言われました。
昼ごはんの後16時半にシャワー、17時すぎくらいに晩御飯、19時くらいに担当医が来て絆創膏を張り替え、21時消灯
術後3日目
この日は採血なし。
めまいが酷くて、ベットで起き上がったらそのままふらーっと倒れてしまって笑った。貧血で倒れるひとってこんな感じなのか。
7時すぎ朝ごはん、晩御飯が17時すぎくらいなのに朝ごはん遅いんですよねー...。
昨日よりもめまいが酷くてごろごろしてました。
9時すぎに退院の手続きをしに行ったのですが、傷口の痛みは我慢できて普通に歩けるけど、めまいのほうがこわい。移動はゆっくり。
病室に戻って水分不足かもなーと思いスポドリを買いに行った帰りに担当に会って少し話
その後担当医が病室に来て傷口チェック。
10時少し前に予定通り退院しました。
術後4日目
まだめまいがある。昨日よりはましな感じだけど、ほぼ1日ごろごろしてました。
傷口に貼っていた絆創膏のテープの部分がすこし荒れて痒くなってきた。
術後5日目
昨日よりはましになったけど、まだ血が足りてない感がありました。
傷部分はかなり治ってきているのか押してもそこまで痛くなくなりました。
退院時に処方された薬
痛み止め : ロキソプロフェン錠(60mg)
鉄剤 : クエン酸第一鉄Na錠(50mg)
術後約4週間 : 術後検診
手術した病院で検診。体重とか血圧とかの他は検尿と採血のみ。ヘモグロビン濃度も正常値に戻って問題なしでした。
感想
なぜドナーになったのか? : やったことなかったでやってみたかった
術後1週間経たずに普通に日常生活に戻れたのでよかったです。
私は比較的仕事の都合がつく方だけど、仕事の都合つけるの大変だろうなと思いました(手術以外にも面談とか健康診断とか自己血採取で何回も病院にいかないといけない)。
もし次の機会があってもやると思う(骨髄採取は人生で2回まで可能)。
追記
手術1ヶ月前くらいからサプリは飲まないでくださいって言われて、BCAA もプロテインも飲まないで欲しいと言われました。
手術1ヶ月前くらいから激しい運動は控えてくださいって言われて、ランニングはその時期やめてました。屋内ならいいかなと思ってリングフィットはしてました。
病院ではいっさい支払いしません。私の入院費や検査費は患者さんの健康保険から出ます。
病院食がご飯多くておかず少なくてちょっと物足りなかったです。写真載せたいけど病院が特定できる写真は載せられないのです。
術後2回くらいアンケート依頼の手紙が来ます。数週間後と数ヶ月後だったはず。
患者さんの情報は性別・年代・住んでる地域(関東圏とか)は教えてもらえます。
私の入院日数は3泊4日です。骨髄採取では平均的な日数だと思う。
末梢血幹細胞採取は、普段は骨髄にある造血幹細胞を血管内に出てくるようにする薬を飲んで、成分献血みたいなやつで採取する方法です。成分献血みたいなやつを6時間くらいするそうです。手術をしないので体への負担は骨髄採取より軽いと思うけど入院日数は長くなります。でも自己血貯血しなくていいので病院に行く回数は骨髄採取より少なくなります。
骨髄バンクの登録者数と年間の手術数(骨髄採取+末梢血幹細胞採取)からざっと計算したら、登録期間中に1回ドナー候補になるかどうかくらいの割合だなって思いました。血液型と同じで白血球の型も多いやつ少ないやつあるので、何回も候補になる人はわりといるそうです。
Hilt が入っていない Dagger のみの構成があります。
@Singleton
@Component(
modules = [
AppModule::class,
]
)
interface AppComponent {
fun inject(app: MainActivity)
}
@Module
class AppModule {
@Singleton
@Provides
fun provideNeedResetRepository(myApi: MyApi): NeedResetRepository {
return NeedResetRepository(myApi)
}
@Singleton
@Provides
fun provideMyApi(): MyApi {
return MyApi()
}
}
class NeedResetRepository(
private val myApi: MyApi
) {
// キャッシュなどのデータを持っている
}
class MyApi {
// 状態を持たない
}
この構成では MyApplication で Dagger の Component である AppComponent のインスタンスを保持しています。
ログアウト時に NeedResetRepository で保持しているキャッシュを削除したいので、MyApplication で保持している AppComponent のインスタンスを null にしてから MainActivity を作り直しています(reset() メソッドのところ)。
(Hilt がない時代の標準的なやり方だと AppComponent のインスタンスは lateinit var にして onCreate() で代入し、作り直すことがないようにすることが多いのですが、それだと Hilt への移行で困ることがないので、今回は困るパターンということでこのような構成例になっています)
class MyApplication : Application() {
private var appComponent: AppComponent? = null
fun getAppComponent(): AppComponent {
return appComponent ?: DaggerAppComponent.builder()
.build()
.also {
appComponent = it
}
}
fun reset() {
appComponent = null
TaskStackBuilder.create(this)
.addNextIntent(
Intent(this, MainActivity::class.java)
)
.startActivities()
}
}
以下のコードで MainActivity の reset ボタンを押すと、AppComponent のインスタンスが新しくなるので AppComponent および NeedResetRepository のインスタンスが新しくなるのがわかります。
AppComponent と NeedResetRepository には @Singleton がついているので、AppComponent が作り直されるまで NeedResetRepository のインスタンスは変わりません。そのため例えば画面回転時には NeedResetRepository のインスタンスは変わりません。
class MainActivity : ComponentActivity() {
@Inject
lateinit var needResetRepository: NeedResetRepository
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val appComponent = (application as MyApplication).getAppComponent()
appComponent.inject(this)
enableEdgeToEdge()
setContent {
MaterialTheme {
Scaffold { innerPadding ->
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.padding(innerPadding)
.padding(16.dp),
) {
Text(
text = "$needResetRepository",
)
Button(
onClick = {
(application as MyApplication).reset()
}
) {
Text("reset")
}
}
}
}
}
}
}
さて、このような構成に対し、新しく作成するクラスや ViewModel では Hilt を使いたいとします。
ステップ1 : Hilt ライブラリの設定
libs.versions.toml
[libraries]
...
hilt = { module = "com.google.dagger:hilt-android", version.ref = "dagger" }
hilt-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "dagger" }
[plugins]
...
hilt = { id = "com.google.dagger.hilt.android", version.ref = "dagger" }
build.gradle
plugins {
...
alias(libs.plugins.hilt) apply false
}
app/build.gradle
plugins {
...
+ alias(libs.plugins.hilt)
}
...
dependencies {
- implementation(libs.dagger)
- ksp(libs.dagger.compiler)
+ implementation(libs.hilt)
+ ksp(libs.hilt.compiler)
...
}
hilt の plugin を build.gradle に設定し、dependencies の依存ライブラリを dagger のものから hilt のものに置き換えてビルドすると、次のようなエラーがでます。
[ksp] /.../AppComponent.kt:21: [Hilt] com.example.sample.AppModule is missing an @InstallIn annotation. If this was intentional, see https://dagger.dev/hilt/flags#disable-install-in-check for how to disable this check.
@Module アノテーションがついている AppModule に @InstallIn がついていないと怒られていますが、AppModule は AppComponent 用なので @DisableInstallInCheck をつけてエラーがでないようにします。
@DisableInstallInCheck
@Module
class AppModule {
...
}
最後に MyApplication に @HiltAndroidApp アノテーションをつけます。
@HiltAndroidApp
class MyApplication : Application() {
...
}
ここまでで、ビルドして以前と同じ動作になっているのが確認できます(MainActivity にはまだ @AndroidEntryPoint はつけません!)。
ステップ2 : SubComponent 化
AppModule を分割して、リセットする必要のない MyApi は @InstallIn(SingletonComponent::class) をつけた Module で管理するようにしたいのですが、
@InstallIn(SingletonComponent::class)
@Module
object AppModule2 {
@Singleton
@Provides
fun provideMyApi(): MyApi {
return MyApi()
}
}
こうすると、NeedResetRepository を @Provides しているところで MyApi が見えなくなって
[ksp] /.../AppComponent.kt:17: [Dagger/MissingBinding] com.example.sample.MyApi cannot be provided without an @Inject constructor or an @Provides-annotated method.
このようなエラーが出てしまいます。
そこで、Module を分割する前に、既存の AppComponent を Hilt の SubComponent に変更します。
1. AppComponent のアノテーションを @Subcomponent に変更し、@Subcomponent.Builder をつけた Builder を用意します
-@Singleton
-@Component(
- modules = [
- AppModule::class,
- ]
-)
+@Subcomponent
interface AppComponent {
+ @Subcomponent.Builder
+ interface Builder {
+ fun build(): AppComponent
+ }
+
fun inject(app: MainActivity)
}
2. 任意のタイミングで AppComponent を作り直せるように、AppComponent の親 Component を用意します。ログアウト時にリセットすることを想定して、ここでは AuthComponent という名前にしています。
AuthComponent では SingletonComponent に InstallIn されている型も見えるようにしたいので、parent に SingletonComponent を指定します。
AuthComponent に対応する Scope も用意します。ここでは AuthScope という名前にしています。AuthComponent に @AuthScope をつけます。
@Scope
@Retention(AnnotationRetention.RUNTIME)
annotation class AuthScope
@AuthScope
@DefineComponent(parent = SingletonComponent::class)
interface AuthComponent {
@DefineComponent.Builder
interface Builder {
fun build(): AuthComponent
}
}
AppComponent を AuthComponent に紐づけるための Module を用意します。@InstallIn(AuthComponent::class) をつけ、@Module の subcomponents で AppComponent を指定します。
@InstallIn(AuthComponent::class)
@Module(
subcomponents = [
AppComponent::class,
]
)
interface AuthModule
3. AppModule の @DisableInstallInCheck を @InstallIn(AuthComponent::class) に変更し、@Singleton を @AuthScope に変更します。
-@DisableInstallInCheck
+@InstallIn(AuthComponent::class)
@Module
object AppModule {
- @Singleton
+ @AuthScope
@Provides
fun provideNeedResetRepository(myApi: MyApi): NeedResetRepository {
return NeedResetRepository(myApi)
}
- @Singleton
+ @AuthScope
@Provides
fun provideMyApi(): MyApi {
return MyApi()
}
}
4. 任意のタイミングで AppComponent を作り直すために、AppComponent の親である AuthComponent を管理する GeneratedComponentManager を用意します。
@Singleton
class AuthComponentRegistry @Inject constructor(
private val authComponentBuilder: AuthComponent.Builder,
) : GeneratedComponentManager<AuthComponent> {
private var authComponent: AuthComponent
init {
authComponent = authComponentBuilder.build()
}
fun reset() {
authComponent = authComponentBuilder.build()
}
override fun generatedComponent(): AuthComponent {
return authComponent
}
fun getAppComponent(): AppComponent {
return EntryPoints.get(
this,
AuthComponentEntryPoint::class.java
)
.appComponentBuilder()
.build()
}
@EntryPoint
@InstallIn(AuthComponent::class)
interface AuthComponentEntryPoint {
fun appComponentBuilder(): AppComponent.Builder
}
}
reset() メソッドを用意して、そこで authComponent のインスタンスを作り直すことによって、任意のタイミングで AppComponent を作り直せるようにしています。
AppComponent.Builder は AuthModule で @InstallIn(AuthComponent::class) されているので、同じように @InstallIn(AuthComponent::class) をつけた EntryPoint を用意することで取得できます。
この EntryPoint のインスタンスは、EntryPoints.get() に AuthComponentRegistry インスタンスを渡すことで取得できます。
5. MyApplication で保持している appComponent を削除して、getAppComponent() では AuthComponentRegistry から取得したインスタンスを返すようにします。reset() メソッドでは AuthComponentRegistry の reset() メソッドを呼ぶように変更します。
@HiltAndroidApp
class MyApplication : Application() {
- private var appComponent: AppComponent? = null
+ @Inject
+ lateinit var authComponentRegistry: AuthComponentRegistry
fun getAppComponent(): AppComponent {
- return appComponent ?: DaggerAppComponent.builder()
- .build()
- .also {
- appComponent = it
- }
+ return authComponentRegistry.getAppComponent()
}
fun reset() {
- appComponent = null
+ authComponentRegistry.reset()
これで AppComponent が Hilt で管理されるようになりました。
reset ボタンが押されるまでは NeedResetRepository のインスタンスが保持され、reset ボタンを押すと NeedResetRepository のインスタンスが新しくなるという以前の挙動を保っています。
ステップ3 : Module 分割
ここまでくれば MyApi を別 Module に分割できます。
@InstallIn(AuthComponent::class)
@Module
object AppModule {
@AuthScope
@Provides
fun provideNeedResetRepository(myApi: MyApi): NeedResetRepository {
return NeedResetRepository(myApi)
}
+}
- @AuthScope
+@InstallIn(SingletonComponent::class)
+@Module
+object AppModule2 {
+
+ @Singleton
@Provides
fun provideMyApi(): MyApi {
return MyApi()
}
}
AppModule2 には @InstallIn(SingletonComponent::class) をつけ、provideMyApi() の scope を @AuthScope から @Singleton に変更します。
reset ボタンが押されても MyApi のインスタンスが保持されるように変わります。
ステップ4 : @HiltViewModel
この段階で、AuthComponent に依存しないクラスだけを引数にとる ViewModel なら @HiltViewModel を使えるようになります。
@HiltViewModel
class SomeViewModel @Inject constructor(
prival val myApi: MyApi
) : ViewModel()
@AndroidEntryPoint
class SomeActivity : ComponentActivity() {
private val viewModel by viewModels<SomeViewModel>()
}
AuthComponent に依存するクラスを引数にとるときは AssistedInject を利用します。
https://dagger.dev/hilt/view-model#assisted-injection
@HiltViewModel(assistedFactory = SomeViewModel.Factory::class)
class SomeViewModel @AssistedInject constructor(
@Assisted private val needResetRepository: NeedResetRepository
) : ViewModel() {
@AssistedFactory
interface Factory {
fun create(
needResetRepository: NeedResetRepository
): SomeViewModel
}
}
@AndroidEntryPoint
class SomeActivity : ComponentActivity() {
private val viewModel by viewModels<SomeViewModel>(
extrasProducer = {
defaultViewModelCreationExtras.withCreationCallback<SomeViewModel.Factory> { factory ->
factory.create(
authComponentEntryPoint().needResetRepository()
)
}
}
)
}
Context から AuthComponent に依存するクラスのインスタンスを取れるように便利メソッドを用意しておきます。
@EntryPoint
@InstallIn(AuthComponent::class)
interface AuthComponentEntryPoint {
fun needResetRepository(): NeedResetRepository
}
@InstallIn(SingletonComponent::class)
@EntryPoint
interface SingletonComponentEntryPoint {
fun authComponentRegistry(): AuthComponentRegistry
}
fun Context.authComponentEntryPoint(): AuthComponentEntryPoint {
val authComponentRegistry = EntryPointAccessors
.fromApplication<SingletonComponentEntryPoint>(this)
.authComponentRegistry()
return EntryPoints.get(authComponentRegistry, AuthComponentEntryPoint::class.java)
}
ステップ4 : AppComponent の inject() メソッド廃止
上で用意した便利メソッドを使って MainActivity の needResetRepository にインスタンスをセットするように変えます。
MyApi は @InstallIn(SingletonComponent::class) なので MainActivity に @AndroidEntryPoint をつければ inject されます。
+@AndroidEntryPoint
class MainActivity : ComponentActivity() {
- @Inject
- lateinit var needResetRepository: NeedResetRepository
+ private lateinit var needResetRepository: NeedResetRepository
@Inject
lateinit var myApi: MyApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- val appComponent = (application as MyApplication).getAppComponent()
- appComponent.inject(this)
+ needResetRepository = authComponentEntryPoint().needResetRepository()
enableEdgeToEdge()
interface AppComponent {
interface Builder {
fun build(): AppComponent
}
- fun inject(app: MainActivity)
}
このように AppComponent の inject() メソッドを段階的に廃止していきます。
ステップ5 : AppComponent の廃止
AppComponent に定義されているメソッドがなくなったら AppComponent 自体を廃止します。
-@Subcomponent
-interface AppComponent {
-
- @Subcomponent.Builder
- interface Builder {
- fun build(): AppComponent
- }
-}
-@InstallIn(AuthComponent::class)
-@Module(
- subcomponents = [
- AppComponent::class,
- ]
-)
-interface AuthModule
@Singleton
class AuthComponentRegistry @Inject constructor(
private val authComponentBuilder: AuthComponent.Builder,
) : GeneratedComponentManager<AuthComponent> {
...
override fun generatedComponent(): AuthComponent {
return authComponent
}
-
- fun getAppComponent(): AppComponent {
- return EntryPoints.get(
- this,
- AuthComponentEntryPoint::class.java
- )
- .appComponentBuilder()
- .build()
- }
-
- @EntryPoint
- @InstallIn(AuthComponent::class)
- interface AuthComponentEntryPoint {
- fun appComponentBuilder(): AppComponent.Builder
- }
}
@HiltAndroidApp
class MyApplication : Application() {
@Inject
lateinit var authComponentRegistry: AuthComponentRegistry
- fun getAppComponent(): AppComponent {
- return authComponentRegistry.getAppComponent()
- }
-
fun reset() {
これで Hilt への移行完了です!
M2 の LinearProgressIndicator の progress 引数は Float でしたが、M3 では () -> Float になっています。
androidx.compose.material.LinearProgressIndicator(
progress = 0.5f,
)
androidx.compose.material3.LinearProgressIndicator(
progress = { 0.5f },
)
いずれも内部の実装は Canvas composable を使って描画しています。
そのため、progress を lambda にすることで Composition と Layout phase をスキップして Drawing phase だけやり直せばよくなり、その分パフォーマンスが良くなります。
(
https://developer.android.com/develop/ui/compose/phases )
実際以下のコードを実行して Layout Inspector で recomposition の回数を見ると、M2 の方は recompositoin されていますが M3 の方は skip されています。
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.fillMaxSize().padding(16.dp),
) {
var progress by remember { mutableFloatStateOf(0f) }
androidx.compose.material3.Button(
onClick = { progress = Random.nextFloat() },
) {
Text("update progress")
}
androidx.compose.material.LinearProgressIndicator(
progress = progress,
modifier = Modifier.fillMaxWidth(),
)
androidx.compose.material3.LinearProgressIndicator(
progress = { progress },
modifier = Modifier.fillMaxWidth(),
)
}
M3 の LinearProgressIndicator を wrap するときは、wrap する component でも progress を lambda で取るように注意してください(より正確に言うと、lamda の中で state から読み出しを行うようにするということ)。そうしないと M3 の LinearProgressIndicator を使っていても recompose が走ります。
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.fillMaxSize().padding(16.dp),
) {
var progress by remember { mutableFloatStateOf(0f) }
androidx.compose.material3.Button(
onClick = { progress = Random.nextFloat() },
) {
Text("update progress")
}
LinearProgressIndicatorM2(progress)
LinearProgressIndicatorM3_Bad(progress)
LinearProgressIndicatorM3_Good({ progress })
}
@Composable
private fun LinearProgressIndicatorM2(progress: Float) {
androidx.compose.material.LinearProgressIndicator(
progress = progress,
modifier = Modifier.fillMaxWidth(),
)
}
@Composable
private fun LinearProgressIndicatorM3_Bad(progress: Float) {
androidx.compose.material3.LinearProgressIndicator(
progress = { progress },
modifier = Modifier.fillMaxWidth(),
)
}
@Composable
private fun LinearProgressIndicatorM3_Good(progress: () -> Float) {
androidx.compose.material3.LinearProgressIndicator(
progress = progress,
modifier = Modifier.fillMaxWidth(),
)
}
そうは言っても、階層のどこかで progress が読み出されていることもあるでしょう(Text Composable で progress の値を表示しているとか)。その場合は rememberUpdatedState を使うことで LinearProgressIndicator の recomposition を skip させることができます。
@Composable
private fun Wrap(progress: Float, onUpdate: () -> Unit) {
val updatedProgress by rememberUpdatedState(progress)
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.fillMaxSize().padding(16.dp),
) {
...
LinearProgressIndicatorM3_Good({ updatedProgress })
}
}
'},ClipboardSwf:null,Version:'1.5.1'}};dp.SyntaxHighlighter=dp.sh;dp.sh.Toolbar.Commands={ExpandSource:{label:'+ expand source',check:function(highlighter){return highlighter.collapse;},func:function(sender,highlighter)
{sender.parentNode.removeChild(sender);highlighter.div.className=highlighter.div.className.replace('collapsed','');}},ViewSource:{label:'view plain',func:function(sender,highlighter)
{var code=dp.sh.Utils.FixForBlogger(highlighter.originalCode).replace(/'+code+'');wnd.document.close();}},CopyToClipboard:{label:'copy to clipboard',check:function(){return window.clipboardData!=null||dp.sh.ClipboardSwf!=null;},func:function(sender,highlighter)
{var code=dp.sh.Utils.FixForBlogger(highlighter.originalCode).replace(/</g,'<').replace(/>/g,'>').replace(/&/g,'&');if(window.clipboardData)
{window.clipboardData.setData('text',code);}
else if(dp.sh.ClipboardSwf!=null)
{var flashcopier=highlighter.flashCopier;if(flashcopier==null)
{flashcopier=document.createElement('div');highlighter.flashCopier=flashcopier;highlighter.div.appendChild(flashcopier);}
flashcopier.innerHTML='';}
alert('The code is in your clipboard now');}},PrintSource:{label:'print',func:function(sender,highlighter)
{var iframe=document.createElement('IFRAME');var doc=null;iframe.style.cssText='position:absolute;width:0px;height:0px;left:-500px;top:-500px;';document.body.appendChild(iframe);doc=iframe.contentWindow.document;dp.sh.Utils.CopyStyles(doc,window.document);doc.write('
'+highlighter.div.innerHTML+'
');doc.close();iframe.contentWindow.focus();iframe.contentWindow.print();alert('Printing...');document.body.removeChild(iframe);}},About:{label:'?',func:function(highlighter)
{var wnd=window.open('','_blank','dialog,width=300,height=150,scrollbars=0');var doc=wnd.document;dp.sh.Utils.CopyStyles(doc,window.document);doc.write(dp.sh.Strings.AboutDialog.replace('{V}',dp.sh.Version));doc.close();wnd.focus();}}};dp.sh.Toolbar.Create=function(highlighter)
{var div=document.createElement('DIV');div.className='tools';for(var name in dp.sh.Toolbar.Commands)
{var cmd=dp.sh.Toolbar.Commands[name];if(cmd.check!=null&&!cmd.check(highlighter))
continue;div.innerHTML+=''+cmd.label+' ';}
return div;}
dp.sh.Toolbar.Command=function(name,sender)
{var n=sender;while(n!=null&&n.className.indexOf('dp-highlighter')==-1)
n=n.parentNode;if(n!=null)
dp.sh.Toolbar.Commands[name].func(sender,n.highlighter);}
dp.sh.Utils.CopyStyles=function(destDoc,sourceDoc)
{var links=sourceDoc.getElementsByTagName('link');for(var i=0;i');}
dp.sh.Utils.FixForBlogger=function(str)
{return(dp.sh.isBloggerMode==true)?str.replace(/ |<br\s*\/?>/gi,''):str;}
dp.sh.RegexLib={MultiLineCComments:new RegExp('/\\*[\\s\\S]*?\\*/','gm'),SingleLineCComments:new RegExp('//.*$','gm'),SingleLinePerlComments:new RegExp('#.*$','gm'),DoubleQuotedString:new RegExp('"(?:\\.|(\\\\\\")|[^\\""\\n])*"','g'),SingleQuotedString:new RegExp("'(?:\\.|(\\\\\\')|[^\\''\\n])*'",'g')};dp.sh.Match=function(value,index,css)
{this.value=value;this.index=index;this.length=value.length;this.css=css;}
dp.sh.Highlighter=function()
{this.noGutter=false;this.addControls=true;this.collapse=false;this.tabsToSpaces=true;this.wrapColumn=80;this.showColumns=true;}
dp.sh.Highlighter.SortCallback=function(m1,m2)
{if(m1.indexm2.index)
return 1;else
{if(m1.lengthm2.length)
return 1;}
return 0;}
dp.sh.Highlighter.prototype.CreateElement=function(name)
{var result=document.createElement(name);result.highlighter=this;return result;}
dp.sh.Highlighter.prototype.GetMatches=function(regex,css)
{var index=0;var match=null;while((match=regex.exec(this.code))!=null)
this.matches[this.matches.length]=new dp.sh.Match(match[0],match.index,css);}
dp.sh.Highlighter.prototype.AddBit=function(str,css)
{if(str==null||str.length==0)
return;var span=this.CreateElement('SPAN');str=str.replace(/ /g,' ');str=str.replace(/');if(css!=null)
{if((/br/gi).test(str))
{var lines=str.split(' ');for(var i=0;ic.index)&&(match.index/gi,'\n');var lines=html.split('\n');if(this.addControls==true)
this.bar.appendChild(dp.sh.Toolbar.Create(this));if(this.showColumns)
{var div=this.CreateElement('div');var columns=this.CreateElement('div');var showEvery=10;var i=1;while(i<=150)
{if(i%showEvery==0)
{div.innerHTML+=i;i+=(i+'').length;}
else
{div.innerHTML+='·';i++;}}
columns.className='columns';columns.appendChild(div);this.bar.appendChild(columns);}
for(var i=0,lineIndex=this.firstLine;i0;i++)
{if(Trim(lines[i]).length==0)
continue;var matches=regex.exec(lines[i]);if(matches!=null&&matches.length>0)
min=Math.min(matches[0].length,min);}
if(min>0)
for(var i=0;i)','gm'),'cdata');this.GetMatches(new RegExp('(\<|<)!--\\s*.*?\\s*--(\>|>)','gm'),'comments');regex=new RegExp('([:\\w-\.]+)\\s*=\\s*(".*?"|\'.*?\'|\\w+)*|(\\w+)','gm');while((match=regex.exec(this.code))!=null)
{if(match[1]==null)
{continue;}
push(this.matches,new dp.sh.Match(match[1],match.index,'attribute'));if(match[2]!=undefined)
{push(this.matches,new dp.sh.Match(match[2],match.index+match[0].indexOf(match[2]),'attribute-value'));}}
this.GetMatches(new RegExp('(\<|<)/*\\?*(?!\\!)|/*\\?*(\>|>)','gm'),'tag');regex=new RegExp('(?:\<|<)/*\\?*\\s*([:\\w-\.]+)','gm');while((match=regex.exec(this.code))!=null)
{push(this.matches,new dp.sh.Match(match[1],match.index+match[0].indexOf(match[1]),'tag-name'));}}
dp.sh.Brushes.JScript=function()
{var keywords='abstract boolean break byte case catch char class const continue debugger '+'default delete do double else enum export extends false final finally float '+'for function goto if implements import in instanceof int interface long native '+'new null package private protected public return short static super switch '+'synchronized this throw throws transient true try typeof var void volatile while with';this.regexList=[{regex:dp.sh.RegexLib.SingleLineCComments,css:'comment'},{regex:dp.sh.RegexLib.MultiLineCComments,css:'comment'},{regex:dp.sh.RegexLib.DoubleQuotedString,css:'string'},{regex:dp.sh.RegexLib.SingleQuotedString,css:'string'},{regex:new RegExp('^\\s*#.*','gm'),css:'preprocessor'},{regex:new RegExp(this.GetKeywords(keywords),'gm'),css:'keyword'}];this.CssClass='dp-c';}
dp.sh.Brushes.JScript.prototype=new dp.sh.Highlighter();dp.sh.Brushes.JScript.Aliases=['js','jscript','javascript'];
dp.sh.Brushes.Ruby=function()
{var keywords='alias and BEGIN begin break case class def define_method defined do each else elsif '+'END end ensure false for if in module new next nil not or raise redo rescue retry return '+'self super then throw true undef unless until when while yield';var builtins='Array Bignum Binding Class Continuation Dir Exception FalseClass File::Stat File Fixnum Fload '+'Hash Integer IO MatchData Method Module NilClass Numeric Object Proc Range Regexp String Struct::TMS Symbol '+'ThreadGroup Thread Time TrueClass'
this.regexList=[{regex:dp.sh.RegexLib.SingleLinePerlComments,css:'comment'},{regex:dp.sh.RegexLib.DoubleQuotedString,css:'string'},{regex:dp.sh.RegexLib.SingleQuotedString,css:'string'},{regex:new RegExp(':[a-z][A-Za-z0-9_]*','g'),css:'symbol'},{regex:new RegExp('(\\$|@@|@)\\w+','g'),css:'variable'},{regex:new RegExp(this.GetKeywords(keywords),'gm'),css:'keyword'},{regex:new RegExp(this.GetKeywords(builtins),'gm'),css:'builtin'}];this.CssClass='dp-rb';this.Style='.dp-rb .symbol { color: #a70; }'+'.dp-rb .variable { color: #a70; font-weight: bold; }';}
dp.sh.Brushes.Ruby.prototype=new dp.sh.Highlighter();dp.sh.Brushes.Ruby.Aliases=['ruby','rails','ror'];
dp.sh.Brushes.JavaFX = function(){
var keywords = 'abstract assert attribute bind break '+'class continue delete false for '+'function if import init insert '+'let new not null package '+'private protected public readonly return '+'super sizeof static this throw '+'try true var while after '+'and as before by catch '+'do dur else exclusive extends '+'finally first from in bound '+'indexof into inverse lazy last '+
'on or replace step typeof '+'with where instanceof override at '+'then tween mod';
this.regexList = [{regex:dp.sh.RegexLib.SingleLineCComments,css: 'comment' },{ regex: dp.sh.RegexLib.MultiLineCComments,css:'comment'},{ regex: dp.sh.RegexLib.DoubleQuotedString,css: 'string' },{ regex: dp.sh.RegexLib.SingleQuotedString,css: 'string' },{ regex: new RegExp('\\b([\\d]+(\\.[\\d]+)?|0x[a-f0-9]+)\\b', 'gi'), css: 'number' },{ regex: new RegExp(this.GetKeywords(keywords), 'gm'),css: 'keyword' }];
this.CssClass='dp-j';
this.Style='.dp-j .annotation { color: #646464; }'+
'.dp-j .number{color:#C00000; }';}
dp.sh.Brushes.JavaFX.prototype=new dp.sh.Highlighter();dp.sh.Brushes.JavaFX.Aliases=['jfx'];
dp.SyntaxHighlighter.BloggerMode();
dp.SyntaxHighlighter.HighlightAll('code');
-->
'},ClipboardSwf:null,Version:'1.5.1'}};dp.SyntaxHighlighter=dp.sh;dp.sh.Toolbar.Commands={ExpandSource:{label:'+ expand source',check:function(highlighter){return highlighter.collapse;},func:function(sender,highlighter)
{sender.parentNode.removeChild(sender);highlighter.div.className=highlighter.div.className.replace('collapsed','');}},ViewSource:{label:'view plain',func:function(sender,highlighter)
{var code=dp.sh.Utils.FixForBlogger(highlighter.originalCode).replace(/'+code+'');wnd.document.close();}},CopyToClipboard:{label:'copy to clipboard',check:function(){return window.clipboardData!=null||dp.sh.ClipboardSwf!=null;},func:function(sender,highlighter)
{var code=dp.sh.Utils.FixForBlogger(highlighter.originalCode).replace(/</g,'<').replace(/>/g,'>').replace(/&/g,'&');if(window.clipboardData)
{window.clipboardData.setData('text',code);}
else if(dp.sh.ClipboardSwf!=null)
{var flashcopier=highlighter.flashCopier;if(flashcopier==null)
{flashcopier=document.createElement('div');highlighter.flashCopier=flashcopier;highlighter.div.appendChild(flashcopier);}
flashcopier.innerHTML='';}
alert('The code is in your clipboard now');}},PrintSource:{label:'print',func:function(sender,highlighter)
{var iframe=document.createElement('IFRAME');var doc=null;iframe.style.cssText='position:absolute;width:0px;height:0px;left:-500px;top:-500px;';document.body.appendChild(iframe);doc=iframe.contentWindow.document;dp.sh.Utils.CopyStyles(doc,window.document);doc.write('
'+highlighter.div.innerHTML+'
');doc.close();iframe.contentWindow.focus();iframe.contentWindow.print();alert('Printing...');document.body.removeChild(iframe);}},About:{label:'?',func:function(highlighter)
{var wnd=window.open('','_blank','dialog,width=300,height=150,scrollbars=0');var doc=wnd.document;dp.sh.Utils.CopyStyles(doc,window.document);doc.write(dp.sh.Strings.AboutDialog.replace('{V}',dp.sh.Version));doc.close();wnd.focus();}}};dp.sh.Toolbar.Create=function(highlighter)
{var div=document.createElement('DIV');div.className='tools';for(var name in dp.sh.Toolbar.Commands)
{var cmd=dp.sh.Toolbar.Commands[name];if(cmd.check!=null&&!cmd.check(highlighter))
continue;div.innerHTML+=''+cmd.label+' ';}
return div;}
dp.sh.Toolbar.Command=function(name,sender)
{var n=sender;while(n!=null&&n.className.indexOf('dp-highlighter')==-1)
n=n.parentNode;if(n!=null)
dp.sh.Toolbar.Commands[name].func(sender,n.highlighter);}
dp.sh.Utils.CopyStyles=function(destDoc,sourceDoc)
{var links=sourceDoc.getElementsByTagName('link');for(var i=0;i');}
dp.sh.Utils.FixForBlogger=function(str)
{return(dp.sh.isBloggerMode==true)?str.replace(/ |<br\s*\/?>/gi,'\n'):str;}
dp.sh.RegexLib={MultiLineCComments:new RegExp('/\\*[\\s\\S]*?\\*/','gm'),SingleLineCComments:new RegExp('//.*$','gm'),SingleLinePerlComments:new RegExp('#.*$','gm'),DoubleQuotedString:new RegExp('"(?:\\.|(\\\\\\")|[^\\""\\n])*"','g'),SingleQuotedString:new RegExp("'(?:\\.|(\\\\\\')|[^\\''\\n])*'",'g')};dp.sh.Match=function(value,index,css)
{this.value=value;this.index=index;this.length=value.length;this.css=css;}
dp.sh.Highlighter=function()
{this.noGutter=false;this.addControls=true;this.collapse=false;this.tabsToSpaces=true;this.wrapColumn=80;this.showColumns=true;}
dp.sh.Highlighter.SortCallback=function(m1,m2)
{if(m1.indexm2.index)
return 1;else
{if(m1.lengthm2.length)
return 1;}
return 0;}
dp.sh.Highlighter.prototype.CreateElement=function(name)
{var result=document.createElement(name);result.highlighter=this;return result;}
dp.sh.Highlighter.prototype.GetMatches=function(regex,css)
{var index=0;var match=null;while((match=regex.exec(this.code))!=null)
this.matches[this.matches.length]=new dp.sh.Match(match[0],match.index,css);}
dp.sh.Highlighter.prototype.AddBit=function(str,css)
{if(str==null||str.length==0)
return;var span=this.CreateElement('SPAN');str=str.replace(/ /g,' ');str=str.replace(/');if(css!=null)
{if((/br/gi).test(str))
{var lines=str.split(' ');for(var i=0;ic.index)&&(match.index/gi,'\n');var lines=html.split('\n');if(this.addControls==true)
this.bar.appendChild(dp.sh.Toolbar.Create(this));if(this.showColumns)
{var div=this.CreateElement('div');var columns=this.CreateElement('div');var showEvery=10;var i=1;while(i<=150)
{if(i%showEvery==0)
{div.innerHTML+=i;i+=(i+'').length;}
else
{div.innerHTML+='·';i++;}}
columns.className='columns';columns.appendChild(div);this.bar.appendChild(columns);}
for(var i=0,lineIndex=this.firstLine;i0;i++)
{if(Trim(lines[i]).length==0)
continue;var matches=regex.exec(lines[i]);if(matches!=null&&matches.length>0)
min=Math.min(matches[0].length,min);}
if(min>0)
for(var i=0;i)','gm'),'cdata');this.GetMatches(new RegExp('(\<|<)!--\\s*.*?\\s*--(\>|>)','gm'),'comments');regex=new RegExp('([:\\w-\.]+)\\s*=\\s*(".*?"|\'.*?\'|\\w+)*|(\\w+)','gm');while((match=regex.exec(this.code))!=null)
{if(match[1]==null)
{continue;}
push(this.matches,new dp.sh.Match(match[1],match.index,'attribute'));if(match[2]!=undefined)
{push(this.matches,new dp.sh.Match(match[2],match.index+match[0].indexOf(match[2]),'attribute-value'));}}
this.GetMatches(new RegExp('(\<|<)/*\\?*(?!\\!)|/*\\?*(\>|>)','gm'),'tag');regex=new RegExp('(?:\<|<)/*\\?*\\s*([:\\w-\.]+)','gm');while((match=regex.exec(this.code))!=null)
{push(this.matches,new dp.sh.Match(match[1],match.index+match[0].indexOf(match[1]),'tag-name'));}}
dp.sh.Brushes.JScript=function()
{var keywords='abstract boolean break byte case catch char class const continue debugger '+'default delete do double else enum export extends false final finally float '+'for function goto if implements import in instanceof int interface long native '+'new null package private protected public return short static super switch '+'synchronized this throw throws transient true try typeof var void volatile while with';this.regexList=[{regex:dp.sh.RegexLib.SingleLineCComments,css:'comment'},{regex:dp.sh.RegexLib.MultiLineCComments,css:'comment'},{regex:dp.sh.RegexLib.DoubleQuotedString,css:'string'},{regex:dp.sh.RegexLib.SingleQuotedString,css:'string'},{regex:new RegExp('^\\s*#.*','gm'),css:'preprocessor'},{regex:new RegExp(this.GetKeywords(keywords),'gm'),css:'keyword'}];this.CssClass='dp-c';}
dp.sh.Brushes.JScript.prototype=new dp.sh.Highlighter();dp.sh.Brushes.JScript.Aliases=['js','jscript','javascript'];
dp.sh.Brushes.Ruby=function()
{var keywords='alias and BEGIN begin break case class def define_method defined do each else elsif '+'END end ensure false for if in module new next nil not or raise redo rescue retry return '+'self super then throw true undef unless until when while yield';var builtins='Array Bignum Binding Class Continuation Dir Exception FalseClass File::Stat File Fixnum Fload '+'Hash Integer IO MatchData Method Module NilClass Numeric Object Proc Range Regexp String Struct::TMS Symbol '+'ThreadGroup Thread Time TrueClass'
this.regexList=[{regex:dp.sh.RegexLib.SingleLinePerlComments,css:'comment'},{regex:dp.sh.RegexLib.DoubleQuotedString,css:'string'},{regex:dp.sh.RegexLib.SingleQuotedString,css:'string'},{regex:new RegExp(':[a-z][A-Za-z0-9_]*','g'),css:'symbol'},{regex:new RegExp('(\\$|@@|@)\\w+','g'),css:'variable'},{regex:new RegExp(this.GetKeywords(keywords),'gm'),css:'keyword'},{regex:new RegExp(this.GetKeywords(builtins),'gm'),css:'builtin'}];this.CssClass='dp-rb';this.Style='.dp-rb .symbol { color: #a70; }'+'.dp-rb .variable { color: #a70; font-weight: bold; }';}
dp.sh.Brushes.Ruby.prototype=new dp.sh.Highlighter();dp.sh.Brushes.Ruby.Aliases=['ruby','rails','ror'];
dp.sh.Brushes.JavaFX = function(){
var keywords = 'abstract assert attribute bind break '+'class continue delete false for '+'function if import init insert '+'let new not null package '+'private protected public readonly return '+'super sizeof static this throw '+'try true var while after '+'and as before by catch '+'do dur else exclusive extends '+'finally first from in bound '+'indexof into inverse lazy last '+
'on or replace step typeof '+'with where instanceof override at '+'then tween mod';
this.regexList = [{regex:dp.sh.RegexLib.SingleLineCComments,css: 'comment' },{ regex: dp.sh.RegexLib.MultiLineCComments,css:'comment'},{ regex: dp.sh.RegexLib.DoubleQuotedString,css: 'string' },{ regex: dp.sh.RegexLib.SingleQuotedString,css: 'string' },{ regex: new RegExp('\\b([\\d]+(\\.[\\d]+)?|0x[a-f0-9]+)\\b', 'gi'), css: 'number' },{ regex: new RegExp(this.GetKeywords(keywords), 'gm'),css: 'keyword' }];
this.CssClass='dp-j';
this.Style='.dp-j .annotation { color: #646464; }'+
'.dp-j .number{color:#C00000; }';}
dp.sh.Brushes.JavaFX.prototype=new dp.sh.Highlighter();dp.sh.Brushes.JavaFX.Aliases=['jfx'];
dp.SyntaxHighlighter.BloggerMode();
dp.SyntaxHighlighter.HighlightAll('code');
-->