Skip to main content

Overview

EnvaSistema provides a collection of reusable Compose components designed for warehouse management interfaces. All components follow Material 3 design principles and support customization through parameters.
All components are located in app/src/main/java/com/example/envasistema/ui/components/

Component Library

ScanningLayout

A comprehensive layout for barcode scanning workflows with integrated counter, info card, and action button.
@Composable
fun ProduccionNuevaScreen(onBackClick: () -> Unit) {
    ScanningLayout(
        title = "INGRESOS",
        subtitle = "Producción Nueva",
        infoText = "Escanee los códigos de los productos terminados nuevos",
        onBackClick = onBackClick,
        onSaveClick = { count ->
            // Handle save with scan count
        },
        primaryColor = Color(0xFF43A047)
    )
}

Parameters

title
String
required
Header title displayed in the secondary header
subtitle
String
required
Header subtitle displayed below the title
infoText
String
required
Instructional text shown in the info card
onBackClick
() -> Unit
required
Callback invoked when the back button is clicked
onSaveClick
(Int) -> Unit
required
Callback invoked when save button is clicked, receives scan count as parameter
counterLabel
String
default:"Códigos escaneados"
Label text displayed next to the scan counter
saveButtonText
String
default:"Guardar Ingreso"
Text displayed on the save button
saveButtonIcon
ImageVector
default:"Icons.Default.Save"
Icon displayed on the save button
primaryColor
Color
default:"Color(0xFF0061A6)"
Primary theme color used for header, accents, and buttons
infoCardBackground
Color
default:"Color(0xFFE1F5FE)"
Background color of the info alert card
infoIcon
ImageVector
default:"Icons.Default.Radar"
Icon displayed in the info card
infoIconColor
Color?
default:"null"
Custom color for the info icon (defaults to primaryColor if null)
isSaveButtonEnabled
((Int) -> Boolean)?
default:"null"
Custom validation function to enable/disable save button based on scan count
extraContent
@Composable (ColumnScope.() -> Unit)?
default:"null"
Additional content to display above the info card

Features

  • Scan Simulation: Click the scan area to increment counter (for testing)
  • Automatic Button State: Save button enables/disables based on scan count
  • Customizable Colors: Supports themed variants for different workflows
  • Scrollable Content: Handles overflow with vertical scrolling
ScanningLayout.kt
package com.example.envasistema.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.QrCodeScanner
import androidx.compose.material.icons.filled.Radar
import androidx.compose.material.icons.filled.Save
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

