Merge pull request #4388 from kiwix/Fixes#4387

Fixed: When changing the app's language, a blank screen appeared instead of the settings screen.
This commit is contained in:
Kelson 2025-08-19 06:14:53 +02:00 committed by GitHub
commit 6bb8512391
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 53 additions and 57 deletions

View File

@ -205,9 +205,13 @@ class DownloadTest : BaseActivityTest() {
clickLanguagePreference(composeTestRule) clickLanguagePreference(composeTestRule)
assertLanguagePrefDialogDisplayed(composeTestRule) assertLanguagePrefDialogDisplayed(composeTestRule)
selectDeviceDefaultLanguage(composeTestRule) selectDeviceDefaultLanguage(composeTestRule)
// Advance the main clock to settle the frame of compose.
composeTestRule.mainClock.advanceTimeByFrame()
clickLanguagePreference(composeTestRule) clickLanguagePreference(composeTestRule)
assertLanguagePrefDialogDisplayed(composeTestRule) assertLanguagePrefDialogDisplayed(composeTestRule)
selectAlbanianLanguage(composeTestRule) selectAlbanianLanguage(composeTestRule)
// Advance the main clock to settle the frame of compose.
composeTestRule.mainClock.advanceTimeByFrame()
} }
} }
clickDownloadOnBottomNav(composeTestRule) clickDownloadOnBottomNav(composeTestRule)
@ -226,6 +230,8 @@ class DownloadTest : BaseActivityTest() {
clickLanguagePreference(composeTestRule) clickLanguagePreference(composeTestRule)
assertLanguagePrefDialogDisplayed(composeTestRule) assertLanguagePrefDialogDisplayed(composeTestRule)
selectDeviceDefaultLanguage(composeTestRule) selectDeviceDefaultLanguage(composeTestRule)
// Advance the main clock to settle the frame of compose.
composeTestRule.mainClock.advanceTimeByFrame()
// check if the device default language is selected or not. // check if the device default language is selected or not.
clickLanguagePreference(composeTestRule) clickLanguagePreference(composeTestRule)
// close the language dialog. // close the language dialog.

View File

@ -144,7 +144,8 @@ class LanguageViewModelTest {
} }
@Test @Test
fun `observeLanguages uses network when no cache and online`() = runTest { fun `observeLanguages uses network when no cache and online`() = flakyTest {
runTest {
every { application.getString(any()) } returns "" every { application.getString(any()) } returns ""
val fetchedLanguages = listOf(language(languageCode = "eng")) val fetchedLanguages = listOf(language(languageCode = "eng"))
LanguageSessionCache.hasFetched = false LanguageSessionCache.hasFetched = false
@ -173,6 +174,7 @@ class LanguageViewModelTest {
} }
) )
} }
}
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@Test @Test

View File

@ -314,7 +314,6 @@ class ZimManageViewModelTest {
version = 100L version = 100L
) )
viewModel.onlineLibraryRequest.test { viewModel.onlineLibraryRequest.test {
skipItems(1)
viewModel.updateOnlineLibraryFilters(newRequest) viewModel.updateOnlineLibraryFilters(newRequest)
assertThat(awaitItem()).isEqualTo(newRequest) assertThat(awaitItem()).isEqualTo(newRequest)
} }

View File

@ -35,6 +35,7 @@ import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -183,12 +184,27 @@ abstract class CoreSettingsFragment : SettingsContract.View, BaseFragment() {
composeView = it composeView = it
} }
/**
* Restarts the Settings screen by popping it from the back stack and reopening it.
*
* This is useful when we need to refresh the Settings UI (e.g., after a app's language
* change) without fully recreating the activity.
*
* Steps:
* 1. Get the CoreMainActivity reference to access the NavController.
* 2. Pop the Settings fragment from the navigation back stack.
* 3. Wait for one frame so the back stack can settle after the pop operation.
* 4. Navigate back to the Settings fragment route.
*/
private fun restartActivity() { private fun restartActivity() {
(activity as CoreMainActivity?)?.let { val coreMainActivity = activity as? CoreMainActivity ?: return
it.navController.apply { val navController = coreMainActivity.navController
popBackStack() navController.popBackStack()
navigate(it.settingsFragmentRoute) coreMainActivity.uiCoroutineScope.launch {
} // Wait for one frame to ensure the back stack has settled before navigation
// Bug fix #4387
withFrameNanos { }
navController.navigate(coreMainActivity.settingsFragmentRoute)
} }
} }

View File

@ -29,7 +29,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
@ -44,21 +43,16 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO
import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem
import org.kiwix.kiwixmobile.core.ui.models.toPainter import org.kiwix.kiwixmobile.core.ui.models.toPainter
import org.kiwix.kiwixmobile.core.ui.theme.Black import org.kiwix.kiwixmobile.core.ui.theme.Black
@ -234,23 +228,3 @@ private fun OverflowMenuItems(
} }
} }
} }
@Composable
fun rememberBottomNavigationVisibility(lazyListState: LazyListState?): Boolean {
var isToolbarVisible by remember { mutableStateOf(true) }
var lastScrollIndex by remember { mutableIntStateOf(ZERO) }
val updatedLazyListState = rememberUpdatedState(lazyListState)
LaunchedEffect(updatedLazyListState) {
updatedLazyListState.value?.let { state ->
snapshotFlow { state.firstVisibleItemIndex }
.collect { newScrollIndex ->
if (newScrollIndex != lastScrollIndex) {
isToolbarVisible = newScrollIndex < lastScrollIndex
lastScrollIndex = newScrollIndex
}
}
}
}
return isToolbarVisible
}

View File

@ -215,7 +215,6 @@ internal class CustomDownloadViewModelTest {
triggerAction = { customDownloadViewModel.actions.tryEmit(action) }, triggerAction = { customDownloadViewModel.actions.tryEmit(action) },
assert = { assert = {
val items = (1..awaitItemCount).map { awaitItem() } val items = (1..awaitItemCount).map { awaitItem() }
print("items = $items")
assertThat(items).contains(endState) assertThat(items).contains(endState)
} }
) )