Skip to content

Commit 65f673e

Browse files
authored
Merge pull request #569 from namehillsoftware/bugfix/item-list-loading-fault-handling
[Bugfix] Item List Loading Fault Handling
2 parents a80b1d1 + cb409eb commit 65f673e

File tree

18 files changed

+620
-88
lines changed

18 files changed

+620
-88
lines changed

projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/browsing/ScopedViewModelDependencies.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.lasthopesoftware.bluewater.client.browsing.files.details.FileDetailsV
66
import com.lasthopesoftware.bluewater.client.browsing.files.details.ListedFileDetailsViewModel
77
import com.lasthopesoftware.bluewater.client.browsing.files.list.FileListViewModel
88
import com.lasthopesoftware.bluewater.client.browsing.files.list.search.SearchFilesViewModel
9+
import com.lasthopesoftware.bluewater.client.browsing.items.LoadItemData
910
import com.lasthopesoftware.bluewater.client.browsing.items.list.ItemListViewModel
1011
import com.lasthopesoftware.bluewater.client.settings.LibrarySettingsViewModel
1112
import com.lasthopesoftware.bluewater.client.stored.library.items.files.view.ActiveFileDownloadsViewModel
@@ -17,6 +18,7 @@ import com.lasthopesoftware.bluewater.shared.android.UndoStack
1718
interface ScopedViewModelDependencies : ReusedViewModelDependencies {
1819
val itemListViewModel: ItemListViewModel
1920
val fileListViewModel: FileListViewModel
21+
val itemDataLoader: LoadItemData
2022
val activeFileDownloadsViewModel: ActiveFileDownloadsViewModel
2123
val searchFilesViewModel: SearchFilesViewModel
2224
val librarySettingsViewModel: LibrarySettingsViewModel

projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/browsing/ScopedViewModelRegistry.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import com.lasthopesoftware.bluewater.client.browsing.files.list.FileListViewMod
99
import com.lasthopesoftware.bluewater.client.browsing.files.list.search.SearchFilesViewModel
1010
import com.lasthopesoftware.bluewater.client.browsing.files.properties.EditableFilePropertyDefinitionProvider
1111
import com.lasthopesoftware.bluewater.client.browsing.files.properties.EditableLibraryFilePropertiesProvider
12+
import com.lasthopesoftware.bluewater.client.browsing.items.AggregateItemViewModel
1213
import com.lasthopesoftware.bluewater.client.browsing.items.list.ItemListViewModel
1314
import com.lasthopesoftware.bluewater.client.settings.LibrarySettingsViewModel
1415
import com.lasthopesoftware.bluewater.client.settings.PermissionsDependencies
@@ -40,6 +41,13 @@ class ScopedViewModelRegistry(
4041
)
4142
}
4243

