From 5f96f63330158681017abd6fdcdd48794e9b3171 Mon Sep 17 00:00:00 2001 From: SatoshiQaziMuhammed Date: Sun, 18 Oct 2026 16:04:52 +0400 Subject: [PATCH] fix(splash): navigate to the first screen once, so a recreated activity cannot crash the app (#26) * test(splash): the initial destination is navigated to once * fix(splash): navigate to the first screen once The splash screen asks for the initial destination on every onResume, which runs again when the activity is recreated during startup while the view model survives. The second navigation found the first already on the welcome screen, with no route from there, and crashed the app with 'Unknown case for welcomeFragment'. The view model now starts it once. --- feature-splash/build.gradle | 1 + .../splash/presentation/SplashViewModel.kt | 11 ++++- .../presentation/SplashViewModelTest.kt | 49 +++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 feature-splash/src/test/java/io/novafoundation/nova/splash/presentation/SplashViewModelTest.kt diff --git a/feature-splash/build.gradle b/feature-splash/build.gradle index 1060b3ad..be3c72d5 100644 --- a/feature-splash/build.gradle +++ b/feature-splash/build.gradle @@ -33,6 +33,7 @@ dependencies { testImplementation jUnitDep testImplementation mockitoDep + testImplementation project(':test-shared') implementation coroutinesDep implementation coroutinesAndroidDep diff --git a/feature-splash/src/main/java/io/novafoundation/nova/splash/presentation/SplashViewModel.kt b/feature-splash/src/main/java/io/novafoundation/nova/splash/presentation/SplashViewModel.kt index 160dcf9d..0046f717 100644 --- a/feature-splash/src/main/java/io/novafoundation/nova/splash/presentation/SplashViewModel.kt +++ b/feature-splash/src/main/java/io/novafoundation/nova/splash/presentation/SplashViewModel.kt @@ -5,6 +5,7 @@ import io.novafoundation.nova.common.base.BaseViewModel import io.novafoundation.nova.common.utils.splash.SplashPassedObserver import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository import io.novafoundation.nova.splash.SplashRouter +import kotlinx.coroutines.Job import kotlinx.coroutines.launch class SplashViewModel( @@ -13,8 +14,16 @@ class SplashViewModel( private val splashPassedObserver: SplashPassedObserver ) : BaseViewModel() { + private var initialDestinationJob: Job? = null + + // Called from the splash screen's onResume, which runs again when the activity is + // recreated (rotation, theme, locale) while this view model survives. Starting a + // second navigation then finds the first one already on the welcome screen, which + // has no route to welcome, and crashes the app. The first call decides. fun openInitialDestination() { - viewModelScope.launch { + if (initialDestinationJob != null) return + + initialDestinationJob = viewModelScope.launch { if (repository.isAccountSelected()) { openPinCode() } else { diff --git a/feature-splash/src/test/java/io/novafoundation/nova/splash/presentation/SplashViewModelTest.kt b/feature-splash/src/test/java/io/novafoundation/nova/splash/presentation/SplashViewModelTest.kt new file mode 100644 index 00000000..84a29240 --- /dev/null +++ b/feature-splash/src/test/java/io/novafoundation/nova/splash/presentation/SplashViewModelTest.kt @@ -0,0 +1,49 @@ +package io.novafoundation.nova.splash.presentation + +import io.novafoundation.nova.common.utils.splash.SplashPassedObserver +import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository +import io.novafoundation.nova.splash.SplashRouter +import io.novafoundation.nova.test_shared.whenever +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito.mock +import org.mockito.Mockito.times +import org.mockito.Mockito.verify + +@OptIn(ExperimentalCoroutinesApi::class) +class SplashViewModelTest { + + private val router = mock(SplashRouter::class.java) + private val repository = mock(AccountRepository::class.java) + private val splashPassedObserver = mock(SplashPassedObserver::class.java) + + @Before + fun setUp() { + Dispatchers.setMain(UnconfinedTestDispatcher()) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + // The splash screen asks again on every onResume, including the one after the + // activity is recreated; navigating twice crashed the app on the welcome screen. + @Test + fun `navigates to the welcome screen once however often it is asked`() { + runBlocking { whenever(repository.isAccountSelected()).thenReturn(false) } + val viewModel = SplashViewModel(router, repository, splashPassedObserver) + + viewModel.openInitialDestination() + viewModel.openInitialDestination() + + verify(router, times(1)).openWelcomeScreen() + } +}