feat(navigation): implement Bottom Navigation Bar with 4 tabs and app shell

MainScreen.kt: new app shell with Scaffold + Material 3 NavigationBar
providing 4 tabs (Uebersicht, Inventur, Warnungen, Einstellungen).

TopLevelDestination.kt: enum defining tab routes, icons (Home, Inventory2,
Warning, Settings), and labels for the navigation bar.

Screen.kt: added Warnings and Settings sealed interface members.

KrisenvorratNavGraph.kt: accepts Modifier, added Warnings/Settings
composables, removed obsolete DashboardScreen navigation callback.

DashboardScreen.kt: removed Scaffold wrapper and onNavigateToItems param,
now uses Column layout (TopAppBar handled inline).

ItemListScreen.kt: removed onDashboardClick param and Dashboard menu entry
(no longer needed with tab navigation).

WarningsScreen.kt, SettingsScreen.kt: placeholder screens for future impl.

MainActivity.kt: delegates to MainScreen instead of NavGraph directly.

Added material-icons-extended dependency for Inventory2 icon.

Closes #33
This commit is contained in:
Jens Reinemann 2026-05-14 02:25:47 +02:00
parent c88d10be10
commit a4c0dc63b4
11 changed files with 214 additions and 47 deletions

View file

@ -53,6 +53,7 @@ dependencies {
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.material.icons.extended)
// Hilt
implementation(libs.hilt.android)

View file

@ -8,9 +8,8 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import androidx.navigation.compose.rememberNavController
import dagger.hilt.android.AndroidEntryPoint
import de.krisenvorrat.app.ui.navigation.KrisenvorratNavGraph
import de.krisenvorrat.app.ui.MainScreen
import de.krisenvorrat.app.ui.theme.KrisenvorratTheme
@AndroidEntryPoint
@ -20,8 +19,7 @@ class MainActivity : ComponentActivity() {
enableEdgeToEdge()
setContent {
KrisenvorratTheme {
val navController = rememberNavController()
KrisenvorratNavGraph(navController = navController)
MainScreen()
}
}
}

View file

