feat: character state management with ViewModel (Closes #23) #60

Merged
shahondin1624 merged 1 commits from feature/issue-23-character-viewmodel into main 2026-03-13 13:33:44 +01:00
2 changed files with 53 additions and 16 deletions

View File

@@ -10,6 +10,7 @@ import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
@@ -17,6 +18,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable 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.UiConstants
import org.shahondin1624.lib.components.charactermodel.CharacterSheetPage import org.shahondin1624.lib.components.charactermodel.CharacterSheetPage
import org.shahondin1624.lib.components.settings.SettingsPage import org.shahondin1624.lib.components.settings.SettingsPage
import org.shahondin1624.model.EXAMPLE_CHARACTER
import org.shahondin1624.navigation.AppRoutes import org.shahondin1624.navigation.AppRoutes
import org.shahondin1624.theme.AppTheme import org.shahondin1624.theme.AppTheme
import org.shahondin1624.theme.LocalThemeIsDark import org.shahondin1624.theme.LocalThemeIsDark
import org.shahondin1624.theme.LocalWindowSizeClass import org.shahondin1624.theme.LocalWindowSizeClass
import org.shahondin1624.theme.WindowSizeClass import org.shahondin1624.theme.WindowSizeClass
import org.shahondin1624.viewmodel.CharacterViewModel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Preview @Preview
@@ -46,20 +48,21 @@ fun App(
CompositionLocalProvider(LocalWindowSizeClass provides windowSizeClass) { CompositionLocalProvider(LocalWindowSizeClass provides windowSizeClass) {
val navController = rememberNavController() val navController = rememberNavController()
AppContent(navController) val characterViewModel: CharacterViewModel = viewModel { CharacterViewModel() }
AppContent(navController, characterViewModel)
} }
} }
} }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun AppContent(navController: NavHostController) { private fun AppContent(navController: NavHostController, characterViewModel: CharacterViewModel) {
val windowSizeClass = LocalWindowSizeClass.current val windowSizeClass = LocalWindowSizeClass.current
when (windowSizeClass) { when (windowSizeClass) {
WindowSizeClass.Compact -> CompactNavigation(navController) WindowSizeClass.Compact -> CompactNavigation(navController, characterViewModel)
WindowSizeClass.Medium -> MediumNavigation(navController) WindowSizeClass.Medium -> MediumNavigation(navController, characterViewModel)
WindowSizeClass.Expanded -> ExpandedNavigation(navController) WindowSizeClass.Expanded -> ExpandedNavigation(navController, characterViewModel)
} }
} }
@@ -91,7 +94,7 @@ private fun navigateTo(navController: NavHostController, route: String) {
*/ */
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun CompactNavigation(navController: NavHostController) { private fun CompactNavigation(navController: NavHostController, characterViewModel: CharacterViewModel) {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val currentRoute = currentRoute(navController) val currentRoute = currentRoute(navController)
@@ -112,6 +115,7 @@ private fun CompactNavigation(navController: NavHostController) {
) { ) {
MainScaffold( MainScaffold(
navController = navController, navController = navController,
characterViewModel = characterViewModel,
showMenuButton = true, showMenuButton = true,
onMenuClick = { onMenuClick = {
scope.launch { scope.launch {
@@ -127,7 +131,7 @@ private fun CompactNavigation(navController: NavHostController) {
*/ */
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun MediumNavigation(navController: NavHostController) { private fun MediumNavigation(navController: NavHostController, characterViewModel: CharacterViewModel) {
val currentRoute = currentRoute(navController) val currentRoute = currentRoute(navController)
Row(modifier = Modifier.fillMaxSize()) { Row(modifier = Modifier.fillMaxSize()) {
@@ -152,6 +156,7 @@ private fun MediumNavigation(navController: NavHostController) {
} }
MainScaffold( MainScaffold(
navController = navController, navController = navController,
characterViewModel = characterViewModel,
showMenuButton = false, showMenuButton = false,
onMenuClick = {}, onMenuClick = {},
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
@@ -164,7 +169,7 @@ private fun MediumNavigation(navController: NavHostController) {
*/ */
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun ExpandedNavigation(navController: NavHostController) { private fun ExpandedNavigation(navController: NavHostController, characterViewModel: CharacterViewModel) {
val currentRoute = currentRoute(navController) val currentRoute = currentRoute(navController)
PermanentNavigationDrawer( PermanentNavigationDrawer(
@@ -179,6 +184,7 @@ private fun ExpandedNavigation(navController: NavHostController) {
) { ) {
MainScaffold( MainScaffold(
navController = navController, navController = navController,
characterViewModel = characterViewModel,
showMenuButton = false, showMenuButton = false,
onMenuClick = {} onMenuClick = {}
) )
@@ -225,25 +231,22 @@ private fun DrawerContent(
/** /**
* Main scaffold with top app bar and routed content area. * 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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun MainScaffold( private fun MainScaffold(
navController: NavHostController, navController: NavHostController,
characterViewModel: CharacterViewModel,
showMenuButton: Boolean, showMenuButton: Boolean,
onMenuClick: () -> Unit, onMenuClick: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
var isDark by LocalThemeIsDark.current var isDark by LocalThemeIsDark.current
val currentRoute = currentRoute(navController) val currentRoute = currentRoute(navController)
val character by characterViewModel.character.collectAsState()
val topBarTitle = when (currentRoute) { val topBarTitle = when (currentRoute) {
AppRoutes.CHARACTER_SHEET -> EXAMPLE_CHARACTER.characterData.name AppRoutes.CHARACTER_SHEET -> character.characterData.name
AppRoutes.SETTINGS -> "Settings" AppRoutes.SETTINGS -> "Settings"
else -> "Shadowrun Character Sheet" else -> "Shadowrun Character Sheet"
} }
@@ -301,7 +304,7 @@ private fun MainScaffold(
.padding(contentPadding) .padding(contentPadding)
) { ) {
composable(AppRoutes.CHARACTER_SHEET) { composable(AppRoutes.CHARACTER_SHEET) {
CharacterSheetPage(EXAMPLE_CHARACTER, contentPadding) CharacterSheetPage(character, contentPadding)
} }
composable(AppRoutes.SETTINGS) { composable(AppRoutes.SETTINGS) {
SettingsPage() SettingsPage()

View File

@@ -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<ShadowrunCharacter> = _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)
}
}