Skip to content

Commit da4139a

Browse files
authored
Merge pull request #557 from namehillsoftware/bugfix/inconsistent-ui-updates
[Bugfix] Inconsistent UI Updates
2 parents ebb5542 + 710a1ee commit da4139a

File tree

5 files changed

+76
-61
lines changed

5 files changed

+76
-61
lines changed

projectBlueWater/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ dependencies {
207207
implementation 'androidx.activity:activity-compose:1.10.1'
208208
implementation 'dev.olshevski.navigation:reimagined:1.5.0'
209209
implementation 'com.google.code.gson:gson:2.13.1'
210+
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-rx3:1.10.2'
210211
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
211212
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
212213
testCompileOnly 'junit:junit:4.13.2'

projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/settings/ApplicationSettingsView.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,10 +166,9 @@ private fun LazyListScope.settingsList(
166166
}
167167
}
168168

169-
items(libraries) { (libraryId, name) ->
169+
items(libraries, key = { (l, _) -> l }) { (libraryId, name) ->
170170
Row(
171-
modifier = standardRowModifier
172-
.clickable { applicationNavigation.viewServerSettings(libraryId) },
171+
modifier = standardRowModifier.clickable { applicationNavigation.viewServerSettings(libraryId) },
173172
verticalAlignment = Alignment.CenterVertically,
174173
) {
175174
Text(

projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/settings/ApplicationSettingsViewModel.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ class ApplicationSettingsViewModel(
4949
libraryChosenSubscription.close()
5050
}
5151

52+
override fun act() {
53+
mutableIsLoading.value = false
54+
}
55+
5256
fun loadSettings(): Promise<*> {
5357
mutableIsLoading.value = true
5458

@@ -79,7 +83,7 @@ class ApplicationSettingsViewModel(
7983
}
8084
)
8185
}
82-
.then { it -> mutableLibraries.value = it.sortedBy { it.first.id }.toList() }
86+
.then { it -> mutableLibraries.value = it.sortedBy { it.first.id } }
8387

8488
return Promise
8589
.whenAll(promisedSimpleValuesUpdate, promisedEngineTypeUpdate, promisedLibrariesUpdate)
@@ -118,8 +122,4 @@ class ApplicationSettingsViewModel(
118122
chosenLibraryId = chosenLibraryId.value.id
119123
)
120124
)
121-
122-
override fun act() {
123-
mutableIsLoading.value = false
124-
}
125125
}
Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,29 @@
11
package com.lasthopesoftware.bluewater.shared.observables
22

33
import androidx.compose.runtime.Composable
4-
import androidx.compose.runtime.DisposableEffect
54
import androidx.compose.runtime.LaunchedEffect
65
import androidx.compose.runtime.MutableState
76
import androidx.compose.runtime.State
87
import androidx.compose.runtime.mutableStateOf
98
import androidx.compose.runtime.remember
109
import androidx.compose.runtime.snapshotFlow
10+
import kotlinx.coroutines.rx3.asFlow
1111
import kotlinx.coroutines.withContext
1212
import kotlin.coroutines.CoroutineContext
1313
import kotlin.coroutines.EmptyCoroutineContext
1414

1515
@Composable
16-
fun <T, S : InteractionState<T>> S.subscribeAsState(): State<T> {
17-
val state = remember { mutableStateOf(value) }
18-
DisposableEffect(this) {
19-
val disposable = subscribe {
20-
state.value = it.value
21-
}
22-
onDispose { disposable.dispose() }
23-
}
24-
return state
25-
}
16+
fun <T, S : InteractionState<T>> S.subscribeAsState(
17+
context: CoroutineContext = EmptyCoroutineContext
18+
): State<T> = updatingState(context)
2619

2720
@Composable
2821
fun <T, S : MutableInteractionState<T>> S.subscribeAsMutableState(
2922
context: CoroutineContext = EmptyCoroutineContext
3023
): MutableState<T> {
31-
val state = remember { mutableStateOf(value) }
32-
DisposableEffect(key1 = this) {
33-
val disposable = subscribe { state.value = it.value }
24+
val state = updatingState(context)
3425

35-
onDispose {
36-
disposable.dispose()
37-
}
38-
}
39-
40-
LaunchedEffect(key1 = this) {
26+
LaunchedEffect(key1 = this, context) {
4127
value = state.value
4228
if (context == EmptyCoroutineContext) {
4329
snapshotFlow { state.value }.collect {
@@ -51,3 +37,20 @@ fun <T, S : MutableInteractionState<T>> S.subscribeAsMutableState(
5137
}
5238
return state
5339
}
40+
41+
@Composable
42+
private fun <T, S : InteractionState<T>> S.updatingState(context: CoroutineContext): MutableState<T> {
43+
val state = remember { mutableStateOf(value) }
44+
LaunchedEffect(this, context) {
45+
if (context == EmptyCoroutineContext) {
46+
asFlow().collect {
47+
state.value = it.value
48+
}
49+
} else withContext(context) {
50+
asFlow().collect {
51+
state.value = it.value
52+
}
53+
}
54+
}
55+
return state
56+
}

projectBlueWater/src/test/java/com/lasthopesoftware/bluewater/client/playback/engine/GivenAPlayingPlaybackEngine/AndPlaybackIsPaused/AndTheTrackIsChanged/When Playback Is Resumed.kt

Lines changed: 45 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,18 @@ import com.lasthopesoftware.bluewater.client.playback.engine.bootstrap.ManagedPl
1111
import com.lasthopesoftware.bluewater.client.playback.engine.preparation.PreparedPlaybackQueueResourceManagement
1212
import com.lasthopesoftware.bluewater.client.playback.file.PositionedFile
1313
import com.lasthopesoftware.bluewater.client.playback.file.PositionedPlayingFile
14-
import com.lasthopesoftware.bluewater.client.playback.file.preparation.FakeMappedPlayableFilePreparationSourceProvider
14+
import com.lasthopesoftware.bluewater.client.playback.file.fakes.FakePreparedPlayableFile
15+
import com.lasthopesoftware.bluewater.client.playback.file.fakes.ResolvablePlaybackHandler
1516
import com.lasthopesoftware.bluewater.client.playback.file.preparation.queues.CompletingFileQueueProvider
1617
import com.lasthopesoftware.bluewater.client.playback.nowplaying.storage.NowPlaying
1718
import com.lasthopesoftware.bluewater.client.playback.nowplaying.storage.NowPlayingRepository
1819
import com.lasthopesoftware.bluewater.client.playback.volume.PlaylistVolumeManager
1920
import com.lasthopesoftware.bluewater.shared.promises.extensions.DeferredPromise
2021
import com.lasthopesoftware.bluewater.shared.promises.extensions.toExpiringFuture
22+
import com.lasthopesoftware.promises.extensions.toPromise
2123
import com.namehillsoftware.handoff.promises.Promise
24+
import io.mockk.every
25+
import io.mockk.mockk
2226
import org.assertj.core.api.Assertions.assertThat
2327
import org.joda.time.Duration
2428
import org.junit.jupiter.api.BeforeAll
@@ -31,15 +35,14 @@ class `When Playback Is Resumed` {
3135
}
3236

3337
private val mut by lazy {
34-
val fakePlaybackPreparerProvider = FakeMappedPlayableFilePreparationSourceProvider(
35-
listOf(
36-
ServiceFile("1"),
37-
ServiceFile("2"),
38-
ServiceFile("3"),
39-
ServiceFile("4"),
40-
ServiceFile("5")
41-
)
42-
)
38+
val playlist = listOf(
39+
ServiceFile("1"),
40+
ServiceFile("2"),
41+
ServiceFile("3"),
42+
ServiceFile("4"),
43+
ServiceFile("5")
44+
)
45+
4346
val library = Library(id = libraryId)
4447
val libraryProvider = FakeLibraryRepository(library)
4548
val nowPlayingRepository =
@@ -49,7 +52,22 @@ class `When Playback Is Resumed` {
4952
)
5053
val preparedPlaybackQueueResourceManagement =
5154
PreparedPlaybackQueueResourceManagement(
52-
fakePlaybackPreparerProvider,
55+
mockk {
56+
every { providePlayableFilePreparationSource() } returns mockk {
57+
every { promisePreparedPlaybackFile(LibraryId(libraryId), any(), any()) } answers {
58+
val prepareAt = thirdArg<Duration>()
59+
preparedAt = prepareAt
60+
val playbackHandler = ResolvablePlaybackHandler()
61+
playbackHandler.setCurrentPosition(prepareAt.millis.toInt())
62+
63+
FakePreparedPlayableFile(playbackHandler).toPromise()
64+
}
65+
66+
val firstPlaybackHandler = ResolvablePlaybackHandler()
67+
firstPlaybackHandler.setCurrentPosition(450)
68+
every { promisePreparedPlaybackFile(LibraryId(libraryId), ServiceFile("1"), Duration.ZERO) } returns FakePreparedPlayableFile(firstPlaybackHandler).toPromise()
69+
}
70+
},
5371
FakePlaybackQueueConfiguration(maxQueueSize = 0)
5472
)
5573
val playbackBootstrapper = ManagedPlaylistPlayer(
@@ -66,7 +84,7 @@ class `When Playback Is Resumed` {
6684
playbackBootstrapper,
6785
playbackBootstrapper,
6886
)
69-
Triple(fakePlaybackPreparerProvider, nowPlayingRepository, playbackEngine)
87+
Triple(playlist, nowPlayingRepository, playbackEngine)
7088
}
7189

7290
private var preparedAt: Duration? = null
@@ -75,34 +93,18 @@ class `When Playback Is Resumed` {
7593

7694
@BeforeAll
7795
fun before() {
78-
val (fakePlaybackPreparerProvider, nowPlayingRepository, playbackEngine) = mut
79-
80-
val deferredResume = DeferredPromise(Unit)
81-
82-
fakePlaybackPreparerProvider.preparationSourceBeingProvided { serviceFile, deferredPreparedPlayableFile ->
83-
val playbackHandler = deferredPreparedPlayableFile.resolve()
84-
if (serviceFile == ServiceFile("1"))
85-
playbackHandler.setCurrentPosition(450)
86-
87-
if (serviceFile == ServiceFile("2")) {
88-
preparedAt = deferredPreparedPlayableFile.preparedAt
89-
deferredResume.resolve()
90-
}
91-
}
96+
val (playlist, nowPlayingRepository, playbackEngine) = mut
9297

93-
val promisedCollectedFiles = Promise {
94-
val collectedFiles = mutableListOf<PositionedPlayingFile?>()
95-
playbackEngine.setOnPlayingFileChanged { _, f ->
96-
collectedFiles.add(f)
98+
val collectedFiles = mutableListOf<PositionedPlayingFile?>()
9799

98-
deferredResume.then { _ -> it.sendResolution(collectedFiles) }
99-
}
100+
playbackEngine.setOnPlayingFileChanged { _, f ->
101+
collectedFiles.add(f)
100102
}
101103

102104
playbackEngine
103105
.startPlaylist(
104106
LibraryId(libraryId),
105-
fakePlaybackPreparerProvider.deferredResolutions.keys.toList(),
107+
playlist,
106108
0
107109
)
108110
.toExpiringFuture()
@@ -112,7 +114,17 @@ class `When Playback Is Resumed` {
112114

113115
playbackEngine.skipToNext().toExpiringFuture().get()
114116

117+
val deferredResume = DeferredPromise(Unit)
118+
val promisedCollectedFiles = Promise {
119+
playbackEngine.setOnPlayingFileChanged { _, f ->
120+
collectedFiles.add(f)
121+
122+
deferredResume.then { _ -> it.sendResolution(collectedFiles) }
123+
}
124+
}
125+
115126
playbackEngine.resume().toExpiringFuture().get()
127+
deferredResume.resolve()
116128

117129
positionedFiles = promisedCollectedFiles.toExpiringFuture().get()
118130

0 commit comments

Comments
 (0)