The ScreenWrapper and BaseScreen components provide foundational layout structures for screens in the Compose Project Template. They implement edge-to-edge design, manage system bars, and provide consistent UI patterns.
Overview
Package: es.mobiledev.commonandroid.ui.base
Module: commonAndroid
Two complementary components for different screen needs:
ScreenWrapper - Edge-to-edge layout with conditional bars (navigation-level)
BaseScreen - Full-featured screen with loading state (feature-level)
ScreenWrapper App-level layout wrapper for navigation
BaseScreen Feature-level screen with loading support
ScreenWrapper
File: commonAndroid/src/main/java/es/mobiledev/commonandroid/ui/base/ScreenWrapper.kt:40
Implements the basic structure for edge-to-edge design with conditional top and bottom bars.
API Reference
@OptIn (ExperimentalMaterial3Api:: class )
@Composable
fun ScreenWrapper (
modifier: Modifier = Modifier,
topBar: @Composable () -> Unit = EmptyComposable,
bottomBar: @Composable () -> Unit = EmptyComposable,
showTopAppBar: Boolean = false ,
showBottomBar: Boolean = false ,
content: @Composable (PaddingValues) -> Unit = { EmptyComposable },
)
Parameters
modifier
Modifier
default: "Modifier"
Modifier applied to the root Scaffold container
topBar
@Composable () -> Unit
default: "EmptyComposable"
Composable function to provide the top bar content (typically CptTopBar)
bottomBar
@Composable () -> Unit
default: "EmptyComposable"
Composable function to provide the bottom bar content (typically CptNavigationBar)
Controls whether the top bar is visible
Controls whether the bottom bar is visible
content
@Composable (PaddingValues) -> Unit
default: "{ EmptyComposable }"
Screen content lambda that receives PaddingValues for proper inset handling
Window Insets Management
The component intelligently manages window insets based on bar visibility:
WindowInsets.systemBars - Handles both status bar and navigation bar insets
WindowInsets.statusBars - Handles only status bar insets
WindowInsets.navigationBars - Handles only navigation bar insets
WindowInsets() - Empty insets for full edge-to-edge content
Implementation
Scaffold (
topBar = {
if (showTopAppBar) {
topBar ()
}
},
bottomBar = {
if (showBottomBar) {
bottomBar ()
}
},
contentWindowInsets = when {
showTopAppBar && showBottomBar -> WindowInsets.systemBars
showTopAppBar -> WindowInsets.statusBars
showBottomBar -> WindowInsets.navigationBars
else -> WindowInsets ()
},
modifier = modifier. fillMaxSize (),
) { paddingValues ->
content (paddingValues)
}
Usage Examples
In App Navigation
@Composable
fun AppNavigation (navController: NavHostController ) {
var currentScreen: AppScreens by remember {
mutableStateOf (AppScreens.Launcher)
}
val showTopAppBar by remember {
derivedStateOf { currentScreen.hasTopBar }
}
val showBottomBar by remember {
derivedStateOf { currentScreen.hasBottomBar }
}
ScreenWrapper (
topBar = { CptTopBar () },
bottomBar = {
CptNavigationBar (
selectedModule = currentScreen.module,
onClickModule = { screen ->
navController. navigate (screen)
}
)
},
showTopAppBar = showTopAppBar,
showBottomBar = showBottomBar,
) { paddingValues ->
NavHost (
navController = navController,
startDestination = AppScreens.Launcher,
modifier = Modifier
. consumeWindowInsets (paddingValues)
. padding (paddingValues)
) {
// Navigation graph
}
}
}
Dynamic Bar Visibility
@Composable
fun DynamicScreen (fullscreen: Boolean ) {
ScreenWrapper (
topBar = { CptTopBar () },
bottomBar = { CptNavigationBar ( /* ... */ ) },
showTopAppBar = ! fullscreen,
showBottomBar = ! fullscreen,
) { paddingValues ->
if (fullscreen) {
// Full screen content
VideoPlayer (modifier = Modifier. fillMaxSize ())
} else {
// Normal content with padding
Content (modifier = Modifier. padding (paddingValues))
}
}
}
BaseScreen
File: commonAndroid/src/main/java/es/mobiledev/commonandroid/ui/base/BaseScreen.kt:39
Provides a feature-level screen component with built-in loading state management.
API Reference
@Composable
fun BaseScreen (
modifier: Modifier = Modifier,
isLoading: Boolean = false ,
topBar: @Composable () -> Unit = EmptyComposable,
bottomBar: @Composable () -> Unit = EmptyComposable,
content: @Composable (PaddingValues) -> Unit = {},
)
Parameters
modifier
Modifier
default: "Modifier"
Modifier applied to the root Scaffold container
Flag indicating whether to show loading indicator instead of content
topBar
@Composable () -> Unit
default: "EmptyComposable"
Composable function for top bar content (typically TopAppBar)
bottomBar
@Composable () -> Unit
default: "EmptyComposable"
Composable function for bottom bar content
content
@Composable (PaddingValues) -> Unit
default: "{}"
Screen content lambda that receives PaddingValues for proper layout
Loading State
When isLoading = true, the component displays a centered CircularProgressIndicator instead of the content:
if (isLoading) {
Box (
modifier = Modifier
. fillMaxSize ()
. padding (paddingValues),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator (
color = MaterialTheme.colorScheme.primary,
)
}
} else {
content (paddingValues)
}
Usage Examples
Feature Screen with ViewModel
@Composable
fun HomeScreen (
navigateToArticleDetail: ( Long ) -> Unit ,
) {
val viewModel: HomeViewModel = hiltViewModel ()
val uiState by viewModel. getUiState (). collectAsStateWithLifecycle ()
BaseScreen (
isLoading = uiState.isLoading,
) { paddingValues ->
HomeScreenContent (
uiState = uiState. data ,
onNavigateToDetail = navigateToArticleDetail,
modifier = Modifier. padding (paddingValues),
)
}
}
With Custom Top Bar
@Composable
fun ArticleDetailScreen () {
val viewModel: ArticleDetailViewModel = hiltViewModel ()
val uiState by viewModel. getUiState (). collectAsStateWithLifecycle ()
BaseScreen (
isLoading = uiState.isLoading,
topBar = {
TopAppBar (
title = { Text ( "Article Details" ) },
navigationIcon = {
IconButton (onClick = { /* Back */ }) {
Icon (Icons.Default.ArrowBack, "Back" )
}
}
)
},
bottomBar = {
uiState. data .article?.url?. let { url ->
ArticleDetailBottomBar (newsUrl = url)
}
},
) { paddingValues ->
uiState. data .article?. let { article ->
ArticleDetailScreenContent (
article = article,
modifier = Modifier. padding (paddingValues),
)
}
}
}
Without Loading State
@Composable
fun LauncherScreen () {
BaseScreen { paddingValues ->
LauncherScreenContent (
modifier = Modifier
. fillMaxSize ()
. paint (
painter = painterResource (R.drawable.launcher_background),
contentScale = ContentScale.Crop,
)
. padding (paddingValues),
)
}
}
Comparison
Use for:
App-level navigation setup
Conditional bar visibility based on route
Edge-to-edge design implementation
Window insets management
Features:
Smart window insets handling
Conditional bar display
Navigation-aware layout
ScreenWrapper (
topBar = { CptTopBar () },
bottomBar = { CptNavigationBar ( /*...*/ ) },
showTopAppBar = currentScreen.hasTopBar,
showBottomBar = currentScreen.hasBottomBar,
) { paddingValues ->
NavHost ( /*...*/ ) {
// Screens
}
}
Use for:
Individual feature screens
Screens with loading states
Screens with dynamic bars
ViewModel integration
Features:
Built-in loading indicator
ViewModel state handling
Automatic padding management
BaseScreen (
isLoading = uiState.isLoading,
topBar = { CustomTopBar () },
) { paddingValues ->
ScreenContent (
modifier = Modifier. padding (paddingValues)
)
}
Integration with BaseViewModel
Both components work seamlessly with BaseViewModel and UiState:
data class UiState < T >(
val data : T ,
val isLoading: Boolean = false
)
abstract class BaseViewModel < T > : ViewModel () {
protected abstract val uiState: MutableStateFlow < UiState < T >>
fun getUiState (): StateFlow < UiState < T >> = uiState. asStateFlow ()
}
@Composable
fun MyScreen () {
val viewModel: MyViewModel = hiltViewModel ()
val uiState by viewModel. getUiState (). collectAsStateWithLifecycle ()
BaseScreen (
isLoading = uiState.isLoading, // Direct binding
) { paddingValues ->
ContentView (
data = uiState. data ,
modifier = Modifier. padding (paddingValues)
)
}
}
Edge-to-Edge Design
Both components support Android’s edge-to-edge design:
class MainActivity : ComponentActivity () {
override fun onCreate (savedInstanceState: Bundle ?) {
super . onCreate (savedInstanceState)
enableEdgeToEdge () // Enable edge-to-edge
setContent {
CPTTheme {
ScreenWrapper (
// Handles system bars automatically
topBar = { CptTopBar () },
bottomBar = { CptNavigationBar ( /*...*/ ) },
showTopAppBar = true ,
showBottomBar = true ,
) { paddingValues ->
// Content with proper insets
}
}
}
}
}
The contentWindowInsets parameter in ScreenWrapper automatically manages system bar overlays, ensuring content doesn’t extend behind system UI.
Best Practices
Use ScreenWrapper at App Level Place ScreenWrapper at the navigation level to manage global bars consistently.
Use BaseScreen in Features Use BaseScreen within feature modules for individual screens with loading states.
Apply Padding Correctly Always apply the provided paddingValues to avoid content being hidden behind bars.
Combine with consumeWindowInsets Use consumeWindowInsets() in navigation hosts to prevent double padding.
Padding Best Practices
Correct
Incorrect
With Scroll
BaseScreen { paddingValues ->
Column (
modifier = Modifier. padding (paddingValues) // ✓ Apply padding
) {
// Content
}
}
BaseScreen { paddingValues ->
Column (
modifier = Modifier // ✗ Missing padding
) {
// Content will be hidden behind bars!
}
}
BaseScreen { paddingValues ->
LazyColumn (
contentPadding = paddingValues // ✓ Use contentPadding
) {
items (list) { item ->
// Items
}
}
}
Preview Support
Both components include preview functions:
@OptIn (ExperimentalMaterial3Api:: class )
@Composable
@Preview
private fun ScreenWrapperPreview () {
MaterialTheme {
ScreenWrapper (
topBar = { CptTopBar () },
bottomBar = {
CptNavigationBar (
selectedModule = NavigationModule.HOME,
modifier = Modifier,
onClickModule = { },
)
},
) { paddingValues ->
Box (
contentAlignment = Alignment.Center,
modifier = Modifier
. fillMaxSize ()
. padding (paddingValues),
) {
Text ( "Hello Android!" )
}
}
}
}
See Also