diff --git a/app/src/main/java/io/horizontalsystems/bankwallet/modules/market/topcoins/MarketTopCoinsModule.kt b/app/src/main/java/io/horizontalsystems/bankwallet/modules/market/topcoins/MarketTopCoinsModule.kt deleted file mode 100644 index 3fc441d7cd..0000000000 --- a/app/src/main/java/io/horizontalsystems/bankwallet/modules/market/topcoins/MarketTopCoinsModule.kt +++ /dev/null @@ -1,61 +0,0 @@ -package io.horizontalsystems.bankwallet.modules.market.topcoins - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import io.horizontalsystems.bankwallet.core.App -import io.horizontalsystems.bankwallet.entities.ViewState -import io.horizontalsystems.bankwallet.modules.market.MarketField -import io.horizontalsystems.bankwallet.modules.market.MarketViewItem -import io.horizontalsystems.bankwallet.modules.market.SortingField -import io.horizontalsystems.bankwallet.modules.market.TimeDuration -import io.horizontalsystems.bankwallet.modules.market.TopMarket -import io.horizontalsystems.bankwallet.ui.compose.Select - -object MarketTopCoinsModule { - - class Factory( - private val topMarket: TopMarket? = null, - private val sortingField: SortingField? = null, - ) : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - val topMarketsRepository = MarketTopMoversRepository(App.marketKit) - val service = MarketTopCoinsService( - topMarketsRepository, - App.currencyManager, - App.marketFavoritesManager, - App.priceManager, - topMarket ?: defaultTopMarket, - sortingField ?: defaultSortingField, - ) - return MarketTopCoinsViewModel(service) as T - } - - companion object { - val defaultSortingField = SortingField.HighestCap - val defaultTopMarket = TopMarket.Top100 - val defaultMarketField = MarketField.PriceDiff - } - } - - data class UiState( - val viewItems: List, - val viewState: ViewState, - val isRefreshing: Boolean, - val sortingField: SortingField, - val topMarket: TopMarket, - val timeDuration: TimeDuration, - ) - - data class Menu( - val sortingFieldSelect: Select, - val topMarketSelect: Select?, - val marketFieldSelect: Select - ) - -} - -sealed class SelectorDialogState() { - object Closed : SelectorDialogState() - class Opened(val select: Select) : SelectorDialogState() -} diff --git a/app/src/main/java/io/horizontalsystems/bankwallet/modules/market/topcoins/MarketTopCoinsService.kt b/app/src/main/java/io/horizontalsystems/bankwallet/modules/market/topcoins/MarketTopCoinsService.kt deleted file mode 100644 index 04e74c49e3..0000000000 --- a/app/src/main/java/io/horizontalsystems/bankwallet/modules/market/topcoins/MarketTopCoinsService.kt +++ /dev/null @@ -1,140 +0,0 @@ -package io.horizontalsystems.bankwallet.modules.market.topcoins - -import io.horizontalsystems.bankwallet.core.managers.CurrencyManager -import io.horizontalsystems.bankwallet.core.managers.MarketFavoritesManager -import io.horizontalsystems.bankwallet.core.managers.PriceManager -import io.horizontalsystems.bankwallet.entities.DataState -import io.horizontalsystems.bankwallet.modules.market.MarketItem -import io.horizontalsystems.bankwallet.modules.market.SortingField -import io.horizontalsystems.bankwallet.modules.market.TimeDuration -import io.horizontalsystems.bankwallet.modules.market.TopMarket -import io.horizontalsystems.bankwallet.modules.market.category.MarketItemWrapper -import io.reactivex.subjects.BehaviorSubject -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch -import kotlinx.coroutines.rx2.asFlow -import kotlinx.coroutines.rx2.await - -class MarketTopCoinsService( - private val marketTopMoversRepository: MarketTopMoversRepository, - private val currencyManager: CurrencyManager, - private val favoritesManager: MarketFavoritesManager, - private val priceManager: PriceManager, - topMarket: TopMarket = TopMarket.Top100, - sortingField: SortingField = SortingField.HighestCap, -) { - private val coroutineScope = CoroutineScope(Dispatchers.Default) - private var syncJob: Job? = null - - private var marketItems: List = listOf() - - val stateObservable: BehaviorSubject>> = - BehaviorSubject.create() - - val periods = listOf( - TimeDuration.OneDay, - TimeDuration.SevenDay, - TimeDuration.ThirtyDay, - TimeDuration.ThreeMonths, - ) - var period: TimeDuration = periods[0] - private set - - val topMarkets = TopMarket.entries - var topMarket: TopMarket = topMarket - private set - - val sortingFields = listOf( - SortingField.HighestCap, - SortingField.LowestCap, - SortingField.TopGainers, - SortingField.TopLosers, - ) - var sortingField: SortingField = sortingField - private set - - fun setSortingField(sortingField: SortingField) { - this.sortingField = sortingField - sync() - } - - fun setTopMarket(topMarket: TopMarket) { - this.topMarket = topMarket - sync() - } - - fun setTimeDuration(period: TimeDuration) { - this.period = period - sync() - } - - private fun sync() { - syncJob?.cancel() - syncJob = coroutineScope.launch { - try { - marketItems = marketTopMoversRepository.get( - topMarket.value, - sortingField, - topMarket.value, - currencyManager.baseCurrency, - period, - ).await() - - syncItems() - } catch (e: CancellationException) { - //do nothing - } catch (e: Throwable) { - stateObservable.onNext(DataState.Error(e)) - } - } - } - - private fun syncItems() { - val favorites = favoritesManager.getAll().map { it.coinUid } - val items = - marketItems.map { MarketItemWrapper(it, favorites.contains(it.fullCoin.coin.uid)) } - stateObservable.onNext(DataState.Success(items)) - } - - fun start() { - coroutineScope.launch { - favoritesManager.dataUpdatedAsync.asFlow().collect { - syncItems() - } - } - - coroutineScope.launch { - currencyManager.baseCurrencyUpdatedFlow.collect { - sync() - } - } - - coroutineScope.launch { - priceManager.priceChangeIntervalFlow.collect { - sync() - } - } - - sync() - } - - fun refresh() { - sync() - } - - fun stop() { - coroutineScope.cancel() - } - - fun addFavorite(coinUid: String) { - favoritesManager.add(coinUid) - } - - fun removeFavorite(coinUid: String) { - favoritesManager.remove(coinUid) - } -} diff --git a/app/src/main/java/io/horizontalsystems/bankwallet/modules/market/topcoins/MarketTopCoinsViewModel.kt b/app/src/main/java/io/horizontalsystems/bankwallet/modules/market/topcoins/MarketTopCoinsViewModel.kt index fae56c879f..643de26d18 100644 --- a/app/src/main/java/io/horizontalsystems/bankwallet/modules/market/topcoins/MarketTopCoinsViewModel.kt +++ b/app/src/main/java/io/horizontalsystems/bankwallet/modules/market/topcoins/MarketTopCoinsViewModel.kt @@ -1,114 +1,246 @@ package io.horizontalsystems.bankwallet.modules.market.topcoins +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope +import io.horizontalsystems.bankwallet.core.App import io.horizontalsystems.bankwallet.core.ViewModelUiState -import io.horizontalsystems.bankwallet.entities.DataState +import io.horizontalsystems.bankwallet.core.managers.CurrencyManager +import io.horizontalsystems.bankwallet.core.managers.MarketFavoritesManager +import io.horizontalsystems.bankwallet.core.managers.MarketKitWrapper import io.horizontalsystems.bankwallet.entities.ViewState +import io.horizontalsystems.bankwallet.modules.market.MarketItem import io.horizontalsystems.bankwallet.modules.market.MarketViewItem import io.horizontalsystems.bankwallet.modules.market.SortingField import io.horizontalsystems.bankwallet.modules.market.TimeDuration import io.horizontalsystems.bankwallet.modules.market.TopMarket -import io.horizontalsystems.bankwallet.modules.market.category.MarketItemWrapper -import kotlinx.coroutines.delay +import io.horizontalsystems.bankwallet.modules.market.favorites.period +import io.horizontalsystems.bankwallet.modules.market.sort +import io.horizontalsystems.marketkit.models.MarketInfo +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.rx2.asFlow +import kotlinx.coroutines.rx2.await +import kotlin.enums.EnumEntries +import kotlin.math.min class MarketTopCoinsViewModel( - private val service: MarketTopCoinsService, -) : ViewModelUiState() { + private var topMarket: TopMarket, + private var sortingField: SortingField, + private val marketKit: MarketKitWrapper, + private val currencyManager: CurrencyManager, + private val favoritesManager: MarketFavoritesManager, +) : ViewModelUiState() { + + private val periods = listOf( + TimeDuration.OneDay, + TimeDuration.SevenDay, + TimeDuration.ThirtyDay, + TimeDuration.ThreeMonths, + ) + private val sortingFields = listOf( + SortingField.HighestCap, + SortingField.LowestCap, + SortingField.TopGainers, + SortingField.TopLosers, + ) + private val topMarkets = TopMarket.entries + private val baseCurrency get() = currencyManager.baseCurrency - private var marketItems: List = listOf() - - val periods by service::periods - val topMarkets by service::topMarkets - val sortingFields by service::sortingFields - private var viewItems = emptyList() - private var viewState: ViewState = ViewState.Loading private var isRefreshing = false + private var viewState: ViewState = ViewState.Loading + private var viewItems: List = listOf() + private var period = periods[0] + private var favoriteCoinUids: List = listOf() + + override fun createState() = MarketTopCoinsUiState( + isRefreshing = isRefreshing, + viewState = viewState, + viewItems = viewItems, + topMarkets = topMarkets, + topMarket = topMarket, + sortingFields = sortingFields, + sortingField = sortingField, + periods = periods, + period = period, + ) + + private var marketInfoList: List? = null + private var marketItemList: List? = null + private var sortedMarketItems: List? = null init { - viewModelScope.launch { - service.stateObservable.asFlow().collect { - syncState(it) + viewModelScope.launch(Dispatchers.Default) { + try { + reload() + + viewState = ViewState.Success + } catch (e: Throwable) { + viewState = ViewState.Error(e) } + + emitState() } - service.start() + viewModelScope.launch(Dispatchers.Default) { + favoritesManager.dataUpdatedAsync.asFlow().collect { + refreshFavoriteCoinUids() + refreshViewItems() + + emitState() + } + } + + viewModelScope.launch(Dispatchers.Default) { + currencyManager.baseCurrencyUpdatedFlow.collect { + try { + reload() + + viewState = ViewState.Success + } catch (e: Throwable) { + viewState = ViewState.Error(e) + } + + emitState() + } + } } - override fun createState(): MarketTopCoinsModule.UiState { - return MarketTopCoinsModule.UiState( - viewItems, - viewState, - isRefreshing, - service.sortingField, - service.topMarket, - service.period - ) + private suspend fun reload() { + fetchMarketInfoList() + + refreshFavoriteCoinUids() + refreshMarketItemList() + refreshSortedMarketItems() + refreshViewItems() } - private fun syncState(state: DataState>) { - state.viewState?.let { - viewState = it - } + private fun refreshFavoriteCoinUids() { + favoriteCoinUids = favoritesManager.getAll().map { it.coinUid } + } - state.dataOrNull?.let { - marketItems = it + private suspend fun fetchMarketInfoList() { + marketInfoList = marketKit.topCoinsMarketInfosSingle(500, baseCurrency.code).await() + } - syncMarketViewItems() + private fun refreshMarketItemList() { + marketItemList = marketInfoList?.map { marketInfo -> + MarketItem.createFromCoinMarket( + marketInfo, + baseCurrency, + period.period, + ) } } - private fun syncMarketViewItems() { - viewItems = marketItems.map { - MarketViewItem.create(it.marketItem, it.favorited) + private fun refreshSortedMarketItems() { + sortedMarketItems = marketItemList?.let { list -> + list + .subList(0, min(list.size, topMarket.value)) + .sort(sortingField) } - emitState() } - private fun refreshWithMinLoadingSpinnerPeriod() { + private fun refreshViewItems() { + sortedMarketItems?.let { list -> + viewItems = list.map { + MarketViewItem.create(it, favoriteCoinUids.contains(it.fullCoin.coin.uid)) + } + } + } + + fun refresh() { isRefreshing = true emitState() - service.refresh() - viewModelScope.launch { - delay(1000) + + viewModelScope.launch(Dispatchers.Default) { + try { + reload() + + viewState = ViewState.Success + } catch (e: Throwable) { + viewState = ViewState.Error(e) + } + isRefreshing = false emitState() } } - fun onSelectPeriod(timeDuration: TimeDuration) { - service.setTimeDuration(timeDuration) - emitState() + fun onAddFavorite(uid: String) { + viewModelScope.launch(Dispatchers.Default) { + favoritesManager.add(uid) + } + } + + fun onRemoveFavorite(uid: String) { + viewModelScope.launch(Dispatchers.Default) { + favoritesManager.remove(uid) + } } fun onSelectSortingField(sortingField: SortingField) { - service.setSortingField(sortingField) + this.sortingField = sortingField emitState() + + viewModelScope.launch(Dispatchers.Default) { + refreshSortedMarketItems() + refreshViewItems() + + emitState() + } } fun onSelectTopMarket(topMarket: TopMarket) { - service.setTopMarket(topMarket) + this.topMarket = topMarket emitState() - } - fun refresh() { - refreshWithMinLoadingSpinnerPeriod() - } + viewModelScope.launch(Dispatchers.Default) { + refreshSortedMarketItems() + refreshViewItems() - fun onErrorClick() { - refreshWithMinLoadingSpinnerPeriod() + emitState() + } } - override fun onCleared() { - service.stop() - } + fun onSelectPeriod(period: TimeDuration) { + this.period = period + emitState() + + viewModelScope.launch(Dispatchers.Default) { + refreshMarketItemList() + refreshSortedMarketItems() + refreshViewItems() - fun onAddFavorite(coinUid: String) { - service.addFavorite(coinUid) + emitState() + } } - fun onRemoveFavorite(coinUid: String) { - service.removeFavorite(coinUid) + class Factory( + private val topMarket: TopMarket, + private val sortingField: SortingField, + ) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return MarketTopCoinsViewModel( + topMarket, + sortingField, + App.marketKit, + App.currencyManager, + App.marketFavoritesManager + ) as T + } } } + +data class MarketTopCoinsUiState( + val isRefreshing: Boolean, + val viewState: ViewState, + val viewItems: List, + val topMarkets: EnumEntries, + val topMarket: TopMarket, + val sortingFields: List, + val sortingField: SortingField, + val periods: List, + val period: TimeDuration, +) diff --git a/app/src/main/java/io/horizontalsystems/bankwallet/modules/market/topcoins/MarketTopMoversRepository.kt b/app/src/main/java/io/horizontalsystems/bankwallet/modules/market/topcoins/MarketTopMoversRepository.kt index e70c6db8f3..d733518dec 100644 --- a/app/src/main/java/io/horizontalsystems/bankwallet/modules/market/topcoins/MarketTopMoversRepository.kt +++ b/app/src/main/java/io/horizontalsystems/bankwallet/modules/market/topcoins/MarketTopMoversRepository.kt @@ -2,14 +2,8 @@ package io.horizontalsystems.bankwallet.modules.market.topcoins import io.horizontalsystems.bankwallet.core.managers.MarketKitWrapper import io.horizontalsystems.bankwallet.entities.Currency -import io.horizontalsystems.bankwallet.modules.market.MarketItem -import io.horizontalsystems.bankwallet.modules.market.SortingField -import io.horizontalsystems.bankwallet.modules.market.TimeDuration -import io.horizontalsystems.bankwallet.modules.market.favorites.period -import io.horizontalsystems.bankwallet.modules.market.sort import io.horizontalsystems.marketkit.models.TopMovers import io.reactivex.Single -import java.lang.Integer.min class MarketTopMoversRepository( private val marketKit: MarketKitWrapper @@ -18,33 +12,4 @@ class MarketTopMoversRepository( fun getTopMovers(baseCurrency: Currency): Single = marketKit.topMoversSingle(baseCurrency.code) - fun get( - size: Int, - sortingField: SortingField, - limit: Int, - baseCurrency: Currency, - timeDuration: TimeDuration - ): Single> = - Single.create { emitter -> - try { - val marketInfoList = marketKit.topCoinsMarketInfosSingle(size, baseCurrency.code).blockingGet() - val marketItemList = marketInfoList.map { marketInfo -> - MarketItem.createFromCoinMarket( - marketInfo, - baseCurrency, - timeDuration.period, - ) - } - - val sortedMarketItems = marketItemList - .subList(0, min(marketInfoList.size, size)) - .sort(sortingField) - .subList(0, min(marketInfoList.size, limit)) - - emitter.onSuccess(sortedMarketItems) - } catch (error: Throwable) { - emitter.onError(error) - } - } - } diff --git a/app/src/main/java/io/horizontalsystems/bankwallet/modules/market/topcoins/SelectorDialogState.kt b/app/src/main/java/io/horizontalsystems/bankwallet/modules/market/topcoins/SelectorDialogState.kt new file mode 100644 index 0000000000..372345a55a --- /dev/null +++ b/app/src/main/java/io/horizontalsystems/bankwallet/modules/market/topcoins/SelectorDialogState.kt @@ -0,0 +1,9 @@ +package io.horizontalsystems.bankwallet.modules.market.topcoins + +import io.horizontalsystems.bankwallet.modules.market.SortingField +import io.horizontalsystems.bankwallet.ui.compose.Select + +sealed class SelectorDialogState { + object Closed : SelectorDialogState() + class Opened(val select: Select) : SelectorDialogState() +} diff --git a/app/src/main/java/io/horizontalsystems/bankwallet/modules/market/topcoins/TopCoins.kt b/app/src/main/java/io/horizontalsystems/bankwallet/modules/market/topcoins/TopCoins.kt index a0ff6eef14..2e482e896b 100644 --- a/app/src/main/java/io/horizontalsystems/bankwallet/modules/market/topcoins/TopCoins.kt +++ b/app/src/main/java/io/horizontalsystems/bankwallet/modules/market/topcoins/TopCoins.kt @@ -3,7 +3,9 @@ package io.horizontalsystems.bankwallet.modules.market.topcoins import androidx.compose.animation.Crossfade import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.height +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable @@ -38,18 +40,17 @@ import io.horizontalsystems.bankwallet.ui.compose.components.ListErrorView @Composable fun TopCoins( onCoinClick: (String) -> Unit, - viewModel: MarketTopCoinsViewModel = viewModel( - factory = MarketTopCoinsModule.Factory( +) { + val viewModel = viewModel( + factory = MarketTopCoinsViewModel.Factory( TopMarket.Top100, SortingField.TopGainers, ) ) -) { var openSortingSelector by rememberSaveable { mutableStateOf(false) } var openTopSelector by rememberSaveable { mutableStateOf(false) } var openPeriodSelector by rememberSaveable { mutableStateOf(false) } - var scrollToTopAfterUpdate by rememberSaveable { mutableStateOf(false) } val uiState = viewModel.uiState @@ -68,13 +69,20 @@ fun TopCoins( } is ViewState.Error -> { - ListErrorView(stringResource(R.string.SyncError), viewModel::onErrorClick) + ListErrorView(stringResource(R.string.SyncError), viewModel::refresh) } ViewState.Success -> { + val listState = rememberLazyListState() + + LaunchedEffect(uiState.period, uiState.topMarket, uiState.sortingField) { + listState.scrollToItem(0) + } + CoinList( + listState = listState, items = uiState.viewItems, - scrollToTop = scrollToTopAfterUpdate, + scrollToTop = false, onAddFavorite = { uid -> viewModel.onAddFavorite(uid) @@ -108,7 +116,7 @@ fun TopCoins( ) HSpacer(width = 12.dp) OptionController( - uiState.timeDuration.titleResId, + uiState.period.titleResId, onOptionClick = { openPeriodSelector = true } @@ -118,9 +126,6 @@ fun TopCoins( } } ) - if (scrollToTopAfterUpdate) { - scrollToTopAfterUpdate = false - } } } } @@ -129,11 +134,10 @@ fun TopCoins( if (openSortingSelector) { AlertGroup( title = R.string.Market_Sort_PopupTitle, - select = Select(uiState.sortingField, viewModel.sortingFields), + select = Select(uiState.sortingField, uiState.sortingFields), onSelect = { selected -> viewModel.onSelectSortingField(selected) openSortingSelector = false - scrollToTopAfterUpdate = true stat(page = StatPage.Markets, section = StatSection.Coins, event = StatEvent.SwitchSortType(selected.statSortType)) }, @@ -145,11 +149,10 @@ fun TopCoins( if (openTopSelector) { AlertGroup( title = R.string.Market_Tab_Coins, - select = Select(uiState.topMarket, viewModel.topMarkets), + select = Select(uiState.topMarket, uiState.topMarkets), onSelect = { viewModel.onSelectTopMarket(it) openTopSelector = false - scrollToTopAfterUpdate = true stat(page = StatPage.Markets, section = StatSection.Coins, event = StatEvent.SwitchMarketTop(it.statMarketTop)) }, @@ -161,11 +164,10 @@ fun TopCoins( if (openPeriodSelector) { AlertGroup( title = R.string.CoinPage_Period, - select = Select(uiState.timeDuration, viewModel.periods), + select = Select(uiState.period, uiState.periods), onSelect = { selected -> viewModel.onSelectPeriod(selected) openPeriodSelector = false - scrollToTopAfterUpdate = true stat(page = StatPage.Markets, section = StatSection.Coins, event = StatEvent.SwitchPeriod(selected.statPeriod)) }, diff --git a/app/src/main/java/io/horizontalsystems/bankwallet/ui/compose/components/CoinListComponents.kt b/app/src/main/java/io/horizontalsystems/bankwallet/ui/compose/components/CoinListComponents.kt index bed6411adf..a5c6fc3f96 100644 --- a/app/src/main/java/io/horizontalsystems/bankwallet/ui/compose/components/CoinListComponents.kt +++ b/app/src/main/java/io/horizontalsystems/bankwallet/ui/compose/components/CoinListComponents.kt @@ -28,6 +28,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState @@ -69,6 +70,7 @@ import kotlinx.coroutines.launch @Composable fun CoinList( + listState: LazyListState = rememberLazyListState(), items: List, scrollToTop: Boolean, onAddFavorite: (String) -> Unit, @@ -78,7 +80,6 @@ fun CoinList( preItems: LazyListScope.() -> Unit ) { val coroutineScope = rememberCoroutineScope() - val listState = rememberLazyListState() var revealedCardId by remember { mutableStateOf(null) } LazyColumn(state = listState, userScrollEnabled = userScrollEnabled) {