# Jetpack Compose面接質問20選(2026年版) > Jetpack Composeの面接で頻出する20の質問を解説。リコンポジション、状態管理、副作用、ナビゲーション、パフォーマンス最適化、アーキテクチャパターンを網羅。 - Published: 2026-01-27 - Updated: 2026-04-12 - Author: SharpSkill - Tags: jetpack compose, android, interview, kotlin, ui - Reading time: 14 min --- Jetpack ComposeはAndroid UI開発の標準フレームワークとしての地位を確立しています。2026年現在、Composeの知識はAndroid開発者の面接で必須項目です。この記事では、面接で実際に問われる20の質問を基礎から応用まで体系的に解説します。 > **面接対策のポイント** > > 各質問の回答を暗記するのではなく、背後にある設計思想を理解することが重要です。面接官はComposeの「なぜ」を説明できる候補者を高く評価します。 ## 基礎概念(Q1〜Q4) ### Q1: Jetpack ComposeとXMLベースUIの主な違いは何ですか? XMLベースのUI構築では、レイアウトファイルでビュー階層を宣言し、Activity/FragmentからfindViewByIdやViewBindingで参照します。状態変更時にはビューを手動で更新する必要があります。 Jetpack Composeは宣言的UIフレームワークです。UIの見た目をComposable関数として記述し、状態が変化するとフレームワークが自動的にUIを再構築します。ビュー参照の管理やXMLとKotlinの切り替えが不要になり、UIロジックが単一のKotlinファイルに集約されます。 ```kotlin // compose-vs-xml.kt // XML approach: separate layout file + Activity code // activity_main.xml -> findViewById or ViewBinding // Compose approach: everything in Kotlin @Composable fun Greeting(name: String) { Text( text = "Hello, $name!", style = MaterialTheme.typography.headlineMedium ) } ``` ### Q2: リコンポジションとは何ですか?どのような場合に発生しますか? リコンポジションは、状態の変化に応じてComposeランタイムがComposable関数を再実行するプロセスです。Composeは変更された状態を読み取るComposableのみを再実行するため、UIツリー全体を再構築する必要がありません。 リコンポジションが発生する条件は、Composable関数が読み取っている`State`オブジェクトの値が変更されたときです。Composeコンパイラが各Composableの依存関係を追跡し、必要な部分のみを効率的に更新します。 ```kotlin // recomposition.kt @Composable fun Counter() { var count by remember { mutableStateOf(0) } // Only this Text recomposes when count changes Text(text = "Count: $count") Button(onClick = { count++ }) { // This Text does NOT recompose (static content) Text("Increment") } } ``` ### Q3: rememberの役割は何ですか? `remember`は、リコンポジション間で値を保持するための仕組みです。Composable関数は再実行されるたびにローカル変数がリセットされます。`remember`を使うことで、前回のコンポジションで計算された値を保持し、不要な再計算を防ぎます。 `remember`はコンポジションのライフサイクルに紐付いており、Composableがコンポジションから離脱すると保持された値も破棄されます。 ```kotlin // remember.kt @Composable fun FilteredList(items: List, query: String) { // Recalculated only when items or query changes val filtered = remember(items, query) { items.filter { it.contains(query, ignoreCase = true) } } LazyColumn { items(filtered) { item -> Text(text = item) } } } ``` ### Q4: rememberとrememberSaveableの違いは何ですか? `remember`はコンポジション内でのみ値を保持します。画面回転やプロセスの終了などの構成変更時には値が失われます。 `rememberSaveable`は`Bundle`に値を保存するため、構成変更やプロセスの再生成後も値が復元されます。フォーム入力やスクロール位置など、ユーザーが意図的に設定した状態の保持に適しています。 ```kotlin // remember-saveable.kt @Composable fun SearchScreen() { // Lost on configuration change var tempFlag by remember { mutableStateOf(false) } // Survives configuration change and process death var searchQuery by rememberSaveable { mutableStateOf("") } OutlinedTextField( value = searchQuery, onValueChange = { searchQuery = it }, label = { Text("Search") } ) } ``` > **使い分けの原則** > > 画面回転後にリセットされても問題ない一時的な状態には`remember`を、ユーザー入力やナビゲーション状態など永続化すべき値には`rememberSaveable`を使用します。 ## 状態管理(Q5〜Q7) ### Q5: 状態ホイスティングとは何ですか? 状態ホイスティングは、Composable内部の状態を呼び出し元に移動するパターンです。状態を持つComposableを「ステートフル」、状態を受け取るだけのComposableを「ステートレス」と呼びます。 ステートレスなComposableは再利用性が高く、テストが容易で、プレビューでの確認も簡単です。状態とイベントコールバックをパラメータとして渡すことで、単一方向データフローを実現します。 ```kotlin // state-hoisting.kt // Stateless composable: receives state, emits events @Composable fun EmailInput( email: String, onEmailChange: (String) -> Unit, modifier: Modifier = Modifier ) { OutlinedTextField( value = email, onValueChange = onEmailChange, label = { Text("Email") }, modifier = modifier ) } // Stateful wrapper: owns and manages state @Composable fun EmailForm() { var email by rememberSaveable { mutableStateOf("") } EmailInput( email = email, onEmailChange = { email = it } ) } ``` ### Q6: derivedStateOfはどのような場合に使用しますか? `derivedStateOf`は、他の状態値から派生する値を効率的に計算するために使用します。入力となる状態が変化した場合のみ再計算が行われ、結果が同じであればリコンポジションはトリガーされません。 リストのフィルタリングやバリデーション結果の算出など、既存の状態を変換する処理に適しています。 ```kotlin // derived-state.kt @Composable fun TodoList(todos: List) { var hideCompleted by remember { mutableStateOf(false) } // Only recalculated when todos or hideCompleted changes val visibleTodos by remember(todos) { derivedStateOf { if (hideCompleted) todos.filter { !it.done } else todos } } val completedCount by remember(todos) { derivedStateOf { todos.count { it.done } } } Text("Completed: $completedCount / ${todos.size}") LazyColumn { items(visibleTodos) { todo -> TodoRow(todo) } } } ``` ### Q7: StateFlowとCompose StateはどちらをViewModelで使うべきですか? `StateFlow`はKotlinのCoroutinesライブラリの一部で、Compose以外のレイヤーでも使用できます。ViewModelでは`StateFlow`でUIの状態を保持し、Composable側で`collectAsState()`を使ってCompose Stateに変換するのが一般的なパターンです。 `mutableStateOf`をViewModel内で直接使用することも技術的には可能ですが、ViewModelがCompose APIに依存してしまいます。`StateFlow`を使うことでViewModelのテスタビリティが向上し、Compose以外のUIフレームワークとの互換性も維持できます。 ```kotlin // stateflow-vs-state.kt class ProfileViewModel : ViewModel() { private val _uiState = MutableStateFlow(ProfileUiState()) val uiState: StateFlow = _uiState.asStateFlow() fun updateName(name: String) { _uiState.update { it.copy(name = name) } } } data class ProfileUiState( val name: String = "", val isLoading: Boolean = false ) @Composable fun ProfileScreen(viewModel: ProfileViewModel = viewModel()) { val uiState by viewModel.uiState.collectAsState() ProfileContent( name = uiState.name, isLoading = uiState.isLoading, onNameChange = viewModel::updateName ) } ``` ## 副作用(Q8〜Q9) ### Q8: LaunchedEffect、DisposableEffect、SideEffectの違いは何ですか? Composeの副作用APIは、コンポジションのライフサイクルに連動した非同期処理やリソース管理を安全に実行するための仕組みです。 **LaunchedEffect**: コルーチンスコープを提供し、keyが変化するとキャンセルして再起動します。API呼び出しやアニメーションなど、サスペンド関数の実行に使用します。 **DisposableEffect**: リソースの確保と解放を行います。`onDispose`ブロックでクリーンアップ処理を記述します。リスナーの登録/解除やコールバックの設定に適しています。 **SideEffect**: リコンポジションが成功するたびに実行されます。Compose外部のシステムとの同期に使用します。サスペンド関数は使用できません。 ```kotlin // side-effects.kt @Composable fun UserProfile(userId: String) { // LaunchedEffect: runs suspend function, restarts when userId changes LaunchedEffect(userId) { viewModel.loadUser(userId) } // DisposableEffect: acquire/release resources DisposableEffect(Unit) { val listener = object : LocationListener { override fun onLocationChanged(location: Location) { // Handle location update } } locationManager.requestLocationUpdates(listener) onDispose { locationManager.removeUpdates(listener) } } // SideEffect: sync with external system on every recomposition SideEffect { analytics.setCurrentScreen("UserProfile") } } ``` ### Q9: rememberCoroutineScopeはいつ使いますか? `rememberCoroutineScope`は、Composable関数の外部(コールバックやイベントハンドラ内など)でコルーチンを起動する場合に使用します。`LaunchedEffect`がComposableのライフサイクルに自動的に紐付くのに対し、`rememberCoroutineScope`はユーザーアクションに応じて手動でコルーチンを起動するケースに適しています。 スコープはComposableのライフサイクルに紐付いており、Composableが破棄されるとスコープ内のコルーチンもキャンセルされます。 ```kotlin // coroutine-scope.kt @Composable fun SnackbarDemo(snackbarHostState: SnackbarHostState) { val scope = rememberCoroutineScope() Button(onClick = { // Launch coroutine from a callback scope.launch { snackbarHostState.showSnackbar( message = "Action completed", duration = SnackbarDuration.Short ) } }) { Text("Show Snackbar") } } ``` ## レイアウト(Q10〜Q12) ### Q10: LazyColumnのパフォーマンスを最適化する方法は? `LazyColumn`はRecyclerViewに相当するComposableで、画面に表示されるアイテムのみをコンポーズします。パフォーマンス最適化のポイントは以下の通りです。 **keyの指定**: `key`パラメータで各アイテムに安定した識別子を設定すると、リスト変更時の不要なリコンポジションを防げます。 **contentTypeの活用**: 異なるアイテムタイプを`contentType`で区別すると、Composeはアイテムの再利用を最適化できます。 **重い処理のキャッシュ**: アイテム内で画像の読み込みや計算を行う場合は`remember`でキャッシュします。 ```kotlin // lazy-column.kt @Composable fun OptimizedList(items: List) { LazyColumn { items( items = items, key = { it.id }, // Stable key for reordering contentType = { it.type } // Efficient item reuse ) { item -> when (item.type) { ItemType.HEADER -> HeaderRow(item) ItemType.CONTENT -> ContentRow(item) ItemType.FOOTER -> FooterRow(item) } } } } ``` ### Q11: カスタムLayoutの作成方法は? Composeの`Layout`コンポーザブルを使用すると、独自の配置ロジックを定義できます。`MeasurePolicy`で子要素の測定と配置を制御します。 各子要素は`measurable.measure(constraints)`で測定し、`layout(width, height)`ブロック内で`placeable.place(x, y)`を呼んで配置します。 ```kotlin // custom-layout.kt @Composable fun StaggeredGrid( modifier: Modifier = Modifier, columns: Int = 2, content: @Composable () -> Unit ) { Layout( content = content, modifier = modifier ) { measurables, constraints -> val columnWidth = constraints.maxWidth / columns val itemConstraints = constraints.copy( minWidth = columnWidth, maxWidth = columnWidth ) val placeables = measurables.map { it.measure(itemConstraints) } val columnHeights = IntArray(columns) val placements = placeables.map { placeable -> val col = columnHeights.indexOfMin() val position = Pair(col * columnWidth, columnHeights[col]) columnHeights[col] += placeable.height position } layout(constraints.maxWidth, columnHeights.max()) { placeables.forEachIndexed { index, placeable -> val (x, y) = placements[index] placeable.place(x, y) } } } } fun IntArray.indexOfMin(): Int { var minIndex = 0 for (i in indices) { if (this[i] < this[minIndex]) minIndex = i } return minIndex } ``` ### Q12: MaterialThemeのカスタマイズ方法は? Material Design 3のテーマは、カラースキーム、タイポグラフィ、シェイプの3つの軸で構成されます。`MaterialTheme`コンポーザブルでカスタム値を提供し、アプリ全体で一貫したデザインシステムを実現します。 ```kotlin // material-theme.kt private val DarkColorScheme = darkColorScheme( primary = Color(0xFFBB86FC), secondary = Color(0xFF03DAC5), background = Color(0xFF121212) ) private val LightColorScheme = lightColorScheme( primary = Color(0xFF6200EE), secondary = Color(0xFF03DAC5), background = Color(0xFFFFFFFF) ) @Composable fun AppTheme( darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit ) { val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme MaterialTheme( colorScheme = colorScheme, typography = AppTypography, shapes = AppShapes, content = content ) } // Usage in composables @Composable fun ThemedCard() { Card( colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surface ) ) { Text( text = "Themed content", style = MaterialTheme.typography.bodyLarge ) } } ``` ## ナビゲーション(Q13〜Q14) ### Q13: ComposeでNavHostを使った画面遷移の実装方法は? `NavHost`はCompose Navigationの中核コンポーネントで、`NavController`と連携して画面遷移を管理します。各画面はルート文字列で識別され、`composable`関数で登録します。 ```kotlin // navigation.kt @Composable fun AppNavigation() { val navController = rememberNavController() NavHost( navController = navController, startDestination = "home" ) { composable("home") { HomeScreen( onNavigateToDetail = { id -> navController.navigate("detail/$id") } ) } composable( route = "detail/{itemId}", arguments = listOf( navArgument("itemId") { type = NavType.StringType } ) ) { backStackEntry -> val itemId = backStackEntry.arguments?.getString("itemId") DetailScreen(itemId = itemId) } } } ``` ### Q14: 画面間でデータを渡す方法は? Compose Navigationでは、ルートパラメータとsavedStateHandleを使ってデータを渡します。単純な値はルートパスやクエリパラメータに埋め込み、複雑なオブジェクトはViewModelやリポジトリ経由で共有します。 `previousBackStackEntry`のsavedStateHandleを使うと、戻り先の画面に結果を返すことも可能です。 ```kotlin // passing-data.kt // Type-safe navigation with arguments @Composable fun AppNavigation() { val navController = rememberNavController() NavHost(navController = navController, startDestination = "list") { composable("list") { ListScreen( onItemClick = { id, title -> navController.navigate("detail/$id?title=$title") } ) } composable( route = "detail/{id}?title={title}", arguments = listOf( navArgument("id") { type = NavType.IntType }, navArgument("title") { type = NavType.StringType defaultValue = "" } ) ) { entry -> DetailScreen( id = entry.arguments?.getInt("id") ?: 0, title = entry.arguments?.getString("title") ?: "" ) } } } // Returning results to previous screen fun NavController.navigateBackWithResult(key: String, value: String) { previousBackStackEntry?.savedStateHandle?.set(key, value) popBackStack() } ``` > **大きなオブジェクトの受け渡しに注意** > > ナビゲーション引数にシリアライズ可能なオブジェクトを直接渡すことは推奨されません。IDを渡し、遷移先のViewModelでデータを取得するパターンが推奨されます。 ## パフォーマンス(Q15〜Q17) ### Q15: 不要なリコンポジションを防ぐ方法は? リコンポジションの最適化は、Composeアプリのパフォーマンスに直結します。主要な手法を以下に示します。 **安定した型の使用**: Composeコンパイラは`@Stable`または`@Immutable`でアノテーションされた型、もしくはプリミティブ型やStringなどの組み込み安定型をスキップ可能と判断します。 **lambdaの安定化**: Composableに渡すラムダをrememberで包むか、メソッド参照を使用してリコンポジションを抑制します。 **derivedStateOfの活用**: 頻繁に変化する状態から派生する値は`derivedStateOf`でラップし、不要なリコンポジションを削減します。 ```kotlin // prevent-recomposition.kt // Mark classes as stable for the Compose compiler @Immutable data class UserData( val id: String, val name: String, val avatarUrl: String ) // Use remember for lambda stability @Composable fun ItemList( items: List, onItemClick: (String) -> Unit ) { val stableCallback = remember(onItemClick) { onItemClick } LazyColumn { items(items, key = { it.id }) { item -> UserRow( user = item, onClick = { stableCallback(item.id) } ) } } } ``` ### Q16: Composeのパフォーマンスプロファイリング方法は? Android Studioには、Composeのパフォーマンスを分析するための専用ツールが用意されています。 **Layout Inspector**: コンポジションツリーをリアルタイムで可視化し、各Composableのリコンポジション回数を確認できます。 **Composition Tracing**: System Traceと統合され、各Composable関数の実行時間を計測できます。`composition-tracing`ライブラリを追加することで利用可能になります。 **コンパイラレポート**: Composeコンパイラのメトリクスを有効にすると、各関数のスキップ可能性やパラメータの安定性に関するレポートが生成されます。 ```kotlin // profiling.kt // Enable compiler metrics in build.gradle.kts // kotlinOptions { // freeCompilerArgs += listOf( // "-P", "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=build/compose-metrics", // "-P", "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=build/compose-reports" // ) // } // Trace specific compositions @Composable fun HeavyScreen() { trace("HeavyScreen") { // Composable content here val data = remember { expensiveComputation() } DataDisplay(data) } } ``` ### Q17: Modifierの順序はなぜ重要ですか? Modifierチェーンは上から下へ順に適用されます。順序によってレイアウトと描画の結果が大きく変わります。 `padding`と`background`の順序は典型的な例です。`background`を先に適用してから`padding`を適用すると、パディング領域にも背景色が描画されます。逆にすると、パディング領域は透明のままです。 ```kotlin // modifier-order.kt @Composable fun ModifierOrderDemo() { // Background covers padding area Text( text = "Option A", modifier = Modifier .background(Color.Red) .padding(16.dp) ) // Background does NOT cover padding area Text( text = "Option B", modifier = Modifier .padding(16.dp) .background(Color.Red) ) // Clickable area includes padding Text( text = "Option C", modifier = Modifier .clickable { /* handle click */ } .padding(16.dp) ) // Clickable area excludes padding Text( text = "Option D", modifier = Modifier .padding(16.dp) .clickable { /* handle click */ } ) } ``` > **Modifierの考え方** > > Modifierチェーンは外側から内側への「ラッパー」と考えると直感的に理解できます。最初に適用されたModifierが最も外側のレイヤーになります。 ## アーキテクチャ(Q18〜Q20) ### Q18: ViewModel + UiStateパターンの実装方法は? ViewModel + UiStateパターンは、ComposeアプリにおけるGoogleの推奨アーキテクチャです。UIの状態を単一のデータクラスで表現し、ViewModelが状態の更新を管理します。 単一方向データフロー(UDF)により、状態の流れが予測可能になり、デバッグとテストが容易になります。 ```kotlin // viewmodel-uistate.kt data class SearchUiState( val query: String = "", val results: List = emptyList(), val isLoading: Boolean = false, val error: String? = null ) sealed interface SearchEvent { data class QueryChanged(val query: String) : SearchEvent data object Search : SearchEvent data object ClearError : SearchEvent } class SearchViewModel( private val repository: SearchRepository ) : ViewModel() { private val _uiState = MutableStateFlow(SearchUiState()) val uiState: StateFlow = _uiState.asStateFlow() fun onEvent(event: SearchEvent) { when (event) { is SearchEvent.QueryChanged -> { _uiState.update { it.copy(query = event.query) } } is SearchEvent.Search -> performSearch() is SearchEvent.ClearError -> { _uiState.update { it.copy(error = null) } } } } private fun performSearch() { viewModelScope.launch { _uiState.update { it.copy(isLoading = true, error = null) } try { val results = repository.search(_uiState.value.query) _uiState.update { it.copy(results = results, isLoading = false) } } catch (e: Exception) { _uiState.update { it.copy(error = e.message, isLoading = false) } } } } } @Composable fun SearchScreen(viewModel: SearchViewModel = viewModel()) { val uiState by viewModel.uiState.collectAsState() Column { SearchBar( query = uiState.query, onQueryChange = { viewModel.onEvent(SearchEvent.QueryChanged(it)) }, onSearch = { viewModel.onEvent(SearchEvent.Search) } ) when { uiState.isLoading -> CircularProgressIndicator() uiState.error != null -> ErrorMessage(uiState.error!!) else -> SearchResults(uiState.results) } } } ``` ### Q19: Composable関数のテスト方法は? ComposeにはUI テスト用の`compose-ui-test`ライブラリが提供されています。`ComposeTestRule`を使用してComposableをテスト環境に配置し、セマンティクスツリーを通じてUIの検証と操作を行います。 ```kotlin // testing.kt class LoginScreenTest { @get:Rule val composeTestRule = createComposeRule() @Test fun loginButton_disabled_whenFieldsEmpty() { composeTestRule.setContent { LoginScreen(onLogin = {}) } // Verify button is disabled composeTestRule .onNodeWithText("Login") .assertIsNotEnabled() } @Test fun loginButton_enabled_whenFieldsFilled() { composeTestRule.setContent { LoginScreen(onLogin = {}) } // Fill in fields composeTestRule .onNodeWithTag("email_field") .performTextInput("user@example.com") composeTestRule .onNodeWithTag("password_field") .performTextInput("password123") // Verify button is enabled composeTestRule .onNodeWithText("Login") .assertIsEnabled() } @Test fun errorMessage_displayed_onLoginFailure() { composeTestRule.setContent { LoginScreen(onLogin = {}) } // Verify error message appears composeTestRule .onNodeWithText("Invalid credentials") .assertExists() } } ``` ### Q20: ComposeとXMLビューの相互運用方法は? 既存プロジェクトへのCompose導入は段階的に行えます。`ComposeView`でXMLレイアウト内にComposableを埋め込み、`AndroidView`でComposable内にXMLビューを表示します。 ```kotlin // interop.kt // Compose inside XML layout class LegacyActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_legacy) val composeView = findViewById(R.id.compose_container) composeView.setContent { MaterialTheme { NewFeatureComposable() } } } } // XML View inside Compose @Composable fun LegacyMapView() { AndroidView( factory = { context -> MapView(context).apply { // Initialize legacy MapView } }, update = { mapView -> // Update when state changes }, modifier = Modifier.fillMaxSize() ) } // In XML layout: // ``` > **段階的な移行戦略** > > 新機能はComposeで、既存画面は必要に応じてXMLからComposeに段階的に移行するのがGoogleの推奨アプローチです。ComposeとXMLは同じアプリ内で共存可能です。 ## まとめ Jetpack Composeの面接対策では、APIの使い方だけでなく設計意図を理解することが求められます。リコンポジションの仕組み、状態ホイスティングの意義、副作用APIの使い分けなど、「なぜそう設計されているか」を説明できることが合格の鍵です。 ### 面接準備チェックリスト - ✅ 宣言的UIとリコンポジションの仕組みを説明できる - ✅ remember、rememberSaveable、derivedStateOfを使い分けられる - ✅ 状態ホイスティングと単一方向データフローを理解している - ✅ LaunchedEffect、DisposableEffect、SideEffectの違いを説明できる - ✅ LazyColumnの最適化手法(key、contentType)を知っている - ✅ Modifierの適用順序が結果に影響することを理解している - ✅ ViewModel + UiStateパターンの実装方法を把握している - ✅ ComposeのUIテスト方法を説明できる - ✅ XMLビューとの相互運用方法を知っている これらの質問への回答を準備する際は、公式ドキュメントとGoogle Codelabsを参照することを推奨します。実際にサンプルコードを書いて動作を確認することで、面接時に自信を持って回答できるようになります。 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ja/blog/android/jetpack-compose-interview-questions