2024年12月9日月曜日

骨髄ドナーとして骨髄採取の手術をしました

本記事は 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回ドナー候補になるかどうかくらいの割合だなって思いました。血液型と同じで白血球の型も多いやつ少ないやつあるので、何回も候補になる人はわりといるそうです。

2024年11月17日日曜日

Component が任意のタイミングで更新される構成になっている Dagger を Hilt に移行する

Hilt が入っていない Dagger のみの構成があります。
  1. @Singleton  
  2. @Component(  
  3.     modules = [  
  4.         AppModule::class,  
  5.     ]  
  6. )  
  7. interface AppComponent {  
  8.   
  9.     fun inject(app: MainActivity)  
  10. }  
  11.   
  12. @Module  
  13. class AppModule {  
  14.   
  15.     @Singleton  
  16.     @Provides  
  17.     fun provideNeedResetRepository(myApi: MyApi): NeedResetRepository {  
  18.         return NeedResetRepository(myApi)  
  19.     }  
  20.   
  21.     @Singleton  
  22.     @Provides  
  23.     fun provideMyApi(): MyApi {  
  24.         return MyApi()  
  25.     }  
  26. }  
  27.   
  28. class NeedResetRepository(  
  29.     private val myApi: MyApi  
  30. ) {  
  31.   
  32.     // キャッシュなどのデータを持っている  
  33. }  
  34.   
  35. class MyApi {  
  36.   
  37.     // 状態を持たない  
  38. }  
この構成では MyApplication で Dagger の Component である AppComponent のインスタンスを保持しています。

ログアウト時に NeedResetRepository で保持しているキャッシュを削除したいので、MyApplication で保持している AppComponent のインスタンスを null にしてから MainActivity を作り直しています(reset() メソッドのところ)。

(Hilt がない時代の標準的なやり方だと AppComponent のインスタンスは lateinit var にして onCreate() で代入し、作り直すことがないようにすることが多いのですが、それだと Hilt への移行で困ることがないので、今回は困るパターンということでこのような構成例になっています)
  1. class MyApplication : Application() {  
  2.   
  3.     private var appComponent: AppComponent? = null  
  4.   
  5.     fun getAppComponent(): AppComponent {  
  6.         return appComponent ?: DaggerAppComponent.builder()  
  7.             .build()  
  8.             .also {  
  9.                 appComponent = it  
  10.             }  
  11.     }  
  12.   
  13.     fun reset() {  
  14.         appComponent = null  
  15.   
  16.         TaskStackBuilder.create(this)  
  17.             .addNextIntent(  
  18.                 Intent(this, MainActivity::class.java)  
  19.             )  
  20.             .startActivities()  
  21.     }  
  22. }  
以下のコードで MainActivity の reset ボタンを押すと、AppComponent のインスタンスが新しくなるので AppComponent および NeedResetRepository のインスタンスが新しくなるのがわかります。

AppComponent と NeedResetRepository には @Singleton がついているので、AppComponent が作り直されるまで NeedResetRepository のインスタンスは変わりません。そのため例えば画面回転時には NeedResetRepository のインスタンスは変わりません。
  1. class MainActivity : ComponentActivity() {  
  2.   
  3.     @Inject  
  4.     lateinit var needResetRepository: NeedResetRepository  
  5.   
  6.     override fun onCreate(savedInstanceState: Bundle?) {  
  7.         super.onCreate(savedInstanceState)  
  8.   
  9.         val appComponent = (application as MyApplication).getAppComponent()  
  10.         appComponent.inject(this)  
  11.   
  12.         enableEdgeToEdge()  
  13.         setContent {  
  14.             MaterialTheme {  
  15.                 Scaffold { innerPadding ->  
  16.                     Column(  
  17.                         verticalArrangement = Arrangement.spacedBy(8.dp),  
  18.                         modifier = Modifier  
  19.                             .padding(innerPadding)  
  20.                             .padding(16.dp),  
  21.                     ) {  
  22.                         Text(  
  23.                             text = "$needResetRepository",  
  24.                         )  
  25.   
  26.                         Button(  
  27.                             onClick = {  
  28.                                 (application as MyApplication).reset()  
  29.                             }  
  30.                         ) {  
  31.                             Text("reset")  
  32.                         }  
  33.                     }  
  34.                 }  
  35.             }  
  36.         }  
  37.     }  
  38. }  
さて、このような構成に対し、新しく作成するクラスや ViewModel では Hilt を使いたいとします。


ステップ1 : Hilt ライブラリの設定


