android-jetpack-compose

bởi expo

Sử dụng khi xây dựng giao diện Android với Jetpack Compose, quản lý trạng thái bằng remember/mutableStateOf, hoặc triển khai các mẫu giao diện khai báo.

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

Thêm skills từ expo

expo-upgrade
expo
Khung (Mã nguồn mở). Hướng dẫn nâng cấp phiên bản Expo SDK và sửa lỗi phụ thuộc
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
Kiểm tra các tính năng Expo Router trên trình giả lập Android bằng ADB. Sử dụng sau khi triển khai các tính năng Android gốc hoặc khi xác minh hành vi giao diện người dùng trên Android.
official
expo-dev-client
expo
Xây dựng các bản phát triển Expo tùy chỉnh để kiểm tra mã gốc trên thiết bị vật lý qua EAS Build hoặc cục bộ. Chỉ cần thiết khi sử dụng các mô-đun gốc tùy chỉnh, mục tiêu Apple (widget, app clips) hoặc mã gốc của bên thứ ba không có trong Expo Go; hãy thử Expo Go trước với lệnh npx expo start. Hỗ trợ xây dựng đám mây với tính năng tự động gửi lên TestFlight hoặc xây dựng cục bộ trên máy của bạn, xuất ra tệp .ipa (iOS) hoặc .apk / .aab (Android). Yêu cầu cấu hình eas.json với một hồ sơ phát triển thiết lập...
official
expo-tailwind-setup
expo
Kiểu dáng Tailwind CSS v4 phổ quát cho ứng dụng Expo trên iOS, Android và Web. Tích hợp Tailwind CSS v4 với react-native-css và NativeWind v5 thông qua Metro transformer, loại bỏ nhu cầu cấu hình Babel. Cung cấp thư viện component bọc CSS có thể tái sử dụng (View, Text, ScrollView, Pressable, TextInput, Image, Link) áp dụng className cho các phần tử gốc. Hỗ trợ kiểu dáng theo nền tảng thông qua truy vấn phương tiện CSS, biến theme tùy chỉnh qua khối @theme, và ngữ nghĩa Apple...
official
swiftui-expert-skill
expo
Viết, xem xét hoặc cải thiện mã SwiftUI theo các phương pháp tốt nhất về quản lý trạng thái, cấu trúc view, hiệu suất, API dành riêng cho macOS và iOS 26+ Liquid…
official
eas-update-insights
expo
Kiểm tra tình trạng của các bản cập nhật EAS đã phát hành: tỷ lệ sập, số lần cài đặt/khởi chạy, người dùng duy nhất, kích thước tải trọng và sự phân chia giữa người dùng nhúng và OTA theo…
official
expo-module
expo
Hướng dẫn tạo và viết các mô-đun gốc và chế độ xem của Expo bằng Expo Modules API (Swift, Kotlin, TypeScript). Bao gồm DSL định nghĩa mô-đun, gốc…
official