@Composable
fun ScanningLayout(
    title: String,
    subtitle: String,
    infoText: String,
    onBackClick: () -> Unit,
    onSaveClick: (Int) -> Unit,
    counterLabel: String = "Códigos escaneados",
    saveButtonText: String = "Guardar Ingreso",
    saveButtonIcon: ImageVector = Icons.Default.Save,
    primaryColor: Color = Color(0xFF0061A6),
    infoCardBackground: Color = Color(0xFFE1F5FE),
    infoIcon: ImageVector = Icons.Default.Radar,
    infoIconColor: Color? = null,
    isSaveButtonEnabled: ((Int) -> Boolean)? = null,
    extraContent: @Composable (ColumnScope.() -> Unit)? = null
) {
    var scannCount by remember { mutableIntStateOf(0) }
    val buttonEnabled = isSaveButtonEnabled?.invoke(scannCount) ?: (scannCount > 0)

    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(Color(0xFFF8F9FA))
    ) {
        SecondaryHeader(
            title = title,
            subtitle = subtitle,
            backgroundColor = primaryColor,
            onBackClick = onBackClick
        )

        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(20.dp)
                .verticalScroll(rememberScrollState()),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            extraContent?.invoke(this)

            if (extraContent != null) {
                Spacer(modifier = Modifier.height(16.dp))
            }

            // Info Alert Card
            Surface(
                modifier = Modifier.fillMaxWidth(),
                color = infoCardBackground,
                shape = RoundedCornerShape(12.dp)
            ) {
                Row(
                    modifier = Modifier.fillMaxWidth(),
                    verticalAlignment = Alignment.CenterVertically
                ) {
                    Box(modifier = Modifier.width(6.dp).height(60.dp).background(primaryColor))
                    
                    Icon(
                        imageVector = infoIcon,
                        contentDescription = null,
                        tint = infoIconColor ?: primaryColor,
                        modifier = Modifier.padding(horizontal = 12.dp).size(24.dp)
                    )
                    Text(
                        text = infoText,
                        color = primaryColor,
                        fontSize = 14.sp,
                        fontWeight = FontWeight.Medium,
                        modifier = Modifier.padding(end = 16.dp, top = 8.dp, bottom = 8.dp)
                    )
                }
            }

            Spacer(modifier = Modifier.height(24.dp))

            // Scan Area
            Box(
                modifier = Modifier
                    .fillMaxWidth()
                    .height(200.dp)
                    .border(1.dp, Color(0xFFBDBDBD), RoundedCornerShape(16.dp))
                    .clip(RoundedCornerShape(16.dp))
                    .clickable { scannCount++ },
                contentAlignment = Alignment.Center
            ) {
                Column(horizontalAlignment = Alignment.CenterHorizontally) {
                    Surface(
                        color = Color(0xFFEEEEEE),
                        shape = CircleShape,
                        modifier = Modifier.size(80.dp)
                    ) {
                        Icon(
                            imageVector = Icons.Default.QrCodeScanner,
                            contentDescription = null,
                            tint = Color(0xFF9E9E9E),
                            modifier = Modifier.padding(20.dp)
                        )
                    }
                    Spacer(modifier = Modifier.height(16.dp))
                    Text(
                        text = "Presione el botón lateral del terminal para escanear",
                        color = Color(0xFF607D8B),
                        fontSize = 14.sp,
                        textAlign = TextAlign.Center
                    )
                }
            }

            Spacer(modifier = Modifier.height(24.dp))

            // Scanned Codes Counter
            Row(
                modifier = Modifier.fillMaxWidth(),
                verticalAlignment = Alignment.CenterVertically
            ) {
                Surface(
                    color = primaryColor,
                    shape = CircleShape,
                    modifier = Modifier.size(32.dp)
                ) {
                    Box(contentAlignment = Alignment.Center) {
                        Text(text = scannCount.toString(), color = Color.White, fontWeight = FontWeight.Bold)
                    }
                }
                Spacer(modifier = Modifier.width(12.dp))
                Text(
                    text = counterLabel,
                    color = Color(0xFF455A64),
                    fontSize = 16.sp,
                    fontWeight = FontWeight.Bold
                )
            }

            Spacer(modifier = Modifier.height(32.dp))
            
            if (scannCount == 0) {
                Text(
                    text = "No hay códigos escaneados",
                    color = Color(0xFFBDBDBD),
                    fontSize = 15.sp
                )
            }

            Spacer(modifier = Modifier.weight(1f))
            Spacer(modifier = Modifier.height(24.dp))

            // Footer Button
            Button(
                onClick = { if (buttonEnabled) onSaveClick(scannCount) },
                modifier = Modifier
                    .fillMaxWidth()
                    .height(56.dp),
                shape = RoundedCornerShape(16.dp),
                colors = ButtonDefaults.buttonColors(
                    containerColor = if (buttonEnabled) primaryColor else Color(0xFFB0BEC5),
                    contentColor = Color.White
                )
            ) {
                Icon(imageVector = saveButtonIcon, contentDescription = null)
                Spacer(modifier = Modifier.width(8.dp))
                Text(text = saveButtonText, fontSize = 18.sp, fontWeight = FontWeight.Bold)
            }
        }
    }
}

A clickable card component for menu navigation with icon, title, subtitle, and accent styling.
MenuCard(
    title = "INGRESOS",
    subtitle = "Producción, armados y devoluciones",
    icon = Icons.Default.AddBox,
    primaryColor = Color(0xFF0061A6),
    onClick = { navController.navigate("ingresos") }
)

Parameters

title
String
required
Primary text displayed prominently
subtitle
String
required
Secondary descriptive text
icon
ImageVector
required
Icon displayed on the left side
primaryColor
Color
default:"Color(0xFF0061A6)"
Color for the accent strip and title text
iconBackgroundColor
Color
default:"Color(0xFFE1F5FE)"
Background color behind the icon
iconTintColor
Color
default:"Color(0xFF01579B)"
Icon tint color
rightIcon
ImageVector?
default:"Icons.AutoMirrored.Filled.KeyboardArrowRight"
Optional icon displayed on the right (null to hide)
onClick
() -> Unit
required
Callback invoked when the card is clicked

