Generated banner for Mobile Android Design
Generated logo for Mobile Android Design

Mobile Android Design

Seth HobsonCommunityCreated by Seth Hobson

  • Skill
  • Tool integration
  • Software development
  • MIT
  • No issues found

From the author

Master Material Design 3 and Jetpack Compose patterns for building native Android apps. Use when designing Android interfaces, implementing Compose UI, or following Google's Material Design guidelines.
— the description by Seth Hobson

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill provides a comprehensive guide to building native Android interfaces using Jetpack Compose and Material Design 3. It covers essential UI components, navigation patterns, and best practices for creating responsive, accessible, and performant Android applications. The content includes practical code examples for common UI elements like lists, forms, and navigation structures, alongside guidance on state management and theming.

What it does

  • Provides patterns for implementing Material Design 3 components and theming.
  • Demonstrates building UI layouts using Jetpack Compose.
  • Explains type-safe navigation implementation with Navigation Compose.
  • Offers best practices for state management and recomposition performance.
  • Includes examples for common UI patterns like lazy lists, swipe-to-dismiss, and forms.
  • Covers accessibility considerations for Android interface design.

When to use it

  • When designing or building native Android application interfaces.
  • When implementing modern UI components using Jetpack Compose.
  • When migrating or adopting Material Design 3 guidelines in an Android project.
  • When setting up navigation flows in a Compose-based Android app.
  • When optimizing Android UI for different screen sizes and configurations.

Advantages

  • Promotes consistent UI design using official Material 3 standards.
  • Provides ready-to-use code snippets for common Android UI components.
  • Encourages best practices for performance and state management in Compose.
  • Facilitates the creation of adaptive and accessible Android layouts.

Usage examples

“How do I implement a list with swipe-to-dismiss functionality in Jetpack Compose?”
The skill provides a SwipeableItem component example using SwipeToDismissBox and rememberSwipeToDismissBoxState to handle deletion logic.
“Show me how to set up type-safe navigation between screens in my Android app.”
The skill demonstrates using @Serializable objects and data classes with NavHost and composable functions to manage navigation routes.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/wshobson/mobile-android-design/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip mobile-android-design.zip -d .agents/skills

Documentation by Seth Hobson

Android Mobile Design

Master Material Design 3 (Material You) and Jetpack Compose to build modern, adaptive Android applications that integrate seamlessly with the Android ecosystem.

When to Use This Skill

  • Designing Android app interfaces following Material Design 3

  • Building Jetpack Compose UI and layouts

  • Implementing Android navigation patterns (Navigation Compose)

  • Creating adaptive layouts for phones, tablets, and foldables

  • Using Material 3 theming with dynamic colors

  • Building accessible Android interfaces

  • Implementing Android-specific gestures and interactions

  • Designing for different screen configurations

Detailed section: Core Concepts

Originally a 9201-byte section in this SKILL.md. Moved to references/details.md to fit Codex's 8 KB skill body cap.

Quick Start Component

@Composable
fun ItemListCard(
    item: Item,
    onItemClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    Card(
        onClick = onItemClick,
        modifier = modifier.fillMaxWidth(),
        shape = RoundedCornerShape(12.dp)
    ) {
        Row(
            modifier = Modifier
                .padding(16.dp)
                .fillMaxWidth(),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Box(
                modifier = Modifier
                    .size(48.dp)
                    .clip(CircleShape)
                    .background(MaterialTheme.colorScheme.primaryContainer),
                contentAlignment = Alignment.Center
            ) {
                Icon(
                    imageVector = Icons.Default.Star,
                    contentDescription = null,
                    tint = MaterialTheme.colorScheme.onPrimaryContainer
                )
            }

            Spacer(modifier = Modifier.width(16.dp))

            Column(modifier = Modifier.weight(1f)) {
                Text(
                    text = item.title,
                    style = MaterialTheme.typography.titleMedium
                )
                Text(
                    text = item.subtitle,
                    style = MaterialTheme.typography.bodyMedium,
                    color = MaterialTheme.colorScheme.onSurfaceVariant
                )
            }

            Icon(
                imageVector = Icons.Default.ChevronRight,
                contentDescription = null,
                tint = MaterialTheme.colorScheme.onSurfaceVariant
            )
        }
    }
}

