Skip to main content
TecMeli implements two primary screens following Material Design 3 principles and Clean Architecture patterns.

HomeScreen

The main entry point of the app, providing product search functionality with real-time updates.

Overview

HomeScreen displays a search interface with dynamic results rendering based on UI state. It uses:
  • Hilt for dependency injection (hiltViewModel())
  • Material3 Scaffold for consistent layout
  • State Management via UiState sealed class
  • Composable Components for modular UI
The HomeScreen manages search query state locally while delegating business logic to HomeViewModel.

Implementation

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(
    navigateToDetail: (String) -> Unit,
    viewModel: HomeViewModel = hiltViewModel()
) {
    var query by remember { mutableStateOf("") }
    var active by remember { mutableStateOf(false) }
    val uiState by viewModel.uiState.collectAsState()

    Scaffold { paddingValues ->
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(paddingValues)
        ) {
            SearchBarComponent(
                query = query,
                onQueryChange = { query = it },
                onSearch = { 
                    viewModel.searchProducts(it)
                    active = false 
                },
                active = active,
                onActiveChange = { active = it }
            )

            Box(modifier = Modifier.fillMaxSize()) {
                when (val state = uiState) {
                    is UiState.Loading -> {
                        CircularProgressIndicator(Modifier.align(Alignment.Center))
                    }
                    is UiState.Success -> {
                        ProductList(
                            products = state.data, 
                            onItemClick = navigateToDetail
                        )
                    }
                    is UiState.Error -> {
                        ErrorState(
                            error = (state.exception as? AppError) 
                                ?: AppError.Unknown(state.exception ?: Exception(state.message)),
                            onRetry = { viewModel.searchProducts(query) }
                        )
                    }
                    is UiState.Empty -> {
                        Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                            Text(
                                text = if (query.isEmpty()) 
                                    "Comienza a buscar productos" 
                                else 
                                    "No se encontraron resultados para \"$query\"",
                                style = MaterialTheme.typography.bodyLarge
                            )
                        }
                    }
                }
            }
        }
    }
}

Key Features

Search Integration

Dynamic search with Material3 SearchBar component

State Management

Handles Loading, Success, Error, and Empty states

Error Handling

Custom ErrorState component with retry functionality

Navigation

Navigates to product details via callback

UI States

The screen responds to four distinct states:
StateDisplayUser Action
LoadingCentered circular progress indicatorWait for results
SuccessScrollable list of productsClick to view details
ErrorError message with retry buttonRetry the search
EmptyInformative text based on queryStart/refine search

Components Used

Provides an expandable Material3 SearchBar with placeholder text “Buscar en Mercado Libre” and search icon.Location: ui/screen/home/components/SearchBar.kt:14
LazyColumn displaying search results with ProductListItem composables.Location: ui/screen/home/components/ProductList.kt:11
Reusable error display component handling AppError types with retry functionality.Location: ui/components/ErrorState.kt

ProductDetailScreen

Displays detailed information about a selected product, including images, description, and attributes.

Overview

The detail screen fetches product information on launch using the product ID and displays:
  • Product Images using Coil’s AsyncImage
  • Title and Description with Material3 typography
  • Attributes in an organized list format
  • Top App Bar with back navigation

Implementation

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ProductDetailScreen(
    id: String,
    onBack: () -> Unit,
    viewModel: ProductDetailViewModel = hiltViewModel()
) {
    val uiState by viewModel.uiState.collectAsState()

    LaunchedEffect(id) {
        viewModel.getProductDetail(id)
    }

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("Detalle del Producto") },
                navigationIcon = {
                    IconButton(onClick = onBack) {
                        Icon(Icons.Default.ArrowBack, contentDescription = "Volver")
                    }
                }
            )
        }
    ) { paddingValues ->
        Box(
            modifier = Modifier
                .fillMaxSize()
                .padding(paddingValues)
        ) {
            when (val state = uiState) {
                is UiState.Loading -> {
                    CircularProgressIndicator(Modifier.align(Alignment.Center))
                }
                is UiState.Success -> {
                    ProductDetailContent(product = state.data)
                }
                is UiState.Error -> {
                    Text(
                        text = state.message,
                        color = MaterialTheme.colorScheme.error,
                        modifier = Modifier.align(Alignment.Center).padding(16.dp)
                    )
                }
                is UiState.Empty -> {
                    Text(
                        "No se encontró información del producto", 
                        Modifier.align(Alignment.Center)
                    )
                }
            }
        }
    }
}

ProductDetailContent

The content composable renders the product information in a scrollable column:
@Composable
fun ProductDetailContent(product: ProductDetail) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .verticalScroll(rememberScrollState())
            .padding(16.dp)
    ) {
        // Product Image
        AsyncImage(
            model = product.pictures.firstOrNull(),
            contentDescription = product.title,
            modifier = Modifier
                .fillMaxWidth()
                .height(300.dp),
            contentScale = ContentScale.Fit
        )

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

        // Product Title
        Text(
            text = product.title,
            fontSize = 20.sp,
            fontWeight = FontWeight.Bold
        )

        // Description Section
        if (!product.description.isNullOrEmpty()) {
            Spacer(modifier = Modifier.height(16.dp))
            Text(
                text = "Descripción",
                fontSize = 18.sp,
                fontWeight = FontWeight.SemiBold
            )
            Spacer(modifier = Modifier.height(4.dp))
            Text(
                text = product.description,
                fontSize = 16.sp,
                lineHeight = 22.sp
            )
        }

        // Attributes Section
        if (product.attributes.isNotEmpty()) {
            Spacer(modifier = Modifier.height(16.dp))
            Text(
                text = "Características",
                fontSize = 18.sp,
                fontWeight = FontWeight.SemiBold
            )
            product.attributes.forEach { attr ->
                ListItem(
                    headlineContent = { 
                        Text(attr.name, fontWeight = FontWeight.Medium) 
                    },
                    supportingContent = { 
                        Text(attr.value ?: "N/A") 
                    }
                )
            }
        }
    }
}

Key Features

LaunchedEffect

Automatically fetches product details when screen loads

Image Loading

Uses Coil for async image loading with ContentScale.Fit

Conditional Rendering

Shows description and attributes only when available

Material3 Components

TopAppBar, ListItem, and typography system

Layout Structure

The detail screen follows a vertical layout pattern:
  1. Top App Bar - Title with back navigation
  2. Product Image - 300dp height, full width, fitted content
  3. Product Title - Bold, 20sp text
  4. Description Section - Conditional, 16sp with 22sp line height
  5. Attributes List - Material3 ListItem components
All spacing uses 16dp increments following Material Design spacing guidelines.
The screen receives:
  • id: String - Product identifier from navigation arguments
  • onBack: () -> Unit - Callback to return to previous screen
IconButton(onClick = onBack) {
    Icon(Icons.Default.ArrowBack, contentDescription = "Volver")
}

Best Practices

  • Collect state as Compose State using collectAsState()
  • Use remember for local UI state (query, active state)
  • Delegate business logic to ViewModels
  • Display user-friendly error messages
  • Provide retry functionality for failed operations
  • Use Material3 error colors for visibility
  • Use LazyColumn for lists to enable virtualization
  • Load images asynchronously with Coil
  • Minimize recompositions with stable state

Navigation

Learn about routing and navigation

Theming

Explore color schemes and typography

Architecture

Understand Clean Architecture implementation

Build docs developers (and LLMs) love