44+
override val itemDataLoader by viewModelStoreOwner.buildViewModelLazily {
45+
AggregateItemViewModel(
46+
itemListViewModel,
47+
fileListViewModel
48+
)
49+
}
50+
4351
override val activeFileDownloadsViewModel by viewModelStoreOwner.buildViewModelLazily {
4452
ActiveFileDownloadsViewModel(
4553
storedFileAccess,

projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/browsing/files/list/FileListViewModel.kt

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.lasthopesoftware.bluewater.client.browsing.files.access.ProvideLibrar
77
import com.lasthopesoftware.bluewater.client.browsing.items.IItem
88
import com.lasthopesoftware.bluewater.client.browsing.items.Item
99
import com.lasthopesoftware.bluewater.client.browsing.items.ItemId
10+
import com.lasthopesoftware.bluewater.client.browsing.items.LoadItemData
1011
import com.lasthopesoftware.bluewater.client.browsing.items.playlists.Playlist
1112
import com.lasthopesoftware.bluewater.client.browsing.library.repository.LibraryId
1213
import com.lasthopesoftware.bluewater.client.stored.library.items.AccessStoredItems
@@ -19,7 +20,7 @@ import kotlinx.coroutines.flow.asStateFlow
1920
class FileListViewModel(
2021
private val itemFileProvider: ProvideLibraryFiles,
2122
private val storedItemAccess: AccessStoredItems,
22-
) : ViewModel(), TrackLoadedViewState, ServiceFilesListState {
23+
) : ViewModel(), TrackLoadedViewState, ServiceFilesListState, LoadItemData {
2324

2425
private val mutableIsLoading = MutableInteractionState(true)
2526
private val mutableFiles = MutableInteractionState(emptyList<ServiceFile>())
@@ -34,40 +35,45 @@ class FileListViewModel(
3435
val itemValue = mutableItemValue.asStateFlow()
3536
val isSynced = mutableIsSynced.asStateFlow()
3637

37-
fun loadItem(libraryId: LibraryId, item: IItem? = null): Promise<Unit> {
38+
override fun loadItem(libraryId: LibraryId, item: IItem?): Promise<Unit> {
3839
mutableIsLoading.value = libraryId != loadedLibraryId || item != loadedItem
3940
mutableItemValue.value = item?.value ?: ""
4041
mutableIsSynced.value = false
4142
loadedLibraryId = libraryId
4243

43-
val promisedFiles = when (item) {
44-
is Item -> itemFileProvider.promiseFiles(libraryId, ItemId(item.key))
45-
is Playlist -> itemFileProvider.promiseFiles(libraryId, item.itemId)
46-
else -> itemFileProvider.promiseFiles(libraryId)
47-
}
44+
return Promise.Proxy { cs ->
45+
val promisedFiles = when (item) {
46+
is Item -> itemFileProvider.promiseFiles(libraryId, ItemId(item.key))
47+
is Playlist -> itemFileProvider.promiseFiles(libraryId, item.itemId)
48+
else -> itemFileProvider.promiseFiles(libraryId)
49+
}
4850

49-
val promisedFilesUpdate = promisedFiles.then { f -> mutableFiles.value = f }
51+
cs.doCancel(promisedFiles)
5052

51-
val promisedSyncUpdate = item
52-
?.let {
53-
storedItemAccess
54-
.isItemMarkedForSync(libraryId, it)
55-
.then { isSynced ->
56-
mutableIsSynced.value = isSynced
57-
}
58-
}
59-
.keepPromise()
53+
val promisedFilesUpdate = promisedFiles.then { f -> mutableFiles.value = f }
6054

61-
return Promise.whenAll(promisedFilesUpdate, promisedSyncUpdate)
62-
.then { _ ->
63-
loadedItem = item
64-
}
65-
.must { _ ->
66-
mutableIsLoading.value = false
67-
}
55+
val promisedSyncUpdate = item
56+
?.let {
57+
storedItemAccess
58+
.isItemMarkedForSync(libraryId, it)
59+
.then { isSynced ->
60+
mutableIsSynced.value = isSynced
61+
}
62+
}
63+
.keepPromise()
64+
65+
Promise
66+
.whenAll(promisedFilesUpdate, promisedSyncUpdate)
67+
.then { _ ->
68+
loadedItem = item
69+
}
70+
.must { _ ->
71+
mutableIsLoading.value = false
72+
}
73+
}
6874
}
6975

70-
fun promiseRefresh(): Promise<Unit> = loadedLibraryId
76+
override fun promiseRefresh(): Promise<Unit> = loadedLibraryId
7177
?.let { l ->
7278
val item = loadedItem
7379
loadedLibraryId = null
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package com.lasthopesoftware.bluewater.client.browsing.items
2+
3+
import androidx.lifecycle.ViewModel
4+
import com.lasthopesoftware.bluewater.client.browsing.library.repository.LibraryId
5+
import com.lasthopesoftware.bluewater.shared.observables.InteractionState
6+
import com.lasthopesoftware.bluewater.shared.observables.LiftedInteractionState
7+
import com.lasthopesoftware.bluewater.shared.observables.mapNotNull
8+
import com.lasthopesoftware.promises.extensions.UnitResponse
9+
import com.lasthopesoftware.resources.closables.AutoCloseableManager
10+
import com.namehillsoftware.handoff.promises.Promise
11+
import io.reactivex.rxjava3.core.Observable
12+
13+
class AggregateItemViewModel(
14+
vararg itemData: LoadItemData
15+
) : ViewModel(), LoadItemData {
16+
private val autoCloseableManager = AutoCloseableManager()
17+
private val itemDataLoaders = arrayOf(*itemData)
18+
19+
override val isLoading: InteractionState<Boolean> by lazy {
20+
autoCloseableManager.manage(
21+
LiftedInteractionState(
22+
Observable.combineLatest(itemDataLoaders.map { it.isLoading.mapNotNull() }) { sources ->
23+
sources.any { it as Boolean }
24+
},
25+
false
26+
)
27+
)
28+
}
29+
30+
override fun loadItem(libraryId: LibraryId, item: IItem?): Promise<Unit> {
31+
val promisedLoads = itemDataLoaders.map { it.loadItem(libraryId, item) }
32+
33+
return Promise.whenAll(promisedLoads)
34+
.then(
35+
UnitResponse.respond(),
36+
{
37+
for (promisedLoad in promisedLoads)
38+
promisedLoad.cancel()
39+
throw it
40+
}
41+
)
42+
}
43+
44+
override fun promiseRefresh(): Promise<Unit> {
45+
val promisedRefreshes = itemDataLoaders.map { it.promiseRefresh() }
46+
return Promise.whenAll(promisedRefreshes)
47+
.then(
48+
UnitResponse.respond(),
49+
{
50+
for (promisedRefresh in promisedRefreshes)
51+
promisedRefresh.cancel()
52+
throw it
53+
}
54+
)
55+
}
56+
57+
override fun onCleared() {
58+
autoCloseableManager.close()
59+
}
60+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.lasthopesoftware.bluewater.client.browsing.items
2+
3+
import com.lasthopesoftware.bluewater.client.browsing.TrackLoadedViewState
4+
import com.lasthopesoftware.bluewater.client.browsing.library.repository.LibraryId
5+
import com.namehillsoftware.handoff.promises.Promise
6+
7+
interface LoadItemData : TrackLoadedViewState {
8+
fun loadItem(libraryId: LibraryId, item: IItem? = null): Promise<Unit>
9+
fun promiseRefresh(): Promise<Unit>
10+
}

projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/browsing/items/list/BrowsableItemListView.kt

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import com.lasthopesoftware.bluewater.client.browsing.ScopedViewModelDependencie
99
import com.lasthopesoftware.bluewater.client.browsing.files.list.FileListViewModel
1010
import com.lasthopesoftware.bluewater.client.browsing.files.list.ReusablePlaylistFileItemViewModelProvider
1111
import com.lasthopesoftware.bluewater.client.browsing.items.IItem
12+
import com.lasthopesoftware.bluewater.client.browsing.items.LoadItemData
1213
import com.lasthopesoftware.bluewater.client.browsing.items.list.ConnectionLostView
1314
import com.lasthopesoftware.bluewater.client.browsing.items.list.ItemListView
1415
import com.lasthopesoftware.bluewater.client.browsing.items.list.ItemListViewModel
@@ -25,7 +26,6 @@ import com.lasthopesoftware.bluewater.shared.android.viewmodels.PooledCloseables
2526
import com.lasthopesoftware.bluewater.shared.android.viewmodels.ViewModelInitAction
2627
import com.lasthopesoftware.promises.extensions.suspend
2728
import com.lasthopesoftware.resources.strings.GetStringResources
28-
import com.namehillsoftware.handoff.promises.Promise
2929
import java.io.IOException
3030

3131
@Composable
@@ -36,6 +36,7 @@ fun LoadedItemListView(viewModelDependencies: ScopedViewModelDependencies, libra
3636
item,
3737
itemListViewModel = itemListViewModel,
3838
fileListViewModel = fileListViewModel,
39+
itemDataLoader = itemDataLoader,
3940
nowPlayingViewModel = nowPlayingFilePropertiesViewModel,
4041
itemListMenuBackPressedHandler = itemListMenuBackPressedHandler,
4142
reusablePlaylistFileItemViewModelProvider = reusablePlaylistFileItemViewModelProvider,
@@ -56,6 +57,7 @@ private fun LoadedItemListView(
5657
item: IItem?,
5758
itemListViewModel: ItemListViewModel,
5859
fileListViewModel: FileListViewModel,
60+
itemDataLoader: LoadItemData,
5961
nowPlayingViewModel: NowPlayingFilePropertiesViewModel,
6062
itemListMenuBackPressedHandler: ItemListMenuBackPressedHandler,
6163
reusablePlaylistFileItemViewModelProvider: ReusablePlaylistFileItemViewModelProvider,
@@ -81,6 +83,7 @@ private fun LoadedItemListView(
8183
ItemListView(
8284
itemListViewModel,
8385
fileListViewModel,
86+
itemDataLoader,
8487
nowPlayingViewModel,
8588
itemListMenuBackPressedHandler,
8689
reusablePlaylistFileItemViewModelProvider,
@@ -104,15 +107,13 @@ private fun LoadedItemListView(
104107
if (!isConnectionLost) {
105108
LaunchedEffect(item) {
106109
try {
107-
Promise.whenAll(
108-
itemListViewModel.loadItem(libraryId, item),
109-
fileListViewModel.loadItem(libraryId, item),
110-
).suspend()
110+
itemDataLoader.loadItem(libraryId, item).suspend()
111111
} catch (e: IOException) {
112-
if (ConnectionLostExceptionFilter.isConnectionLostException(e))
112+
if (ConnectionLostExceptionFilter.isConnectionLostException(e)) {
113113
isConnectionLost = true
114-
else
114+
} else {
115115
applicationNavigation.backOut().suspend()
116+
}
116117
} catch (_: Exception) {
117118
applicationNavigation.backOut().suspend()
118119
}

projectBlueWater/src/main/java/com/lasthopesoftware/bluewater/client/browsing/items/list/ItemListView.kt

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ import com.lasthopesoftware.bluewater.client.browsing.files.list.ViewPlaylistFil
9494
import com.lasthopesoftware.bluewater.client.browsing.items.IItem
9595
import com.lasthopesoftware.bluewater.client.browsing.items.Item
9696
import com.lasthopesoftware.bluewater.client.browsing.items.ItemId
97+
import com.lasthopesoftware.bluewater.client.browsing.items.LoadItemData
9798
import com.lasthopesoftware.bluewater.client.browsing.items.list.menus.LabelledActiveDownloadsButton
9899
import com.lasthopesoftware.bluewater.client.browsing.items.list.menus.LabelledSearchButton
99100
import com.lasthopesoftware.bluewater.client.browsing.items.list.menus.LabelledSettingsButton
@@ -355,6 +356,7 @@ fun ChildItem(
355356
fun ItemListView(
356357
itemListViewModel: ItemListViewModel,
357358
fileListViewModel: FileListViewModel,
359+
itemDataLoader: LoadItemData,
358360
nowPlayingViewModel: NowPlayingFilePropertiesViewModel,
359361
itemListMenuBackPressedHandler: ItemListMenuBackPressedHandler,
360362
trackHeadlineViewModelProvider: PooledCloseablesViewModel<ViewPlaylistFileItem>,
@@ -557,30 +559,28 @@ fun ItemListView(
557559
)
558560
}
559561

560-
val isFilesLoading by fileListViewModel.isLoading.subscribeAsState()
562+
val isLoading by itemDataLoader.isLoading.subscribeAsState()
561563

562564
BoxWithConstraints(modifier = Modifier
563565
.fillMaxSize()
564566
.focusGroup()
565567
) {
566568
ControlSurface {
567569
DetermineWindowControlColors()
568-
val isItemsLoading by itemListViewModel.isLoading.subscribeAsState()
569570

570571
val collapsedHeight = appBarHeight
571572
val expandedHeightPx = LocalDensity.current.remember { boxHeight.toPx() }
572573
val collapsedHeightPx = LocalDensity.current.remember { collapsedHeight.toPx() }
573574
val items by itemListViewModel.items.subscribeAsState()
574575

575576
var minVisibleItemsForScroll by remember { mutableIntStateOf(30) }
576-
LaunchedEffect(lazyListState, itemListViewModel, fileListViewModel) {
577+
LaunchedEffect(lazyListState, itemDataLoader) {
577578
combine(
578579
snapshotFlow { lazyListState.layoutInfo },
579-
itemListViewModel.isLoading.mapNotNull().asFlow(),
580-
fileListViewModel.isLoading.mapNotNull().asFlow(),
581-
) { info, i, f -> Triple(info, i, f) }
582-
.filterNot { (_, i, f) -> i || f }
583-
.map { (info, _, _) -> info.visibleItemsInfo.size }
580+
itemDataLoader.isLoading.mapNotNull().asFlow(),
581+
) { info, i -> Pair(info, i) }
582+
.filterNot { (_, i) -> i }
583+
.map { (info, _) -> info.visibleItemsInfo.size }
584584
.filter { it == 0 }
585585
.distinctUntilChanged()
586586
.take(1)
@@ -658,9 +658,7 @@ fun ItemListView(
658658
) {
659659
val actualExpandedHeight by remember { derivedStateOf { if (isHeaderTall) boxHeight else collapsedHeight } }
660660
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
661-
val isLoaded = !isItemsLoading && !isFilesLoading
662-
663-
if (isLoaded) LoadedItemListView(
661+
if (!isLoading) LoadedItemListView(
664662
anchoredScrollConnectionState,
665663
{ _, p -> labeledAnchors.firstOrNull { (_, lp) -> p == lp }?.let { (s, _) -> Text(s) } },
666664
anchoredScrollConnectionDispatcher::progressTo,
@@ -685,8 +683,7 @@ fun ItemListView(
685683
)
686684

687685
UnlabelledRefreshButton(
688-
itemListViewModel,
689-
fileListViewModel,
686+
itemDataLoader,
690687
Modifier
691688
.align(Alignment.TopEnd)
692689
.padding(
@@ -772,10 +769,9 @@ fun ItemListView(
772769
)
773770
}
774771

775-
if (!isFilesLoading && !isItemsLoading) {
772+
if (!isLoading) {
776773
UnlabelledRefreshButton(
777-
itemListViewModel,
778-
fileListViewModel,
774+
itemDataLoader,
779775
Modifier.padding(horizontal = viewPaddingUnit)
780776
)
781777
}

0 commit comments

Comments
 (0)