AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Compose Ui

skill-piyushverma0-android-agent-skills-compose-ui · by piyushverma0

|

No reviews yet
0 installs
31 views
0.0% view→install

Install

$ agentstack add skill-piyushverma0-android-agent-skills-compose-ui

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-piyushverma0-android-agent-skills-compose-ui)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
4mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Compose Ui? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Compose UI

24 rules that fix what AI agents consistently get wrong in Jetpack Compose.

CRITICAL rules — get these wrong and the app is broken

1. Edge-to-edge + Scaffold innerPadding

// ✅ Always consume Scaffold's innerPadding
Scaffold(
    topBar = { TopAppBar(title = { Text("Screen") }) },
    bottomBar = { BottomNavBar() }
) { innerPadding ->
    LazyColumn(
        modifier = Modifier.fillMaxSize(),
        contentPadding = innerPadding   // ← pass to content, never ignore
    ) { ... }
}

// ❌ Never ignore innerPadding — content hides under bars
Scaffold { _ ->
    LazyColumn { ... }  // content hidden under top/bottom bars
}

2. ModalBottomSheet navigation bar padding

// ✅ Always add navigationBarsPadding inside BottomSheet content
ModalBottomSheet(onDismissRequest = { ... }) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .navigationBarsPadding()   // ← required, content shifts above nav bar
            .padding(16.dp)
    ) { ... }
}

// ❌ Missing navigationBarsPadding — content hidden behind gesture nav bar
ModalBottomSheet(onDismissRequest = { ... }) {
    Column(modifier = Modifier.padding(16.dp)) { ... }
}

3. Single UiState sealed class per screen

// ✅ One sealed class represents all screen states
sealed interface HomeUiState {
    data object Loading : HomeUiState
    data class Success(val items: List) : HomeUiState
    data class Error(val message: String) : HomeUiState
    data object Empty : HomeUiState
}

// ✅ Always handle all states in the Composable
@Composable
fun HomeScreen(uiState: HomeUiState) {
    when (uiState) {
        is HomeUiState.Loading -> LoadingIndicator()
        is HomeUiState.Success -> ItemList(uiState.items)
        is HomeUiState.Error -> ErrorMessage(uiState.message)
        is HomeUiState.Empty -> EmptyState()
    }
}

// ❌ Never use multiple booleans for state — causes impossible states
data class HomeState(
    val isLoading: Boolean = false,
    val isError: Boolean = false,
    val isEmpty: Boolean = false,
    val items: List = emptyList()
)

4. collectAsStateWithLifecycle — never collectAsState

// ✅ Lifecycle-aware collection — pauses when app is in background
val uiState by viewModel.uiState.collectAsStateWithLifecycle()

// ❌ collectAsState — keeps collecting even when app is backgrounded
val uiState by viewModel.uiState.collectAsState()

5. SharedFlow for one-shot events

// ✅ SharedFlow for navigation events, toasts, dialogs
class HomeViewModel : ViewModel() {
    private val _events = MutableSharedFlow()
    val events: SharedFlow = _events.asSharedFlow()

    fun onItemClick(id: String) {
        viewModelScope.launch {
            _events.emit(HomeEvent.NavigateToDetail(id))
        }
    }
}

// ✅ Collect events with LaunchedEffect
LaunchedEffect(Unit) {
    viewModel.events.collect { event ->
        when (event) {
            is HomeEvent.NavigateToDetail -> navController.navigate("detail/${event.id}")
        }
    }
}

// ❌ Never use StateFlow for one-shot events — events replay on recomposition
private val _navigationEvent = MutableStateFlow(null)

6. LaunchedEffect key rules

// ✅ Use Unit key when effect runs once on composition
LaunchedEffect(Unit) {
    viewModel.loadData()
}

// ✅ Use value key when effect should rerun when value changes
LaunchedEffect(userId) {
    viewModel.loadUser(userId)
}

// ❌ Never use constantly-changing keys — causes infinite re-execution
LaunchedEffect(System.currentTimeMillis()) { ... }

7. No logic in composition — only rendering