Best Practices

  1. Use Material Theme: Access colors via MaterialTheme.colorScheme for automatic dark mode support

  2. Support Dynamic Color: Enable dynamic color on Android 12+ for personalization

  3. Adaptive Layouts: Use WindowSizeClass for responsive designs

  4. Content Descriptions: Add contentDescription to all interactive elements

  5. Touch Targets: Minimum 48dp touch targets for accessibility

  6. State Hoisting: Hoist state to make components reusable and testable

  7. Remember Properly: Use remember and rememberSaveable appropriately

  8. Preview Annotations: Add @Preview with different configurations

Common Issues

  • Recomposition Issues: Avoid passing unstable lambdas; use remember

  • State Loss: Use rememberSaveable for configuration changes

  • Performance: Use LazyColumn instead of Column for long lists

  • Theme Leaks: Ensure MaterialTheme wraps all composables

  • Navigation Crashes: Handle back press and deep links properly

  • Memory Leaks: Cancel coroutines in DisposableEffect

Files

references/android-navigation.md

Android Navigation Patterns

Navigation Compose Basics

Setup and Dependencies
// build.gradle.kts
dependencies {
    implementation("androidx.navigation:navigation-compose:2.7.7")
    // For type-safe navigation (recommended)
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
}
Basic Navigation
@Serializable
object Home

@Serializable
data class Detail(val itemId: String)

@Serializable
object Settings

@Composable
fun AppNavigation() {
    val navController = rememberNavController()

    NavHost(
        navController = navController,
        startDestination = Home
    ) {
        composable<Home> {
            HomeScreen(
                onItemClick = { itemId ->
                    navController.navigate(Detail(itemId))
                },
                onSettingsClick = {
                    navController.navigate(Settings)
                }
            )
        }

        composable<Detail> { backStackEntry ->
            val detail: Detail = backStackEntry.toRoute()
            DetailScreen(
                itemId = detail.itemId,
                onBack = { navController.popBackStack() }
            )
        }

        composable<Settings> {
            SettingsScreen(
                onBack = { navController.popBackStack() }
            )
        }
    }
}
Navigation with Arguments
// Type-safe routes with arguments
@Serializable
data class ProductDetail(
    val productId: String,
    val category: String,
    val fromSearch: Boolean = false
)

@Serializable
data class UserProfile(
    val userId: Long
)

@Composable
fun NavigationWithArgs() {
    val navController = rememberNavController()

    NavHost(navController = navController, startDestination = Home) {
        composable<Home> {
            HomeScreen(
                onProductClick = { productId, category ->
                    navController.navigate(
                        ProductDetail(
                            productId = productId,
                            category = category,
                            fromSearch = false
                        )
                    )
                }
            )
        }

        composable<ProductDetail> { backStackEntry ->
            val args: ProductDetail = backStackEntry.toRoute()
            ProductDetailScreen(
                productId = args.productId,
                category = args.category,
                showBackToSearch = args.fromSearch
            )
        }

        composable<UserProfile> { backStackEntry ->
            val args: UserProfile = backStackEntry.toRoute()
            UserProfileScreen(userId = args.userId)
        }
    }
}

Bottom Navigation

Standard Implementation
enum class BottomNavDestination(
    val route: Any,
    val icon: ImageVector,
    val label: String
) {
    HOME(Home, Icons.Default.Home, "Home"),
    SEARCH(Search, Icons.Default.Search, "Search"),
    FAVORITES(Favorites, Icons.Default.Favorite, "Favorites"),
    PROFILE(Profile, Icons.Default.Person, "Profile")
}

