From 5111b6e06608e0d90ea241434e6e5391b7ca1a1a Mon Sep 17 00:00:00 2001 From: shahondin1624 Date: Fri, 13 Mar 2026 13:33:44 +0100 Subject: [PATCH] feat: character state management with ViewModel (Closes #23) (#60) --- .../kotlin/org/shahondin1624/App.kt | 35 ++++++++++--------- .../viewmodel/CharacterViewModel.kt | 34 ++++++++++++++++++ 2 files changed, 53 insertions(+), 16 deletions(-) create mode 100644 sharedUI/src/commonMain/kotlin/org/shahondin1624/viewmodel/CharacterViewModel.kt diff --git a/sharedUI/src/commonMain/kotlin/org/shahondin1624/App.kt b/sharedUI/src/commonMain/kotlin/org/shahondin1624/App.kt index b691a73..9760b1c 100644 --- a/sharedUI/src/commonMain/kotlin/org/shahondin1624/App.kt +++ b/sharedUI/src/commonMain/kotlin/org/shahondin1624/App.kt @@ -10,6 +10,7 @@ import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import androidx.compose.runtime.rememberCoroutineScope @@ -17,6 +18,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -28,12 +30,12 @@ import org.shahondin1624.lib.components.TestTags import org.shahondin1624.lib.components.UiConstants import org.shahondin1624.lib.components.charactermodel.CharacterSheetPage import org.shahondin1624.lib.components.settings.SettingsPage -import org.shahondin1624.model.EXAMPLE_CHARACTER import org.shahondin1624.navigation.AppRoutes import org.shahondin1624.theme.AppTheme import org.shahondin1624.theme.LocalThemeIsDark import org.shahondin1624.theme.LocalWindowSizeClass import org.shahondin1624.theme.WindowSizeClass +import org.shahondin1624.viewmodel.CharacterViewModel @OptIn(ExperimentalMaterial3Api::class) @Preview @@ -46,20 +48,21 @@ fun App( CompositionLocalProvider(LocalWindowSizeClass provides windowSizeClass) { val navController = rememberNavController() - AppContent(navController) + val characterViewModel: CharacterViewModel = viewModel { CharacterViewModel() } + AppContent(navController, characterViewModel) } } } @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun AppContent(navController: NavHostController) { +private fun AppContent(navController: NavHostController, characterViewModel: CharacterViewModel) { val windowSizeClass = LocalWindowSizeClass.current when (windowSizeClass) { - WindowSizeClass.Compact -> CompactNavigation(navController) - WindowSizeClass.Medium -> MediumNavigation(navController) - WindowSizeClass.Expanded -> ExpandedNavigation(navController) + WindowSizeClass.Compact -> CompactNavigation(navController, characterViewModel) + WindowSizeClass.Medium -> MediumNavigation(navController, characterViewModel) + WindowSizeClass.Expanded -> ExpandedNavigation(navController, characterViewModel) } } @@ -91,7 +94,7 @@ private fun navigateTo(navController: NavHostController, route: String) { */ @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun CompactNavigation(navController: NavHostController) { +private fun CompactNavigation(navController: NavHostController, characterViewModel: CharacterViewModel) { val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) val scope = rememberCoroutineScope() val currentRoute = currentRoute(navController) @@ -112,6 +115,7 @@ private fun CompactNavigation(navController: NavHostController) { ) { MainScaffold( navController = navController, + characterViewModel = characterViewModel, showMenuButton = true, onMenuClick = { scope.launch { @@ -127,7 +131,7 @@ private fun CompactNavigation(navController: NavHostController) { */ @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun MediumNavigation(navController: NavHostController) { +private fun MediumNavigation(navController: NavHostController, characterViewModel: CharacterViewModel) { val currentRoute = currentRoute(navController) Row(modifier = Modifier.fillMaxSize()) { @@ -152,6 +156,7 @@ private fun MediumNavigation(navController: NavHostController) { } MainScaffold( navController = navController, + characterViewModel = characterViewModel, showMenuButton = false, onMenuClick = {}, modifier = Modifier.weight(1f) @@ -164,7 +169,7 @@ private fun MediumNavigation(navController: NavHostController) { */ @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun ExpandedNavigation(navController: NavHostController) { +private fun ExpandedNavigation(navController: NavHostController, characterViewModel: CharacterViewModel) { val currentRoute = currentRoute(navController) PermanentNavigationDrawer( @@ -179,6 +184,7 @@ private fun ExpandedNavigation(navController: NavHostController) { ) { MainScaffold( navController = navController, + characterViewModel = characterViewModel, showMenuButton = false, onMenuClick = {} ) @@ -225,25 +231,22 @@ private fun DrawerContent( /** * Main scaffold with top app bar and routed content area. - * - * @param navController the NavHostController for page routing - * @param showMenuButton whether to show the hamburger menu icon (hidden when nav is persistent) - * @param onMenuClick callback for hamburger menu button click - * @param modifier optional modifier (e.g., weight in Row for Medium layout) */ @OptIn(ExperimentalMaterial3Api::class) @Composable private fun MainScaffold( navController: NavHostController, + characterViewModel: CharacterViewModel, showMenuButton: Boolean, onMenuClick: () -> Unit, modifier: Modifier = Modifier ) { var isDark by LocalThemeIsDark.current val currentRoute = currentRoute(navController) + val character by characterViewModel.character.collectAsState() val topBarTitle = when (currentRoute) { - AppRoutes.CHARACTER_SHEET -> EXAMPLE_CHARACTER.characterData.name + AppRoutes.CHARACTER_SHEET -> character.characterData.name AppRoutes.SETTINGS -> "Settings" else -> "Shadowrun Character Sheet" } @@ -301,7 +304,7 @@ private fun MainScaffold( .padding(contentPadding) ) { composable(AppRoutes.CHARACTER_SHEET) { - CharacterSheetPage(EXAMPLE_CHARACTER, contentPadding) + CharacterSheetPage(character, contentPadding) } composable(AppRoutes.SETTINGS) { SettingsPage() diff --git a/sharedUI/src/commonMain/kotlin/org/shahondin1624/viewmodel/CharacterViewModel.kt b/sharedUI/src/commonMain/kotlin/org/shahondin1624/viewmodel/CharacterViewModel.kt new file mode 100644 index 0000000..3ea4cbf --- /dev/null +++ b/sharedUI/src/commonMain/kotlin/org/shahondin1624/viewmodel/CharacterViewModel.kt @@ -0,0 +1,34 @@ +package org.shahondin1624.viewmodel + +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.shahondin1624.model.EXAMPLE_CHARACTER +import org.shahondin1624.model.charactermodel.ShadowrunCharacter + +/** + * ViewModel holding the current character as observable state. + * Foundation for all editing stories (6.2-6.6) and interactive features. + */ +class CharacterViewModel : ViewModel() { + + private val _character = MutableStateFlow(EXAMPLE_CHARACTER) + + /** Observable character state. */ + val character: StateFlow = _character.asStateFlow() + + /** + * Replace the current character entirely. + */ + fun setCharacter(character: ShadowrunCharacter) { + _character.value = character + } + + /** + * Update the character by applying a transformation function. + */ + fun updateCharacter(transform: (ShadowrunCharacter) -> ShadowrunCharacter) { + _character.value = transform(_character.value) + } +}