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