feat(navigation): implement CRUD navigation and MainActivity integration

ui/navigation/Screen.kt:
- Sealed interface with @Serializable routes: ItemList, ItemForm,
  CategoryManagement, LocationManagement
- ItemForm accepts optional itemId for edit mode (type-safe navigation)

ui/navigation/KrisenvorratNavGraph.kt:
- NavHost with ItemList as start destination
- Screen wiring: ItemList -> ItemForm (create/edit), CategoryManagement,
  LocationManagement with back navigation via popBackStack()

ui/item/ItemListScreen.kt:
- Added onItemClick, onCategoriesClick, onLocationsClick callbacks
- TopAppBar MoreVert dropdown menu for category/location management
- ItemCard now clickable to navigate to edit mode

MainActivity.kt:
- Replaced placeholder with NavHost via KrisenvorratNavGraph
- rememberNavController() as root navigation controller

ui/navigation/ScreenTest.kt:
- 7 tests covering route instantiation, nullable itemId, equality

Closes #28
This commit is contained in:
Jens Reinemann 2026-05-14 01:20:54 +02:00
parent f0ad946140
commit 10d19f0321
5 changed files with 206 additions and 28 deletions

View file

@ -4,18 +4,13 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.theme.KrisenvorratTheme
@AndroidEntryPoint
@ -25,25 +20,8 @@ class MainActivity : ComponentActivity() {
enableEdgeToEdge()
setContent {
KrisenvorratTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "Krisenvorrat",
style = MaterialTheme.typography.headlineLarge
)
Text(
text = "v${BuildConfig.VERSION_NAME}",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
val navController = rememberNavController()
KrisenvorratNavGraph(navController = navController)
}
}
}
@ -53,6 +31,9 @@ class MainActivity : ComponentActivity() {
@Composable
fun DefaultPreview() {
KrisenvorratTheme {
Text("Krisenvorrat")
Text(
text = "Krisenvorrat",
style = MaterialTheme.typography.headlineLarge
)
}
}

View file

@ -1,5 +1,6 @@
package de.krisenvorrat.app.ui.item
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -14,8 +15,11 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
@ -27,6 +31,9 @@ import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@ -39,14 +46,45 @@ import java.time.format.DateTimeFormatter
@Composable
internal fun ItemListScreen(
onAddItem: () -> Unit,
onItemClick: (String) -> Unit,
onCategoriesClick: () -> Unit,
onLocationsClick: () -> Unit,
viewModel: ItemListViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
var isMenuExpanded by remember { mutableStateOf(false) }
Scaffold(
topBar = {
TopAppBar(
title = { Text("Artikel") }
title = { Text("Artikel") },
actions = {
IconButton(onClick = { isMenuExpanded = true }) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = "Menü"
)
}
DropdownMenu(
expanded = isMenuExpanded,
onDismissRequest = { isMenuExpanded = false }
) {
DropdownMenuItem(
text = { Text("Kategorien") },
onClick = {
isMenuExpanded = false
onCategoriesClick()
}
)
DropdownMenuItem(
text = { Text("Lagerorte") },
onClick = {
isMenuExpanded = false
onLocationsClick()
}
)
}
}
)
},
floatingActionButton = {
@ -67,6 +105,7 @@ internal fun ItemListScreen(
} else {
ItemList(
groupedItems = uiState.groupedItems,
onItemClick = onItemClick,
onDeleteClick = viewModel::showDeleteDialog,
modifier = Modifier
.fillMaxSize()
@ -109,6 +148,7 @@ private fun EmptyState(modifier: Modifier = Modifier) {
@Composable
private fun ItemList(
groupedItems: Map<String, List<ItemUiModel>>,
onItemClick: (String) -> Unit,
onDeleteClick: (ItemUiModel) -> Unit,
modifier: Modifier = Modifier
) {
@ -123,6 +163,7 @@ private fun ItemList(
items(items, key = { it.id }) { item ->
ItemCard(
item = item,
onClick = { onItemClick(item.id) },
onDeleteClick = { onDeleteClick(item) }
)
}
@ -145,12 +186,14 @@ private fun CategoryHeader(categoryName: String) {
@Composable
private fun ItemCard(
item: ItemUiModel,
onClick: () -> Unit,
onDeleteClick: () -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 2.dp)
.clickable(onClick = onClick)
) {
Row(
modifier = Modifier

View file

@ -0,0 +1,59 @@
package de.krisenvorrat.app.ui.navigation
import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import de.krisenvorrat.app.ui.category.CategoryListScreen
import de.krisenvorrat.app.ui.item.ItemFormScreen
import de.krisenvorrat.app.ui.item.ItemListScreen
import de.krisenvorrat.app.ui.location.LocationListScreen
@Composable
internal fun KrisenvorratNavGraph(navController: NavHostController) {
NavHost(
navController = navController,
startDestination = Screen.ItemList
) {
composable<Screen.ItemList> {
ItemListScreen(
onAddItem = {
navController.navigate(Screen.ItemForm())
},
onItemClick = { itemId ->
navController.navigate(Screen.ItemForm(itemId = itemId))
},
onCategoriesClick = {
navController.navigate(Screen.CategoryManagement)
},
onLocationsClick = {
navController.navigate(Screen.LocationManagement)
}
)
}
composable<Screen.ItemForm> {
ItemFormScreen(
onNavigateBack = {
navController.popBackStack()
}
)
}
composable<Screen.CategoryManagement> {
CategoryListScreen(
onNavigateBack = {
navController.popBackStack()
}
)
}
composable<Screen.LocationManagement> {
LocationListScreen(
onNavigateBack = {
navController.popBackStack()
}
)
}
}
}

View file

@ -0,0 +1,19 @@
package de.krisenvorrat.app.ui.navigation
import kotlinx.serialization.Serializable
@Serializable
internal sealed interface Screen {
@Serializable
data object ItemList : Screen
@Serializable
data class ItemForm(val itemId: String? = null) : Screen
@Serializable
data object CategoryManagement : Screen
@Serializable
data object LocationManagement : Screen
}

View file

@ -0,0 +1,76 @@
package de.krisenvorrat.app.ui.navigation
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
class ScreenTest {
@Test
fun test_itemListRoute_isSingleton() {
// Given & When
val route = Screen.ItemList
// Then
assertEquals(Screen.ItemList, route)
}
@Test
fun test_itemFormRoute_withoutItemId_hasNullItemId() {
// Given & When
val route = Screen.ItemForm()
// Then
assertNull(route.itemId)
}
@Test
fun test_itemFormRoute_withItemId_hasCorrectItemId() {
// Given
val expectedId = "test-item-123"
// When
val route = Screen.ItemForm(itemId = expectedId)
// Then
assertEquals(expectedId, route.itemId)
}
@Test
fun test_categoryManagementRoute_isSingleton() {
// Given & When
val route = Screen.CategoryManagement
// Then
assertEquals(Screen.CategoryManagement, route)
}
@Test
fun test_locationManagementRoute_isSingleton() {
// Given & When
val route = Screen.LocationManagement
// Then
assertEquals(Screen.LocationManagement, route)
}
@Test
fun test_itemFormRoute_equality_sameItemId_areEqual() {
// Given
val route1 = Screen.ItemForm(itemId = "abc")
val route2 = Screen.ItemForm(itemId = "abc")
// Then
assertEquals(route1, route2)
}
@Test
fun test_itemFormRoute_equality_differentItemId_areNotEqual() {
// Given
val route1 = Screen.ItemForm(itemId = "abc")
val route2 = Screen.ItemForm(itemId = "xyz")
// Then
assert(route1 != route2)
}
}