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.
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),
)
}
}
Inject ViewModel
Use hiltViewModel() to inject the screen’s ViewModel with dependency injection.
Collect UI state
Collect state as lifecycle-aware composable state using collectAsStateWithLifecycle().
Handle lifecycle events
Use LifecycleStartEffect to trigger data loading when the screen starts.
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.