Communitygithub.com

lerchenflo/claudeskills

使用此技能迁移你的 Jetpack Compose 应用,添加自适应的边到边支持并排查常见问题。修复被导航栏或状态栏遮挡的 UI 组件(如按钮或列表)、IME 插入区域以及系统栏可读性。

claudeskills 是什么?

此技能为 Android 开发者提供了一个全面的指南,用于将 Jetpack Compose 应用迁移至自适应边到边(edge-to-edge)体验。它支持所有使用 Jetpack Compose 并编译目标 SDK 为 35 及以上的 Android 项目。技能涵盖关键步骤:通过 `enableEdgeToEdge` 为每个 Activity 启用边到边支持;正确应用系统插入区域(insets),包括推荐使用 Scaffold 及其 innerPadding、Material 3 组件的自动处理、以及 `safeDrawingPadding` 等修饰符;处理自适应脚手架(如 NavigationSuiteScaffold);通过 `adjustResize` 和 `fitInside` 或 `imePadding` 解决 IME 问题;设置系统栏图标颜色(亮/暗);禁用导航栏对比度强制(`isNavigationBarContrastEnforced`);为列表应用正确的内容内边距;以及使全屏对话框支持边到边。该技能还提供了一个检查清单,用于验证迁移是否完整。通过遵循此技能,开发者可以确保他们的 Compose 应用在各种屏幕尺寸和形态上提供一致且无遮挡的用户界面。

兼容平台✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/lerchenflo/claudeskills/tree/HEAD/android/edge-to-edge

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

前置条件

  • 项目必须使用 Android Jetpack Compose。
  • 项目必须以 SDK 35 或更高版本为目标。如果 SDK 低于 35,请将 SDK 提升至 35。

第一步:规划

  1. 定位并分析所有 Activity 类,检测哪些已有边到边支持。对于每个没有边到边的 Activity,计划使其支持边到边。
  2. 在每个 Activity 中,定位并分析所有列表和 FAB 组件,检测哪些已有边到边支持。对于每个没有边到边支持的组件,计划使其支持边到边。
  3. 在每个 Activity 中,扫描 TextField、OutlinedTextField 或 BasicTextField。如果找到,你必须按照本技能的 IME 部分验证 IME 不会隐藏输入字段。

第二步:添加边到边支持

  1. 在每个尚未调用 enableEdgeToEdge 的 Activity 的 onCreate 中,在 setContent 之前添加 enableEdgeToEdge。
  2. 在 AndroidManifest.xml 中为所有使用软键盘的 Activity 添加 android:windowSoftInputMode="adjustResize"。

第三步:应用插入区域

  • 应用必须应用系统插入区域,或将内容与标尺对齐,以确保关键 UI 保持可点击状态。仅选择一种方法以避免双重内边距:

    1. 首选: 如果可用,使用 Scaffold 并将 PaddingValues 传递给内容 lambda。
    Scaffold { innerPadding ->
        // innerPadding 考虑了系统栏和任何 Scaffold 组件
        LazyColumn(
            modifier = Modifier
                .fillMaxSize()
                .consumeWindowInsets(innerPadding),
            contentPadding = innerPadding
        ) { /* 内容 */ }
    }
    
    1. 首选: 如果可用,使用 Material 组件的自动插入区域处理或内边距修饰符。

      • 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))。请参阅 列表 部分的代码示例。

自适应脚手架

  • NavigationSuiteScaffold 为其自身组件管理安全区域,例如 NavigationRail 或 NavigationBar。但是,自适应脚手架(例如 NavigationSuiteScaffold、ListDetailPaneScaffold)不会将 PaddingValues 传播到其内部内容。你必须按照 第三步 中的描述,将插入区域应用于每个独立的屏幕或组件(例如列表的 contentPadding 或 FAB 内边距)。不要将 safeDrawingPadding 或类似修饰符应用于 NavigationSuiteScaffold 的父级。这会裁剪并阻止边到边屏幕。

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 传递给内容 lambda。

// 正确
Scaffold(contentWindowInsets = WindowInsets.safeDrawing) { innerPadding ->
    Column(
        modifier = Modifier
            .padding(innerPadding)
            .consumeWindowInsets(innerPadding)
            .verticalScroll(rememberScrollState())
    ) { /* 内容 */ }
}

正确,因为 fitInside 使内容适应 IME 插入区域,无论 contentWindowInsets 如何。

// 正确
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 插入区域被应用了两次,一次是通过 innerPadding(它包含从传递的 contentWindowInsets 值中获得的 IME 插入区域),另一次是通过 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,请在相应的 Activity 中为 SDK 29+ 设置 window.isNavigationBarContrastEnforced = false。这可以防止系统向导航栏添加半透明背景,从而确保你的底部栏颜色延伸到屏幕底部。

列表

  • 将插入内边距(如 Scaffold 的 innerPadding)应用于可滚动组件的 contentPadding 参数(例如 LazyColumn、LazyRow)。不要将其作为 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.

相关技能