- androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidxCore" }
+ androidx-core = { module = "androidx.core:core", version.ref = "androidxCore" }
2026年7月6日月曜日
androidx の core-ktx の内容は core に含まれるようになった
core 1.19.0-alpha01 から core-ktx API は core library に統合されて、すべての Kotlin 拡張機能はメインの core artifact の一部になりました。
互換性のために core-ktx は空のアーティファクトとして残されていますが、空のアーティファクトを含める意味もないので早めに core に変更しておきましょう。
2026年6月8日月曜日
onSubscription で load を開始するとはまる問題
class ItemListViewModel(...) : ViewModel() {
val uiState: StateFlow<UiState> = baseFlow
.onSubscription {
loadInitialPage()
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = UiState.Initial
)
このコードで取得した ItemList を ItemListScreen で表示している。ItemList にはページングがあり、loadInitialPage() は初期ページを読み込む処理である。
この画面から別の画面、例えば ItemDetailScreen に行って戻ってくる場合、5秒以上経ってから戻ってくると onSubscription { } が再度呼ばれる。そのため、スクロールして2ページ目以降を読み込んで表示している場合、5秒以上経ってから戻ってくると1ページ目に戻されてしまう。
val uiState: StateFlow<UiState> = baseFlow
.onSubscription {
if (uiState.value == UiState.Initial) {
loadInitialPage()
}
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = UiState.Initial
)
このように状態チェックを入れればいいのだが、それなら
init {
loadInitialPage()
}
でいいじゃん。と思うのである。私は init 派である。
(実際これで問題になっていたコードを直したので、実装時に気をつけることが少ないほうがいいと思うんだ)
2025年12月4日木曜日
adk-java を Android アプリに組み込むための長い道のり
この記事は JP_Google Developer Experts Advent Calendar 2025 の4日目の記事です。
adk-java を Android アプリに入れるのなんて簡単でしょ。 https://google.github.io/adk-docs/#java に書いてある通りに build.gradle.kts に
Android Studio の [New Project...] からまっさらなプロジェクトを作って(もちろんこの時点ではビルドできるしアプリも実行できる)、上の依存を一行追加して Sync してビルドすると....
はい、エラー...
Duplicate class io.modelcontextprotocol.json.McpJsonInternal found in modules mcp-core-0.14.0.jar -> mcp-core-0.14.0 (io.modelcontextprotocol.sdk:mcp-core:0.14.0) and mcp-json-0.14.0.jar -> mcp-json-0.14.0 (io.modelcontextprotocol.sdk:mcp-json:0.14.0) ...
同じクラスファイル(io.modelcontextprotocol.json.McpJsonInternal)が異なる2つのライブラリ/モジュール(mcp-coreとmcp-json)から重複して含まれてますね...
ということで片方を exclude します。全部で3のライブラリで重複があるようです。
これでうまくいくかと思いきや、またしてもエラー
Invalid build configuration. Attempt to create a global synthetic for 'Record desugaring' without a global-synthetics consumer.
adk-java の中で Java 16 以降で導入された Record クラスを使用しているが、それを古い Android バージョンでも動作させるための処理(Desugaring: デシュガーリング)が設定されてないと言われていますね...
一番簡単な解決策はプロジェクトの Javaの互換性バージョンを Java 17 に統一することなのでそうします。
今度こそビルド通るかと思いきや、またしてもエラー
MethodHandle.invoke and MethodHandle.invokeExact are only supported starting with Android O (--min-api 26): Lautovalue/shaded/com/google/common/hash/ChecksumHashFunction$ChecksumMethodHandles;->updateByteBuffer(Ljava/util/zip/Checksum;Ljava/nio/ByteBuffer;)Z
MethodHandle.invoke と MethodHandle.invokeExact は Android O 以降じゃないとサポートしてないと言われています...
ということで minSdk バージョンを 26 にします。
4度目...さすがにビルド通ってほしいが、まだエラー
Execution failed for task ':app:mergeDebugJavaResource'.
> A failure occurred while executing com.android.build.gradle.internal.tasks.MergeJavaResWorkAction
> 2 files found with path 'mozilla/public-suffix-list.txt' from inputs:
- org.apache.httpcomponents:httpclient:4.5.14/httpclient-4.5.14.jar
- org.apache.httpcomponents.client5:httpclient5:5.3.1/httpclient5-5.3.1.jar
異なる2つのライブラリに、全く同じパスとファイル名を持つリソースファイルが存在するという典型的なエラーです。
競合しているファイル(このエラーだと mozilla/public-suffix-list.txt)をビルドに含めないように設定します。
一つ対応しても同じエラーが別のファイルに対して何回も出てくるので、全部 excludes に追加します。
これでようやくビルドが通りました!エラーが多い...
ようやくビルドが通ったので Quickstart (https://google.github.io/adk-docs/get-started/java/) の HelloTimeAgent を作ってみました。
が、これもハードルが高かった〜
まず困ったのが API key の渡し方。公式ドキュメントには
echo 'export GOOGLE_API_KEY="YOUR_API_KEY"' > .env
しろとしか書いてない。しかし Android は環境変数は使えません。
そこで、API key がセットされなくてクラッシュするところから呼び出し元をたどっていくと、LlmRegistry にデフォルト登録される LlmFactory を利用していることがわかりました。
Tool はリフレクションで呼ばれていて、Java の static メソッドになっている必要がありました。
Caused by: java.lang.IllegalArgumentException: Static method getCurrentTime not found in class com.example.myapplication.HelloTimeAgent.
Kotlin の object のメソッドを Java の static メソッドにするには @JvmStatic をつけます。
以下のように時刻の文字列だけ返せばいいかなと思ったのですが、これではうまく行きませんでした。
うごいたーーー
全体のコードはこんな感じです。
adk-java ライブラリをアプリに追加する
adk-java を Android アプリに入れるのなんて簡単でしょ。 https://google.github.io/adk-docs/#java に書いてある通りに build.gradle.kts に
dependencies {
implementation("com.google.adk:google-adk:0.4.0")
}
を書けばいいんでしょ。
そう思っていました....
Android Studio の [New Project...] からまっさらなプロジェクトを作って(もちろんこの時点ではビルドできるしアプリも実行できる)、上の依存を一行追加して Sync してビルドすると....
はい、エラー...
Duplicate class io.modelcontextprotocol.json.McpJsonInternal found in modules mcp-core-0.14.0.jar -> mcp-core-0.14.0 (io.modelcontextprotocol.sdk:mcp-core:0.14.0) and mcp-json-0.14.0.jar -> mcp-json-0.14.0 (io.modelcontextprotocol.sdk:mcp-json:0.14.0) ...
同じクラスファイル(io.modelcontextprotocol.json.McpJsonInternal)が異なる2つのライブラリ/モジュール(mcp-coreとmcp-json)から重複して含まれてますね...
ということで片方を exclude します。全部で3のライブラリで重複があるようです。
dependencies {
implementation("com.google.adk:google-adk:0.4.0") {
exclude(group = "io.modelcontextprotocol.sdk", module = "mcp-json")
exclude(group = "javax.annotation", module = "javax.annotation-api")
exclude(group = "org.slf4j", module = "jcl-over-slf4j")
}
...
}
これでうまくいくかと思いきや、またしてもエラー
Invalid build configuration. Attempt to create a global synthetic for 'Record desugaring' without a global-synthetics consumer.
adk-java の中で Java 16 以降で導入された Record クラスを使用しているが、それを古い Android バージョンでも動作させるための処理(Desugaring: デシュガーリング)が設定されてないと言われていますね...
一番簡単な解決策はプロジェクトの Javaの互換性バージョンを Java 17 に統一することなのでそうします。
kotlin {
jvmToolchain(17)
}
android {
...
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
今度こそビルド通るかと思いきや、またしてもエラー
MethodHandle.invoke and MethodHandle.invokeExact are only supported starting with Android O (--min-api 26): Lautovalue/shaded/com/google/common/hash/ChecksumHashFunction$ChecksumMethodHandles;->updateByteBuffer(Ljava/util/zip/Checksum;Ljava/nio/ByteBuffer;)Z
MethodHandle.invoke と MethodHandle.invokeExact は Android O 以降じゃないとサポートしてないと言われています...
ということで minSdk バージョンを 26 にします。
android {
...
defaultConfig {
...
minSdk = 26
...
}
...
}
4度目...さすがにビルド通ってほしいが、まだエラー
Execution failed for task ':app:mergeDebugJavaResource'.
> A failure occurred while executing com.android.build.gradle.internal.tasks.MergeJavaResWorkAction
> 2 files found with path 'mozilla/public-suffix-list.txt' from inputs:
- org.apache.httpcomponents:httpclient:4.5.14/httpclient-4.5.14.jar
- org.apache.httpcomponents.client5:httpclient5:5.3.1/httpclient5-5.3.1.jar
異なる2つのライブラリに、全く同じパスとファイル名を持つリソースファイルが存在するという典型的なエラーです。
競合しているファイル(このエラーだと mozilla/public-suffix-list.txt)をビルドに含めないように設定します。
一つ対応しても同じエラーが別のファイルに対して何回も出てくるので、全部 excludes に追加します。
android {
...
packaging {
resources {
excludes += "mozilla/public-suffix-list.txt"
excludes += "META-INF/DEPENDENCIES"
excludes += "META-INF/LICENSE.md"
excludes += "META-INF/NOTICE.md"
excludes += "META-INF/io.netty.versions.properties"
excludes += "META-INF/INDEX.LIST"
}
}
}
これでようやくビルドが通りました!エラーが多い...
都市の時間を返すエージェントを作ってみる
ようやくビルドが通ったので Quickstart (https://google.github.io/adk-docs/get-started/java/) の HelloTimeAgent を作ってみました。
が、これもハードルが高かった〜
API Key どう渡す?
まず困ったのが API key の渡し方。公式ドキュメントには
echo 'export GOOGLE_API_KEY="YOUR_API_KEY"' > .env
しろとしか書いてない。しかし Android は環境変数は使えません。
そこで、API key がセットされなくてクラッシュするところから呼び出し元をたどっていくと、LlmRegistry にデフォルト登録される LlmFactory を利用していることがわかりました。
public final class LlmRegistry {
...
/** Map of model name patterns regex to factories. */
private static final Map<String, LlmFactory> llmFactories = new ConcurrentHashMap<>();
/** Registers default LLM factories, e.g. for Gemini models. */
static {
registerLlm("gemini-.*", modelName -> Gemini.builder().modelName(modelName).build());
registerLlm("apigee/.*", modelName -> ApigeeLlm.builder().modelName(modelName).build());
}
この LlmRegistry に登録されている "gemini-.*" に対応する LlmFactory を、API key をセットした Gemini に置き換えればいけそうだとわかりました。
object HelloTimeAgent {
init {
LlmRegistry.registerLlm("gemini-.*") { modelName ->
Gemini.builder()
.modelName(modelName)
.apiKey("MY API KEY HERE")
.build()
}
}
...
}
これで API key がセットされてなくてクラッシュすることはなくなりました。
tool 名の statec method がないと言われる
Tool はリフレクションで呼ばれていて、Java の static メソッドになっている必要がありました。
public class FunctionTool extends BaseTool {
...
public static FunctionTool create(Class<?> cls, String methodName, boolean requireConfirmation) {
for (Method method : cls.getMethods()) {
if (method.getName().equals(methodName) && Modifier.isStatic(method.getModifiers())) {
return create(null, method, requireConfirmation);
}
}
throw new IllegalArgumentException(
String.format("Static method %s not found in class %s.", methodName, cls.getName()));
}
Java の static メソッドになっていないとこんなエラーでクラッシュします。
Caused by: java.lang.IllegalArgumentException: Static method getCurrentTime not found in class com.example.myapplication.HelloTimeAgent.
Kotlin の object のメソッドを Java の static メソッドにするには @JvmStatic をつけます。
object HelloTimeAgent {
...
@Annotations.Schema(description = "Get the current time for a given city")
@JvmStatic
fun getCurrentTime(
@Annotations.Schema(
name = "timeZone",
description = "timeZone of the city to get the time for"
) timeZone: String
): ...
...
}
tool の戻り値は Map じゃないとだめ?
以下のように時刻の文字列だけ返せばいいかなと思ったのですが、これではうまく行きませんでした。
object HelloTimeAgent {
...
@Annotations.Schema(description = "Get the current time for a given city")
@JvmStatic
fun getCurrentTime(
...
): String {
val zoneId = ZoneId.of(timeZone)
val zonedDateTime = ZonedDateTime.now(zoneId)
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
return zonedDateTime.format(formatter)
}
}
このように Map で返すとうまくいきました。ちなみに timeZone は Map に含めなくても問題ありませんでした。
object HelloTimeAgent {
...
@Annotations.Schema(description = "Get the current time for a given city")
@JvmStatic
fun getCurrentTime(
...
): Map<String, String> {
val zoneId = ZoneId.of(timeZone)
val zonedDateTime = ZonedDateTime.now(zoneId)
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
return mapOf(
"timeZone" to timeZone, // なくてもよい
"forecast" to zonedDateTime.format(formatter)
)
}
}
うごいたーーー
全体
全体のコードはこんな感じです。
class MainActivity : ComponentActivity() {
private val viewModel by viewModels<MainViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MyApplicationTheme {
ChatScreen(
messages = viewModel.messages,
loading = viewModel.loading,
onClickSubmit = { viewModel.submit(it) }
)
}
}
}
}
@Composable
fun ChatScreen(
messages: List<MainViewModel.Message>,
loading: Boolean,
onClickSubmit: (String) -> Unit,
) {
Scaffold { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
) {
LazyColumn(modifier = Modifier.weight(1f)) {
items(messages) {
when (it.from) {
MainViewModel.From.User -> {
Row {
Spacer(Modifier.weight(1f))
Box(
Modifier
.weight(4f)
.padding(8.dp)
.background(
MaterialTheme.colorScheme.surfaceVariant,
RoundedCornerShape(8.dp)
)
.padding(8.dp)
) {
Text(text = it.text)
}
}
}
MainViewModel.From.Agent -> {
Row {
Box(
Modifier
.weight(4f)
.padding(8.dp)
.background(
MaterialTheme.colorScheme.surfaceVariant,
RoundedCornerShape(8.dp)
)
.padding(8.dp)
) {
Text(text = it.text)
}
Spacer(Modifier.weight(1f))
}
}
}
}
if (loading) {
item("loading") {
CircularProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.wrapContentWidth()
)
}
}
}
Divider()
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(8.dp)
) {
val state = rememberTextFieldState()
TextField(
state = state,
lineLimits = TextFieldLineLimits.SingleLine,
enabled = !loading,
modifier = Modifier.weight(1f),
)
Button(
onClick = {
onClickSubmit(state.text.toString())
state.clearText()
},
enabled = !loading,
) {
Text(text = "送信")
}
}
}
}
}
@Preview(showBackground = true)
@Composable
private fun Preview() {
MyApplicationTheme {
ChatScreen(
messages = listOf(
MainViewModel.Message(from = MainViewModel.From.User, text = "東京の時間は?"),
MainViewModel.Message(from = MainViewModel.From.Agent, text = "9時です"),
),
loading = false,
onClickSubmit = {}
)
}
}
class MainViewModel() : ViewModel() {
private val runConfig = RunConfig.builder().build()
private val runner = InMemoryRunner(HelloTimeAgent.initAgent())
private val session: Session = runner
.sessionService()
.createSession(runner.appName(), "user1234")
.blockingGet()
var messages = mutableStateListOf<Message>()
var loading by mutableStateOf(false)
fun submit(userInput: String) {
if (loading) {
return
}
messages.add(Message(from = From.User, text = userInput))
loading = true
viewModelScope.launch {
val event = withContext(Dispatchers.IO) {
val userMsg = Content.fromParts(Part.fromText(userInput))
val events = runner.runAsync(session.userId(), session.id(), userMsg, runConfig)
events.filter { it.finalResponse() }.blockingFirst()
}
messages.add(Message(from = From.Agent, text = event.stringifyContent()))
loading = false
}
}
data class Message(val from: From, val text: String)
enum class From {
User,
Agent
}
}
object HelloTimeAgent {
init {
LlmRegistry.registerLlm("gemini-.*") { modelName ->
Gemini.builder()
.modelName(modelName)
.apiKey("API KEY HERE")
.build()
}
}
fun initAgent(): BaseAgent? {
return LlmAgent.builder()
.name("hello-time-agent")
.description("Tells the current time in a specified city")
.instruction(
"""
You are a helpful assistant that tells the current time in a city.
Use the 'getCurrentTime' tool for this purpose.
"""
)
.model("gemini-2.5-flash")
.tools(FunctionTool.create(HelloTimeAgent::class.java, "getCurrentTime"))
.build();
}
@Annotations.Schema(description = "Get the current time for a given city")
@JvmStatic
fun getCurrentTime(
@Annotations.Schema(
name = "timeZone",
description = "timeZone of the city to get the time for"
) timeZone: String
): Map<String, String> {
val zoneId = ZoneId.of(timeZone)
val zonedDateTime = ZonedDateTime.now(zoneId)
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
return mapOf(
"timeZone" to timeZone,
"forecast" to zonedDateTime.format(formatter)
)
}
}
2024年12月9日月曜日
骨髄ドナーとして骨髄採取の手術をしました
本記事は mhidaka が建立した Advent Calendar 2024 Vol.1 の 9日目です(Vol. 2 はこちら)。
2024年のとある時期にとある病院で骨髄採取の手術をしました(非血縁者間骨髄移植です)(時期とか病院とかをはっきり書いてはいけないのです)。
なかなか馴染みのないことだと思うので、時系列で起こったことを紹介します。
秋葉原の献血ルームに献血しに行った時に、そこに骨髄バンクの人がいて「今日採血した血で登録できます」って言われたのでじゃあ登録するかって登録しました。
それまで何度も献血してるけど、その時初めて骨髄バンクの人に会ったのでもっといろんなところでやればいいのにって思いました。
日本骨髄バンクから「あなたと患者さんのHLA型が一致し、ドナー候補者に選ばれました。」という SMS が届きました。ちょうど旅行中だったのでよく覚えています。
SMS の中の URL から問診票に回答します。その際面談希望の病院を選択するんですけど、一覧に病院の名前しか載ってなくて一個一個 google map で検索してどこが行きやすいか調べないといけなかったので、ぜひマップで一覧できるようにして欲しいです。
日本骨髄バンクから担当のコーディネーターさんのお知らせが郵送で届き、その後コーディネーターさんから SMS で連絡が来ました。ここからは担当のコーディネーターさんとずっとやりとりします。
まず確認検査の日程調整をします。候補の病院は複数あげてたのですが病院側の都合とこちらの日程が合わなかったりで決まるまで1週間くらいかかってました。
ドナーのためのハンドブックに沿って一通り説明を受けました。注射器で骨髄を採取する方法しかないと思ってたんですけど、今は末梢血幹細胞採取という方法もあるということを知りました。
ドナーになるための健康状態を満たしているかの血液検査が必要なので、そのための採血もしました。全部で2時間くらいです。
1週間後くらいの血液検査の結果が郵送されてきました。もしここで健康状態が基準を満たしていない場合コーディネート終了になります。
家族が同席して、改めてドナーのためのハンドブックに沿って一通り説明を受け、同意書を作成します。このとき第三者の立会人もいます。採血がないので1時間半くらいです。
骨髄採取と末梢血幹細胞採取で違いがいろいろあり、こちらの方法は承諾できないと言えばその採取方法にはなりません。私は説明を聞いた上でどっちにもそれなりのメリデメがありどっちでもいいなとなったので両方OKと伝えました。ドナーがどちらでもいいよとなった場合患者側がどちらの採取方法にするか決めます。私の場合は骨髄採取になりました。
私の都合で最終同意面談から手術まで最短日程で行くことになり、最終同意面談の次の日に健康診断を受けました。
健康診断は骨髄採取手術を受ける病院で受けます。
手術のときに骨髄を900ml(私の場合)採取するので貧血になってしまいます。そこで自分の血をあらかじめ取っておいて(自己血貯血)、手術時に体に戻します。
そのための自己血を2回に分けて採取します。最初にヘモグロビン濃度をチェックする採血をして、400ml貯血しました。
このとき麻酔科にも行って全身麻酔の説明を受けました。あと、入院の手続きもしました。
ヘモグロビン濃度をチェックする採血をして、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本採血。朝ごはんのあと院内をちょっと散歩しました。
歩くのは全然問題なかったのですが、ちょっとくらっとしたので早めに戻りました。
担当医が来て傷口のチェックをして絆創膏を張り替えました。
シャワー入れるか聞いたら、絆創膏の上からサランラップみたいなぴっちりしたシートを貼ってくれました。
担当医が来てヘモグロビンが10切ってるから鉄剤出しますね、って言われました。
昼ごはんの後16時半にシャワー、17時すぎくらいに晩御飯、19時くらいに担当医が来て絆創膏を張り替え、21時消灯
この日は採血なし。
めまいが酷くて、ベットで起き上がったらそのままふらーっと倒れてしまって笑った。貧血で倒れるひとってこんな感じなのか。
7時すぎ朝ごはん、晩御飯が17時すぎくらいなのに朝ごはん遅いんですよねー...。
昨日よりもめまいが酷くてごろごろしてました。
9時すぎに退院の手続きをしに行ったのですが、傷口の痛みは我慢できて普通に歩けるけど、めまいのほうがこわい。移動はゆっくり。
病室に戻って水分不足かもなーと思いスポドリを買いに行った帰りに担当に会って少し話
その後担当医が病室に来て傷口チェック。
10時少し前に予定通り退院しました。
まだめまいがある。昨日よりはましな感じだけど、ほぼ1日ごろごろしてました。
傷口に貼っていた絆創膏のテープの部分がすこし荒れて痒くなってきた。
昨日よりはましになったけど、まだ血が足りてない感がありました。
傷部分はかなり治ってきているのか押してもそこまで痛くなくなりました。
退院時に処方された薬
手術した病院で検診。体重とか血圧とかの他は検尿と採血のみ。ヘモグロビン濃度も正常値に戻って問題なしでした。
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 のみの構成があります。
ログアウト時に NeedResetRepository で保持しているキャッシュを削除したいので、MyApplication で保持している AppComponent のインスタンスを null にしてから MainActivity を作り直しています(reset() メソッドのところ)。
(Hilt がない時代の標準的なやり方だと AppComponent のインスタンスは lateinit var にして onCreate() で代入し、作り直すことがないようにすることが多いのですが、それだと Hilt への移行で困ることがないので、今回は困るパターンということでこのような構成例になっています)
AppComponent と NeedResetRepository には @Singleton がついているので、AppComponent が作り直されるまで NeedResetRepository のインスタンスは変わりません。そのため例えば画面回転時には NeedResetRepository のインスタンスは変わりません。
libs.versions.toml
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 をつけてエラーがでないようにします。
AppModule を分割して、リセットする必要のない MyApi は @InstallIn(SingletonComponent::class) をつけた Module で管理するようにしたいのですが、
[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 を用意します
2. 任意のタイミングで AppComponent を作り直せるように、AppComponent の親 Component を用意します。ログアウト時にリセットすることを想定して、ここでは AuthComponent という名前にしています。
AuthComponent では SingletonComponent に InstallIn されている型も見えるようにしたいので、parent に SingletonComponent を指定します。
AuthComponent に対応する Scope も用意します。ここでは AuthScope という名前にしています。AuthComponent に @AuthScope をつけます。
3. AppModule の @DisableInstallInCheck を @InstallIn(AuthComponent::class) に変更し、@Singleton を @AuthScope に変更します。
4. 任意のタイミングで AppComponent を作り直すために、AppComponent の親である AuthComponent を管理する GeneratedComponentManager を用意します。
AppComponent.Builder は AuthModule で @InstallIn(AuthComponent::class) されているので、同じように @InstallIn(AuthComponent::class) をつけた EntryPoint を用意することで取得できます。
この EntryPoint のインスタンスは、EntryPoints.get() に AuthComponentRegistry インスタンスを渡すことで取得できます。
5. MyApplication で保持している appComponent を削除して、getAppComponent() では AuthComponentRegistry から取得したインスタンスを返すようにします。reset() メソッドでは AuthComponentRegistry の reset() メソッドを呼ぶように変更します。
これで AppComponent が Hilt で管理されるようになりました。
reset ボタンが押されるまでは NeedResetRepository のインスタンスが保持され、reset ボタンを押すと NeedResetRepository のインスタンスが新しくなるという以前の挙動を保っています。
ここまでくれば MyApi を別 Module に分割できます。
この段階で、AuthComponent に依存しないクラスだけを引数にとる ViewModel なら @HiltViewModel を使えるようになります。
AuthComponent に依存するクラスを引数にとるときは AssistedInject を利用します。
https://dagger.dev/hilt/view-model#assisted-injection
上で用意した便利メソッドを使って MainActivity の needResetRepository にインスタンスをセットするように変えます。
MyApi は @InstallIn(SingletonComponent::class) なので MainActivity に @AndroidEntryPoint をつければ inject されます。
AppComponent に定義されているメソッドがなくなったら AppComponent 自体を廃止します。
これで Hilt への移行完了です!
@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 への移行完了です!
2024年11月14日木曜日
M3 の LinearProgressIndicator の progress は lambda になっているが、使い方に注意しないと recomposition が走ることがある
M2 の LinearProgressIndicator の progress 引数は Float でしたが、M3 では () -> Float になっています。
そのため、progress を lambda にすることで Composition と Layout phase をスキップして Drawing phase だけやり直せばよくなり、その分パフォーマンスが良くなります。
(https://developer.android.com/develop/ui/compose/phases)
実際以下のコードを実行して Layout Inspector で recomposition の回数を見ると、M2 の方は recompositoin されていますが M3 の方は skip されています。
M3 の LinearProgressIndicator を wrap するときは、wrap する component でも progress を lambda で取るように注意してください(より正確に言うと、lamda の中で state から読み出しを行うようにするということ)。そうしないと M3 の LinearProgressIndicator を使っていても recompose が走ります。
そうは言っても、階層のどこかで progress が読み出されていることもあるでしょう(Text Composable で progress の値を表示しているとか)。その場合は rememberUpdatedState を使うことで LinearProgressIndicator の recomposition を skip させることができます。
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 })
}
}
2024年11月9日土曜日
LazyRow で snap させる
SnapLayoutInfoProvider での snap 位置の指定方法が変わっていた。
左端に snap する場合、以前は
任意の位置に snap したい場合は SnapPosition の実装を用意すればよい。
左端に snap する場合、以前は
SnapLayoutInfoProvider(
lazyListState = state,
positionInLayout = { layoutSize, itemSize, beforeContentPadding, afterContentPadding, _->
0 // 左端
},
)
だったが、SnapPosition という interface が用意され、 左端の場合は用意されている SnapPosition.Start を指定すれば良くなった。
Start の他に End と Center も用意されている。
任意の位置に snap したい場合は SnapPosition の実装を用意すればよい。
SnapLayoutInfoProvider(
lazyListState = state,
snapPosition = SnapPosition.Start,
)
全体のコードはこんな感じ。
@Composable
fun LazyRowSnapSample() {
val state = rememberLazyListState()
val snappingLayout = remember(state) {
SnapLayoutInfoProvider(
lazyListState = state,
snapPosition = SnapPosition.Start,
)
}
val flingBehavior = rememberSnapFlingBehavior(snappingLayout)
LazyRow(
modifier = Modifier.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically,
state = state,
flingBehavior = flingBehavior,
) {
items(200) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.height(400.dp)
.width(300.dp)
.padding(8.dp)
.background(Color.LightGray),
) {
Text(text = it.toString(), fontSize = 32.sp)
}
}
}
}
2024年10月23日水曜日
AutoScrollHorizontalPager を作る
- (擬似的に)無限ループしたい
- タップされているときは自動送りしない
private const val PAGE_COUNT = 10_000
private const val INITIAL_PAGE = PAGE_COUNT / 2
private fun Int.floorMod(other: Int): Int = when (other) {
0 -> this
else -> this - this.floorDiv(other) * other
}
@Composable
fun AutoScrollHorizontalPager(
itemSize: Int,
modifier: Modifier = Modifier,
autoScrollDuration: Long = 2_000L,
onPageChanged: ((page: Int) -> Unit)? = null,
pageContent: @Composable PagerScope.(page: Int) -> Unit,
) {
val pagerState = rememberPagerState(INITIAL_PAGE) { PAGE_COUNT }
fun Int.toIndex(): Int = (this - INITIAL_PAGE).floorMod(itemSize)
if (onPageChanged != null) {
LaunchedEffect(onPageChanged) {
snapshotFlow { pagerState.currentPage }
.collect { page -> onPageChanged(page.toIndex()) }
}
}
val dragged by pagerState.interactionSource.collectIsDraggedAsState()
if (!dragged) {
LaunchedEffect(Unit) {
while (true) {
delay(autoScrollDuration)
val nextPage = pagerState.currentPage + 1
if (nextPage < PAGE_COUNT) {
pagerState.animateScrollToPage(nextPage)
} else {
pagerState.scrollToPage(0)
}
}
}
}
HorizontalPager(
state = pagerState,
modifier = modifier,
) { page ->
pageContent(page.toIndex())
}
}
2024年9月17日火曜日
Arrangement.spacedBy()
Arrangement.spacedBy() を使うと、Column や Row の要素間に同じ大きさの余白を設けることができます。
Spacer で余白を実装する場合、上端や下端に余白が入らないように index を使った制御が必要になります。
Spacer で余白を実装する場合、上端や下端に余白が入らないように index を使った制御が必要になります。
Column {
list.forEachIndexed { index, item ->
if (index > 0) {
Spacer(Modifier.height(8.dp))
}
ListItem(...)
}
}
Arrangement.spacedBy() を使う場合、Spacer や index を使った制御が不要になるほか、if 文で要素を表示しないときに余白も自動で表示されなくなります。
Column(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
list.forEach { item ->
ListItem(...)
}
}
2024年6月23日日曜日
Kotlinらしいコードを書こう - Convert Java File to Kotlin File のあとにやること
KotlinFest 2024 で話すはずだった講演内容です。
みなさん、こんにちは。あんざいゆきです。Android の Google Developer Expert をしています。よろしくお願いします。
私はいろんなクライアントさんの Android アプリ開発のお手伝いをさせていただいていまして、Java から Kotlin に変換した Pull Request のレビューをすることがあります。
プロジェクトの大多数がまだ Java だったり、最近 Android 開発をはじめたばかりだったりして Kotlin になじみがない場合だと、自動変換されただけのような状態でレビュー依頼されることがままあります。
そこでこのセッションでは、Java から Kotlin に自動変換したあと、より Kotlin らしいコードにするためにどういうことをしてほしいのかを紹介したいと思います。 Kotlin らしいコードの話をする前に、Java から Kotlin に変換する Pull Request について話したいと思います。 1 commit で Kotlin 化すると、Java ファイルの削除と Kotlin のファイルの新規追加の履歴になり、Kotlin 化前後のコードを比較するのがけっこう大変です。 そこで、Kotlin 化する前に拡張子を .java から .kt に rename する commit を入れておきます。中身は Java のままです。
commit したら拡張子をまた .kt から .java に戻し、その後に Convert Java File to Kotlin File などで Kotlin 化します。 こうした場合、rename と Kotlin 化の 2つの commit の Pull Request になります。
Files changed タブだと 1 commit で Kotlin 化したときと同じように Kotlin 化前後のコードを比較するのが大変なんですが、 Commits タグを開いて 2つめの commit をみると 同じ .kt ファイルの変更なので、Kotlin 化前後のコードが比較しやすい表示になります。 では本題に入りましょう。 残念ながら Java コード側の情報が少ないと変換したコードに !! が出てくることがあります。
!! が不要になるようにコードを修正しましょう。
この例では引数の newItems を non-null にすれば !! は不要になります。 @NonNull アノテーションがついていない場合、@Nullable なメソッドに渡されている変数は nullable だと解釈されます。
例えば Bundle の putString() メソッドは key も value も @Nullable アノテーションがついています。 そのため Kotlin に自動変換すると、createInstance() メソッドの title 引数は nullable String になります。
もともと null が来ることを想定しているならこのままでよいですが、 null が来ることがありえない、あってはいけないという場合はそれを表現するよう non-null にしましょう。 型パラメータも自動変換時に nullable と判定されることがあるので注意しましょう。 初期化の後に利用されることが保証できる場合(初期化前アクセスが発生するのはコーディングエラー時のみという場合)、 lateinit var を使うことで non-null にすることができます。 var を val にできないか考えましょう。
Kotlin の標準ライブラリに用意されている関数を利用することで val にできることがよくあります。 様々な便利関数があるので、変換されたやつでいいやってなる前に、活用できるものがないか調べましょう。 ?.let や ?: を使ってより簡潔に記述できるようにならないか考えましょう。 apply や also は初期化処理をまとめるのによく使います。 Kotlin では使っていない lambda の引数を _ にすることができます。
自動変換ではやってくれませんが、変換後のコードでグレーの波線が出るので対応しましょう。 A から B に変換する処理は Aの拡張関数にすると呼び出し側がすっきりします。 自動変換では Smart cast が効いている部分の cast を外してくれないので自分で外しましょう。 また、as? を使うことでチェックと呼び出し部分を1行で書くこともできます。 Java の switch 文は自動で when にしてくれますが、if else の連続は自動で when にしてくれません。 必要に応じて自分で when にしましょう。 Java から Kotlin の lambda を引数にとるメソッドを呼んでいる部分があるとします。
これを Kotlin 化した場合、関数参照を使ったほうが記述が簡潔になる場合があります。 List を操作する Java コードを Kotlin に自動変換した場合、MutableList を使ったものになります。 Kotlin std lib に用意されているメソッドを使うと MutableList を不要にできることがあります。 Java では Mutable Collection と Immutable Collection で型が分かれていないので、自動変換すると mutable として外部に公開するべきでないところでも mutable として公開されてしまいます。 Mutable Collection の変数は private にし、公開用に Immutable Collection 型のプロパティを定義するようにします。 Java で ArrayList を new しているところは自動変換しても同じです。 mutableListOf() を使ってより Kotlin らしいコードにしましょう。
同様に HashMap, LinkedHashMap には mutableMapOf(), HashSet, LinkedHashSet には mutableSetOf() が使えます。 List や Map を構成する部分は、自動変換ではほぼそのままの形にしかなりません。 初期化時のみ List や Map を編集するのであれば buildList { } や buildMap { } を使って Mutable Collection の変数が定義されないようにしましょう。 Android 特有の内容も紹介します。 Bundle の生成用に bundleOf() メソッドが用意されています。 TextUtils.isEmpty() は Kotlin の isNullOrEmpty() に置き換えましょう。 TextUtils.equals() は Kotlin の == に置き換えましょう。 Kotlin では、Activity や Fragment で ViewModel のインスタンスを取得するのに by viewModels() を利用することができます。 DI で値がセットされるフィールドは Java から Kotlin に自動変換すると nullable の var になってしまいます。
しかしこれらは参照されるときには値がすでにセットされていることが期待されるものなので、lateinit var に変えましょう。 最後にチェックリストをまとめました。ありがとうございました。
みなさん、こんにちは。あんざいゆきです。Android の Google Developer Expert をしています。よろしくお願いします。
私はいろんなクライアントさんの Android アプリ開発のお手伝いをさせていただいていまして、Java から Kotlin に変換した Pull Request のレビューをすることがあります。
プロジェクトの大多数がまだ Java だったり、最近 Android 開発をはじめたばかりだったりして Kotlin になじみがない場合だと、自動変換されただけのような状態でレビュー依頼されることがままあります。
そこでこのセッションでは、Java から Kotlin に自動変換したあと、より Kotlin らしいコードにするためにどういうことをしてほしいのかを紹介したいと思います。 Kotlin らしいコードの話をする前に、Java から Kotlin に変換する Pull Request について話したいと思います。 1 commit で Kotlin 化すると、Java ファイルの削除と Kotlin のファイルの新規追加の履歴になり、Kotlin 化前後のコードを比較するのがけっこう大変です。 そこで、Kotlin 化する前に拡張子を .java から .kt に rename する commit を入れておきます。中身は Java のままです。
commit したら拡張子をまた .kt から .java に戻し、その後に Convert Java File to Kotlin File などで Kotlin 化します。 こうした場合、rename と Kotlin 化の 2つの commit の Pull Request になります。
Files changed タブだと 1 commit で Kotlin 化したときと同じように Kotlin 化前後のコードを比較するのが大変なんですが、 Commits タグを開いて 2つめの commit をみると 同じ .kt ファイルの変更なので、Kotlin 化前後のコードが比較しやすい表示になります。 では本題に入りましょう。 残念ながら Java コード側の情報が少ないと変換したコードに !! が出てくることがあります。
!! が不要になるようにコードを修正しましょう。
この例では引数の newItems を non-null にすれば !! は不要になります。 @NonNull アノテーションがついていない場合、@Nullable なメソッドに渡されている変数は nullable だと解釈されます。
例えば Bundle の putString() メソッドは key も value も @Nullable アノテーションがついています。 そのため Kotlin に自動変換すると、createInstance() メソッドの title 引数は nullable String になります。
もともと null が来ることを想定しているならこのままでよいですが、 null が来ることがありえない、あってはいけないという場合はそれを表現するよう non-null にしましょう。 型パラメータも自動変換時に nullable と判定されることがあるので注意しましょう。 初期化の後に利用されることが保証できる場合(初期化前アクセスが発生するのはコーディングエラー時のみという場合)、 lateinit var を使うことで non-null にすることができます。 var を val にできないか考えましょう。
Kotlin の標準ライブラリに用意されている関数を利用することで val にできることがよくあります。 様々な便利関数があるので、変換されたやつでいいやってなる前に、活用できるものがないか調べましょう。 ?.let や ?: を使ってより簡潔に記述できるようにならないか考えましょう。 apply や also は初期化処理をまとめるのによく使います。 Kotlin では使っていない lambda の引数を _ にすることができます。
自動変換ではやってくれませんが、変換後のコードでグレーの波線が出るので対応しましょう。 A から B に変換する処理は Aの拡張関数にすると呼び出し側がすっきりします。 自動変換では Smart cast が効いている部分の cast を外してくれないので自分で外しましょう。 また、as? を使うことでチェックと呼び出し部分を1行で書くこともできます。 Java の switch 文は自動で when にしてくれますが、if else の連続は自動で when にしてくれません。 必要に応じて自分で when にしましょう。 Java から Kotlin の lambda を引数にとるメソッドを呼んでいる部分があるとします。
これを Kotlin 化した場合、関数参照を使ったほうが記述が簡潔になる場合があります。 List を操作する Java コードを Kotlin に自動変換した場合、MutableList を使ったものになります。 Kotlin std lib に用意されているメソッドを使うと MutableList を不要にできることがあります。 Java では Mutable Collection と Immutable Collection で型が分かれていないので、自動変換すると mutable として外部に公開するべきでないところでも mutable として公開されてしまいます。 Mutable Collection の変数は private にし、公開用に Immutable Collection 型のプロパティを定義するようにします。 Java で ArrayList を new しているところは自動変換しても同じです。 mutableListOf() を使ってより Kotlin らしいコードにしましょう。
同様に HashMap, LinkedHashMap には mutableMapOf(), HashSet, LinkedHashSet には mutableSetOf() が使えます。 List や Map を構成する部分は、自動変換ではほぼそのままの形にしかなりません。 初期化時のみ List や Map を編集するのであれば buildList { } や buildMap { } を使って Mutable Collection の変数が定義されないようにしましょう。 Android 特有の内容も紹介します。 Bundle の生成用に bundleOf() メソッドが用意されています。 TextUtils.isEmpty() は Kotlin の isNullOrEmpty() に置き換えましょう。 TextUtils.equals() は Kotlin の == に置き換えましょう。 Kotlin では、Activity や Fragment で ViewModel のインスタンスを取得するのに by viewModels() を利用することができます。 DI で値がセットされるフィールドは Java から Kotlin に自動変換すると nullable の var になってしまいます。
しかしこれらは参照されるときには値がすでにセットされていることが期待されるものなので、lateinit var に変えましょう。 最後にチェックリストをまとめました。ありがとうございました。
登録:
投稿 (Atom)
























