Layout Structure

┌─────────────────────────────────────┐
│┃ [Icon] │ TITLE            →       │
│┃        │ Subtitle text             │
└─────────────────────────────────────┘

HomeHeader

The main application header with status badge, user info, and logout button.
HomeHeader(
    statusText = "C66 · ONLINE",
    userName = "J. PÉREZ",
    onLogoutClick = { /* Handle logout */ }
)

Parameters

statusText
String
required
Status text displayed with green indicator (e.g., “C66 · ONLINE”)
userName
String
required
User name displayed in the header badge
onLogoutClick
() -> Unit
required
Callback invoked when logout button is clicked

Features

  • Online Status Indicator: Green dot with status text
  • User Badge: Displays current user with person icon
  • Logout Button: Material icon button with semi-transparent background
  • App Title: Multi-line title with subtitle styling
  • Rounded Bottom: Curved bottom corners for modern design

SecondaryHeader

A simplified header for secondary screens with back navigation, title, and subtitle.
SecondaryHeader(
    title = "INGRESOS",
    subtitle = "Producción Nueva",
    backgroundColor = Color(0xFF43A047),
    onBackClick = { navController.popBackStack() }
)

Parameters

title
String
required
Primary header title
subtitle
String
required
Secondary descriptive text
backgroundColor
Color
default:"Color(0xFF0061A6)"
Background color of the header
onBackClick
() -> Unit
required
Callback invoked when back button is clicked

ShiftInfoRow

A compact row displaying date and shift information with badge styling.
ShiftInfoRow(
    dateText = "Lun, 9 de Marzo 2026",
    shiftText = "Turno Mañana"
)

Parameters

dateText
String
required
Date text displayed on the left
shiftText
String
required
Shift text displayed in a green badge on the right

Visual Design

  • Date: Gray text, medium weight
  • Shift Badge: Green background (#E8F5E9) with green text (#4CAF50)

Color Schemes

EnvaSistema uses contextual colors for different workflows:

Ingresos

Green theme (#43A047)

Salidas

Red theme (#E53935)

Movimientos

Orange theme (#FB8C00)

Transformaciones

Brown theme (#6D4C41)

Best Practices

Prefer composing existing components rather than creating new ones. For example, use ScanningLayout with extraContent parameter for custom sections.
Components manage their own internal UI state (like scannCount in ScanningLayout). Parent screens receive state through callbacks when actions occur.
Use the primaryColor parameter to maintain visual consistency across related screens. Each workflow category should use its designated color.
All interactive components include proper content descriptions and maintain minimum touch target sizes of 48dp.

Common Patterns

Screen with Scanning

@Composable
fun MyScreen(onBackClick: () -> Unit) {
    ScanningLayout(
        title = "CATEGORY",
        subtitle = "Screen Title",
        infoText = "Instructions for the user",
        onBackClick = onBackClick,
        onSaveClick = { count ->
            // Process scanned items
        },
        primaryColor = Color(0xFF43A047)
    )
}
@Composable
fun CategoryScreen(
    onBackClick: () -> Unit,
    onOption1Click: () -> Unit,
    onOption2Click: () -> Unit
) {
    Column(modifier = Modifier.fillMaxSize()) {
        SecondaryHeader(
            title = "CATEGORY",
            subtitle = "Select an option",
            onBackClick = onBackClick
        )
        
        Column(modifier = Modifier.padding(20.dp)) {
            MenuCard(
                title = "Option 1",
                subtitle = "Description",
                icon = Icons.Default.Add,
                onClick = onOption1Click
            )
            Spacer(modifier = Modifier.height(16.dp))
            MenuCard(
                title = "Option 2",
                subtitle = "Description",
                icon = Icons.Default.Remove,
                onClick = onOption2Click
            )
        }
    }
}

Component Testing

All components support Compose previews for rapid development:
@Preview(showBackground = true)
@Composable
fun MenuCardPreview() {
    EnvaSistemaTheme {
        MenuCard(
            title = "INGRESOS",
            subtitle = "Producción, armados y devoluciones",
            icon = Icons.Default.AddBox,
            onClick = {}
        )
    }
}
Use Android Studio’s Compose Preview feature to view and interact with components without running the full app.

Next Steps

Architecture

Understand the app structure

Development Setup

Set up your development environment

Build docs developers (and LLMs) love