Communitygithub.com

lerchenflo/claudeskills

Utilisez cette compétence pour migrer votre application Jetpack Compose afin d'ajouter la prise en charge adaptative du mode edge-to-edge et résoudre les problèmes courants. Elle permet de corriger les composants d'interface utilisateur (comme les boutons ou les listes) masqués ou chevauchant la barre de navigation ou la barre d'état, de résoudre les problèmes d'IME et d'assurer la lisibilité des barres système.

Qu'est-ce que claudeskills ?

Cette compétence est conçue pour les développeurs Android utilisant Jetpack Compose (ciblant SDK 35 ou supérieur) qui souhaitent ajouter ou corriger le mode edge-to-edge dans leur application. Elle automatise les étapes essentielles : détection des activités sans edge-to-edge, ajout de `enableEdgeToEdge` dans `onCreate`, configuration de `android:windowSoftInputMode="adjustResize"` pour le clavier logiciel, et application correcte des insets pour les composants Material (Scaffold, TopAppBar, BottomAppBar, etc.). La compétence propose des choix de méthodes (modifiers de padding, WindowInsetsRulers, contentWindowInsets) et fournit des exemples de code corrects et incorrects pour éviter les doubles marges. Elle traite également les IME avec des motifs spécifiques pour Scaffold et sans Scaffold, le contraste des barres de navigation, les listes, les boîtes de dialogue plein écran, et inclut une checklist de validation. Compatible avec les agents comme Claude Code, elle guide le développeur étape par étape dans la migration tout en évitant les erreurs fréquentes de configuration des insets.

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

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Prérequis

  • Le projet DOIT utiliser Android Jetpack Compose.
  • Le projet DOIT cibler le SDK 35 ou supérieur. Si le SDK est inférieur à 35, augmentez-le à 35.

Étape 1 : planification

  1. Localisez et analysez toutes les classes Activity pour détecter celles qui ont déjà une prise en charge edge-to-edge. Pour chaque Activity sans edge-to-edge, planifiez de la rendre edge-to-edge.
  2. Dans chaque Activity, localisez et analysez tous les composants de liste et FAB pour détecter ceux qui ont déjà une prise en charge edge-to-edge. Pour chaque composant sans edge-to-edge, planifiez de le rendre edge-to-edge.
  3. Dans chaque Activity, recherchez les TextField, OutlinedTextField ou BasicTextField. Si vous en trouvez, vous DEVEZ vérifier que l'IME ne masque pas le champ de saisie en suivant la section IME de cette compétence.

Étape 2 : ajouter la prise en charge edge-to-edge

  1. Ajoutez enableEdgeToEdge avant setContent dans onCreate pour chaque Activity qui n'appelle pas déjà enableEdgeToEdge.
  2. Ajoutez android:windowSoftInputMode="adjustResize" dans AndroidManifest.xml pour toutes les Activities qui utilisent un clavier logiciel.

