Skip to main content
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)
showTopAppBar
Boolean
default:"false"
Controls whether the top bar is visible
showBottomBar
Boolean
default:"false"
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:
Both Bars Visible
WindowInsets
WindowInsets.systemBars - Handles both status bar and navigation bar insets
Only Top Bar
WindowInsets
WindowInsets.statusBars - Handles only status bar insets
Only Bottom Bar
WindowInsets
WindowInsets.navigationBars - Handles only navigation bar insets
No Bars
WindowInsets
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
isLoading
Boolean
default:"false"
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
    }
}

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

BaseScreen { paddingValues ->
    Column(
        modifier = Modifier.padding(paddingValues)  // ✓ Apply padding
    ) {
        // Content
    }
}

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

Build docs developers (and LLMs) love