2024年11月17日日曜日

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

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 への移行完了です!


2024年11月14日木曜日

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

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 }) } }

2024年11月9日土曜日

LazyRow で snap させる

SnapLayoutInfoProvider での snap 位置の指定方法が変わっていた。

左端に 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 を使った制御が必要になります。 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 に変えましょう。
最後にチェックリストをまとめました。ありがとうございました。