Étape 3 : appliquer les insets

  • L'application DOIT appliquer les insets système, ou aligner le contenu sur les règles, afin que les éléments critiques de l'interface restent cliquables. Choisissez une seule méthode pour éviter un double espacement :

    1. PRÉFÉRÉ : Lorsque disponible, utilisez Scaffold et passez PaddingValues au lambda de contenu.
    Scaffold { innerPadding ->
        // innerPadding prend en compte les barres système et les composants du Scaffold
        LazyColumn(
            modifier = Modifier
                .fillMaxSize()
                .consumeWindowInsets(innerPadding),
            contentPadding = innerPadding
        ) { /* Contenu */ }
    }
    
    1. PRÉFÉRÉ : Lorsque disponible, utilisez la gestion automatique des insets ou les modificateurs de remplissage dans les composants Material.

      • Les composants Material 3 gèrent les zones de sécurité pour leurs propres composants, notamment :
        • TopAppBar
        • SmallTopAppBar
        • CenterAlignedTopAppBar
        • MediumTopAppBar
        • LargeTopAppBar
        • BottomAppBar
        • ModalDrawerSheet
        • DismissibleDrawerSheet
        • PermanentDrawerSheet
        • ModalBottomSheet
        • NavigationBar
        • NavigationRail
      • Pour les composants Material 2, utilisez le paramètre windowInsets pour appliquer manuellement les insets pour BottomAppBar, TopAppBar et BottomNavigation. N'APPLIQUEZ PAS de remplissage au conteneur parent ; passez plutôt les insets directement au composant App Bar. Appliquer un remplissage au conteneur parent empêche le fond de l'App Bar de s'étendre dans la zone de la barre système. Par exemple, pour TopAppBar, choisissez une seule des options suivantes :
        1. PRÉFÉRÉ : TopAppBar(windowInsets = AppBarDefaults.topAppBarWindowInsets)
        2. TopAppBar(windowInsets = WindowInsets.systemBars.exclude(WindowInsets.navigationBars))
        3. TopAppBar(windowInsets = WindowInsets.systemBars.add(WindowInsets.captionBar))
    2. Pour les composants en dehors d'un Scaffold, utilisez des modificateurs de remplissage, tels que Modifier.safeDrawingPadding() ou Modifier.windowInsetsPadding(WindowInsets.safeDrawing).

      Box(
          modifier = Modifier
              .fillMaxSize()
              .safeDrawingPadding()
      ) {
          Button(
              onClick = {},
              modifier = Modifier.align(Alignment.BottomCenter)
          ) {
              Text("Connexion")
          }
      }
      
    3. Pour les composants profondément imbriqués avec un espacement excessif, utilisez WindowInsetsRulers (par exemple Modifier.fitInside(WindowInsetsRulers.SafeDrawing.current)). Voir la section IME pour un exemple de code.

    4. Lorsque vous avez besoin qu'un élément (par exemple un en-tête personnalisé ou un effet de scrim décoratif) ait les mêmes dimensions qu'une barre système, utilisez des modificateurs de taille d'inset (par exemple Modifier.windowInsetsTopHeight(WindowInsets.systemBars)). Voir la section Listes pour un exemple de code.

Scaffolds adaptatifs

  • NavigationSuiteScaffold gère les zones de sécurité pour ses propres composants, comme NavigationRail ou NavigationBar. Cependant, les scaffolds adaptatifs (par exemple NavigationSuiteScaffold, ListDetailPaneScaffold) ne propagent pas PaddingValues à leur contenu interne. Vous DEVEZ appliquer les insets aux écrans ou composants individuels (par exemple contentPadding d'une liste ou remplissage d'un FAB) comme décrit dans Étape 3. N'APPLIQUEZ PAS safeDrawingPadding ou des modificateurs similaires au parent NavigationSuiteScaffold. Cela couperait le contenu et empêcherait un affichage edge-to-edge.

IME

  • Pour chaque Activity avec un clavier logiciel, vérifiez que android:windowSoftInputMode="adjustResize" est défini dans AndroidManifest.xml. N'utilisez PAS SOFT_INPUT_ADJUST_RESIZE car il est déprécié. Ensuite, conservez le focus sur le champ de saisie. Choisissez une option :
      1. PRÉFÉRÉ : Ajoutez Modifier.fitInside(WindowInsetsRulers.Ime.current) au conteneur de contenu. Ceci est préféré à imePadding() car cela réduit les à-coups et le remplissage supplémentaire causé par l'oubli de consommer les insets en amont dans la hiérarchie.
      1. Ajoutez imePadding au conteneur de contenu. Le modificateur de remplissage DOIT être placé avant Modifier.verticalScroll(). N'utilisez PAS Modifier.imePadding() si le parent prend déjà en compte l'IME avec contentWindowInsets (par exemple contentWindowInsets = WindowInsets.safeDrawing). Cela provoquerait un double remplissage.

Motifs de code IME avec Scaffold

CORRECT

