android-jetpack-compose

par expo

À utiliser lors de la création d'interfaces utilisateur Android avec Jetpack Compose, de la gestion d'état avec remember/mutableStateOf, ou de l'implémentation de motifs d'interface déclaratifs.

npx skills add https://github.com/expo/agent-templates --skill android-jetpack-compose

Android - Jetpack Compose

Modern declarative UI toolkit for building native Android interfaces.

Key Concepts

State Management

Compose provides several ways to manage state:

  • remember: Survives recomposition
  • rememberSaveable: Survives configuration changes
  • mutableStateOf: Creates observable state
  • derivedStateOf: Computed state that updates when dependencies change
@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }

    Column {
        Text("Count: $count")
        Button(onClick = { count++ }) {
            Text("Increment")
        }
    }
}

// With saveable for configuration changes
@Composable
fun SearchField() {
    var query by rememberSaveable { mutableStateOf("") }

    TextField(
        value = query,
        onValueChange = { query = it },
        placeholder = { Text("Search...") }
    )
}

State Hoisting

Lift state up to make composables stateless and reusable:

// Stateless composable
@Composable
fun NameInput(
    name: String,
    onNameChange: (String) -> Unit,
    modifier: Modifier = Modifier
) {
    TextField(
        value = name,
        onValueChange = onNameChange,
        label = { Text("Name") },
        modifier = modifier
    )
}

// Stateful parent
@Composable
fun UserForm() {
    var name by remember { mutableStateOf("") }

    NameInput(
        name = name,
        onNameChange = { name = it }
    )
}

ViewModel Integration

class UserViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(UserUiState())
    val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()

    fun updateName(name: String) {
        _uiState.update { it.copy(name = name) }
    }

    fun saveUser() {
        viewModelScope.launch {
            _uiState.update { it.copy(isLoading = true) }
            try {
                userRepository.save(_uiState.value.toUser())
                _uiState.update { it.copy(isLoading = false, isSaved = true) }
            } catch (e: Exception) {
                _uiState.update { it.copy(isLoading = false, error = e.message) }
            }
        }
    }
}

@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    UserContent(
        uiState = uiState,
        onNameChange = viewModel::updateName,
        onSave = viewModel::saveUser
    )
}

Best Practices

Composable Function Guidelines

// Use Modifier as first optional parameter
@Composable
fun CustomCard(
    title: String,
    modifier: Modifier = Modifier,
    onClick: () -> Unit = {}
) {
    Card(
        modifier = modifier.clickable(onClick = onClick)
    ) {
        Text(
            text = title,
            modifier = Modifier.padding(16.dp)
        )
    }
}

// Use slot APIs for flexible content
@Composable
fun CustomScaffold(
    topBar: @Composable () -> Unit = {},
    bottomBar: @Composable () -> Unit = {},
    content: @Composable (PaddingValues) -> Unit
) {
    Scaffold(
        topBar = topBar,
        bottomBar = bottomBar,
        content = content
    )
}

Efficient Recomposition

// Use keys for list items
@Composable
fun UserList(users: List<User>) {
    LazyColumn {
        items(
            items = users,
            key = { it.id }  // Stable key for efficient updates
        ) { user ->
            UserItem(user)
        }
    }
}

// Use derivedStateOf for expensive computations
@Composable
fun FilteredList(items: List<Item>, query: String) {
    val filteredItems by remember(items, query) {
        derivedStateOf {
            items.filter { it.name.contains(query, ignoreCase = true) }
        }
    }

    LazyColumn {
        items(filteredItems) { item ->
            ItemRow(item)
        }
    }
}

Side Effects

// LaunchedEffect for coroutine-based side effects
@Composable
fun UserProfile(userId: String, viewModel: UserViewModel) {
    LaunchedEffect(userId) {
        viewModel.loadUser(userId)
    }

    // UI content
}

// DisposableEffect for cleanup
@Composable
fun LifecycleAwareComponent(lifecycle: Lifecycle) {
    DisposableEffect(lifecycle) {
        val observer = LifecycleEventObserver { _, event ->
            // Handle lifecycle events
        }
        lifecycle.addObserver(observer)

        onDispose {
            lifecycle.removeObserver(observer)
        }
    }
}

// SideEffect for non-suspend side effects
@Composable
fun AnalyticsScreen(screenName: String) {
    SideEffect {
        analytics.logScreenView(screenName)
    }
}

Common Patterns

Navigation with Navigation Compose

@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)
        }
    }
}

Material 3 Theming

@Composable
fun AppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        darkTheme -> darkColorScheme(
            primary = Purple80,
            secondary = PurpleGrey80,
            tertiary = Pink80
        )
        else -> lightColorScheme(
            primary = Purple40,
            secondary = PurpleGrey40,
            tertiary = Pink40
        )
    }

    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}

// Using theme values
@Composable
fun ThemedCard() {
    Card(
        colors = CardDefaults.cardColors(
            containerColor = MaterialTheme.colorScheme.surfaceVariant
        )
    ) {
        Text(
            text = "Themed content",
            style = MaterialTheme.typography.bodyLarge,
            color = MaterialTheme.colorScheme.onSurfaceVariant
        )
    }
}