@Composable
fun MainScreenWithBottomNav() {
    val navController = rememberNavController()
    val navBackStackEntry by navController.currentBackStackEntryAsState()
    val currentDestination = navBackStackEntry?.destination

    Scaffold(
        bottomBar = {
            NavigationBar {
                BottomNavDestination.entries.forEach { destination ->
                    NavigationBarItem(
                        icon = {
                            Icon(destination.icon, contentDescription = destination.label)
                        },
                        label = { Text(destination.label) },
                        selected = currentDestination?.hasRoute(destination.route::class) == true,
                        onClick = {
                            navController.navigate(destination.route) {
                                // Pop up to start destination to avoid building up stack
                                popUpTo(navController.graph.findStartDestination().id) {
                                    saveState = true
                                }
                                // Avoid multiple copies of same destination
                                launchSingleTop = true
                                // Restore state when reselecting
                                restoreState = true
                            }
                        }
                    )
                }
            }
        }
    ) { innerPadding ->
        NavHost(
            navController = navController,
            startDestination = Home,
            modifier = Modifier.padding(innerPadding)
        ) {
            composable<Home> { HomeScreen() }
            composable<Search> { SearchScreen() }
            composable<Favorites> { FavoritesScreen() }
            composable<Profile> { ProfileScreen() }
        }
    }
}
Bottom Nav with Badges
@Composable
fun BottomNavWithBadges(
    cartCount: Int,
    notificationCount: Int
) {
    NavigationBar {
        NavigationBarItem(
            icon = { Icon(Icons.Default.Home, null) },
            label = { Text("Home") },
            selected = true,
            onClick = { }
        )

        NavigationBarItem(
            icon = {
                BadgedBox(
                    badge = {
                        if (cartCount > 0) {
                            Badge { Text("$cartCount") }
                        }
                    }
                ) {
                    Icon(Icons.Default.ShoppingCart, null)
                }
            },
            label = { Text("Cart") },
            selected = false,
            onClick = { }
        )

        NavigationBarItem(
            icon = {
                BadgedBox(
                    badge = {
                        if (notificationCount > 0) {
                            Badge {
                                Text(
                                    if (notificationCount > 99) "99+"
                                    else "$notificationCount"
                                )
                            }
                        }
                    }
                ) {
                    Icon(Icons.Default.Notifications, null)
                }
            },
            label = { Text("Alerts") },
            selected = false,
            onClick = { }
        )
    }
}

Navigation Drawer

Modal Navigation Drawer
@Composable
fun ModalDrawerNavigation() {
    val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
    val scope = rememberCoroutineScope()
    var selectedItem by remember { mutableStateOf(0) }

    val items = listOf(
        DrawerItem(Icons.Default.Home, "Home"),
        DrawerItem(Icons.Default.Settings, "Settings"),
        DrawerItem(Icons.Default.Info, "About"),
        DrawerItem(Icons.Default.Help, "Help")
    )

    ModalNavigationDrawer(
        drawerState = drawerState,
        drawerContent = {
            ModalDrawerSheet {
                // Header
                Box(
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(180.dp)
                        .background(MaterialTheme.colorScheme.primaryContainer),
                    contentAlignment = Alignment.BottomStart
                ) {
                    Column(modifier = Modifier.padding(16.dp)) {
                        AsyncImage(
                            model = "avatar_url",
                            contentDescription = "Profile",
                            modifier = Modifier
                                .size(64.dp)
                                .clip(CircleShape)
                        )
                        Spacer(Modifier.height(8.dp))
                        Text(
                            "John Doe",
                            style = MaterialTheme.typography.titleMedium
                        )
                        Text(
                            "[email protected]",
                            style = MaterialTheme.typography.bodySmall
                        )
                    }
                }

                Spacer(Modifier.height(12.dp))

                // Navigation items
                items.forEachIndexed { index, item ->
                    NavigationDrawerItem(
                        icon = { Icon(item.icon, contentDescription = null) },
                        label = { Text(item.label) },
                        selected = index == selectedItem,
                        onClick = {
                            selectedItem = index
                            scope.launch { drawerState.close() }
                        },
                        modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
                    )
                }

                Spacer(Modifier.weight(1f))

                // Footer
                HorizontalDivider()
                NavigationDrawerItem(
                    icon = { Icon(Icons.Default.Logout, null) },
                    label = { Text("Sign Out") },
                    selected = false,
                    onClick = { },
                    modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
                )
                Spacer(Modifier.height(12.dp))
            }
        }
    ) {
        Scaffold(
            topBar = {
                TopAppBar(
                    title = { Text(items[selectedItem].label) },
                    navigationIcon = {
                        IconButton(onClick = { scope.launch { drawerState.open() } }) {
                            Icon(Icons.Default.Menu, "Open drawer")
                        }
                    }
                )
            }
        ) { padding ->
            Content(modifier = Modifier.padding(padding))
        }
    }
}

