Keep the camera FAB stable across folder navigation

- Skip rendering the FAB until saved preferences load, so it is never
  drawn at the default corner and never jumps to its dragged position
- Share user preferences as one hot process-wide StateFlow instead of a
  cold disk read per screen, so a newly created gallery screen receives
  the FAB position synchronously in the ViewModel constructor
This commit is contained in:
OneWay authored and Evgenii committed 2026-10-15 13:50:31 +08:00
1 parent 428749d8a8
commit 761db7a69d
3 files changed
+36 -17

No files matched your search

@@ -9,8 +9,13 @@ import androidx.datastore.preferences.core.floatPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject
import javax.inject.Singleton
@@ -41,17 +46,26 @@ class UserPreferencesRepository @Inject constructor(
val SCREEN_TRANSITIONS_ENABLED = booleanPreferencesKey("screen_transitions_enabled")
}
val userPreferencesFlow: Flow<UserPreferences> = context.dataStore.data.map { prefs ->
UserPreferences(
rootFolderUri = prefs[Keys.ROOT_FOLDER_URI],
cameraFabXFraction = prefs[Keys.CAMERA_FAB_X_FRACTION]?.coerceIn(0f, 1f) ?: 1f,
cameraFabYFraction = prefs[Keys.CAMERA_FAB_Y_FRACTION]?.coerceIn(0f, 1f) ?: 1f,
gallerySortAscending = prefs[Keys.GALLERY_SORT_ASCENDING] ?: true,
galleryGridMode = prefs[Keys.GALLERY_GRID_MODE] ?: true,
zipJpegCompression = prefs[Keys.ZIP_JPEG_COMPRESSION] ?: true,
screenTransitionsEnabled = prefs[Keys.SCREEN_TRANSITIONS_ENABLED] ?: true,
// Hot, process-wide cache: the DataStore file is read once, and every screen
// created later receives the current value synchronously on subscription,
// instead of re-reading disk per screen and flashing default positions.
val userPreferencesFlow: StateFlow<UserPreferences> = context.dataStore.data
.map { prefs ->
UserPreferences(
rootFolderUri = prefs[Keys.ROOT_FOLDER_URI],
cameraFabXFraction = prefs[Keys.CAMERA_FAB_X_FRACTION]?.coerceIn(0f, 1f) ?: 1f,
cameraFabYFraction = prefs[Keys.CAMERA_FAB_Y_FRACTION]?.coerceIn(0f, 1f) ?: 1f,
gallerySortAscending = prefs[Keys.GALLERY_SORT_ASCENDING] ?: true,
galleryGridMode = prefs[Keys.GALLERY_GRID_MODE] ?: true,
zipJpegCompression = prefs[Keys.ZIP_JPEG_COMPRESSION] ?: true,
screenTransitionsEnabled = prefs[Keys.SCREEN_TRANSITIONS_ENABLED] ?: true,
)
}
.stateIn(
scope = CoroutineScope(SupervisorJob() + Dispatchers.IO),
started = SharingStarted.Eagerly,
initialValue = UserPreferences(),
)
}
suspend fun setRootFolderUri(uri: String) {
context.dataStore.edit { prefs ->
@@ -474,12 +474,14 @@ fun GalleryScreen(
enter = fadeIn(tween(250)),
exit = fadeOut(tween(150)),
) {
DraggableCameraFab(
xFraction = uiState.cameraFabXFraction,
yFraction = uiState.cameraFabYFraction,
onPositionChange = viewModel::saveCameraFabPosition,
onClick = { onNavigateToCamera(uiState.currentFolderUri ?: folderUri) },
)
if (uiState.viewPreferencesLoaded) {
DraggableCameraFab(
xFraction = uiState.cameraFabXFraction,
yFraction = uiState.cameraFabYFraction,
onPositionChange = viewModel::saveCameraFabPosition,
onClick = { onNavigateToCamera(uiState.currentFolderUri ?: folderUri) },
)
}
}
SelectionActionsBar(
@@ -205,6 +205,9 @@ class GalleryViewModel @Inject constructor(
loadJob = viewModelScope.launch {
uiState.value = uiState.value.copy(
currentFolderUri = folderUriString,
folderName = state.folderName.ifEmpty {
folderUriString.substringAfterLast(':').substringAfterLast('/')
},
isLoading = true,
error = null,
)