// ✅ Logic in ViewModel, only UI state in Composable
@Composable
fun ItemCard(item: Item, onLike: (String) -> Unit) {
    Card(onClick = { onLike(item.id) }) {
        Text(item.title)
    }
}

// ❌ Never compute in composition — triggers recomposition loops
@Composable
fun ItemCard(items: List) {
    val filtered = items.filter { it.isActive }  // computation in composition!
    ...
}

HIGH impact rules

8. LazyColumn — stable keys and contentPadding

// ✅ Always provide stable key= for list items
LazyColumn(
    contentPadding = PaddingValues(16.dp),
    verticalArrangement = Arrangement.spacedBy(8.dp)
) {
    items(items, key = { it.id }) { item ->   // ← stable key prevents recompose
        ItemCard(item)
    }
}

// ❌ No key — full list recomposition on any change
LazyColumn { items(items) { item -> ItemCard(item) } }

9. Modifier order matters

// ✅ Correct order: size → padding → background → clickable
Modifier
    .fillMaxWidth()
    .padding(16.dp)
    .background(MaterialTheme.colorScheme.surface)
    .clickable { ... }

// ❌ Wrong order changes visual result and hit area
Modifier
    .clickable { ... }         // clickable area doesn't include padding
    .padding(16.dp)
    .fillMaxWidth()

10. AnimatedVisibility — always specify enter/exit

// ✅ Explicit animation specs
AnimatedVisibility(
    visible = isVisible,
    enter = fadeIn() + expandVertically(),
    exit = fadeOut() + shrinkVertically()
) { Content() }

// ❌ Default enter/exit looks janky
AnimatedVisibility(visible = isVisible) { Content() }

11. Type-safe Navigation destinations

// ✅ Kotlin Serializable destinations — no string routes
@Serializable
object HomeRoute

@Serializable
data class DetailRoute(val itemId: String)

NavHost(navController, startDestination = HomeRoute) {
    composable { HomeScreen() }
    composable { backStackEntry ->
        val route: DetailRoute = backStackEntry.toRoute()
        DetailScreen(itemId = route.itemId)
    }
}

// ❌ String routes — fragile, no type safety, typos compile
navController.navigate("detail/$itemId")
composable("detail/{itemId}") { ... }

12. Remember variants — use the right one

// ✅ remember for values that survive recomposition but not config change
val scrollState = rememberScrollState()

// ✅ rememberSaveable for values that survive config change (rotation)
var selectedTab by rememberSaveable { mutableStateOf(0) }

// ✅ remember with key — recalculate when key changes
val computation = remember(inputList) { inputList.sortedBy { it.name } }

// ❌ remember without key for derived state — stale value
val sorted = remember { inputList.sortedBy { it.name } }  // never updates

13. DisposableEffect for cleanup

// ✅ DisposableEffect cleans up when composable leaves
DisposableEffect(lifecycleOwner) {
    val observer = LifecycleEventObserver { _, event ->
        if (event == Lifecycle.Event.ON_RESUME) viewModel.refreshData()
    }
    lifecycleOwner.lifecycle.addObserver(observer)
    onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
}

14. Accessibility semantics

// ✅ Custom actions and descriptions for non-text clickables
IconButton(
    onClick = { onLike() },
    modifier = Modifier.semantics {
        contentDescription = "Like ${item.title}"
        role = Role.Button
    }
) { Icon(Icons.Default.Favorite, contentDescription = null) }

// ✅ mergeDescendants for card accessibility
Card(
    modifier = Modifier.semantics(mergeDescendants = true) {}
) { ... }

15. TextField security

// ✅ Password field with correct input type
TextField(
    value = password,
    onValueChange = { password = it },
    visualTransformation = PasswordVisualTransformation(),
    keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
    keyboardActions = KeyboardActions(onDone = { onLogin() })
)

MEDIUM impact rules

16. Multi-preview annotations

// ✅ Test across phones, large screens, and dark mode
@Preview(name = "Phone", device = Devices.PHONE)
@Preview(name = "Tablet", device = Devices.TABLET)
@Preview(name = "Dark", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun HomeScreenPreview() {
    MyAppTheme { HomeScreen(uiState = HomeUiState.Success(sampleItems)) }
}

17. Material 3 — no hardcoded colors

// ✅ Always use theme color roles
Text(text = title, color = MaterialTheme.colorScheme.onSurface)
Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant))

