Jetpack Compose面接質問20選(2026年版)
Jetpack Composeの面接で頻出する20の質問を解説。リコンポジション、状態管理、副作用、ナビゲーション、パフォーマンス最適化、アーキテクチャパターンを網羅。

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ファイルに集約されます。
// 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の依存関係を追跡し、必要な部分のみを効率的に更新します。
@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がコンポジションから離脱すると保持された値も破棄されます。
@Composable
fun FilteredList(items: List<String>, 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に値を保存するため、構成変更やプロセスの再生成後も値が復元されます。フォーム入力やスクロール位置など、ユーザーが意図的に設定した状態の保持に適しています。
@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は再利用性が高く、テストが容易で、プレビューでの確認も簡単です。状態とイベントコールバックをパラメータとして渡すことで、単一方向データフローを実現します。
// 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は、他の状態値から派生する値を効率的に計算するために使用します。入力となる状態が変化した場合のみ再計算が行われ、結果が同じであればリコンポジションはトリガーされません。
リストのフィルタリングやバリデーション結果の算出など、既存の状態を変換する処理に適しています。
@Composable
fun TodoList(todos: List<Todo>) {
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フレームワークとの互換性も維持できます。
class ProfileViewModel : ViewModel() {
private val _uiState = MutableStateFlow(ProfileUiState())
val uiState: StateFlow<ProfileUiState> = _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
)
}Androidの面接対策はできていますか?
インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。
副作用(Q8〜Q9)
Q8: LaunchedEffect、DisposableEffect、SideEffectの違いは何ですか?
Composeの副作用APIは、コンポジションのライフサイクルに連動した非同期処理やリソース管理を安全に実行するための仕組みです。
LaunchedEffect: コルーチンスコープを提供し、keyが変化するとキャンセルして再起動します。API呼び出しやアニメーションなど、サスペンド関数の実行に使用します。
DisposableEffect: リソースの確保と解放を行います。onDisposeブロックでクリーンアップ処理を記述します。リスナーの登録/解除やコールバックの設定に適しています。
SideEffect: リコンポジションが成功するたびに実行されます。Compose外部のシステムとの同期に使用します。サスペンド関数は使用できません。
@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が破棄されるとスコープ内のコルーチンもキャンセルされます。
@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でキャッシュします。
@Composable
fun OptimizedList(items: List<Item>) {
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)を呼んで配置します。
@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コンポーザブルでカスタム値を提供し、アプリ全体で一貫したデザインシステムを実現します。
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関数で登録します。
@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を使うと、戻り先の画面に結果を返すことも可能です。
// 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でラップし、不要なリコンポジションを削減します。
// 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<UserData>,
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コンパイラのメトリクスを有効にすると、各関数のスキップ可能性やパラメータの安定性に関するレポートが生成されます。
// 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を適用すると、パディング領域にも背景色が描画されます。逆にすると、パディング領域は透明のままです。
@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が最も外側のレイヤーになります。
アーキテクチャ(Q18〜Q20)
Q18: ViewModel + UiStateパターンの実装方法は?
ViewModel + UiStateパターンは、ComposeアプリにおけるGoogleの推奨アーキテクチャです。UIの状態を単一のデータクラスで表現し、ViewModelが状態の更新を管理します。
単一方向データフロー(UDF)により、状態の流れが予測可能になり、デバッグとテストが容易になります。
data class SearchUiState(
val query: String = "",
val results: List<SearchResult> = 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<SearchUiState> = _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の検証と操作を行います。
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ビューを表示します。
// Compose inside XML layout
class LegacyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_legacy)
val composeView = findViewById<ComposeView>(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:
// <androidx.compose.ui.platform.ComposeView
// android:id="@+id/compose_container"
// android:layout_width="match_parent"
// android:layout_height="wrap_content" />新機能は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を参照することを推奨します。実際にサンプルコードを書いて動作を確認することで、面接時に自信を持って回答できるようになります。
タグ
共有
関連記事

AndroidのMVVM vs MVI:2026年に選ぶべきアーキテクチャとは?
AndroidにおけるMVVMとMVIの徹底比較。メリット・デメリット、ユースケース、そして2026年に適切なアーキテクチャを選択するための実践的ガイド。

Kotlinコルーチン完全ガイド2026:Android開発の非同期処理をマスターする
Android開発に必要なKotlinコルーチンの基礎から応用まで解説します。suspend関数、スコープ、ディスパッチャー、Flowまで体系的に学べる実践ガイドです。

データアナリストのためのSQL:ウィンドウ関数、CTE、高度なクエリ技法
SQLのウィンドウ関数、CTE(共通テーブル式)、高度な分析クエリをコード例付きで解説。データアナリスト面接対策と実務に直結する必須テクニック。