diff --git a/core/designsystem/build.gradle.kts b/core/designsystem/build.gradle.kts index d1d67636..72699f1c 100644 --- a/core/designsystem/build.gradle.kts +++ b/core/designsystem/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { api(libs.androidx.compose.material3) api(libs.androidx.compose.runtime) api(libs.androidx.compose.runtime.livedata) + api(libs.androidx.compose.runtime.tracing) api(libs.androidx.compose.ui.tooling.preview) api(libs.androidx.compose.ui.util) api(libs.androidx.metrics) diff --git a/core/designsystem/src/main/kotlin/com/naveenapps/expensemanager/core/designsystem/ui/components/IconAndBackgroundView.kt b/core/designsystem/src/main/kotlin/com/naveenapps/expensemanager/core/designsystem/ui/components/IconAndBackgroundView.kt index 55bf64e6..e78e9754 100644 --- a/core/designsystem/src/main/kotlin/com/naveenapps/expensemanager/core/designsystem/ui/components/IconAndBackgroundView.kt +++ b/core/designsystem/src/main/kotlin/com/naveenapps/expensemanager/core/designsystem/ui/components/IconAndBackgroundView.kt @@ -11,8 +11,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -76,7 +77,7 @@ private fun IconView( modifier = Modifier .size(iconSize) .align(Alignment.Center), - painter = painterResource(id = context.getDrawable(icon)), + imageVector = ImageVector.vectorResource(id = context.getDrawable(icon)), colorFilter = ColorFilter.tint(color = Color.White), contentDescription = name, ) diff --git a/core/domain/src/main/kotlin/com/naveenapps/expensemanager/core/domain/usecase/budget/GetBudgetsUseCase.kt b/core/domain/src/main/kotlin/com/naveenapps/expensemanager/core/domain/usecase/budget/GetBudgetsUseCase.kt index dd90e05e..e1b8aac2 100644 --- a/core/domain/src/main/kotlin/com/naveenapps/expensemanager/core/domain/usecase/budget/GetBudgetsUseCase.kt +++ b/core/domain/src/main/kotlin/com/naveenapps/expensemanager/core/domain/usecase/budget/GetBudgetsUseCase.kt @@ -1,6 +1,7 @@ package com.naveenapps.expensemanager.core.domain.usecase.budget import com.naveenapps.expensemanager.core.common.R +import com.naveenapps.expensemanager.core.common.utils.AppCoroutineDispatchers import com.naveenapps.expensemanager.core.domain.usecase.settings.currency.GetCurrencyUseCase import com.naveenapps.expensemanager.core.domain.usecase.settings.currency.GetFormattedAmountUseCase import com.naveenapps.expensemanager.core.domain.usecase.transaction.GetTransactionWithFilterUseCase @@ -13,6 +14,7 @@ import com.naveenapps.expensemanager.core.model.toTransactionUIModel import com.naveenapps.expensemanager.core.repository.BudgetRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn import javax.inject.Inject class GetBudgetsUseCase @Inject constructor( @@ -21,6 +23,7 @@ class GetBudgetsUseCase @Inject constructor( private val getCurrencyUseCase: GetCurrencyUseCase, private val getFormattedAmountUseCase: GetFormattedAmountUseCase, private val getBudgetTransactionsUseCase: GetBudgetTransactionsUseCase, + private val appCoroutineDispatchers: AppCoroutineDispatchers ) { operator fun invoke(): Flow> { return combine( @@ -53,7 +56,7 @@ class GetBudgetsUseCase @Inject constructor( }, ) } - } + }.flowOn(appCoroutineDispatchers.computation) } } diff --git a/feature/category/src/main/kotlin/com/naveenapps/expensemanager/feature/category/details/CategoryDetailScreen.kt b/feature/category/src/main/kotlin/com/naveenapps/expensemanager/feature/category/details/CategoryDetailScreen.kt index 67192ca9..3f3a5dde 100644 --- a/feature/category/src/main/kotlin/com/naveenapps/expensemanager/feature/category/details/CategoryDetailScreen.kt +++ b/feature/category/src/main/kotlin/com/naveenapps/expensemanager/feature/category/details/CategoryDetailScreen.kt @@ -10,6 +10,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Close @@ -129,8 +131,8 @@ fun CategoryDetailScreen( textAlign = TextAlign.Center, ) } else { - Column(modifier = Modifier.padding(top = 16.dp)) { - state.transactions.forEach { item -> + LazyColumn(modifier = Modifier.padding(top = 16.dp)) { + items(state.transactions, key = { it.id }) { item -> TransactionItem( categoryName = item.categoryName, fromAccountName = item.fromAccountName, @@ -151,11 +153,14 @@ fun CategoryDetailScreen( transactionType = item.transactionType, ) } - Spacer( - modifier = Modifier - .fillMaxWidth() - .padding(36.dp), - ) + + item { + Spacer( + modifier = Modifier + .fillMaxWidth() + .padding(36.dp), + ) + } } } } diff --git a/feature/category/src/main/kotlin/com/naveenapps/expensemanager/feature/category/list/CategoryListScreen.kt b/feature/category/src/main/kotlin/com/naveenapps/expensemanager/feature/category/list/CategoryListScreen.kt index 3b4138d3..47e046a5 100644 --- a/feature/category/src/main/kotlin/com/naveenapps/expensemanager/feature/category/list/CategoryListScreen.kt +++ b/feature/category/src/main/kotlin/com/naveenapps/expensemanager/feature/category/list/CategoryListScreen.kt @@ -101,7 +101,7 @@ private fun CategoryListScreenContentView( .fillMaxSize(), ) { PrimaryTabRow(selectedTabIndex = state.selectedTab.index) { - CategoryTabItems.entries.forEach { item -> + state.tabs.forEach { item -> Tab( selected = state.selectedTab.categoryType == item.categoryType, onClick = { diff --git a/feature/transaction/src/main/kotlin/com/naveenapps/expensemanager/feature/transaction/list/TransactionListScreen.kt b/feature/transaction/src/main/kotlin/com/naveenapps/expensemanager/feature/transaction/list/TransactionListScreen.kt index 2c8249db..bcbc71fe 100644 --- a/feature/transaction/src/main/kotlin/com/naveenapps/expensemanager/feature/transaction/list/TransactionListScreen.kt +++ b/feature/transaction/src/main/kotlin/com/naveenapps/expensemanager/feature/transaction/list/TransactionListScreen.kt @@ -36,14 +36,13 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import com.naveenapps.expensemanager.core.common.utils.UiState import com.naveenapps.expensemanager.core.common.utils.fromCompleteDate import com.naveenapps.expensemanager.core.common.utils.toCompleteDateWithDate import com.naveenapps.expensemanager.core.common.utils.toDate import com.naveenapps.expensemanager.core.common.utils.toDay import com.naveenapps.expensemanager.core.common.utils.toMonthYear +import com.naveenapps.expensemanager.core.designsystem.AppPreviewsLightAndDarkMode import com.naveenapps.expensemanager.core.designsystem.components.EmptyItem -import com.naveenapps.expensemanager.core.designsystem.components.LoadingItem import com.naveenapps.expensemanager.core.designsystem.ui.components.IconAndBackgroundView import com.naveenapps.expensemanager.core.designsystem.ui.components.TopNavigationBar import com.naveenapps.expensemanager.core.designsystem.ui.extensions.getDrawable @@ -64,7 +63,7 @@ fun TransactionListScreen( viewModel: TransactionListViewModel = hiltViewModel() ) { - val transactionUiState by viewModel.transactions.collectAsState() + val state by viewModel.state.collectAsState() Scaffold( topBar = { @@ -87,7 +86,7 @@ fun TransactionListScreen( modifier = Modifier .fillMaxSize() .padding(top = innerPadding.calculateTopPadding()), - transactionGroup = transactionUiState, + state = state, ) { transaction -> viewModel.openCreateScreen(transaction.id) } @@ -96,84 +95,79 @@ fun TransactionListScreen( @Composable private fun TransactionListScreen( - transactionGroup: UiState>, + state: TransactionListState, modifier: Modifier = Modifier, onItemClick: ((TransactionUiItem) -> Unit)? = null, ) { - Column(modifier = modifier) { - FilterView( - modifier = Modifier - .fillMaxWidth() - .padding(end = 6.dp), - ) - when (transactionGroup) { - UiState.Empty -> { + LazyColumn(modifier = modifier.fillMaxWidth()) { + item { + FilterView( + modifier = Modifier + .fillMaxWidth() + .padding(end = 6.dp), + ) + } + if (state.transactionListItem.isEmpty()) { + item { EmptyItem( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .height(400.dp), emptyItemText = stringResource(id = R.string.no_transactions_available), icon = com.naveenapps.expensemanager.core.designsystem.R.drawable.ic_no_transaction ) } + } else { - UiState.Loading -> { - LoadingItem() - } + items(state.transactionListItem) { transactionListItem -> + when (transactionListItem) { + TransactionListItem.Divider -> { + HorizontalDivider( + modifier = Modifier.padding( + top = 8.dp, + bottom = 8.dp + ) + ) + } - is UiState.Success -> { - LazyColumn { - items(transactionGroup.data) { - TransactionGroupItem( - it, - onItemClick, + is TransactionListItem.HeaderItem -> { + TransactionHeaderItem( + transactionListItem.date, + transactionListItem.amountTextColor, + transactionListItem.totalAmount, ) } - item { - Spacer(modifier = Modifier.height(48.dp)) + + is TransactionListItem.TransactionItem -> { + val item = transactionListItem.date + TransactionItem( + modifier = Modifier + .fillMaxWidth() + .clickable { + onItemClick?.invoke(item) + } + .then(ItemSpecModifier), + categoryName = item.categoryName, + categoryColor = item.categoryIcon.backgroundColor, + categoryIcon = item.categoryIcon.name, + amount = item.amount, + date = item.date, + notes = item.notes, + transactionType = item.transactionType, + fromAccountName = item.fromAccountName, + fromAccountIcon = item.fromAccountIcon.name, + fromAccountColor = item.fromAccountIcon.backgroundColor, + toAccountName = item.toAccountName, + toAccountIcon = item.toAccountIcon?.name, + toAccountColor = item.toAccountIcon?.backgroundColor, + ) } } } - } - } -} - -@Composable -fun TransactionGroupItem( - transactionGroup: TransactionGroup, - onItemClick: ((TransactionUiItem) -> Unit)?, - isLastItem: Boolean = false, -) { - Column { - TransactionHeaderItem( - transactionGroup.date, - transactionGroup.amountTextColor, - transactionGroup.totalAmount, - ) - transactionGroup.transactions.forEach { - TransactionItem( - modifier = Modifier - .fillMaxWidth() - .clickable { - onItemClick?.invoke(it) - } - .then(ItemSpecModifier), - categoryName = it.categoryName, - categoryColor = it.categoryIcon.backgroundColor, - categoryIcon = it.categoryIcon.name, - amount = it.amount, - date = it.date, - notes = it.notes, - transactionType = it.transactionType, - fromAccountName = it.fromAccountName, - fromAccountIcon = it.fromAccountIcon.name, - fromAccountColor = it.fromAccountIcon.backgroundColor, - toAccountName = it.toAccountName, - toAccountIcon = it.toAccountIcon?.name, - toAccountColor = it.toAccountIcon?.backgroundColor, - ) - } - if (isLastItem.not()) { - HorizontalDivider(modifier = Modifier.padding(top = 8.dp, bottom = 8.dp)) + item { + Spacer(modifier = Modifier.height(48.dp)) + } } } } @@ -182,7 +176,7 @@ fun TransactionGroupItem( fun TransactionHeaderItem( date: String, textColor: Int, - totalAmount: Amount, + totalAmount: String, ) { Row( modifier = Modifier @@ -214,7 +208,7 @@ fun TransactionHeaderItem( modifier = Modifier .padding(start = 16.dp) .align(Alignment.CenterVertically), - text = totalAmount.amountString ?: "", + text = totalAmount, color = colorResource(id = textColor), style = MaterialTheme.typography.titleMedium, ) @@ -350,7 +344,7 @@ private fun AccountNameWithIcon( } } -@com.naveenapps.expensemanager.core.designsystem.AppPreviewsLightAndDarkMode +@AppPreviewsLightAndDarkMode @Composable fun TransactionUiStatePreview() { ExpenseManagerTheme { @@ -371,34 +365,12 @@ fun TransactionUiStatePreview() { } } -@Preview -@Composable -fun TransactionItemPreview() { - ExpenseManagerTheme { - TransactionGroupItem( - getTransactionUiState(), - {}, - ) - } -} - -@Preview -@Composable -fun TransactionListItemLoadingStatePreview() { - ExpenseManagerTheme { - TransactionListScreen( - transactionGroup = UiState.Loading, - modifier = Modifier.fillMaxSize(), - ) - } -} - @Preview @Composable fun TransactionListItemEmptyStatePreview() { ExpenseManagerTheme { TransactionListScreen( - transactionGroup = UiState.Success(emptyList()), + state = TransactionListState(emptyList()), modifier = Modifier.fillMaxSize(), ) } @@ -439,14 +411,12 @@ private fun getTransactionUiState() = TransactionGroup( }, ) -@Preview +@AppPreviewsLightAndDarkMode @Composable fun TransactionListItemSuccessStatePreview() { ExpenseManagerTheme { TransactionListScreen( - transactionGroup = UiState.Success( - DUMMY_DATA, - ), + state = TransactionListState(DUMMY_DATA.convertGroupToTransactionListItems()), modifier = Modifier.fillMaxSize(), ) } diff --git a/feature/transaction/src/main/kotlin/com/naveenapps/expensemanager/feature/transaction/list/TransactionListState.kt b/feature/transaction/src/main/kotlin/com/naveenapps/expensemanager/feature/transaction/list/TransactionListState.kt new file mode 100644 index 00000000..7f8f2835 --- /dev/null +++ b/feature/transaction/src/main/kotlin/com/naveenapps/expensemanager/feature/transaction/list/TransactionListState.kt @@ -0,0 +1,5 @@ +package com.naveenapps.expensemanager.feature.transaction.list + +data class TransactionListState( + val transactionListItem: List +) \ No newline at end of file diff --git a/feature/transaction/src/main/kotlin/com/naveenapps/expensemanager/feature/transaction/list/TransactionListViewModel.kt b/feature/transaction/src/main/kotlin/com/naveenapps/expensemanager/feature/transaction/list/TransactionListViewModel.kt index a50679dc..79a9cbf9 100644 --- a/feature/transaction/src/main/kotlin/com/naveenapps/expensemanager/feature/transaction/list/TransactionListViewModel.kt +++ b/feature/transaction/src/main/kotlin/com/naveenapps/expensemanager/feature/transaction/list/TransactionListViewModel.kt @@ -1,8 +1,9 @@ package com.naveenapps.expensemanager.feature.transaction.list +import androidx.annotation.DrawableRes import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.naveenapps.expensemanager.core.common.utils.UiState +import com.naveenapps.expensemanager.core.common.utils.AppCoroutineDispatchers import com.naveenapps.expensemanager.core.common.utils.getAmountTextColor import com.naveenapps.expensemanager.core.common.utils.toCompleteDateWithDate import com.naveenapps.expensemanager.core.domain.usecase.settings.currency.GetCurrencyUseCase @@ -11,6 +12,7 @@ import com.naveenapps.expensemanager.core.domain.usecase.transaction.GetTransact import com.naveenapps.expensemanager.core.model.Transaction import com.naveenapps.expensemanager.core.model.TransactionGroup import com.naveenapps.expensemanager.core.model.TransactionType +import com.naveenapps.expensemanager.core.model.TransactionUiItem import com.naveenapps.expensemanager.core.model.toTransactionUIModel import com.naveenapps.expensemanager.core.navigation.AppComposeNavigator import com.naveenapps.expensemanager.core.navigation.ExpenseManagerScreens @@ -18,7 +20,9 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.update import javax.inject.Inject @HiltViewModel @@ -26,45 +30,45 @@ class TransactionListViewModel @Inject constructor( getCurrencyUseCase: GetCurrencyUseCase, getFormattedAmountUseCase: GetFormattedAmountUseCase, getTransactionWithFilterUseCase: GetTransactionWithFilterUseCase, + appCoroutineDispatchers: AppCoroutineDispatchers, private val appComposeNavigator: AppComposeNavigator, ) : ViewModel() { - private val _transactions = MutableStateFlow>>( - UiState.Loading, - ) - val transactions = _transactions.asStateFlow() + private val _transactions = MutableStateFlow(TransactionListState(emptyList())) + val state = _transactions.asStateFlow() init { - combine( getCurrencyUseCase.invoke(), getTransactionWithFilterUseCase.invoke(), ) { currency, transactions -> - _transactions.value = if (transactions.isNullOrEmpty()) { - UiState.Empty - } else { - UiState.Success( - transactions.groupBy { - it.createdOn.toCompleteDateWithDate() - }.map { - val totalAmount = it.value.toTransactionSum() - TransactionGroup( - date = it.key, - amountTextColor = totalAmount.getAmountTextColor(), - totalAmount = getFormattedAmountUseCase.invoke(totalAmount, currency), - transactions = it.value.map { transaction -> - transaction.toTransactionUIModel( - getFormattedAmountUseCase.invoke( - transaction.amount.amount, - currency, - ), - ) - }, + + val groupedItem = transactions?.groupBy { + it.createdOn.toCompleteDateWithDate() + }?.map { + val totalAmount = it.value.toTransactionSum() + TransactionGroup( + date = it.key, + amountTextColor = totalAmount.getAmountTextColor(), + totalAmount = getFormattedAmountUseCase.invoke(totalAmount, currency), + transactions = it.value.map { transaction -> + transaction.toTransactionUIModel( + getFormattedAmountUseCase.invoke( + transaction.amount.amount, + currency, + ), ) }, ) } - }.launchIn(viewModelScope) + + _transactions.update { + it.copy( + transactionListItem = groupedItem?.convertGroupToTransactionListItems() + ?: emptyList() + ) + } + }.flowOn(appCoroutineDispatchers.computation).launchIn(viewModelScope) } fun openCreateScreen(transactionId: String? = null) { @@ -94,3 +98,38 @@ fun List.toTransactionSum() = } } } + +fun List.convertGroupToTransactionListItems(): List { + return buildList { + this@convertGroupToTransactionListItems.forEach { + add( + TransactionListItem.HeaderItem( + date = it.date, + amountTextColor = it.amountTextColor, + totalAmount = it.totalAmount.amountString ?: "" + ) + ) + + it.transactions.forEach { + add(TransactionListItem.TransactionItem(date = it)) + } + + add(TransactionListItem.Divider) + } + } +} + +sealed class TransactionListItem { + + data class HeaderItem( + val date: String, + @DrawableRes val amountTextColor: Int, + val totalAmount: String, + ) : TransactionListItem() + + data class TransactionItem( + val date: TransactionUiItem, + ) : TransactionListItem() + + data object Divider : TransactionListItem() +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index eabb4da7..8232b7b2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -74,6 +74,7 @@ sain = "2.0.2" uiGraphicsAndroid = "1.6.7" pdf-viewer = "3.2.0-beta.1" dependency-analysis = "1.27.0" +coil = "2.6.0" [libraries] accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanist" } @@ -171,6 +172,7 @@ vico-compose-m3 = { group = "com.patrykandpatrick.vico", name = "compose-m3", ve vico-core = { group = "com.patrykandpatrick.vico", name = "core", version.ref = "vico" } backup-restore = { group = "de.raphaelebner", name = "roomdatabasebackup", version.ref = "backup-restore" } gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } +coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" } # Dependencies of the included build-logic android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" }