@ -0,0 +1,72 @@
package de.krisenvorrat.app.ui
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import de.krisenvorrat.app.ui.navigation.KrisenvorratNavGraph
import de.krisenvorrat.app.ui.navigation.TopLevelDestination
@Composable
internal fun MainScreen() {
val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
val showBottomBar = TopLevelDestination.entries.any { topLevel ->
currentDestination?.hasRoute(topLevel.route::class) == true
}
Scaffold(
bottomBar = {
if (showBottomBar) {
NavigationBar {
TopLevelDestination.entries.forEach { destination ->
val isSelected = currentDestination?.hasRoute(destination.route::class) == true
NavigationBarItem(
selected = isSelected,
onClick = {
navController.navigate(destination.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
},
icon = {
Icon(
imageVector = if (isSelected) {
destination.selectedIcon
} else {
destination.unselectedIcon
},
contentDescription = destination.label
)
},
label = { Text(destination.label) }
)
}
}
}
}
) { innerPadding ->
KrisenvorratNavGraph(
navController = navController,
modifier = Modifier
.padding(innerPadding)
.consumeWindowInsets(innerPadding)
)
}
}

View file

@ -10,16 +10,11 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.List
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
@ -42,38 +37,23 @@ import kotlin.math.roundToInt
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun DashboardScreen(
onNavigateToItems: () -> Unit,
viewModel: DashboardViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
Scaffold(
topBar = {
TopAppBar(
title = { Text("Dashboard") },
actions = {
IconButton(onClick = onNavigateToItems) {
Icon(
imageVector = Icons.Default.List,
contentDescription = "Artikelliste"
)
}
}
)
}
) { padding ->
Column(modifier = Modifier.fillMaxSize()) {
TopAppBar(title = { Text("Übersicht") })
if (uiState.isLoading) {
CircularProgressIndicator(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(16.dp)
)
} else {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {

View file

@ -49,7 +49,6 @@ internal fun ItemListScreen(
onItemClick: (String) -> Unit,
onCategoriesClick: () -> Unit,
onLocationsClick: () -> Unit,
onDashboardClick: () -> Unit,
viewModel: ItemListViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
@ -70,13 +69,6 @@ internal fun ItemListScreen(
expanded = isMenuExpanded,
onDismissRequest = { isMenuExpanded = false }
) {
DropdownMenuItem(
text = { Text("Dashboard") },
onClick = {
isMenuExpanded = false
onDashboardClick()
}
)
DropdownMenuItem(
text = { Text("Kategorien") },
onClick = {

View file

@ -1,6 +1,7 @@
package de.krisenvorrat.app.ui.navigation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
@ -9,19 +10,21 @@ import de.krisenvorrat.app.ui.dashboard.DashboardScreen
import de.krisenvorrat.app.ui.item.ItemFormScreen
import de.krisenvorrat.app.ui.item.ItemListScreen
import de.krisenvorrat.app.ui.location.LocationListScreen
import de.krisenvorrat.app.ui.settings.SettingsScreen
import de.krisenvorrat.app.ui.warnings.WarningsScreen
@Composable
internal fun KrisenvorratNavGraph(navController: NavHostController) {
internal fun KrisenvorratNavGraph(
navController: NavHostController,
modifier: Modifier = Modifier
) {
NavHost(
navController = navController,
startDestination = Screen.Dashboard
startDestination = Screen.Dashboard,
modifier = modifier
) {
composable<Screen.Dashboard> {
DashboardScreen(
onNavigateToItems = {
navController.navigate(Screen.ItemList)
}
)
DashboardScreen()
}
composable<Screen.ItemList> {
@ -37,11 +40,6 @@ internal fun KrisenvorratNavGraph(navController: NavHostController) {
},
onLocationsClick = {
navController.navigate(Screen.LocationManagement)
},
onDashboardClick = {
navController.navigate(Screen.Dashboard) {
popUpTo(Screen.Dashboard) { inclusive = true }
}
}
)
}
@ -69,5 +67,13 @@ internal fun KrisenvorratNavGraph(navController: NavHostController) {
}
)
}
composable<Screen.Warnings> {
WarningsScreen()
}
composable<Screen.Settings> {
SettingsScreen()
}
}
}

View file

@ -19,4 +19,10 @@ internal sealed interface Screen {
@Serializable
data object LocationManagement : Screen
@Serializable
data object Warnings : Screen
@Serializable
data object Settings : Screen
}

View file

@ -0,0 +1,43 @@
package de.krisenvorrat.app.ui.navigation
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material.icons.outlined.Home
import androidx.compose.material.icons.outlined.Inventory2
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.Warning
import androidx.compose.ui.graphics.vector.ImageVector
internal enum class TopLevelDestination(
val route: Screen,
val selectedIcon: ImageVector,
val unselectedIcon: ImageVector,
val label: String
) {
OVERVIEW(
route = Screen.Dashboard,
selectedIcon = Icons.Filled.Home,
unselectedIcon = Icons.Outlined.Home,
label = "Übersicht"
),
INVENTORY(
route = Screen.ItemList,
selectedIcon = Icons.Outlined.Inventory2,
unselectedIcon = Icons.Outlined.Inventory2,
label = "Inventur"
),
WARNINGS(
route = Screen.Warnings,
selectedIcon = Icons.Filled.Warning,
unselectedIcon = Icons.Outlined.Warning,
label = "Warnungen"
),
SETTINGS(
route = Screen.Settings,
selectedIcon = Icons.Filled.Settings,
unselectedIcon = Icons.Outlined.Settings,
label = "Einstellungen"
);
}

View file

@ -0,0 +1,34 @@
package de.krisenvorrat.app.ui.settings
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun SettingsScreen() {
Column(modifier = Modifier.fillMaxSize()) {
TopAppBar(title = { Text("Einstellungen") })
Box(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
Text(
text = "Einstellungen werden in einem späteren Schritt implementiert.",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}

View file

@ -0,0 +1,34 @@
package de.krisenvorrat.app.ui.warnings
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun WarningsScreen() {
Column(modifier = Modifier.fillMaxSize()) {
TopAppBar(title = { Text("Warnungen") })
Box(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
Text(
text = "Warnungen werden in einem späteren Schritt implementiert.",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}

View file

@ -32,6 +32,7 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
androidx-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigationCompose" }