From daa994a7b0c61633c70f472cfc64bfee6a34eb63 Mon Sep 17 00:00:00 2001 From: Vishwa Raghavendra K S Date: Thu, 12 May 2022 16:13:08 +0530 Subject: [PATCH] CategoryScreen Improvements & BugFixes --- .../wallet/ui/category/CategoriesScreen.kt | 124 ++++++------ .../wallet/ui/category/CategoriesViewModel.kt | 178 ++++++++++++------ .../ui/theme/components/ReorderModal.kt | 6 +- .../java/com/ivy/fp/action/Composition.kt | 20 ++ 4 files changed, 205 insertions(+), 123 deletions(-) diff --git a/app/src/main/java/com/ivy/wallet/ui/category/CategoriesScreen.kt b/app/src/main/java/com/ivy/wallet/ui/category/CategoriesScreen.kt index 2de0618bd3..a7ce1fc9d0 100644 --- a/app/src/main/java/com/ivy/wallet/ui/category/CategoriesScreen.kt +++ b/app/src/main/java/com/ivy/wallet/ui/category/CategoriesScreen.kt @@ -1,9 +1,15 @@ package com.ivy.wallet.ui.category -import androidx.compose.foundation.* +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -23,7 +29,6 @@ import com.ivy.design.l0_system.UI import com.ivy.design.l0_system.style import com.ivy.wallet.R import com.ivy.wallet.domain.data.core.Category -import com.ivy.wallet.domain.deprecated.logic.model.CreateCategoryData import com.ivy.wallet.ui.Categories import com.ivy.wallet.ui.ItemStatistic import com.ivy.wallet.ui.IvyWalletPreview @@ -41,76 +46,67 @@ import com.ivy.wallet.utils.onScreenStart @Composable fun BoxWithConstraintsScope.CategoriesScreen(screen: Categories) { val viewModel: CategoriesViewModel = viewModel() - - val currency by viewModel.currency.collectAsState() - val categories by viewModel.categories.collectAsState() + val state by viewModel.state().collectAsState() onScreenStart { viewModel.start() } UI( - currency = currency, - categories = categories, - - onCreateCategory = viewModel::createCategory, - onReorder = viewModel::reorder, + state = state, + onEventHandler = viewModel::onEvent ) } @Composable private fun BoxWithConstraintsScope.UI( - currency: String, - categories: List, - - onCreateCategory: (CreateCategoryData) -> Unit, - onReorder: (List) -> Unit, + state: CategoriesScreenState = CategoriesScreenState(), + onEventHandler: (CategoriesScreenEvent) -> Unit = {} ) { - var reorderVisible by remember { mutableStateOf(false) } - var categoryModalData: CategoryModalData? by remember { mutableStateOf(null) } + val nav = navigation() - Column( + LazyColumn( modifier = Modifier .fillMaxSize() .statusBarsPadding() - .navigationBarsPadding() - .verticalScroll(rememberScrollState()), + .navigationBarsPadding(), ) { - Spacer(Modifier.height(32.dp)) + item { + Spacer(Modifier.height(32.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(Modifier.width(24.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Spacer(Modifier.width(24.dp)) - Text( - text = stringResource(R.string.categories), - style = UI.typo.h2.style( - color = UI.colors.pureInverse, - fontWeight = FontWeight.ExtraBold + Text( + text = stringResource(R.string.categories), + style = UI.typo.h2.style( + color = UI.colors.pureInverse, + fontWeight = FontWeight.ExtraBold + ) ) - ) - Spacer(Modifier.weight(1f)) + Spacer(Modifier.weight(1f)) - ReorderButton { - reorderVisible = true - } + ReorderButton { + onEventHandler.invoke(CategoriesScreenEvent.OnReorderModalVisible(true)) + } - Spacer(Modifier.width(24.dp)) - } + Spacer(Modifier.width(24.dp)) + } - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(16.dp)) + } - val nav = navigation() - for (categoryData in categories) { + items(state.categories) { categoryData -> CategoryCard( - currency = currency, + currency = state.baseCurrency, categoryData = categoryData, onLongClick = { - reorderVisible = true + onEventHandler.invoke(CategoriesScreenEvent.OnReorderModalVisible(true)) } ) { nav.navigateTo( @@ -122,14 +118,16 @@ private fun BoxWithConstraintsScope.UI( } } - Spacer(Modifier.height(150.dp)) //scroll hack + item { + Spacer(Modifier.height(150.dp)) //scroll hack + } } - - val nav = navigation() CategoriesBottomBar( onAddCategory = { - categoryModalData = CategoryModalData( - category = null + onEventHandler.invoke( + CategoriesScreenEvent.OnCategoryModalVisible( + CategoryModalData(category = null) + ) ) }, onClose = { @@ -138,12 +136,14 @@ private fun BoxWithConstraintsScope.UI( ) ReorderModalSingleType( - visible = reorderVisible, - initialItems = categories, + visible = state.reorderModalVisible, + initialItems = state.categories, dismiss = { - reorderVisible = false + onEventHandler.invoke(CategoriesScreenEvent.OnReorderModalVisible(false)) }, - onReordered = onReorder + onReordered = { + onEventHandler.invoke(CategoriesScreenEvent.OnReorder(it)) + } ) { _, item -> Text( modifier = Modifier @@ -159,11 +159,13 @@ private fun BoxWithConstraintsScope.UI( } CategoryModal( - modal = categoryModalData, - onCreateCategory = onCreateCategory, + modal = state.categoryModalData, + onCreateCategory = { + onEventHandler.invoke(CategoriesScreenEvent.OnCreateCategory(it)) + }, onEditCategory = { }, dismiss = { - categoryModalData = null + onEventHandler.invoke(CategoriesScreenEvent.OnCategoryModalVisible(null)) } ) @@ -375,8 +377,8 @@ private fun CategoryHeader( @Composable private fun Preview() { IvyWalletPreview { - UI( - currency = "BGN", + val state = CategoriesScreenState( + baseCurrency = "BGN", categories = listOf( CategoryData( category = Category( @@ -425,10 +427,8 @@ private fun Preview() { monthlyIncome = 400.0 ), - ), - - onCreateCategory = { }, - onReorder = {}, + ) ) + UI(state = state) } } \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/ui/category/CategoriesViewModel.kt b/app/src/main/java/com/ivy/wallet/ui/category/CategoriesViewModel.kt index e50e62ef6f..8271cba8c4 100644 --- a/app/src/main/java/com/ivy/wallet/ui/category/CategoriesViewModel.kt +++ b/app/src/main/java/com/ivy/wallet/ui/category/CategoriesViewModel.kt @@ -1,21 +1,29 @@ package com.ivy.wallet.ui.category -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.ivy.fp.action.mapAsync +import com.ivy.fp.action.thenFinishWith +import com.ivy.fp.action.thenMap +import com.ivy.fp.viewmodel.IvyViewModel +import com.ivy.wallet.domain.action.account.AccountsAct import com.ivy.wallet.domain.action.category.CategoriesAct +import com.ivy.wallet.domain.action.category.CategoryIncomeWithAccountFiltersAct import com.ivy.wallet.domain.action.settings.BaseCurrencyAct +import com.ivy.wallet.domain.action.transaction.TrnsWithRangeAndAccFiltersAct +import com.ivy.wallet.domain.data.core.Account +import com.ivy.wallet.domain.data.core.Transaction import com.ivy.wallet.domain.deprecated.logic.CategoryCreator -import com.ivy.wallet.domain.deprecated.logic.WalletCategoryLogic import com.ivy.wallet.domain.deprecated.logic.model.CreateCategoryData import com.ivy.wallet.domain.deprecated.sync.item.CategorySync import com.ivy.wallet.io.persistence.dao.CategoryDao -import com.ivy.wallet.io.persistence.dao.SettingsDao import com.ivy.wallet.ui.IvyWalletCtx import com.ivy.wallet.ui.onboarding.model.TimePeriod +import com.ivy.wallet.ui.theme.modal.edit.CategoryModalData import com.ivy.wallet.utils.TestIdlingResource import com.ivy.wallet.utils.ioThread -import com.ivy.wallet.utils.readOnly +import com.ivy.wallet.utils.scopedIOThread import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import javax.inject.Inject @@ -23,89 +31,139 @@ import javax.inject.Inject @HiltViewModel class CategoriesViewModel @Inject constructor( private val categoryDao: CategoryDao, - private val settingsDao: SettingsDao, - private val categoryLogic: WalletCategoryLogic, private val categorySync: CategorySync, private val categoryCreator: CategoryCreator, private val categoriesAct: CategoriesAct, private val ivyContext: IvyWalletCtx, - private val baseCurrencyAct: BaseCurrencyAct -) : ViewModel() { + private val baseCurrencyAct: BaseCurrencyAct, + private val accountsAct: AccountsAct, + private val trnsWithRangeAndAccFiltersAct: TrnsWithRangeAndAccFiltersAct, + private val categoryIncomeWithAccountFiltersAct: CategoryIncomeWithAccountFiltersAct +) : IvyViewModel() { - private val _currency = MutableStateFlow("") - val currency = _currency.readOnly() + override val mutableState: MutableStateFlow = MutableStateFlow( + CategoriesScreenState() + ) - private val _categories = MutableStateFlow>(emptyList()) - val categories = _categories.readOnly() + private var allAccounts = emptyList() + private var baseCurrency = "" + private var transactions = emptyList() fun start() { - viewModelScope.launch { + viewModelScope.launch(Dispatchers.IO) { TestIdlingResource.increment() + initialise() + loadCategories() + + TestIdlingResource.decrement() + } + } + + private suspend fun initialise() { + ioThread { val range = TimePeriod.currentMonth( startDayOfMonth = ivyContext.startDayOfMonth ).toRange(ivyContext.startDayOfMonth) //this must be monthly - _currency.value = baseCurrencyAct(Unit) - - _categories.value = ioThread { - categoriesAct(Unit) - .map { - CategoryData( - category = it, - monthlyBalance = categoryLogic.calculateCategoryBalance( - it, - range - ), - monthlyIncome = categoryLogic.calculateCategoryIncome( - category = it, - range = range - ), - monthlyExpenses = categoryLogic.calculateCategoryExpenses( - category = it, - range = range - ), - ) - } - }!! + allAccounts = accountsAct(Unit) + baseCurrency = baseCurrencyAct(Unit) - TestIdlingResource.decrement() + transactions = trnsWithRangeAndAccFiltersAct( + TrnsWithRangeAndAccFiltersAct.Input( + range = range, + accountIdFilterSet = suspend { allAccounts } thenMap { it.id } thenFinishWith { it.toHashSet() } + ) + ) } } - fun reorder(newOrder: List) { - viewModelScope.launch { - TestIdlingResource.increment() - - ioThread { - newOrder.forEachIndexed { index, categoryData -> - categoryDao.save( - categoryData.category.toEntity().copy( - orderNum = index.toDouble(), - isSynced = false - ) + private suspend fun loadCategories() { + scopedIOThread { scope -> + val categories = categoriesAct(Unit).mapAsync(scope) { + val catIncomeExpense = categoryIncomeWithAccountFiltersAct( + CategoryIncomeWithAccountFiltersAct.Input( + transactions = transactions, + accountFilterList = allAccounts, + category = it, + baseCurrency = baseCurrency ) - } + ) + + CategoryData( + category = it, + monthlyBalance = (catIncomeExpense.income - catIncomeExpense.expense).toDouble(), + monthlyIncome = catIncomeExpense.income.toDouble(), + monthlyExpenses = catIncomeExpense.expense.toDouble() + ) + }.sortedBy { + it.category.orderNum } - start() - ioThread { - categorySync.sync() + updateState { + it.copy(baseCurrency = baseCurrency, categories = categories) } - - TestIdlingResource.decrement() } } - fun createCategory(data: CreateCategoryData) { - viewModelScope.launch { - TestIdlingResource.increment() + private suspend fun reorder(newOrder: List) { + TestIdlingResource.increment() - categoryCreator.createCategory(data) { - start() + ioThread { + newOrder.forEachIndexed { index, categoryData -> + categoryDao.save( + categoryData.category.toEntity().copy( + orderNum = index.toDouble(), + isSynced = false + ) + ) } + } - TestIdlingResource.decrement() + updateState { + it.copy(categories = newOrder) + } + + ioThread { + categorySync.sync() + } + + TestIdlingResource.decrement() + } + + private suspend fun createCategory(data: CreateCategoryData) { + TestIdlingResource.increment() + + categoryCreator.createCategory(data) { + loadCategories() + } + + TestIdlingResource.decrement() + } + + fun onEvent(event: CategoriesScreenEvent) { + viewModelScope.launch(Dispatchers.Default) { + when (event) { + is CategoriesScreenEvent.OnReorder -> reorder(event.newOrder) + is CategoriesScreenEvent.OnCreateCategory -> createCategory(event.createCategoryData) + is CategoriesScreenEvent.OnReorderModalVisible -> updateState { it.copy(reorderModalVisible = event.visible) } + is CategoriesScreenEvent.OnCategoryModalVisible -> updateState { it.copy(categoryModalData = event.categoryModalData) } + } } } -} \ No newline at end of file +} + +data class CategoriesScreenState( + val baseCurrency: String = "", + val categories: List = emptyList(), + val reorderModalVisible: Boolean = false, + val categoryModalData: CategoryModalData? = null +) + +sealed class CategoriesScreenEvent { + data class OnReorder(val newOrder: List) : CategoriesScreenEvent() + data class OnCreateCategory(val createCategoryData: CreateCategoryData) : + CategoriesScreenEvent() + data class OnReorderModalVisible(val visible :Boolean) : CategoriesScreenEvent() + data class OnCategoryModalVisible(val categoryModalData: CategoryModalData?) : CategoriesScreenEvent() +} diff --git a/app/src/main/java/com/ivy/wallet/ui/theme/components/ReorderModal.kt b/app/src/main/java/com/ivy/wallet/ui/theme/components/ReorderModal.kt index 3eb052a491..bc483016b6 100644 --- a/app/src/main/java/com/ivy/wallet/ui/theme/components/ReorderModal.kt +++ b/app/src/main/java/com/ivy/wallet/ui/theme/components/ReorderModal.kt @@ -99,6 +99,9 @@ fun BoxScope.ReorderModal( ItemContent: @Composable RowScope.(Int, Any) -> Unit ) { var items by remember(id, initialItems) { mutableStateOf(initialItems) } + var reOrderedList by remember { + mutableStateOf(emptyList()) + } var orderNumUpdates by remember { mutableStateOf( mapOf() @@ -123,7 +126,7 @@ fun BoxScope.ReorderModal( onUpdateItemOrderNum(items, item, newOrderNum) } - onReordered?.invoke(items) + onReordered?.invoke(reOrderedList) dismiss() } } @@ -156,6 +159,7 @@ fun BoxScope.ReorderModal( }, onReorderInternalList = { reorderedItems -> items = reorderedItems + reOrderedList = reorderedItems } ) layoutManager = LinearLayoutManager(it) diff --git a/ivy-fp/src/main/java/com/ivy/fp/action/Composition.kt b/ivy-fp/src/main/java/com/ivy/fp/action/Composition.kt index 2e33c0b663..9260e17f71 100644 --- a/ivy-fp/src/main/java/com/ivy/fp/action/Composition.kt +++ b/ivy-fp/src/main/java/com/ivy/fp/action/Composition.kt @@ -1,5 +1,9 @@ package com.ivy.fp.action +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll + suspend infix fun (suspend (A) -> B).then(f: suspend (B) -> C): suspend (A) -> C = { a -> @@ -38,6 +42,11 @@ suspend infix fun (() -> B).then(f: suspend (B) -> C): suspend () -> C = f(b) } +suspend infix fun (suspend () -> B).thenFinishWith(f: suspend (B) -> C): C { + val b = this@thenFinishWith() + return f(b) +} + fun (() -> C).fixUnit(): suspend (Unit) -> C = { this() @@ -55,4 +64,15 @@ fun (suspend (Unit) -> C).fixUnit(): suspend () -> C = fun (Action).lambda(): suspend (A) -> B = { a -> this(a) +} + +suspend inline fun Iterable.mapAsync( + scope: CoroutineScope, + crossinline transform: suspend (T) -> R +): List { + return this.map { + scope.async { + transform(it) + } + }.awaitAll() } \ No newline at end of file