// ❌ Never hardcode colors — breaks dark mode and dynamic color
Text(text = title, color = Color(0xFF333333))

18. Coil image loading

// ✅ AsyncImage with contentScale and placeholder
AsyncImage(
    model = ImageRequest.Builder(LocalContext.current)
        .data(url)
        .crossfade(true)
        .build(),
    contentDescription = description,
    contentScale = ContentScale.Crop,
    placeholder = painterResource(R.drawable.placeholder),
    error = painterResource(R.drawable.error_image),
    modifier = Modifier.fillMaxWidth().aspectRatio(16f/9f)
)

19. WindowInsets — IME keyboard handling

// ✅ Shift content above keyboard automatically
Scaffold(
    modifier = Modifier.imePadding()
) { innerPadding ->
    Column(modifier = Modifier.padding(innerPadding)) {
        TextField(value = text, onValueChange = { text = it })
    }
}

20. Runtime permissions in Compose

// ✅ rememberPermissionState from Accompanist/Compose Permissions
val cameraPermission = rememberPermissionState(Manifest.permission.CAMERA)

LaunchedEffect(Unit) {
    if (!cameraPermission.status.isGranted) {
        cameraPermission.launchPermissionRequest()
    }
}

21. Scaffold with FAB and SnackbarHost

// ✅ Complete Scaffold setup
val snackbarHostState = remember { SnackbarHostState() }

Scaffold(
    topBar = { TopAppBar(title = { Text("Home") }) },
    floatingActionButton = {
        FloatingActionButton(onClick = onCreate) {
            Icon(Icons.Default.Add, contentDescription = "Create")
        }
    },
    snackbarHost = { SnackbarHost(snackbarHostState) }
) { innerPadding ->
    Content(modifier = Modifier.padding(innerPadding))
}

22. Surface vs Box — use Surface for clickable containers

// ✅ Surface handles elevation, ripple, shape, and color role
Surface(
    onClick = { onItemClick(item.id) },
    shape = MaterialTheme.shapes.medium,
    tonalElevation = 2.dp
) { CardContent(item) }

// ❌ Box has no elevation or ripple semantics
Box(modifier = Modifier.clickable { onItemClick(item.id) }) { CardContent(item) }

23. Stateless Composables — hoist state up

// ✅ Stateless composable — testable and reusable
@Composable
fun SearchBar(
    query: String,
    onQueryChange: (String) -> Unit,
    modifier: Modifier = Modifier
) {
    TextField(value = query, onValueChange = onQueryChange, modifier = modifier)
}

// ❌ Stateful — owns state internally, hard to test, not reusable
@Composable
fun SearchBar() {
    var query by remember { mutableStateOf("") }
    TextField(value = query, onValueChange = { query = it })
}

24. Adaptive layout with WindowSizeClass

// ✅ Respond to window size — see adaptive-ui skill for full rules
val windowSizeClass = calculateWindowSizeClass(activity)
when (windowSizeClass.widthSizeClass) {
    WindowWidthSizeClass.Compact -> PhoneLayout()
    WindowWidthSizeClass.Medium -> TabletLayout()
    WindowWidthSizeClass.Expanded -> DesktopLayout()
}

Common Mistakes (Quick Reference)

| ❌ Wrong | ✅ Right | |---|---| | collectAsState() | collectAsStateWithLifecycle() | | Ignoring innerPadding | contentPadding = innerPadding | | Multiple state booleans | Single sealed interface UiState | | String routes | @Serializable route objects | | Hardcoded colors | MaterialTheme.colorScheme.* | | Logic in @Composable | Logic in ViewModel | | StateFlow for events | SharedFlow for events | | No key= in LazyColumn | items(list, key = { it.id }) |

Deep-dive references

  • references/side-effects.md — complete LaunchedEffect / DisposableEffect / SideEffect guide
  • references/state-management.md — derivedStateOf, snapshotFlow, state in multi-module apps
  • references/compose-performance.md — stability, skippable composables, baseline profiles

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.