Communitygithub.com

lerchenflo/claudeskills

이 스킬을 사용하여 Jetpack Compose 앱을 적응형 엣지 투 엣지(edge-to-edge)로 마이그레이션하고 일반적인 문제를 해결하세요. 내비게이션 바나 상태 표시줄에 가려지거나 겹치는 UI 구성 요소(버튼, 리스트 등), IME 삽입(inset) 문제, 시스템 표시줄 가독성 문제를 수정할 수 있습니다.

claudeskills란 무엇인가요?

이 스킬은 Android Jetpack Compose 앱에 엣지 투 엣지(edge-to-edge) 지원을 추가하고 관련 문제를 해결하는 자동화된 코드 변환 및 문제 해결 솔루션입니다. 기본적으로 `enableEdgeToEdge` 호출 추가, `AndroidManifest.xml`의 `adjustResize` 설정, `Scaffold`의 `PaddingValues` 전달 또는 `safeDrawingPadding`과 같은 수정자를 사용한 시스템 삽입 적용 등 일련의 단계를 수행합니다. 특히 IME(소프트 키보드) 삽입으로 인한 입력 필드 가려짐 문제를 해결하고, `WindowInsetsRulers.Ime`를 사용한 `fitInside` 또는 `imePadding` 패턴을 통해 잘못된 이중 패딩을 방지합니다. 네비게이션 바 대비 및 시스템 표시줄 아이콘 가독성을 개선하며, `NavigationSuiteScaffold`와 같은 적응형 스캐폴드(Scaffold)의 특수 케이스와 전체 화면 다이얼로그의 엣지 투 엣지 처리를 다룹니다. 이 스킬은 Android Jetpack Compose 앱을 대상으로 하며 Android Studio, Claude Code, Cursor 등 일반적인 개발 환경에서 사용할 수 있습니다.

지원 대상✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/lerchenflo/claudeskills/tree/HEAD/android/edge-to-edge

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

사전 요구 사항

  • 프로젝트 반드시 Android Jetpack Compose를 사용해야 합니다.
  • 프로젝트 반드시 SDK 35 이상을 대상(target)으로 해야 합니다. SDK가 35보다 낮으면 SDK를 35로 올리세요.

1단계: 계획

  1. 모든 Activity 클래스를 찾아 분석하여 엣지 투 엣지 지원이 이미 있는지 확인합니다. 엣지 투 엣지가 없는 모든 Activity에 대해 각 Activity를 엣지 투 엣지로 만들 계획을 세웁니다.
  2. 각 Activity에서 모든 리스트와 FAB 구성 요소를 찾아 분석하여 엣지 투 엣지 지원이 이미 있는지 확인합니다. 엣지 투 엣지 지원이 없는 각 구성 요소에 대해 이 구성 요소를 엣지 투 엣지로 만들 계획을 세웁니다.
  3. 각 Activity에서 TextField, OutlinedTextField 또는 BasicTextField를 검색합니다. 발견되면 이 스킬의 IME 섹션을 따라 IME가 입력 필드를 가리지 않는지 반드시 확인해야 합니다.

2단계: 엣지 투 엣지 지원 추가

  1. 아직 enableEdgeToEdge를 호출하지 않는 각 Activity의 onCreate에서 setContent 전에 enableEdgeToEdge를 추가합니다.
  2. 소프트 키보드를 사용하는 모든 Activity에 대해 AndroidManifest.xml에 android:windowSoftInputMode="adjustResize"를 추가합니다.