data class DrawerItem(val icon: ImageVector, val label: String)
Permanent Navigation Drawer (Tablets)
@Composable
fun PermanentDrawerLayout() {
    PermanentNavigationDrawer(
        drawerContent = {
            PermanentDrawerSheet(
                modifier = Modifier.width(240.dp)
            ) {
                Spacer(Modifier.height(12.dp))
                Text(
                    "App Name",
                    modifier = Modifier.padding(16.dp),
                    style = MaterialTheme.typography.titleLarge
                )
                HorizontalDivider()

                drawerItems.forEach { item ->
                    NavigationDrawerItem(
                        icon = { Icon(item.icon, null) },
                        label = { Text(item.label) },
                        selected = item == selectedItem,
                        onClick = { selectedItem = item },
                        modifier = Modifier.padding(horizontal = 12.dp)
                    )
                }
            }
        }
    ) {
        // Main content takes remaining space
        MainContent()
    }
}

Navigation Rail

@Composable
fun NavigationRailLayout() {
    var selectedItem by remember { mutableStateOf(0) }

    Row(modifier = Modifier.fillMaxSize()) {
        NavigationRail(
            header = {
                FloatingActionButton(
                    onClick = { },
                    elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation()
                ) {
                    Icon(Icons.Default.Add, "Create")
                }
            }
        ) {
            Spacer(Modifier.weight(1f))

            railItems.forEachIndexed { index, item ->
                NavigationRailItem(
                    icon = { Icon(item.icon, null) },
                    label = { Text(item.label) },
                    selected = selectedItem == index,
                    onClick = { selectedItem = index }
                )
            }

            Spacer(Modifier.weight(1f))
        }

        // Main content
        Box(
            modifier = Modifier
                .weight(1f)
                .fillMaxHeight()
        ) {
            when (selectedItem) {
                0 -> HomeContent()
                1 -> SearchContent()
                2 -> ProfileContent()
            }
        }
    }
}

Deep Linking

// In AndroidManifest.xml
// <intent-filter>
//     <action android:name="android.intent.action.VIEW" />
//     <category android:name="android.intent.category.DEFAULT" />
//     <category android:name="android.intent.category.BROWSABLE" />
//     <data android:scheme="myapp" />
//     <data android:scheme="https" android:host="myapp.com" />
// </intent-filter>

@Composable
fun DeepLinkNavigation() {
    val navController = rememberNavController()

    NavHost(
        navController = navController,
        startDestination = Home
    ) {
        composable<Home> {
            HomeScreen()
        }

        composable<ProductDetail>(
            deepLinks = listOf(
                navDeepLink<ProductDetail>(
                    basePath = "https://myapp.com/product"
                ),
                navDeepLink<ProductDetail>(
                    basePath = "myapp://product"
                )
            )
        ) { backStackEntry ->
            val args: ProductDetail = backStackEntry.toRoute()
            ProductDetailScreen(productId = args.productId)
        }

        composable<UserProfile>(
            deepLinks = listOf(
                navDeepLink<UserProfile>(
                    basePath = "https://myapp.com/user"
                )
            )
        ) { backStackEntry ->
            val args: UserProfile = backStackEntry.toRoute()
            UserProfileScreen(userId = args.userId)
        }
    }
}
Handling Intent in Activity
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            AppTheme {
                val navController = rememberNavController()

                // Handle deep link from intent
                LaunchedEffect(Unit) {
                    intent?.data?.let { uri ->
                        navController.handleDeepLink(intent)
                    }
                }

                AppNavigation(navController = navController)
            }
        }
    }

    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        // Handle new intents when activity is already running
        setIntent(intent)
    }
}

Nested Navigation

@Composable
fun NestedNavigation() {
    val navController = rememberNavController()

    NavHost(navController = navController, startDestination = MainGraph) {
        // Main graph with bottom navigation
        navigation<MainGraph>(startDestination = Home) {
            composable<Home> {
                HomeScreen(
                    onItemClick = { navController.navigate(Detail(it)) }
                )
            }
            composable<Search> { SearchScreen() }
            composable<Profile> {
                ProfileScreen(
                    onSettingsClick = { navController.navigate(SettingsGraph) }
                )
            }
        }

        // Nested detail graph
        composable<Detail> { backStackEntry ->
            val args: Detail = backStackEntry.toRoute()
            DetailScreen(itemId = args.itemId)
        }

        // Separate settings graph (full screen, no bottom nav)
        navigation<SettingsGraph>(startDestination = SettingsMain) {
            composable<SettingsMain> {
                SettingsScreen(
                    onAccountClick = { navController.navigate(AccountSettings) },
                    onNotificationsClick = { navController.navigate(NotificationSettings) }
                )
            }
            composable<AccountSettings> { AccountSettingsScreen() }
            composable<NotificationSettings> { NotificationSettingsScreen() }
        }
    }
}

