Skip to main content
The UI layer implements the visual presentation using Jetpack Compose with Material3 design system. It includes theming, reusable components, and screen implementations following modern Android best practices.

Material3 theming

The project uses Material3’s dynamic color system with support for light and dark themes.

Theme configuration

The CPTTheme composable provides centralized theming with dynamic color support for Android 12+:
app/src/main/java/es/mobiledev/cpt/ui/theme/Theme.kt
@Composable
fun CPTTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    val colorScheme =
        when {
            dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
                val context = LocalContext.current
                if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
            }
            darkTheme -> DarkColorScheme
            else -> LightColorScheme
        }

    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}
Dynamic colors automatically adapt to the user’s wallpaper on Android 12+ devices, providing a personalized experience.

Color schemes

Define custom color palettes for light and dark themes:
app/src/main/java/es/mobiledev/cpt/ui/theme/Color.kt
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)

val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
app/src/main/java/es/mobiledev/cpt/ui/theme/Theme.kt
private val DarkColorScheme = darkColorScheme(
    primary = Purple80,
    secondary = PurpleGrey80,
    tertiary = Pink80
)

private val LightColorScheme = lightColorScheme(
    primary = Purple40,
    secondary = PurpleGrey40,
    tertiary = Pink40
)

Typography

Customize text styles across your application:
app/src/main/java/es/mobiledev/cpt/ui/theme/Type.kt
val Typography = Typography(
    bodyLarge = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp,
        lineHeight = 24.sp,
        letterSpacing = 0.5.sp
    )
)

Reusable components

The commonAndroid module provides reusable UI components for consistent design across features.

CPTButton

A custom button with loading state support:
commonAndroid/src/main/java/es/mobiledev/commonandroid/ui/component/button/CPTButton.kt
@Composable
fun CPTButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    submitting: Boolean = false,
    content: @Composable () -> Unit,
) {
    Button(
        onClick = onClick,
        modifier = modifier,
        enabled = enabled && !submitting,
    ) {
        if (submitting) {
            CircularProgressIndicator(
                color = MaterialTheme.colorScheme.onPrimary,
            )
        } else {
            content()
        }
    }
}
CPTButton(
    onClick = { viewModel.submitForm() },
    submitting = uiState.isSubmitting
) {
    Text("Submit")
}

CptTopBar

A standardized top app bar with navigation icon:
commonAndroid/src/main/java/es/mobiledev/commonandroid/ui/component/topBar/CptTopBar.kt
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CptTopBar(
    modifier: Modifier = Modifier,
) {
    TopAppBar(
        title = {
            Image(
                painter = painterResource(R.drawable.topbar_cpt_title),
                contentDescription = null,
            )
        },
        navigationIcon = {
            IconButton(
                onClick = { /* TODO: Add navigation */ },
            ) {
                Icon(
                    painter = painterResource(R.drawable.ic_cpt_drawer),
                    contentDescription = null,
                )
            }
        },
        modifier = modifier,
    )
}

ArticleItem

A complex list item component for displaying articles with favorite functionality:
commonAndroid/src/main/java/es/mobiledev/commonandroid/ui/component/article/ArticleItem.kt
@Composable
fun ArticleItem(
    article: ArticleBo,
    isFavorite: Boolean,
    onItemClick: () -> Unit,
    onFavoriteClick: () -> Unit
) {
    Column(
        modifier = Modifier
            .clickable { onItemClick() }
            .padding(dimensionResource(R.dimen.article_item__content_padding)),
        verticalArrangement = Arrangement.spacedBy(
            dimensionResource(R.dimen.article_item__vertical_arrangement)
        )
    ) {
        article.imageUrl.takeIf { it.isNotEmpty() }?.let {
            AsyncImage(
                contentScale = ContentScale.Crop,
                model = article.imageUrl,
                contentDescription = null,
                modifier = Modifier.height(
                    dimensionResource(R.dimen.article_item__image_height)
                ),
            )
        }
        Text(text = article.title, style = MaterialTheme.typography.headlineSmall)
        Row(
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.SpaceBetween,
            modifier = Modifier.fillMaxWidth()
        ) {
            Text(text = article.newsSite)
            IconButton(
                onClick = { onFavoriteClick() },
                colors = IconButtonDefaults.iconButtonColors(
                    contentColor = if (isFavorite) Color(0xFFF9A825) else Color.Gray
                )
            ) {
                Icon(
                    painter = painterResource(
                        if (isFavorite) R.drawable.ic_cpt_favorites_filled
                        else R.drawable.ic_cpt_favorites_outlined
                    ),
                    contentDescription = null,
                )
            }
        }
    }
}