libs.versions.toml
  1. [libraries]  
  2. ...  
  3. hilt = { module = "com.google.dagger:hilt-android", version.ref = "dagger" }  
  4. hilt-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "dagger" }  
  5.   
  6. [plugins]  
  7. ...  
  8. hilt = { id = "com.google.dagger.hilt.android", version.ref = "dagger" }  
build.gradle
  1. plugins {  
  2.     ...  
  3.     alias(libs.plugins.hilt) apply false  
  4. }  
app/build.gradle
  1.  plugins {  
  2.      ...  
  3. +    alias(libs.plugins.hilt)  
  4.  }  
  5.   
  6.   ...  
  7.   
  8.  dependencies {  
  9. -    implementation(libs.dagger)  
  10. -    ksp(libs.dagger.compiler)  
  11. +    implementation(libs.hilt)  
  12. +    ksp(libs.hilt.compiler)  
  13.   ...  
  14.  }  

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 をつけてエラーがでないようにします。
  1. @DisableInstallInCheck  
  2. @Module  
  3. class AppModule {  
  4.   
  5.     ...  
  6. }  
最後に MyApplication に @HiltAndroidApp アノテーションをつけます。
  1. @HiltAndroidApp  
  2. class MyApplication : Application() {  
  3.   
  4.     ...  
  5. }  
ここまでで、ビルドして以前と同じ動作になっているのが確認できます(MainActivity にはまだ @AndroidEntryPoint はつけません!)。


ステップ2 : SubComponent 化


AppModule を分割して、リセットする必要のない MyApi は @InstallIn(SingletonComponent::class) をつけた Module で管理するようにしたいのですが、
  1. @InstallIn(SingletonComponent::class)  
  2. @Module  
  3. object AppModule2 {  
  4.   
  5.     @Singleton  
  6.     @Provides  
  7.     fun provideMyApi(): MyApi {  
  8.         return MyApi()  
  9.     }  
  10. }  
こうすると、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 を用意します
  1. -@Singleton  
  2. -@Component(  
  3. -    modules = [  
  4. -        AppModule::class,  
  5. -    ]  
  6. -)  
  7. +@Subcomponent  
  8.  interface AppComponent {  
  9.    
  10. +    @Subcomponent.Builder  
  11. +    interface Builder {  
  12. +        fun build(): AppComponent  
  13. +    }  
  14. +  
  15.      fun inject(app: MainActivity)  
  16.  }  


2. 任意のタイミングで AppComponent を作り直せるように、AppComponent の親 Component を用意します。ログアウト時にリセットすることを想定して、ここでは AuthComponent という名前にしています。

AuthComponent では SingletonComponent に InstallIn されている型も見えるようにしたいので、parent に SingletonComponent を指定します。

AuthComponent に対応する Scope も用意します。ここでは AuthScope という名前にしています。AuthComponent に @AuthScope をつけます。
  1. @Scope  
  2. @Retention(AnnotationRetention.RUNTIME)  
  3. annotation class AuthScope  
  4.   
  5. @AuthScope  
  6. @DefineComponent(parent = SingletonComponent::class)  
  7. interface AuthComponent {  
  8.   
  9.     @DefineComponent.Builder  
  10.     interface Builder {  
  11.         fun build(): AuthComponent  
  12.     }  
  13. }  
AppComponent を AuthComponent に紐づけるための Module を用意します。@InstallIn(AuthComponent::class) をつけ、@Module の subcomponents で AppComponent を指定します。
  1. @InstallIn(AuthComponent::class)  
  2. @Module(  
  3.     subcomponents = [  
  4.         AppComponent::class,  
  5.     ]  
  6. )  
  7. interface AuthModule  


3. AppModule の @DisableInstallInCheck を @InstallIn(AuthComponent::class) に変更し、@Singleton を @AuthScope に変更します。
  1. -@DisableInstallInCheck  
  2. +@InstallIn(AuthComponent::class)  
  3.  @Module  
  4.  object AppModule {  
  5.    
  6. -    @Singleton  
  7. +    @AuthScope  
  8.      @Provides  
  9.      fun provideNeedResetRepository(myApi: MyApi): NeedResetRepository {  
  10.          return NeedResetRepository(myApi)  
  11.      }  
  12.    
  13. -    @Singleton  
  14. +    @AuthScope  
  15.      @Provides  
  16.      fun provideMyApi(): MyApi {  
  17.          return MyApi()  
  18.      }  
  19.  }  


