Keep the loaded detail on screen while it re-reads (#196)

This commit is contained in:
2026-08-17 21:32:29 +02:00
parent 2bf7d00f39
commit a5b38aec9e
2 changed files with 42 additions and 13 deletions

View File

@@ -48,9 +48,14 @@ class EventDetailViewModel @Inject constructor(
) : ViewModel() {
private val _target = MutableStateFlow<Target?>(null)
// Bumped by retry() to re-run the load for the same target.
// Bumped by retry() and by re-opening the target already shown, to re-run
// the load without changing the target.
private val _reload = MutableStateFlow(0)
// Last target whose content is already on screen; a re-read of it skips the
// Loading skeleton so the sheet doesn't blank out between two identical reads.
private var loadedTarget: Target? = null
private val _deleteState = MutableStateFlow<DeleteUiState>(DeleteUiState.Idle)
val deleteState: StateFlow<DeleteUiState> = _deleteState.asStateFlow()
@@ -58,11 +63,14 @@ class EventDetailViewModel @Inject constructor(
combine(_target, _reload) { target, _ -> target }
.flatMapLatest { target ->
if (target == null) {
loadedTarget = null
flowOf<EventDetailUiState>(EventDetailUiState.Loading)
} else {
flow {
emit(EventDetailUiState.Loading)
emit(loadDetail(target))
if (loadedTarget != target) emit(EventDetailUiState.Loading)
val loaded = loadDetail(target)
loadedTarget = target.takeIf { loaded is EventDetailUiState.Success }
emit(loaded)
}
}
}

View File

@@ -32,6 +32,8 @@ import kotlin.time.Instant
/**
* Re-opening an occurrence must re-read it (#196): the view model outlives the
* sheet, so an edit that changed no time would otherwise show the pre-save row.
* The re-read stays silent — the loaded content must not blink back to the
* skeleton on the way.
*/
@OptIn(ExperimentalCoroutinesApi::class)
class EventDetailViewModelTest {
@@ -72,19 +74,20 @@ class EventDetailViewModelTest {
return EventDetailViewModel(repo, IcsExporter(ContextWrapper(null)), dispatcher)
}
@Test
fun `re-opening the same occurrence re-reads it`(@TempDir tempDir: Path) = runTest(dispatcher) {
var stored: String? = null
val fake = FakeCalendarDataSource().apply {
private fun fakeSource(description: () -> String?) = FakeCalendarDataSource().apply {
calendarsResult = listOf(
CalendarSource(
id = 1L, displayName = "Cal", accountName = "acc@local", accountType = "LOCAL",
color = 0xFF112233.toInt(), isVisibleInSystem = true, canModifyContents = true,
),
)
eventDetailResult = { detail(stored) }
eventDetailResult = { detail(description()) }
}
val vm = viewModel(tempDir, fake)
@Test
fun `re-opening the same occurrence re-reads it`(@TempDir tempDir: Path) = runTest(dispatcher) {
var stored: String? = null
val vm = viewModel(tempDir, fakeSource { stored })
val collector = launch(Job()) { vm.state.collect {} }
vm.open(42L, beginMillis, endMillis)
@@ -100,4 +103,22 @@ class EventDetailViewModelTest {
collector.cancel()
}
@Test
fun `the re-read does not fall back to the skeleton`(@TempDir tempDir: Path) = runTest(dispatcher) {
val vm = viewModel(tempDir, fakeSource { null })
val seen = mutableListOf<EventDetailUiState>()
val collector = launch(Job()) { vm.state.collect { seen += it } }
vm.open(42L, beginMillis, endMillis)
advanceUntilIdle()
assertThat(vm.state.value).isInstanceOf(EventDetailUiState.Success::class.java)
seen.clear()
vm.open(42L, beginMillis, endMillis)
advanceUntilIdle()
assertThat(seen).doesNotContain(EventDetailUiState.Loading)
collector.cancel()
}
}