Base screen architecture

BaseScreen

Provides a consistent scaffold structure with loading state management:
commonAndroid/src/main/java/es/mobiledev/commonandroid/ui/base/BaseScreen.kt
@Composable
fun BaseScreen(
    modifier: Modifier = Modifier,
    isLoading: Boolean = false,
    topBar: @Composable () -> Unit = EmptyComposable,
    bottomBar: @Composable () -> Unit = EmptyComposable,
    content: @Composable (PaddingValues) -> Unit = {},
) {
    Scaffold(
        topBar = topBar,
        bottomBar = bottomBar,
        modifier = modifier.fillMaxSize()
    ) { paddingValues ->
        if (isLoading) {
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .padding(paddingValues),
                contentAlignment = Alignment.Center
            ) {
                CircularProgressIndicator(
                    color = MaterialTheme.colorScheme.primary,
                )
            }
        } else {
            content(paddingValues)
        }
    }
}
The BaseScreen automatically handles loading states and padding for top/bottom bars, ensuring edge-to-edge display support.

BaseViewModel

Manages UI state with helper functions for common state transitions:
commonAndroid/src/main/java/es/mobiledev/commonandroid/ui/base/BaseViewModel.kt
abstract class BaseViewModel<T> : ViewModel() {
    protected abstract val uiState: MutableStateFlow<UiState<T>>

    fun getUiState(): StateFlow<UiState<T>> = uiState.asStateFlow()

    fun MutableStateFlow<UiState<T>>.updateState(block: (T) -> T) {
        update { currentUiState ->
            currentUiState.copy(data = block(currentUiState.data))
        }
    }

    fun MutableStateFlow<UiState<T>>.loadingState() {
        update { currentUiState ->
            currentUiState.copy(isLoading = true)
        }
    }

    fun MutableStateFlow<UiState<T>>.successState(block: (T) -> T) {
        update { currentUiState ->
            currentUiState.copy(data = block(currentUiState.data), isLoading = false)
        }
    }
}

Screen implementation example

Feature screens integrate with the base architecture and ViewModel:
feature/home/src/main/java/es/mobiledev/feature/home/screen/HomeScreen.kt
@Composable
fun HomeScreen(
    navigateToArticleDetail: (Long) -> Unit,
) {
    val viewModel: HomeViewModel = hiltViewModel()
    val uiState by viewModel.getUiState().collectAsStateWithLifecycle()

    LifecycleStartEffect(Unit) {
        viewModel.getFavoriteArticles()
        onStopOrDispose { /* no-op */ }
    }

    BaseScreen(
        isLoading = uiState.isLoading,
    ) { paddingValues ->
        HomeScreenContent(
            uiState = uiState.data,
            onNavigateToDetail = { id ->
                navigateToArticleDetail(id)
            },
            onFavoriteClick = viewModel::onFavoriteClick,
            modifier = Modifier.padding(paddingValues),
        )
    }
}
1

Inject ViewModel

Use hiltViewModel() to inject the screen’s ViewModel with dependency injection.
2

Collect UI state

Collect state as lifecycle-aware composable state using collectAsStateWithLifecycle().
3

Handle lifecycle events

Use LifecycleStartEffect to trigger data loading when the screen starts.
4

Render with BaseScreen

Wrap content in BaseScreen to handle loading states and scaffold structure automatically.

Best practices

Stateless composables

Keep composables stateless by passing data and callbacks as parameters.

Preview support

Add @PreviewLightDark annotations to preview components in light and dark themes.

Material3 tokens

Use MaterialTheme.colorScheme and MaterialTheme.typography instead of hardcoded values.

Dimension resources

Reference dimension resources with dimensionResource() for consistent spacing.
Always apply padding from PaddingValues to screen content to avoid overlapping with system bars and navigation components.

Build docs developers (and LLMs) love