android-jetpack-compose

oleh expo

Gunakan saat membangun UI Android dengan Jetpack Compose, mengelola state dengan remember/mutableStateOf, atau menerapkan pola UI deklaratif.

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

Lebih banyak skill dari expo

expo-upgrade
expo
Kerangka kerja (OSS). Panduan untuk meningkatkan versi SDK Expo dan memperbaiki masalah dependensi
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
Uji fitur Expo Router di emulator Android menggunakan ADB. Gunakan setelah mengimplementasikan fitur native Android atau saat memverifikasi perilaku UI di Android.
official
expo-dev-client
expo
Bangun klien pengembangan Expo kustom untuk menguji kode native pada perangkat fisik melalui EAS Build atau secara lokal. Hanya diperlukan saat menggunakan modul native kustom, target Apple (widget, app clips), atau kode native pihak ketiga yang tidak ada di Expo Go; coba Expo Go terlebih dahulu dengan npx expo start. Mendukung build cloud dengan pengiriman TestFlight otomatis atau build lokal di mesin Anda, menghasilkan file .ipa (iOS) atau .apk / .aab (Android). Memerlukan konfigurasi eas.json dengan profil pengembangan yang mengatur...
official
swiftui-expert-skill
expo
Write, review, or improve SwiftUI code following best practices for state management, view composition, performance, macOS-specific APIs, and iOS 26+ Liquid…
official
android-e2e-testing
expo
Uji fitur Expo Router pada emulator Android menggunakan ADB. Gunakan setelah mengimplementasikan fitur Android native atau saat memverifikasi perilaku UI di Android.
official
expo-api-docs
expo
Tulis komentar TSDoc untuk API Expo SDK mengikuti konvensi resmi. WAJIB DIGUNAKAN saat memperkenalkan API TypeScript baru yang menghadap pengguna di paket expo-* - dokumentasikan…
official
expo-review
expo
Jalankan peninjau kode AI Expo yang telah dikonfigurasi pada perubahan lokal atau pull request expo/expo, rangkum temuan dan cakupan peninjau, pertahankan pratinjau PR secara default…
official