CORRECT car contentWindowInsets contient les insets IME, qui sont transmis au lambda de contenu sous forme de innerPadding.

// CORRECT
Scaffold(contentWindowInsets = WindowInsets.safeDrawing) { innerPadding ->
    Column(
        modifier = Modifier
            .padding(innerPadding)
            .consumeWindowInsets(innerPadding)
            .verticalScroll(rememberScrollState())
    ) { /* Contenu */ }
}

CORRECT car fitInside ajuste le contenu aux insets IME indépendamment de contentWindowInsets.

// CORRECT
Scaffold() { innerPadding ->
    Column(
        modifier = Modifier
            .padding(innerPadding)
            .consumeWindowInsets(innerPadding)
            .fitInside(WindowInsetsRulers.Ime.current)
            .verticalScroll(rememberScrollState())
    ) { /* Contenu */ }
}

CORRECT car le contentWindowInsets par défaut ne contient pas les insets IME, et imePadding() applique les insets IME :

// CORRECT
Scaffold() { innerPadding ->
    Column(
        modifier = Modifier
            .padding(innerPadding)
            .consumeWindowInsets(innerPadding)
            .imePadding()
            .verticalScroll(rememberScrollState())
    ) { /* Contenu */ }
}

INCORRECT

INCORRECT car il y aura un remplissage excessif lorsque l'IME s'ouvre. Les insets IME sont appliqués deux fois, une fois avec innerPadding, qui contient les insets IME des valeurs contentWindowInsets transmises, et une fois avec imePadding :

// INCORRECT
Scaffold(contentWindowInsets = WindowInsets.safeDrawing) { innerPadding ->
    Column(
        modifier = Modifier
            .padding(innerPadding)
            .imePadding()
            .verticalScroll(rememberScrollState())
    ) { /* Contenu */ }
}

INCORRECT car l'IME masquera le contenu. Le contentWindowInsets par défaut de Scaffold ne contient PAS les insets IME.

// INCORRECT
Scaffold() { innerPadding ->
    Column(
        modifier = Modifier
            .padding(innerPadding)
            .verticalScroll(rememberScrollState())
    ) { /* Contenu */ }
}

Motifs de code IME sans Scaffold

CORRECT

Les exemples de code suivants NE PROVOQUERONT PAS de remplissage excessif.

// CORRECT
Box(
    // Insets consommés
    modifier = Modifier.safeDrawingPadding() // ou imePadding(), safeContentPadding(), safeGesturesPadding()
) {
    Column(
        modifier = Modifier.imePadding()
    ) { /* Contenu */ }
}

// CORRECT
Box(
    // Insets consommés
    modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing) // ou WindowInsets.ime, WindowInsets.safeContent, WindowInsets.safeGestures
) {
    Column(
        modifier = Modifier.imePadding()
    ) { /* Contenu */ }
}

// CORRECT
Box(
    // Insets non consommés, mais sans importance à cause de fitInside
    modifier = Modifier.padding(WindowInsets.safeDrawing.asPaddingValues()) // ou WindowInsets.ime.asPaddingValues(), WindowInsets.safeContent.asPaddingValues(), WindowInsets.safeGestures.asPaddingValues()
) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .fitInside(WindowInsetsRulers.Ime.current)
    ) { /* Contenu */ }
}

INCORRECT

L'exemple de code suivant PROVOQUERA un remplissage excessif car les insets IME sont appliqués deux fois :

// INCORRECT
Box(
    // Insets non consommés
    modifier = Modifier.padding(WindowInsets.safeDrawing.asPaddingValues()) // ou WindowInsets.ime.asPaddingValues(), WindowInsets.safeContent.asPaddingValues(), WindowInsets.safeGestures.asPaddingValues()
) {
    Column(
        modifier = Modifier.imePadding()
    ) { /* Contenu */ }
}

