vm: let paging errors surface so retry() re-runs the load
Stream/ExpandedStream/Review/Search paging VMs were swallowing exceptions inside manualPager and returning emptyList, so the pager never entered LoadState.Error and the error-placeholder retry buttons were dead. Let non-Auth exceptions propagate; keep the AuthException → SessionExpired branch so 401 still hands off to Splash. Also drop supervisorScope wrappers from CategoryStream/DevProfile/TopChart VMs that launch no children — the surrounding try/catch already handles everything.
This commit is contained in:
@@ -45,7 +45,7 @@ fun CategoryBrowseScreen(
|
|||||||
painter = painterResource(R.drawable.ic_refresh),
|
painter = painterResource(R.drawable.ic_refresh),
|
||||||
message = stringResource(R.string.error),
|
message = stringResource(R.string.error),
|
||||||
actionLabel = stringResource(R.string.action_retry),
|
actionLabel = stringResource(R.string.action_retry),
|
||||||
onAction = { viewModel.fetchNextPage() }
|
onAction = { viewModel.fetch() }
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
val bundle = (uiState as? ViewState.Success<*>)?.data as? StreamBundle
|
val bundle = (uiState as? ViewState.Success<*>)?.data as? StreamBundle
|
||||||
@@ -60,7 +60,7 @@ fun CategoryBrowseScreen(
|
|||||||
},
|
},
|
||||||
onAppClick = { onNavigateTo(Destination.AppDetails(it.packageName)) },
|
onAppClick = { onNavigateTo(Destination.AppDetails(it.packageName)) },
|
||||||
onClusterScrolled = { viewModel.fetchNextCluster(it) },
|
onClusterScrolled = { viewModel.fetchNextCluster(it) },
|
||||||
onScrolledToEnd = { viewModel.fetchNextPage() }
|
onScrolledToEnd = { viewModel.fetch() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,9 +90,6 @@ class ExpandedStreamBrowseViewModel @AssistedInject constructor(
|
|||||||
Log.w(TAG, "Expanded stream returned ${exception.code}, redirecting to Splash")
|
Log.w(TAG, "Expanded stream returned ${exception.code}, redirecting to Splash")
|
||||||
AuroraApp.events.send(AuthEvent.SessionExpired())
|
AuroraApp.events.send(AuthEvent.SessionExpired())
|
||||||
emptyList()
|
emptyList()
|
||||||
} catch (exception: Exception) {
|
|
||||||
Log.e(TAG, "Failed to fetch apps for page $page", exception)
|
|
||||||
emptyList()
|
|
||||||
}
|
}
|
||||||
PageResult(items)
|
PageResult(items)
|
||||||
}.flow.distinctUntilChanged()
|
}.flow.distinctUntilChanged()
|
||||||
|
|||||||
@@ -27,8 +27,11 @@ import androidx.paging.cachedIn
|
|||||||
import com.aurora.extensions.TAG
|
import com.aurora.extensions.TAG
|
||||||
import com.aurora.gplayapi.data.models.App
|
import com.aurora.gplayapi.data.models.App
|
||||||
import com.aurora.gplayapi.data.models.StreamCluster
|
import com.aurora.gplayapi.data.models.StreamCluster
|
||||||
|
import com.aurora.gplayapi.exceptions.GooglePlayException
|
||||||
import com.aurora.gplayapi.helpers.web.WebStreamHelper
|
import com.aurora.gplayapi.helpers.web.WebStreamHelper
|
||||||
|
import com.aurora.store.AuroraApp
|
||||||
import com.aurora.store.data.PageResult
|
import com.aurora.store.data.PageResult
|
||||||
|
import com.aurora.store.data.event.AuthEvent
|
||||||
import com.aurora.store.data.paging.GenericPagingSource.Companion.manualPager
|
import com.aurora.store.data.paging.GenericPagingSource.Companion.manualPager
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
@@ -55,10 +58,10 @@ class StreamBrowseViewModel @AssistedInject constructor(
|
|||||||
val apps = _apps.asStateFlow()
|
val apps = _apps.asStateFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
fetchApps()
|
fetch()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchApps() {
|
fun fetch() {
|
||||||
var nextPageUrl: String = streamCluster.clusterNextPageUrl
|
var nextPageUrl: String = streamCluster.clusterNextPageUrl
|
||||||
|
|
||||||
manualPager { page ->
|
manualPager { page ->
|
||||||
@@ -79,8 +82,9 @@ class StreamBrowseViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (exception: Exception) {
|
} catch (exception: GooglePlayException.AuthException) {
|
||||||
Log.e(TAG, "Failed to fetch apps for $page: $nextPageUrl", exception)
|
Log.w(TAG, "Stream returned ${exception.code}, redirecting to Splash")
|
||||||
|
AuroraApp.events.send(AuthEvent.SessionExpired())
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
PageResult(items)
|
PageResult(items)
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.supervisorScope
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class DevProfileViewModel @Inject constructor(
|
class DevProfileViewModel @Inject constructor(
|
||||||
@@ -55,39 +54,35 @@ class DevProfileViewModel @Inject constructor(
|
|||||||
|
|
||||||
fun getStreamBundle(devId: String) {
|
fun getStreamBundle(devId: String) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
supervisorScope {
|
try {
|
||||||
try {
|
devStream = appDetailsHelper.getDeveloperStream(devId)
|
||||||
devStream = appDetailsHelper.getDeveloperStream(devId)
|
streamBundle = devStream.streamBundle
|
||||||
streamBundle = devStream.streamBundle
|
liveData.postValue(ViewState.Success(devStream))
|
||||||
liveData.postValue(ViewState.Success(devStream))
|
} catch (e: GooglePlayException.AuthException) {
|
||||||
} catch (e: GooglePlayException.AuthException) {
|
Log.w(TAG, "Developer stream fetch returned ${e.code}, redirecting to Splash")
|
||||||
Log.w(TAG, "Developer stream fetch returned ${e.code}, redirecting to Splash")
|
AuroraApp.events.send(AuthEvent.SessionExpired())
|
||||||
AuroraApp.events.send(AuthEvent.SessionExpired())
|
} catch (e: Exception) {
|
||||||
} catch (e: Exception) {
|
liveData.postValue(ViewState.Error(e.message))
|
||||||
liveData.postValue(ViewState.Error(e.message))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun observeCluster(streamCluster: StreamCluster) {
|
fun observeCluster(streamCluster: StreamCluster) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
supervisorScope {
|
try {
|
||||||
try {
|
if (streamCluster.hasNext()) {
|
||||||
if (streamCluster.hasNext()) {
|
val newCluster = streamHelper.getNextStreamCluster(
|
||||||
val newCluster = streamHelper.getNextStreamCluster(
|
streamCluster.id,
|
||||||
streamCluster.id,
|
streamCluster.clusterNextPageUrl
|
||||||
streamCluster.clusterNextPageUrl
|
)
|
||||||
)
|
updateCluster(newCluster)
|
||||||
updateCluster(newCluster)
|
devStream = devStream.copy(streamBundle = streamBundle)
|
||||||
devStream = devStream.copy(streamBundle = streamBundle)
|
liveData.postValue(ViewState.Success(devStream))
|
||||||
liveData.postValue(ViewState.Success(devStream))
|
} else {
|
||||||
} else {
|
Log.i(TAG, "End of cluster")
|
||||||
Log.i(TAG, "End of cluster")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
liveData.postValue(ViewState.Error(e.message))
|
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
liveData.postValue(ViewState.Error(e.message))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,9 +71,6 @@ class ReviewViewModel @AssistedInject constructor(
|
|||||||
Log.w(TAG, "Reviews fetch returned ${exception.code}, redirecting to Splash")
|
Log.w(TAG, "Reviews fetch returned ${exception.code}, redirecting to Splash")
|
||||||
AuroraApp.events.send(AuthEvent.SessionExpired(packageName))
|
AuroraApp.events.send(AuthEvent.SessionExpired(packageName))
|
||||||
emptyList()
|
emptyList()
|
||||||
} catch (exception: Exception) {
|
|
||||||
Log.e(TAG, "Failed to fetch reviews for $page: $reviewsNextPageUrl", exception)
|
|
||||||
emptyList()
|
|
||||||
}
|
}
|
||||||
PageResult(items)
|
PageResult(items)
|
||||||
}.flow.distinctUntilChanged()
|
}.flow.distinctUntilChanged()
|
||||||
|
|||||||
@@ -113,9 +113,6 @@ class SearchViewModel @Inject constructor(
|
|||||||
Log.w(TAG, "Search returned ${exception.code}, redirecting to Splash")
|
Log.w(TAG, "Search returned ${exception.code}, redirecting to Splash")
|
||||||
AuroraApp.events.send(AuthEvent.SessionExpired())
|
AuroraApp.events.send(AuthEvent.SessionExpired())
|
||||||
emptyList()
|
emptyList()
|
||||||
} catch (exception: Exception) {
|
|
||||||
Log.e(TAG, "Failed to search results for $query", exception)
|
|
||||||
emptyList()
|
|
||||||
}
|
}
|
||||||
PageResult(items)
|
PageResult(items)
|
||||||
}.flow.distinctUntilChanged()
|
}.flow.distinctUntilChanged()
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.supervisorScope
|
|
||||||
|
|
||||||
@HiltViewModel(assistedFactory = CategoryStreamViewModel.Factory::class)
|
@HiltViewModel(assistedFactory = CategoryStreamViewModel.Factory::class)
|
||||||
class CategoryStreamViewModel @AssistedInject constructor(
|
class CategoryStreamViewModel @AssistedInject constructor(
|
||||||
@@ -47,66 +46,62 @@ class CategoryStreamViewModel @AssistedInject constructor(
|
|||||||
private var streamBundle = StreamBundle.EMPTY
|
private var streamBundle = StreamBundle.EMPTY
|
||||||
|
|
||||||
init {
|
init {
|
||||||
fetchNextPage()
|
fetch()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fetchNextPage() {
|
fun fetch() {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
supervisorScope {
|
try {
|
||||||
try {
|
if (!streamBundle.hasCluster() || streamBundle.hasNext()) {
|
||||||
if (!streamBundle.hasCluster() || streamBundle.hasNext()) {
|
val newBundle = if (streamBundle.streamClusters.isEmpty()) {
|
||||||
val newBundle = if (streamBundle.streamClusters.isEmpty()) {
|
categoryStreamContract.fetch(browseUrl)
|
||||||
categoryStreamContract.fetch(browseUrl)
|
|
||||||
} else {
|
|
||||||
categoryStreamContract.nextStreamBundle(
|
|
||||||
streamBundle.id,
|
|
||||||
StreamContract.Category.NONE,
|
|
||||||
streamBundle.streamNextPageUrl
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
streamBundle = streamBundle.copy(
|
|
||||||
streamClusters = streamBundle.streamClusters + newBundle.streamClusters,
|
|
||||||
streamNextPageUrl = newBundle.streamNextPageUrl
|
|
||||||
)
|
|
||||||
|
|
||||||
_viewState.value = ViewState.Success(streamBundle)
|
|
||||||
} else {
|
} else {
|
||||||
Log.i(TAG, "End of Bundle")
|
categoryStreamContract.nextStreamBundle(
|
||||||
|
streamBundle.id,
|
||||||
|
StreamContract.Category.NONE,
|
||||||
|
streamBundle.streamNextPageUrl
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
_viewState.value = ViewState.Error(e.message)
|
streamBundle = streamBundle.copy(
|
||||||
|
streamClusters = streamBundle.streamClusters + newBundle.streamClusters,
|
||||||
|
streamNextPageUrl = newBundle.streamNextPageUrl
|
||||||
|
)
|
||||||
|
|
||||||
|
_viewState.value = ViewState.Success(streamBundle)
|
||||||
|
} else {
|
||||||
|
Log.i(TAG, "End of Bundle")
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_viewState.value = ViewState.Error(e.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fetchNextCluster(streamCluster: StreamCluster) {
|
fun fetchNextCluster(streamCluster: StreamCluster) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
supervisorScope {
|
try {
|
||||||
try {
|
if (streamCluster.hasNext()) {
|
||||||
if (streamCluster.hasNext()) {
|
val newCluster = categoryStreamContract.nextStreamCluster(
|
||||||
val newCluster = categoryStreamContract.nextStreamCluster(
|
streamCluster.id,
|
||||||
streamCluster.id,
|
streamCluster.clusterNextPageUrl
|
||||||
streamCluster.clusterNextPageUrl
|
)
|
||||||
)
|
val mergedCluster = streamCluster.copy(
|
||||||
val mergedCluster = streamCluster.copy(
|
clusterNextPageUrl = newCluster.clusterNextPageUrl,
|
||||||
clusterNextPageUrl = newCluster.clusterNextPageUrl,
|
clusterAppList =
|
||||||
clusterAppList =
|
streamCluster.clusterAppList + newCluster.clusterAppList
|
||||||
streamCluster.clusterAppList + newCluster.clusterAppList
|
)
|
||||||
)
|
|
||||||
|
|
||||||
val newClusters = streamBundle.streamClusters.toMutableMap().apply {
|
val newClusters = streamBundle.streamClusters.toMutableMap().apply {
|
||||||
this[streamCluster.id] = mergedCluster
|
this[streamCluster.id] = mergedCluster
|
||||||
}
|
|
||||||
streamBundle = streamBundle.copy(streamClusters = newClusters)
|
|
||||||
_viewState.value = ViewState.Success(streamBundle)
|
|
||||||
} else {
|
|
||||||
Log.i(TAG, "End of cluster")
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
streamBundle = streamBundle.copy(streamClusters = newClusters)
|
||||||
Log.e(TAG, "Failed to fetch next cluster", e)
|
_viewState.value = ViewState.Success(streamBundle)
|
||||||
|
} else {
|
||||||
|
Log.i(TAG, "End of cluster")
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to fetch next cluster", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.supervisorScope
|
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class TopChartViewModel @Inject constructor(
|
class TopChartViewModel @Inject constructor(
|
||||||
@@ -69,21 +68,19 @@ class TopChartViewModel @Inject constructor(
|
|||||||
|
|
||||||
fun nextCluster(type: TopChartsContract.Type, chart: TopChartsContract.Chart) {
|
fun nextCluster(type: TopChartsContract.Type, chart: TopChartsContract.Chart) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
supervisorScope {
|
try {
|
||||||
try {
|
val target = targetCluster(type, chart)
|
||||||
val target = targetCluster(type, chart)
|
if (target.hasNext()) {
|
||||||
if (target.hasNext()) {
|
val newCluster = topChartsContract.getNextStreamCluster(
|
||||||
val newCluster = topChartsContract.getNextStreamCluster(
|
target.id,
|
||||||
target.id,
|
target.clusterNextPageUrl
|
||||||
target.clusterNextPageUrl
|
)
|
||||||
)
|
|
||||||
|
|
||||||
updateCluster(type, chart, newCluster)
|
updateCluster(type, chart, newCluster)
|
||||||
|
|
||||||
_state.value = ViewState.Success(targetCluster(type, chart))
|
_state.value = ViewState.Success(targetCluster(type, chart))
|
||||||
}
|
|
||||||
} catch (_: Exception) {
|
|
||||||
}
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user