fix: smart navigation waits for DataStore before routing

Use flow.first() instead of collectAsState(initial = null) to
determine start destination. The previous approach fired immediately
with the initial null value before DataStore loaded from disk, always
routing to Setup. Now the loading screen suspends until preferences
are actually read.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-03-18 17:02:16 +01:00
commit ebdf72e2c1
2 changed files with 8 additions and 29 deletions

View file

@ -8,7 +8,6 @@ import androidx.activity.enableEdgeToEdge
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import no.naiv.implausibly.data.AppPreferences import no.naiv.implausibly.data.AppPreferences
import no.naiv.implausibly.data.repository.InstanceRepository import no.naiv.implausibly.data.repository.InstanceRepository
import no.naiv.implausibly.data.repository.SiteRepository
import no.naiv.implausibly.ui.navigation.AppNavHost import no.naiv.implausibly.ui.navigation.AppNavHost
import no.naiv.implausibly.ui.theme.ImplausiblyTheme import no.naiv.implausibly.ui.theme.ImplausiblyTheme
import javax.inject.Inject import javax.inject.Inject
@ -18,7 +17,6 @@ class MainActivity : ComponentActivity() {
@Inject lateinit var appPreferences: AppPreferences @Inject lateinit var appPreferences: AppPreferences
@Inject lateinit var instanceRepository: InstanceRepository @Inject lateinit var instanceRepository: InstanceRepository
@Inject lateinit var siteRepository: SiteRepository
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -27,8 +25,7 @@ class MainActivity : ComponentActivity() {
ImplausiblyTheme { ImplausiblyTheme {
AppNavHost( AppNavHost(
appPreferences = appPreferences, appPreferences = appPreferences,
instanceRepository = instanceRepository, instanceRepository = instanceRepository
siteRepository = siteRepository
) )
} }
} }

View file

@ -3,19 +3,14 @@ package no.naiv.implausibly.ui.navigation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.navigation.NavType import androidx.navigation.NavType
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument import androidx.navigation.navArgument
import kotlinx.coroutines.flow.first
import no.naiv.implausibly.data.AppPreferences import no.naiv.implausibly.data.AppPreferences
import no.naiv.implausibly.data.repository.InstanceRepository import no.naiv.implausibly.data.repository.InstanceRepository
import no.naiv.implausibly.data.repository.SiteRepository
import no.naiv.implausibly.ui.common.LoadingIndicator import no.naiv.implausibly.ui.common.LoadingIndicator
import no.naiv.implausibly.ui.dashboard.DashboardScreen import no.naiv.implausibly.ui.dashboard.DashboardScreen
import no.naiv.implausibly.ui.setup.SetupScreen import no.naiv.implausibly.ui.setup.SetupScreen
@ -25,42 +20,29 @@ import no.naiv.implausibly.ui.sites.SiteListScreen
* App-level navigation host. * App-level navigation host.
* *
* On launch, checks DataStore for a previously selected instance/site. * On launch, checks DataStore for a previously selected instance/site.
* If found, navigates directly to the dashboard (or site list if no site * If found, navigates directly to the dashboard. Otherwise, shows setup.
* is selected). Otherwise, shows the setup screen.
*/ */
@Composable @Composable
fun AppNavHost( fun AppNavHost(
appPreferences: AppPreferences, appPreferences: AppPreferences,
instanceRepository: InstanceRepository, instanceRepository: InstanceRepository
siteRepository: SiteRepository
) { ) {
val navController = rememberNavController() val navController = rememberNavController()
// Collect saved preferences to determine start destination
val savedInstanceId by appPreferences.selectedInstanceId.collectAsState(initial = null)
val savedSiteId by appPreferences.selectedSiteId.collectAsState(initial = null)
var hasNavigated by remember { mutableStateOf(false) }
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = Routes.LOADING startDestination = Routes.LOADING
) { ) {
// Loading screen — resolves start destination from preferences
composable(Routes.LOADING) { composable(Routes.LOADING) {
LoadingIndicator() LoadingIndicator()
LaunchedEffect(savedInstanceId, savedSiteId) { // Use flow.first() to suspend until DataStore actually loads
// Wait for preferences to load (initial null vs actual null) LaunchedEffect(Unit) {
// Once we have a value (even if null), navigate val instanceId = appPreferences.selectedInstanceId.first()
if (hasNavigated) return@LaunchedEffect val siteId = appPreferences.selectedSiteId.first()
hasNavigated = true
val instanceId = savedInstanceId
val siteId = savedSiteId
val destination = when { val destination = when {
instanceId != null && siteId != null -> { instanceId != null && siteId != null -> {
// Verify the instance still exists
val instance = instanceRepository.getById(instanceId) val instance = instanceRepository.getById(instanceId)
if (instance != null) { if (instance != null) {
Routes.dashboard(instanceId, siteId) Routes.dashboard(instanceId, siteId)