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

2024年11月9日土曜日

LazyRow で snap させる

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

左端に snap する場合、以前は
  1. SnapLayoutInfoProvider(  
  2.     lazyListState = state,  
  3.     positionInLayout = { layoutSize, itemSize, beforeContentPadding, afterContentPadding, _->  
  4.        0 // 左端  
  5.     },  
  6. )  
だったが、SnapPosition という interface が用意され、 左端の場合は用意されている SnapPosition.Start を指定すれば良くなった。 Start の他に End と Center も用意されている。
任意の位置に snap したい場合は SnapPosition の実装を用意すればよい。
  1. SnapLayoutInfoProvider(  
  2.     lazyListState = state,  
  3.     snapPosition = SnapPosition.Start,  
  4. )  
全体のコードはこんな感じ。
  1. @Composable  
  2. fun LazyRowSnapSample() {  
  3.     val state = rememberLazyListState()  
  4.     val snappingLayout = remember(state) {  
  5.         SnapLayoutInfoProvider(  
  6.             lazyListState = state,  
  7.             snapPosition = SnapPosition.Start,  
  8.         )  
  9.     }  
  10.     val flingBehavior = rememberSnapFlingBehavior(snappingLayout)  
  11.   
  12.     LazyRow(  
  13.         modifier = Modifier.fillMaxSize(),  
  14.         verticalAlignment = Alignment.CenterVertically,  
  15.         state = state,  
  16.         flingBehavior = flingBehavior,  
  17.     ) {  
  18.         items(200) {  
  19.             Box(  
  20.                 contentAlignment = Alignment.Center,  
  21.                 modifier = Modifier  
  22.                     .height(400.dp)  
  23.                     .width(300.dp)  
  24.                     .padding(8.dp)  
  25.                     .background(Color.LightGray),  
  26.             ) {  
  27.                 Text(text = it.toString(), fontSize = 32.sp)  
  28.             }  
  29.         }  
  30.     }  
  31. }