3단계: 삽입(insets) 적용

  • 앱은 반드시 시스템 삽입을 적용하거나 콘텐츠를 눈금자(rulers)에 정렬하여 중요한 UI를 터치 가능한 상태로 유지해야 합니다. 이중 패딩을 방지하려면 한 가지 방법만 선택하세요.

    1. 권장: 사용 가능한 경우 Scaffold를 사용하고 PaddingValues를 콘텐츠 람다에 전달하세요.
    Scaffold { innerPadding ->
        // innerPadding은 시스템 표시줄과 모든 Scaffold 구성 요소를 고려합니다.
        LazyColumn(
            modifier = Modifier
                .fillMaxSize()
                .consumeWindowInsets(innerPadding),
            contentPadding = innerPadding
        ) { /* 콘텐츠 */ }
    }
    
    1. 권장: 사용 가능한 경우 머티리얼 구성 요소의 자동 삽입 처리 또는 패딩 수정자를 사용하세요.

      • Material 3 구성 요소는 자체 구성 요소의 안전 영역을 관리합니다. 여기에는 다음이 포함됩니다:
        • TopAppBar
        • SmallTopAppBar
        • CenterAlignedTopAppBar
        • MediumTopAppBar
        • LargeTopAppBar
        • BottomAppBar
        • ModalDrawerSheet
        • DismissibleDrawerSheet
        • PermanentDrawerSheet
        • ModalBottomSheet
        • NavigationBar
        • NavigationRail
      • Material 2 구성 요소의 경우 windowInsets 매개변수를 사용하여 BottomAppBar, TopAppBar 및 BottomNavigation에 수동으로 삽입을 적용하세요. 상위 컨테이너에 패딩을 적용하지 마세요. 대신 App Bar 구성 요소에 직접 삽입을 전달하세요. 상위 컨테이너에 패딩을 적용하면 App Bar 배경이 시스템 표시줄 영역으로 그려지는 것을 방지합니다. 예를 들어 TopAppBar의 경우 다음 옵션 중 하나만 선택하세요:
        1. 권장: TopAppBar(windowInsets = AppBarDefaults.topAppBarWindowInsets)
        2. TopAppBar(windowInsets = WindowInsets.systemBars.exclude(WindowInsets.navigationBars))
        3. TopAppBar(windowInsets = WindowInsets.systemBars.add(WindowInsets.captionBar))
    2. Scaffold 외부의 구성 요소의 경우 패딩 수정자(예: Modifier.safeDrawingPadding() 또는 Modifier.windowInsetsPadding(WindowInsets.safeDrawing))를 사용하세요.

      Box(
          modifier = Modifier
              .fillMaxSize()
              .safeDrawingPadding()
      ) {
          Button(
              onClick = {},
              modifier = Modifier.align(Alignment.BottomCenter)
          ) {
              Text("로그인")
          }
      }
      
    3. 과도한 패딩이 있는 깊이 중첩된 구성 요소의 경우 WindowInsetsRulers(예: Modifier.fitInside(WindowInsetsRulers.SafeDrawing.current))를 사용하세요. 코드 샘플은 IME 섹션을 참조하세요.

    4. 요소(예: 사용자 정의 헤더 또는 장식용 스크림)가 시스템 표시줄의 치수와 같아야 하는 경우 삽입 크기 수정자(예: Modifier.windowInsetsTopHeight(WindowInsets.systemBars))를 사용하세요. 코드 샘플은 리스트 섹션을 참조하세요.

적응형 스캐폴드(Scaffolds)

  • NavigationSuiteScaffold는 NavigationRail 또는 NavigationBar와 같은 자체 구성 요소의 안전 영역을 관리합니다. 그러나 적응형 스캐폴드(예: NavigationSuiteScaffold, ListDetailPaneScaffold)는 PaddingValues를 내부 콘텐츠로 전파하지 않습니다. 3단계에 설명된 대로 개별 화면 또는 구성 요소(예: 리스트 contentPadding 또는 FAB 패딩)에 삽입을 반드시 적용해야 합니다. NavigationSuiteScaffold 상위에 safeDrawingPadding 또는 유사한 수정자를 적용하지 마세요. 이렇게 하면 클리핑이 발생하고 엣지 투 엣지 화면이 방지됩니다.