4. 任意のタイミングで AppComponent を作り直すために、AppComponent の親である AuthComponent を管理する GeneratedComponentManager を用意します。
  1. @Singleton  
  2. class AuthComponentRegistry @Inject constructor(  
  3.     private val authComponentBuilder: AuthComponent.Builder,  
  4. ) : GeneratedComponentManager<AuthComponent> {  
  5.   
  6.     private var authComponent: AuthComponent  
  7.   
  8.     init {  
  9.         authComponent = authComponentBuilder.build()  
  10.     }  
  11.   
  12.     fun reset() {  
  13.         authComponent = authComponentBuilder.build()  
  14.     }  
  15.   
  16.     override fun generatedComponent(): AuthComponent {  
  17.         return authComponent  
  18.     }  
  19.   
  20.     fun getAppComponent(): AppComponent {  
  21.         return EntryPoints.get(  
  22.             this,  
  23.             AuthComponentEntryPoint::class.java  
  24.         )  
  25.             .appComponentBuilder()  
  26.             .build()  
  27.     }  
  28.   
  29.     @EntryPoint  
  30.     @InstallIn(AuthComponent::class)  
  31.     interface AuthComponentEntryPoint {  
  32.         fun appComponentBuilder(): AppComponent.Builder  
  33.     }  
  34. }  
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() メソッドを呼ぶように変更します。
  1.  @HiltAndroidApp  
  2.  class MyApplication : Application() {  
  3.    
  4. -    private var appComponent: AppComponent? = null  
  5. +    @Inject  
  6. +    lateinit var authComponentRegistry: AuthComponentRegistry  
  7.    
  8.      fun getAppComponent(): AppComponent {  
  9. -        return appComponent ?: DaggerAppComponent.builder()  
  10. -            .build()  
  11. -            .also {  
  12. -                appComponent = it  
  13. -            }  
  14. +        return authComponentRegistry.getAppComponent()  
  15.      }  
  16.    
  17.      fun reset() {  
  18. -        appComponent = null  
  19. +        authComponentRegistry.reset()  


これで AppComponent が Hilt で管理されるようになりました。

reset ボタンが押されるまでは NeedResetRepository のインスタンスが保持され、reset ボタンを押すと NeedResetRepository のインスタンスが新しくなるという以前の挙動を保っています。



ステップ3 : Module 分割


ここまでくれば MyApi を別 Module に分割できます。
  1.  @InstallIn(AuthComponent::class)  
  2.  @Module  
  3.  object AppModule {  
  4.   
  5.      @AuthScope  
  6.      @Provides    
  7.      fun provideNeedResetRepository(myApi: MyApi): NeedResetRepository {  
  8.          return NeedResetRepository(myApi)  
  9.      }  
  10. +}  
  11.    
  12. -    @AuthScope  
  13. +@InstallIn(SingletonComponent::class)  
  14. +@Module  
  15. +object AppModule2 {  
  16. +  
  17. +    @Singleton  
  18.      @Provides  
  19.      fun provideMyApi(): MyApi {  
  20.          return MyApi()  
  21.      }  
  22.  }  
AppModule2 には @InstallIn(SingletonComponent::class) をつけ、provideMyApi() の scope を @AuthScope から @Singleton に変更します。 reset ボタンが押されても MyApi のインスタンスが保持されるように変わります。



ステップ4 : @HiltViewModel


この段階で、AuthComponent に依存しないクラスだけを引数にとる ViewModel なら @HiltViewModel を使えるようになります。
  1. @HiltViewModel  
  2. class SomeViewModel @Inject constructor(  
  3.   prival val myApi: MyApi  
  4. ) : ViewModel()  
  1. @AndroidEntryPoint  
  2. class SomeActivity : ComponentActivity() {  
  3.   
  4.     private val viewModel by viewModels<SomeViewModel>()  
  5.   
  6. }  

AuthComponent に依存するクラスを引数にとるときは AssistedInject を利用します。
https://dagger.dev/hilt/view-model#assisted-injection
  1. @HiltViewModel(assistedFactory = SomeViewModel.Factory::class)  
  2. class SomeViewModel @AssistedInject constructor(  
  3.     @Assisted private val needResetRepository: NeedResetRepository  
  4. ) : ViewModel() {  
  5.   
  6.     @AssistedFactory  
  7.     interface Factory {  
  8.         fun create(  
  9.             needResetRepository: NeedResetRepository  
  10.         ): SomeViewModel  
  11.     }  
  12. }  
  1. @AndroidEntryPoint  
  2. class SomeActivity : ComponentActivity() {  
  3.   
  4.     private val viewModel by viewModels<SomeViewModel>(  
  5.         extrasProducer = {  
  6.             defaultViewModelCreationExtras.withCreationCallback<SomeViewModel.Factory> { factory ->  
  7.                 factory.create(  
  8.                     authComponentEntryPoint().needResetRepository()  
  9.                 )  
  10.             }  
  11.         }  
  12.     )  
  13. }  
Context から AuthComponent に依存するクラスのインスタンスを取れるように便利メソッドを用意しておきます。
  1. @EntryPoint  
  2. @InstallIn(AuthComponent::class)  
  3. interface AuthComponentEntryPoint {  
  4.   
  5.     fun needResetRepository(): NeedResetRepository  
  6. }  
  7.   
  8. @InstallIn(SingletonComponent::class)  
  9. @EntryPoint  
  10. interface SingletonComponentEntryPoint {  
  11.     fun authComponentRegistry(): AuthComponentRegistry  
  12. }  
  13.   
  14. fun Context.authComponentEntryPoint(): AuthComponentEntryPoint {  
  15.     val authComponentRegistry = EntryPointAccessors  
  16.         .fromApplication<SingletonComponentEntryPoint>(this)  
  17.         .authComponentRegistry()  
  18.   
  19.     return EntryPoints.get(authComponentRegistry, AuthComponentEntryPoint::class.java)  
  20. }  



ステップ4 : AppComponent の inject() メソッド廃止


上で用意した便利メソッドを使って MainActivity の needResetRepository にインスタンスをセットするように変えます。

MyApi は @InstallIn(SingletonComponent::class) なので MainActivity に @AndroidEntryPoint をつければ inject されます。
  1. +@AndroidEntryPoint  
  2.  class MainActivity : ComponentActivity() {  
  3.    
  4. -    @Inject  
  5. -    lateinit var needResetRepository: NeedResetRepository  
  6. +    private lateinit var needResetRepository: NeedResetRepository  
  7.    
  8.      @Inject  
  9.      lateinit var myApi: MyApi  
  10.   
  11.      override fun onCreate(savedInstanceState: Bundle?) {  
  12.          super.onCreate(savedInstanceState)  
  13.    
  14. -        val appComponent = (application as MyApplication).getAppComponent()  
  15. -        appComponent.inject(this)  
  16. +        needResetRepository = authComponentEntryPoint().needResetRepository()  
  17.    
  18.          enableEdgeToEdge()  
  1.  interface AppComponent {  
  2.      interface Builder {  
  3.          fun build(): AppComponent  
  4.      }  
  5.   
  6. -    fun inject(app: MainActivity)  
  7.  }  
このように AppComponent の inject() メソッドを段階的に廃止していきます。



ステップ5 : AppComponent の廃止


AppComponent に定義されているメソッドがなくなったら AppComponent 自体を廃止します。
  1. -@Subcomponent  
  2. -interface AppComponent {  
  3. -  
  4. -    @Subcomponent.Builder  
  5. -    interface Builder {  
  6. -        fun build(): AppComponent  
  7. -    }  
  8. -}  
  1. -@InstallIn(AuthComponent::class)  
  2. -@Module(  
  3. -    subcomponents = [  
  4. -        AppComponent::class,  
  5. -    ]  
  6. -)  
  7. -interface AuthModule  
  1.  @Singleton  
  2.  class AuthComponentRegistry @Inject constructor(  
  3.      private val authComponentBuilder: AuthComponent.Builder,  
  4.  ) : GeneratedComponentManager<AuthComponent> {    
  5.     
  6.      ...  
  7.     
  8.      override fun generatedComponent(): AuthComponent {  
  9.          return authComponent  
  10.      }  
  11. -  
  12. -    fun getAppComponent(): AppComponent {  
  13. -        return EntryPoints.get(  
  14. -            this,  
  15. -            AuthComponentEntryPoint::class.java  
  16. -        )  
  17. -            .appComponentBuilder()  
  18. -            .build()  
  19. -    }  
  20. -  
  21. -    @EntryPoint  
  22. -    @InstallIn(AuthComponent::class)  
  23. -    interface AuthComponentEntryPoint {  
  24. -        fun appComponentBuilder(): AppComponent.Builder  
  25. -    }  
  26.  }  
  1.  @HiltAndroidApp  
  2.  class MyApplication : Application() {  
  3.   
  4.      @Inject  
  5.      lateinit var authComponentRegistry: AuthComponentRegistry  
  6.    
  7. -    fun getAppComponent(): AppComponent {  
  8. -        return authComponentRegistry.getAppComponent()  
  9. -    }  
  10. -  
  11.      fun reset() {  



これで Hilt への移行完了です!


2024年11月14日木曜日

M3 の LinearProgressIndicator の progress は lambda になっているが、使い方に注意しないと recomposition が走ることがある

M2 の LinearProgressIndicator の progress 引数は Float でしたが、M3 では () -> Float になっています。
  1. androidx.compose.material.LinearProgressIndicator(  
  2.     progress = 0.5f,  
  3. )  
  4.   
  5. androidx.compose.material3.LinearProgressIndicator(  
  6.     progress = { 0.5f },  
  7. )  
いずれも内部の実装は Canvas composable を使って描画しています。
そのため、progress を lambda にすることで Composition と Layout phase をスキップして Drawing phase だけやり直せばよくなり、その分パフォーマンスが良くなります。
https://developer.android.com/develop/ui/compose/phases

実際以下のコードを実行して Layout Inspector で recomposition の回数を見ると、M2 の方は recompositoin されていますが M3 の方は skip されています。
  1. Column(  
  2.     verticalArrangement = Arrangement.spacedBy(16.dp),  
  3.     modifier = Modifier.fillMaxSize().padding(16.dp),  
  4. ) {  
  5.     var progress by remember { mutableFloatStateOf(0f) }  
  6.   
  7.     androidx.compose.material3.Button(  
  8.         onClick = { progress = Random.nextFloat() },  
  9.     ) {  
  10.         Text("update progress")  
  11.     }  
  12.   
  13.     androidx.compose.material.LinearProgressIndicator(  
  14.         progress = progress,  
  15.         modifier = Modifier.fillMaxWidth(),  
  16.     )  
  17.   
  18.     androidx.compose.material3.LinearProgressIndicator(  
  19.         progress = { progress },  
  20.         modifier = Modifier.fillMaxWidth(),  
  21.     )  
  22. }  



M3 の LinearProgressIndicator を wrap するときは、wrap する component でも progress を lambda で取るように注意してください(より正確に言うと、lamda の中で state から読み出しを行うようにするということ)。そうしないと M3 の LinearProgressIndicator を使っていても recompose が走ります。
  1. Column(  
  2.     verticalArrangement = Arrangement.spacedBy(16.dp),  
  3.     modifier = Modifier.fillMaxSize().padding(16.dp),  
  4. ) {  
  5.     var progress by remember { mutableFloatStateOf(0f) }  
  6.   
  7.     androidx.compose.material3.Button(  
  8.         onClick = { progress = Random.nextFloat() },  
  9.     ) {  
  10.         Text("update progress")  
  11.     }  
  12.   
  13.     LinearProgressIndicatorM2(progress)  
  14.   
  15.     LinearProgressIndicatorM3_Bad(progress)  
  16.   
  17.     LinearProgressIndicatorM3_Good({ progress })  
  18. }  
  1. @Composable  
  2. private fun LinearProgressIndicatorM2(progress: Float) {  
  3.     androidx.compose.material.LinearProgressIndicator(  
  4.         progress = progress,  
  5.         modifier = Modifier.fillMaxWidth(),  
  6.     )  
  7. }  
  8.   
  9. @Composable  
  10. private fun LinearProgressIndicatorM3_Bad(progress: Float) {  
  11.     androidx.compose.material3.LinearProgressIndicator(  
  12.         progress = { progress },  
  13.         modifier = Modifier.fillMaxWidth(),  
  14.     )  
  15. }  
  16.   
  17. @Composable  
  18. private fun LinearProgressIndicatorM3_Good(progress: () -> Float) {  
  19.     androidx.compose.material3.LinearProgressIndicator(  
  20.         progress = progress,  
  21.         modifier = Modifier.fillMaxWidth(),  
  22.     )  
  23. }  



そうは言っても、階層のどこかで progress が読み出されていることもあるでしょう(Text Composable で progress の値を表示しているとか)。その場合は rememberUpdatedState を使うことで LinearProgressIndicator の recomposition を skip させることができます。
  1. @Composable  
  2. private fun Wrap(progress: Float, onUpdate: () -> Unit) {  
  3.     val updatedProgress by rememberUpdatedState(progress)  
  4.     Column(  
  5.         verticalArrangement = Arrangement.spacedBy(16.dp),  
  6.         modifier = Modifier.fillMaxSize().padding(16.dp),  
  7.     ) {  
  8.   
  9.         ...  
  10.   
  11.         LinearProgressIndicatorM3_Good({ updatedProgress })  
  12.     }  
  13. }