fix: share ConfigViewModel via nested NavGraph to prevent infinite spinner

List and Detail screens were creating separate ViewModel instances, causing
a race condition where Detail's schema was always null. Using a nested
navigation graph scopes a single ViewModel to the parent back stack entry.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
KoheiYamashita 2026-02-27 18:19:36 +09:00
parent 790cd6a411
commit 86550e0ab6
4 changed files with 38 additions and 20 deletions

View file

@ -9,18 +9,22 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.compose.runtime.remember
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.navigation
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import io.clawdroid.backend.config.ConfigSectionDetailScreen
import io.clawdroid.backend.config.ConfigSectionListScreen
import io.clawdroid.backend.config.ConfigViewModel
import io.clawdroid.core.ui.theme.ClawDroidTheme
import io.clawdroid.feature.chat.screen.ChatScreen
import io.clawdroid.feature.chat.screen.SettingsScreen
import io.clawdroid.navigation.NavRoutes
import io.clawdroid.settings.AppSettingsScreen
import org.koin.androidx.compose.koinViewModel
class MainActivity : ComponentActivity() {
@ -48,22 +52,37 @@ class MainActivity : ComponentActivity() {
onNavigateToAppSettings = { navController.navigate(NavRoutes.APP_SETTINGS) },
)
}
composable(NavRoutes.BACKEND_SETTINGS) {
ConfigSectionListScreen(
onNavigateBack = { navController.popBackStack() },
onSectionSelected = { sectionKey ->
navController.navigate("backend_settings/$sectionKey")
},
)
}
composable(
NavRoutes.BACKEND_SETTINGS_SECTION,
arguments = listOf(navArgument("sectionKey") { type = NavType.StringType }),
) { backStackEntry ->
ConfigSectionDetailScreen(
sectionKey = backStackEntry.arguments?.getString("sectionKey") ?: "",
onNavigateBack = { navController.popBackStack() },
)
navigation(
route = NavRoutes.BACKEND_SETTINGS,
startDestination = NavRoutes.BACKEND_SETTINGS_LIST,
) {
composable(NavRoutes.BACKEND_SETTINGS_LIST) { entry ->
val parentEntry = remember(entry) {
navController.getBackStackEntry(NavRoutes.BACKEND_SETTINGS)
}
val viewModel: ConfigViewModel = koinViewModel(viewModelStoreOwner = parentEntry)
ConfigSectionListScreen(
onNavigateBack = { navController.popBackStack() },
onSectionSelected = { sectionKey ->
navController.navigate("backend_settings/$sectionKey")
},
viewModel = viewModel,
)
}
composable(
NavRoutes.BACKEND_SETTINGS_SECTION,
arguments = listOf(navArgument("sectionKey") { type = NavType.StringType }),
) { entry ->
val parentEntry = remember(entry) {
navController.getBackStackEntry(NavRoutes.BACKEND_SETTINGS)
}
val viewModel: ConfigViewModel = koinViewModel(viewModelStoreOwner = parentEntry)
ConfigSectionDetailScreen(
sectionKey = entry.arguments?.getString("sectionKey") ?: "",
onNavigateBack = { navController.popBackStack() },
viewModel = viewModel,
)
}
}
composable(NavRoutes.APP_SETTINGS) {
AppSettingsScreen(

View file

@ -4,6 +4,7 @@ object NavRoutes {
const val CHAT = "chat"
const val SETTINGS = "settings"
const val BACKEND_SETTINGS = "backend_settings"
const val BACKEND_SETTINGS_LIST = "backend_settings/list"
const val BACKEND_SETTINGS_SECTION = "backend_settings/{sectionKey}"
const val APP_SETTINGS = "app_settings"
}

View file

@ -59,14 +59,13 @@ import io.clawdroid.core.ui.theme.NeonCyan
import io.clawdroid.core.ui.theme.TextPrimary
import io.clawdroid.core.ui.theme.TextSecondary
import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ConfigSectionDetailScreen(
sectionKey: String,
onNavigateBack: () -> Unit,
viewModel: ConfigViewModel = koinViewModel(),
viewModel: ConfigViewModel,
) {
val uiState by viewModel.uiState.collectAsState()
val snackbarHostState = remember { SnackbarHostState() }

View file

@ -42,14 +42,13 @@ import io.clawdroid.core.ui.theme.GlassWhite
import io.clawdroid.core.ui.theme.NeonCyan
import io.clawdroid.core.ui.theme.TextPrimary
import io.clawdroid.core.ui.theme.TextSecondary
import org.koin.androidx.compose.koinViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ConfigSectionListScreen(
onNavigateBack: () -> Unit,
onSectionSelected: (sectionKey: String) -> Unit,
viewModel: ConfigViewModel = koinViewModel(),
viewModel: ConfigViewModel,
) {
val uiState by viewModel.uiState.collectAsState()