IME

  • 소프트 키보드가 있는 각 Activity에 대해 AndroidManifest.xml에 android:windowSoftInputMode="adjustResize"가 설정되어 있는지 확인하세요. SOFT_INPUT_ADJUST_RESIZE는 더 이상 사용되지 않으므로 사용하지 마세요. 그런 다음 입력 필드에 포커스를 유지하세요. 다음 중 하나를 선택하세요:
      1. 권장: 콘텐츠 컨테이너에 Modifier.fitInside(WindowInsetsRulers.Ime.current)를 추가하세요. 계층 구조 상류에서 삽입을 소비하는 것을 잊어서 발생하는 버벅임과 추가 패딩을 줄이므로 imePadding()보다 권장됩니다.
      1. 콘텐츠 컨테이너에 imePadding을 추가하세요. 패딩 수정자는 Modifier.verticalScroll() 전에 배치되어야 합니다. 상위 항목이 이미 contentWindowInsets(예: contentWindowInsets = WindowInsets.safeDrawing)으로 IME를 처리하는 경우 Modifier.imePadding()을 사용하지 마세요. 그렇게 하면 이중 패딩이 발생합니다.

Scaffold가 있는 IME 코드 패턴

올바름

contentWindowInsets에 IME 삽입이 포함되어 있으므로 올바릅니다. 이는 innerPadding으로 콘텐츠 람다에 전달됩니다.

// 올바름
Scaffold(contentWindowInsets = WindowInsets.safeDrawing) { innerPadding ->
    Column(
        modifier = Modifier
            .padding(innerPadding)
            .consumeWindowInsets(innerPadding)
            .verticalScroll(rememberScrollState())
    ) { /* 콘텐츠 */ }
}

contentWindowInsets에 관계없이 fitInside가 콘텐츠를 IME 삽입에 맞추므로 올바릅니다.

// 올바름
Scaffold() { innerPadding ->
    Column(
        modifier = Modifier
            .padding(innerPadding)
            .consumeWindowInsets(innerPadding)
            .fitInside(WindowInsetsRulers.Ime.current)
            .verticalScroll(rememberScrollState())
    ) { /* 콘텐츠 */ }
}

기본 contentWindowInsets는 IME 삽입을 포함하지 않고 imePadding()이 IME 삽입을 적용하므로 올바릅니다.

// 올바름
Scaffold() { innerPadding ->
    Column(
        modifier = Modifier
            .padding(innerPadding)
            .consumeWindowInsets(innerPadding)
            .imePadding()
            .verticalScroll(rememberScrollState())
    ) { /* 콘텐츠 */ }
}

잘못됨

IME가 열릴 때 초과 패딩이 발생하므로 잘못되었습니다. IME 삽입이 두 번 적용됩니다. 한 번은 전달된 contentWindowInsets 값의 IME 삽입을 포함하는 innerPadding으로, 다른 한 번은 imePadding으로 적용됩니다.

// 잘못됨
Scaffold(contentWindowInsets = WindowInsets.safeDrawing) { innerPadding ->
    Column(
        modifier = Modifier
            .padding(innerPadding)
            .imePadding()
            .verticalScroll(rememberScrollState())
    ) { /* 콘텐츠 */ }
}

IME가 콘텐츠를 덮게 되므로 잘못되었습니다. Scaffold의 기본 contentWindowInsets는 IME 삽입을 포함하지 않습니다.

// 잘못됨
Scaffold() { innerPadding ->
    Column(
        modifier = Modifier
            .padding(innerPadding)
            .verticalScroll(rememberScrollState())
    ) { /* 콘텐츠 */ }
}

Scaffold가 없는 IME 코드 패턴

올바름

다음 코드 샘플은 과도한 패딩을 발생시키지 않습니다.

// 올바름
Box(
    // 삽입 소비됨
    modifier = Modifier.safeDrawingPadding() // 또는 imePadding(), safeContentPadding(), safeGesturesPadding()
) {
    Column(
        modifier = Modifier.imePadding()
    ) { /* 콘텐츠 */ }
}

// 올바름
Box(
    // 삽입 소비됨
    modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing) // 또는 WindowInsets.ime, WindowInsets.safeContent, WindowInsets.safeGestures
) {
    Column(
        modifier = Modifier.imePadding()
    ) { /* 콘텐츠 */ }
}