Contraste de la barre de navigation et icônes des barres système

  • Si l'Activity utilise enableEdgeToEdge depuis WindowCompat, vous DEVEZ définir isAppearanceLightNavigationBars et isAppearanceLightStatusBars sur l'inverse du thème de l'appareil pour les applications prenant en charge les thèmes clair et sombre, afin que les icônes de la barre système soient lisibles. Il est recommandé de le faire dans votre fichier de thème. NE le faites PAS si les Activities utilisent enableEdgeToEdge depuis ComponentActivity car il gère automatiquement les couleurs des icônes.

    // À utiliser uniquement si vous appelez `enableEdgeToEdge` depuis `WindowCompat`.
    // Appliquez à votre fichier de thème.
    @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)
    
                // Icônes sombres pour le mode clair (!darkTheme), icônes claires pour le mode sombre
                controller.isAppearanceLightStatusBars = !darkTheme
                controller.isAppearanceLightNavigationBars = !darkTheme
            }
        }
    
        MaterialTheme(content = content)
    }
    
  • Si un écran utilise un Scaffold ou un NavigationSuiteScaffold avec une barre inférieure (par exemple BottomAppBar, NavigationBar), définissez window.isNavigationBarContrastEnforced = false dans l'Activity correspondante pour le SDK 29+. Cela empêche le système d'ajouter un fond translucide à la barre de navigation, garantissant ainsi que les couleurs de votre barre inférieure s'étendent jusqu'en bas de l'écran.

Listes

  • Appliquez le remplissage des insets (comme innerPadding de Scaffold) au paramètre contentPadding des composants défilables (par exemple LazyColumn, LazyRow). NE l'appliquez PAS comme Modifier.padding() au conteneur parent de la liste, car cela couperait le contenu et l'empêcherait de défiler derrière les barres système.
  • Créez un composable translucide recouvrant la barre système pour que les icônes restent lisibles.
class SystemBarProtectionSnippets : ComponentActivity() {

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

        // enableEdgeToEdge définit window.isNavigationBarContrastEnforced = true
        // ce qui sert à ajouter un voile translucide pour la navigation à trois boutons
        enableEdgeToEdge()

        setContent {
            MyTheme {
                // Contenu principal
                MyContent()

                // Après avoir dessiné le contenu principal, dessinez la protection de la barre d'état
                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
                    )
                )
            )
    )
}

Boîtes de dialogue

Si les deux conditions suivantes sont vraies, alors la boîte de dialogue est en plein écran et doit être rendue edge-to-edge :

  1. Les DialogProperties contiennent usePlatformDefaultWidth = false.
  2. La boîte de dialogue appelle Modifier.fillMaxSize().

Pour rendre une boîte de dialogue plein écran edge-to-edge, définissez decorFitsSystemWindows = false dans les DialogProperties.

Dialog(
    onDismissRequest = { /* Gérer la fermeture */ },
    properties = DialogProperties(
        // 1. Permet à la boîte de dialogue de s'étendre sur toute la largeur de l'écran
        usePlatformDefaultWidth = false,
        // 2. Permet à la boîte de dialogue de se dessiner derrière les barres de navigation et d'état
        decorFitsSystemWindows = false
    )
) { /* Contenu */ }

Liste de vérification

  • Chaque Activity appelle-t-elle enableEdgeToEdge() ?
  • adjustResize est-il défini dans AndroidManifest.xml ?
  • Chaque TextField, OutlinedTextField ou BasicTextField a-t-il un parent avec imePadding(), fitInside, Modifier.safeDrawingPadding(), Modifier.safeContentPadding(), Modifier.safeGesturesPadding(), ou contentWindowInsets défini sur WindowInsets.safeDrawing ou WindowInsets.ime ?
  • Le premier et le dernier élément de la liste sont-ils éloignés des barres système en passant les insets à contentPadding ?
  • Les FAB sont-ils dessinés au-dessus des barres de navigation, soit en étant à l'intérieur d'un Scaffold, soit en appliquant Modifier.safeDrawingPadding() ?
  • Le projet se compile-t-il ? Exécutez ./gradlew build pour vous en assurer.

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.

Skills associés