Keep the loaded detail on screen while it re-reads (#196)
This commit is contained in:
@@ -48,9 +48,14 @@ class EventDetailViewModel @Inject constructor(
|
|||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _target = MutableStateFlow<Target?>(null)
|
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)
|
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)
|
private val _deleteState = MutableStateFlow<DeleteUiState>(DeleteUiState.Idle)
|
||||||
val deleteState: StateFlow<DeleteUiState> = _deleteState.asStateFlow()
|
val deleteState: StateFlow<DeleteUiState> = _deleteState.asStateFlow()
|
||||||
|
|
||||||
@@ -58,11 +63,14 @@ class EventDetailViewModel @Inject constructor(
|
|||||||
combine(_target, _reload) { target, _ -> target }
|
combine(_target, _reload) { target, _ -> target }
|
||||||
.flatMapLatest { target ->
|
.flatMapLatest { target ->
|
||||||
if (target == null) {
|
if (target == null) {
|
||||||
|
loadedTarget = null
|
||||||
flowOf<EventDetailUiState>(EventDetailUiState.Loading)
|
flowOf<EventDetailUiState>(EventDetailUiState.Loading)
|
||||||
} else {
|
} else {
|
||||||
flow {
|
flow {
|
||||||
emit(EventDetailUiState.Loading)
|
if (loadedTarget != target) emit(EventDetailUiState.Loading)
|
||||||
emit(loadDetail(target))
|
val loaded = loadDetail(target)
|
||||||
|
loadedTarget = target.takeIf { loaded is EventDetailUiState.Success }
|
||||||
|
emit(loaded)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ import kotlin.time.Instant
|
|||||||
/**
|
/**
|
||||||
* Re-opening an occurrence must re-read it (#196): the view model outlives the
|
* 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.
|
* 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)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
class EventDetailViewModelTest {
|
class EventDetailViewModelTest {
|
||||||
@@ -72,19 +74,20 @@ class EventDetailViewModelTest {
|
|||||||
return EventDetailViewModel(repo, IcsExporter(ContextWrapper(null)), dispatcher)
|
return EventDetailViewModel(repo, IcsExporter(ContextWrapper(null)), dispatcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
private fun fakeSource(description: () -> String?) = FakeCalendarDataSource().apply {
|
||||||
fun `re-opening the same occurrence re-reads it`(@TempDir tempDir: Path) = runTest(dispatcher) {
|
|
||||||
var stored: String? = null
|
|
||||||
val fake = FakeCalendarDataSource().apply {
|
|
||||||
calendarsResult = listOf(
|
calendarsResult = listOf(
|
||||||
CalendarSource(
|
CalendarSource(
|
||||||
id = 1L, displayName = "Cal", accountName = "acc@local", accountType = "LOCAL",
|
id = 1L, displayName = "Cal", accountName = "acc@local", accountType = "LOCAL",
|
||||||
color = 0xFF112233.toInt(), isVisibleInSystem = true, canModifyContents = true,
|
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 {} }
|
val collector = launch(Job()) { vm.state.collect {} }
|
||||||
|
|
||||||
vm.open(42L, beginMillis, endMillis)
|
vm.open(42L, beginMillis, endMillis)
|
||||||
@@ -100,4 +103,22 @@ class EventDetailViewModelTest {
|
|||||||
|
|
||||||
collector.cancel()
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user