// 올바름
Box(
    // 삽입이 소비되지는 않지만 fitInside 때문에 관련 없음
    modifier = Modifier.padding(WindowInsets.safeDrawing.asPaddingValues()) // 또는 WindowInsets.ime.asPaddingValues(), WindowInsets.safeContent.asPaddingValues(), WindowInsets.safeGestures.asPaddingValues()
) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .fitInside(WindowInsetsRulers.Ime.current)
    ) { /* 콘텐츠 */ }
}

잘못됨

다음 코드 샘플은 IME 삽입이 두 번 적용되어 과도한 패딩을 발생시킵니다.

// 잘못됨
Box(
    // 삽입이 소비되지 않음
    modifier = Modifier.padding(WindowInsets.safeDrawing.asPaddingValues()) // 또는 WindowInsets.ime.asPaddingValues(), WindowInsets.safeContent.asPaddingValues(), WindowInsets.safeGestures.asPaddingValues()
) {
    Column(
        modifier = Modifier.imePadding()
    ) { /* 콘텐츠 */ }
}

내비게이션 바 대비 및 시스템 표시줄 아이콘

  • Activity가 WindowCompat의 enableEdgeToEdge를 사용하는 경우, 밝은 테마와 어두운 테마를 모두 지원하는 앱의 경우 isAppearanceLightNavigationBars 및 isAppearanceLightStatusBars를 장치 테마의 반대로 설정하여 시스템 표시줄 아이콘을 읽을 수 있게 반드시 해야 합니다. 테마 파일에서 이 작업을 수행하는 것이 좋습니다. Activity가 ComponentActivity의 enableEdgeToEdge를 사용하는 경우 이 작업을 수행하지 마세요. 아이콘 색상을 자동으로 처리하기 때문입니다.

    // `WindowCompat`에서 `enableEdgeToEdge`를 호출하는 경우에만 사용하세요.
    // 테마 파일에 적용하세요.
    @Composable
    fun MyTheme(
        darkTheme: Boolean = isSystemInDarkTheme(),
        content: @Composable () -> Unit
    ) {
        val view = LocalView.current
        if (!view.isInEditMode) {
            SideEffect {
                val window = (view.context as? Activity)?.window ?: return@SideEffect
                val controller = WindowCompat.getInsetsController(window, view)
    
                // 밝은 모드(!darkTheme)에는 어두운 아이콘, 어두운 모드에는 밝은 아이콘
                controller.isAppearanceLightStatusBars = !darkTheme
                controller.isAppearanceLightNavigationBars = !darkTheme
            }
        }
    
        MaterialTheme(content = content)
    }
    
  • 화면이 하단 바(예: BottomAppBar, NavigationBar)와 함께 Scaffold 또는 NavigationSuiteScaffold를 사용하는 경우, SDK 29+의 해당 Activity에서 window.isNavigationBarContrastEnforced = false를 설정하세요. 이렇게 하면 시스템이 네비게이션 바에 반투명 배경을 추가하는 것을 방지하여 하단 바 색상이 화면 하단까지 확장되도록 합니다.

리스트

  • 스크롤 가능한 구성 요소(예: LazyColumn, LazyRow)의 contentPadding 매개변수에 삽입 패딩(예: Scaffold의 innerPadding)을 적용하세요. 리스트의 상위 컨테이너에 Modifier.padding()으로 적용하지 마세요. 이렇게 하면 콘텐츠가 잘리고 시스템 표시줄 뒤로 스크롤되는 것이 방지됩니다.
  • 시스템 표시줄을 덮는 반투명 컴포저블을 만들어 아이콘이 계속 읽기 쉽도록 하세요.
class SystemBarProtectionSnippets : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // enableEdgeToEdge는 window.isNavigationBarContrastEnforced = true로 설정합니다.
        // 이는 세 버튼 내비게이션에 반투명 스크림을 추가하는 데 사용됩니다.
        enableEdgeToEdge()

        setContent {
            MyTheme {
                // 주요 콘텐츠
                MyContent()

                // 주요 콘텐츠를 그린 후 상태 표시줄 보호 그리기
                StatusBarProtection()
            }
        }
    }
}