@Serializable object MainGraph
@Serializable object SettingsGraph
@Serializable object SettingsMain
@Serializable object AccountSettings
@Serializable object NotificationSettings

Navigation State Management

ViewModel Integration
@HiltViewModel
class NavigationViewModel @Inject constructor(
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    private val _navigationEvents = MutableSharedFlow<NavigationEvent>()
    val navigationEvents = _navigationEvents.asSharedFlow()

    fun navigateToDetail(itemId: String) {
        viewModelScope.launch {
            _navigationEvents.emit(NavigationEvent.NavigateToDetail(itemId))
        }
    }

    fun navigateBack() {
        viewModelScope.launch {
            _navigationEvents.emit(NavigationEvent.NavigateBack)
        }
    }
}

sealed class NavigationEvent {
    data class NavigateToDetail(val itemId: String) : NavigationEvent()
    object NavigateBack : NavigationEvent()
}

@Composable
fun NavigationHandler(
    navController: NavHostController,
    viewModel: NavigationViewModel = hiltViewModel()
) {
    LaunchedEffect(Unit) {
        viewModel.navigationEvents.collect { event ->
            when (event) {
                is NavigationEvent.NavigateToDetail -> {
                    navController.navigate(Detail(event.itemId))
                }
                NavigationEvent.NavigateBack -> {
                    navController.popBackStack()
                }
            }
        }
    }
}
Back Handler
@Composable
fun ScreenWithBackHandler(
    onBack: () -> Unit
) {
    var showExitDialog by remember { mutableStateOf(false) }

    // Intercept back press
    BackHandler {
        showExitDialog = true
    }

    if (showExitDialog) {
        AlertDialog(
            onDismissRequest = { showExitDialog = false },
            title = { Text("Exit App?") },
            text = { Text("Are you sure you want to exit?") },
            confirmButton = {
                TextButton(onClick = onBack) {
                    Text("Exit")
                }
            },
            dismissButton = {
                TextButton(onClick = { showExitDialog = false }) {
                    Text("Cancel")
                }
            }
        )
    }

    // Screen content
    Content()
}

Navigation Animations

@Composable
fun AnimatedNavigation() {
    val navController = rememberNavController()

    NavHost(
        navController = navController,
        startDestination = Home,
        enterTransition = {
            slideIntoContainer(
                towards = AnimatedContentTransitionScope.SlideDirection.Left,
                animationSpec = tween(300)
            )
        },
        exitTransition = {
            slideOutOfContainer(
                towards = AnimatedContentTransitionScope.SlideDirection.Left,
                animationSpec = tween(300)
            )
        },
        popEnterTransition = {
            slideIntoContainer(
                towards = AnimatedContentTransitionScope.SlideDirection.Right,
                animationSpec = tween(300)
            )
        },
        popExitTransition = {
            slideOutOfContainer(
                towards = AnimatedContentTransitionScope.SlideDirection.Right,
                animationSpec = tween(300)
            )
        }
    ) {
        composable<Home> {
            HomeScreen()
        }

        composable<Detail>(
            // Custom transition for specific route
            enterTransition = {
                fadeIn(animationSpec = tween(500)) +
                    scaleIn(initialScale = 0.9f, animationSpec = tween(500))
            },
            exitTransition = {
                fadeOut(animationSpec = tween(500))
            }
        ) {
            DetailScreen()
        }
    }
}

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Security

No issues found by our static scan· No issues found

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Created by
Seth Hobson
Publisher
Seth Hobson (Community)
Source
wshobson/agents · plugins/ui-design/skills/mobile-android-design · commit 4bde316
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Seth Hobson or Seth Hobson.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download mobile-android-design.zip