Lists and Grids

@Composable
fun ProductGrid(products: List<Product>) {
    LazyVerticalGrid(
        columns = GridCells.Adaptive(minSize = 160.dp),
        contentPadding = PaddingValues(16.dp),
        horizontalArrangement = Arrangement.spacedBy(16.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        items(products, key = { it.id }) { product ->
            ProductCard(product)
        }
    }
}

// Sticky headers
@Composable
fun ContactList(contacts: Map<Char, List<Contact>>) {
    LazyColumn {
        contacts.forEach { (initial, contactsForInitial) ->
            stickyHeader {
                Text(
                    text = initial.toString(),
                    modifier = Modifier
                        .fillMaxWidth()
                        .background(MaterialTheme.colorScheme.surface)
                        .padding(16.dp),
                    style = MaterialTheme.typography.titleMedium
                )
            }
            items(contactsForInitial) { contact ->
                ContactItem(contact)
            }
        }
    }
}

Anti-Patterns

Avoid Side Effects in Composition

Bad:

@Composable
fun BadExample(viewModel: ViewModel) {
    viewModel.loadData()  // Called on every recomposition!

    Text("Data loaded")
}

Good:

@Composable
fun GoodExample(viewModel: ViewModel) {
    LaunchedEffect(Unit) {
        viewModel.loadData()
    }

    Text("Data loaded")
}

Don't Read State in Remember Block

Bad:

@Composable
fun BadCounter(initial: Int) {
    // Won't update when initial changes
    var count by remember { mutableStateOf(initial) }
}

Good:

@Composable
fun GoodCounter(initial: Int) {
    var count by remember(initial) { mutableStateOf(initial) }
}

Avoid Heavy Computation During Composition

Bad:

@Composable
fun BadList(items: List<Item>) {
    // Runs on every recomposition
    val sorted = items.sortedBy { it.name }
    LazyColumn { /* ... */ }
}

Good:

@Composable
fun GoodList(items: List<Item>) {
    val sorted by remember(items) {
        derivedStateOf { items.sortedBy { it.name } }
    }
    LazyColumn { /* ... */ }
}

Related Skills

  • android-architecture: MVVM and clean architecture patterns
  • android-kotlin-coroutines: Async operations in Compose

Plus de skills de expo

expo-upgrade
expo
Framework (OSS). Directives pour la mise à jour des versions du SDK Expo et la résolution des problèmes de dépendances.
apidevelopmentofficial
expo-data-fetching
expo
Framework (OSS). Use when implementing or debugging ANY network request, API call, or data fetching. Covers fetch API, React Query, SWR, error handling, caching, offline support, and Expo Router data loaders (`useLoaderData`).
official
android-e2e-testing
expo
Testez les fonctionnalités d'Expo Router sur des émulateurs Android à l'aide d'ADB. À utiliser après avoir implémenté des fonctionnalités Android natives ou lors de la vérification du comportement de l'interface utilisateur sur Android.
official
expo-dev-client
expo
Construisez des clients de développement Expo personnalisés pour tester du code natif sur des appareils physiques via EAS Build ou localement. Nécessaire uniquement lors de l'utilisation de modules natifs personnalisés, de cibles Apple (widgets, app clips) ou de code natif tiers non présent dans Expo Go ; essayez d'abord Expo Go avec npx expo start. Prend en charge les builds cloud avec soumission automatique à TestFlight ou les builds locales sur votre machine, produisant des fichiers .ipa (iOS) ou .apk/.aab (Android). Nécessite une configuration eas.json avec un profil de développement qui définit...
official
expo-tailwind-setup
expo
Style universel Tailwind CSS v4 pour les applications Expo sur iOS, Android et Web. Intègre Tailwind CSS v4 avec react-native-css et NativeWind v5 via le transformateur Metro, éliminant le besoin de configuration Babel. Fournit une bibliothèque de composants réutilisables enveloppés en CSS (View, Text, ScrollView, Pressable, TextInput, Image, Link) qui appliquent les styles className aux éléments natifs. Prend en charge le style spécifique à la plateforme via les requêtes média CSS, les variables de thème personnalisées via les blocs @theme, et la sémantique Apple...
official
swiftui-expert-skill
expo
Écrire, réviser ou améliorer du code SwiftUI en suivant les meilleures pratiques pour la gestion d'état, la composition de vues, les performances, les API spécifiques à macOS et iOS 26+ Liquid…
official
eas-update-insights
expo
Vérifier l’état des mises à jour EAS publiées : taux de crash, nombres d’installations/lancements, utilisateurs uniques, taille des charges utiles, et la répartition entre utilisateurs embarqués et OTA par…
official
expo-module
expo
Guide pour créer et rédiger des modules et vues natifs Expo en utilisant l'API Expo Modules (Swift, Kotlin, TypeScript). Couvre le DSL de définition de module, les éléments natifs…
official