@Composable
private fun StatusBarProtection(
    color: Color = MaterialTheme.colorScheme.surfaceContainer,
) {
    Spacer(
        modifier = Modifier
            .fillMaxWidth()
            .height(
                with(LocalDensity.current) {
                    (WindowInsets.statusBars.getTop(this) * 1.2f).toDp()
                }
            )
            .background(
                brush = Brush.verticalGradient(
                    colors = listOf(
                        color.copy(alpha = 1f),
                        color.copy(alpha = 0.8f),
                        Color.Transparent
                    )
                )
            )
    )
}

다이얼로그

다음 두 조건이 모두 참이면 다이얼로그는 전체 화면이며 엣지 투 엣지로 만들어야 합니다.

  1. DialogProperties에 usePlatformDefaultWidth = false가 포함되어 있습니다.
  2. 다이얼로그가 Modifier.fillMaxSize()를 호출합니다.

전체 화면 다이얼로그를 엣지 투 엣지로 만들려면 DialogProperties에서 decorFitsSystemWindows = false를 설정하세요.

Dialog(
    onDismissRequest = { /* 닫기 처리 */ },
    properties = DialogProperties(
        // 1. 다이얼로그가 화면 전체 너비로 확장되도록 허용
        usePlatformDefaultWidth = false,
        // 2. 다이얼로그가 상태 및 내비게이션 표시줄 뒤에 그려지도록 허용
        decorFitsSystemWindows = false
    )
) { /* 콘텐츠 */ }

체크리스트

  • 모든 Activity가 enableEdgeToEdge()를 호출합니까?
  • AndroidManifest.xml에 adjustResize가 설정되어 있습니까?
  • 모든 TextField, OutlinedTextField 또는 BasicTextField에 imePadding(), fitInside, Modifier.safeDrawingPadding(), Modifier.safeContentPadding(), Modifier.safeGesturesPadding()을 사용하는 부모가 있거나 contentWindowInsets가 WindowInsets.safeDrawing 또는 WindowInsets.ime으로 설정되어 있습니까?
  • 첫 번째 및 마지막 리스트 항목이 삽입을 contentPadding에 전달하여 시스템 표시줄에서 멀어지게 그려집니까?
  • FAB가 Scaffold 내부에 있거나 Modifier.safeDrawingPadding()을 적용하여 내비게이션 바 위에 그려집니까?
  • 프로젝트가 빌드됩니까? ./gradlew build를 실행하여 확인하세요.

Individual skills in this repo

This repo contains 4 individual skills — each has its own dedicated page.

lerchenflo/claudeskills

Upgrades, or migrates, an Android project to use Android Gradle Plugin (AGP) version 9. Do not use this skill for migrating Kotlin Multiplatform (KMP) projects.

lerchenflo/claudeskills

Provides instructions for installing and using the `android` CLI. The `android` command-line tool is a critical tool for Android development and helps you create new Android projects, run Android apps on devices, manage and interact with Android virtual devices (including screenshots and UI inspection), manage Android SDK components, look up official Android documentation, and discover and install official Android skills.

lerchenflo/claudeskills

Manages Android performance profiling and debugging. Triggers when the user asks to record or analyze Android performance data, such as system traces, heap dumps, method recordings, callstack samples, memory allocations, or investigate bottlenecks, jank, memory leaks, and app startup issues on Android, or when the user asks to write, debug, or execute ad-hoc SQL queries. Applies to both user and system apps or services.

lerchenflo/claudeskills

Analyzes Android apps to identify key user workflows for AppFunctions such as creating a note, playing media, or sending an automated or AI agent triggered message, voice commands, or system shortcuts, without needing to open the app UI. Generates Kotlin code to expose these workflows to the Android system, allowing agents to discover and execute them on-device. Also refines KDoc documentation to ensure AI agents correctly understand and use the provided functionality.

관련 스킬