From 3847e311a5263a4baf7b5e695cdfe4012d3fa1a5 Mon Sep 17 00:00:00 2001 From: Rafal Wisniewski <2krafal.wisniewski@gmail.com> Date: Wed, 8 Apr 2026 13:31:38 +0200 Subject: [PATCH] init --- app/build.gradle.kts | 15 +- app/src/main/AndroidManifest.xml | 1 + .../java/cc/n0th1ng/tripmoney/MainActivity.kt | 41 +++-- .../n0th1ng/tripmoney/data/dao/ExpenseDao.kt | 88 +++++++--- .../data/repository/ExpenseRepository.kt | 43 +++-- .../cc/n0th1ng/tripmoney/navigation/TopBar.kt | 156 ++++++++++++++++-- .../screens/listexpense/ListExpenseScreen.kt | 12 +- .../viewmodel/ExpenseAndCategoryViewModel.kt | 20 ++- baselineprofile/build.gradle.kts | 1 + .../BaselineProfileGenerator.kt | 12 +- gradle/libs.versions.toml | 4 + 11 files changed, 308 insertions(+), 85 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d582721..6e2da08 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -30,6 +30,19 @@ android { "proguard-rules.pro" ) } + create("benchmark") { + initWith(getByName("release")) + + // 🔑 Critical settings for Macrobenchmark + isDebuggable = false + isMinifyEnabled = false + isShrinkResources = false + + // Use release signing if needed (optional for local) + signingConfig = signingConfigs.getByName("debug") + + matchingFallbacks += listOf("release") + } } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 @@ -95,7 +108,7 @@ dependencies { implementation("androidx.paging:paging-runtime:3.4.2") implementation("androidx.paging:paging-compose:3.4.2") - implementation("androidx.datastore:datastore-preferences:1.2.1") + implementation(libs.androidx.datastore.preferences) implementation(libs.icons.material.symbols.outlined.android) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e16262c..72d3e33 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,6 +13,7 @@ android:supportsRtl="true" android:theme="@style/Theme.TripMoney"> diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/MainActivity.kt b/app/src/main/java/cc/n0th1ng/tripmoney/MainActivity.kt index a344b1b..6d6f4cc 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/MainActivity.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/MainActivity.kt @@ -19,16 +19,12 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController -import androidx.navigation.navArgument -import androidx.paging.compose.collectAsLazyPagingItems +import cc.n0th1ng.tripmoney.data.entity.Category import cc.n0th1ng.tripmoney.data.entity.Trip import cc.n0th1ng.tripmoney.navigation.BottomNavigation import cc.n0th1ng.tripmoney.navigation.CustomNavigationDrawer @@ -45,9 +41,7 @@ import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel import cc.n0th1ng.tripmoney.viewmodel.TripViewModel import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking @AndroidEntryPoint class MainActivity : ComponentActivity() { @@ -69,6 +63,8 @@ class MainActivity : ComponentActivity() { fun NavigationDrawer() { val settingsViewModel: SettingsViewModel = hiltViewModel() val tripViewModel: TripViewModel = hiltViewModel() + val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel() + val categories by expenseAndCategoryViewModel.getCategories().collectAsState(emptyList()) val currentTripId by settingsViewModel.currentTrip.collectAsState() val currentTrip by tripViewModel.getTrip(currentTripId).collectAsState(Trip.DUMMY) val navController = rememberNavController() @@ -76,7 +72,8 @@ fun NavigationDrawer() { val current = navBackStack?.destination?.route val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) val scope = rememberCoroutineScope() - var filter by remember { mutableStateOf("") } + var search by remember { mutableStateOf("") } + var filter by remember { mutableStateOf(Filter()) } val autoOpenPref by settingsViewModel.autoOpenStartupPref.collectAsState() var hasHandledStartupOpen by rememberSaveable { mutableStateOf(false) } val shouldTriggerAutoOpen = autoOpenPref == true && !hasHandledStartupOpen @@ -98,7 +95,11 @@ fun NavigationDrawer() { } }, isSearchable = current == Screens.LIST_EXPENSE, - onFilterChange = { newFilter -> filter = newFilter }) + onSearchChange = { newSearch -> search = newSearch }, + onFilterChange = { newFilter -> filter = newFilter }, + categories = categories, + filter = filter + ) }, bottomBar = { BottomNavigation(navController) }) { innerPadding -> @@ -109,7 +110,7 @@ fun NavigationDrawer() { ) { composable(Screens.LIST_EXPENSE) { ListExpenseScreen( - filter, + filter = filter, search = search, initialAutoOpen = shouldTriggerAutoOpen, onAutoOpenConsumed = { hasHandledStartupOpen = true }) } @@ -128,5 +129,25 @@ fun NavigationDrawer() { } } } +} +data class Filter( + val categories: List = emptyList(), val startAmount: Double = 0.0, + val endAmount: Double = Double.MAX_VALUE +) { + fun with(category: Category): Filter { + return this.copy(categories = categories + category) + } + + fun withStartAmount(amount: Double): Filter { + return this.copy(startAmount = amount) + } + + fun withEndAmount(amount: Double): Filter { + return this.copy(endAmount = amount) + } + + fun without(category: Category): Filter { + return this.copy(categories = categories - category) + } } \ No newline at end of file diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/data/dao/ExpenseDao.kt b/app/src/main/java/cc/n0th1ng/tripmoney/data/dao/ExpenseDao.kt index 7485d66..601c5ac 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/data/dao/ExpenseDao.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/data/dao/ExpenseDao.kt @@ -6,55 +6,93 @@ import androidx.room.Delete import androidx.room.Query import androidx.room.Transaction import androidx.room.Upsert -import cc.n0th1ng.tripmoney.data.dto.SummaryPerCategoryRaw +import cc.n0th1ng.tripmoney.data.entity.Category import cc.n0th1ng.tripmoney.data.entity.Expense import cc.n0th1ng.tripmoney.data.entity.ExpenseDto import kotlinx.coroutines.flow.Flow @Dao interface ExpenseDao { + @Upsert suspend fun insert(expense: Expense) @Query( """ - SELECT expense.*, category.* - FROM expense - JOIN category ON expense.category_id = category.id - WHERE expense.trip_id = :tripId - AND - ( - (:filter IS NULL OR category.name LIKE '%' || :filter || '%') - OR (:filter IS NULL OR expense.note LIKE '%' || :filter || '%') + SELECT * FROM expense + JOIN category ON expense.category_id = category.id + WHERE trip_id = :tripId + AND ( + :search IS NULL + OR category.name LIKE '%' || :search || '%' + OR expense.note LIKE '%' || :search || '%' ) - ORDER BY expense.datetime DESC + AND ( + :categoriesEmpty = 1 + OR expense.category_id IN (:categoryIds) + ) + AND ( + :startAmount IS NULL OR expense.amount >= :startAmount + ) + AND ( + :endAmount IS NULL OR expense.amount <= :endAmount + ) + + ORDER BY expense.datetime DESC """ ) - fun expenseDtoPaged(tripId: Int, filter: String): PagingSource + fun expenseDtoPaged( + tripId: Int, + search: String?, + categoryIds: List, + categoriesEmpty: Boolean, + startAmount: Double?, + endAmount: Double? + ): PagingSource @Transaction @Query( - """ - SELECT * FROM expense - JOIN category ON expense.category_id = category.id - WHERE trip_id = :tripId - AND - ( - (:filter IS NULL OR category.name LIKE '%' || :filter || '%') - OR (:filter IS NULL OR expense.note LIKE '%' || :filter || '%') + """ + SELECT * FROM expense + JOIN category ON expense.category_id = category.id + WHERE trip_id = :tripId + AND ( + :search IS NULL + OR category.name LIKE '%' || :search || '%' + OR expense.note LIKE '%' || :search || '%' + ) + AND ( + :categoriesEmpty = 1 + OR expense.category_id IN (:categoryIds) + ) + AND ( + :startAmount IS NULL OR expense.amount >= :startAmount + ) + AND ( + :endAmount IS NULL OR expense.amount <= :endAmount ) - ORDER BY expense.datetime DESC - """ - ) - fun expenseDto(tripId: Int, filter: String): Flow> - @Query(""" + ORDER BY expense.datetime DESC +""" + ) + fun expenseDto( + tripId: Int, + search: String?, + categoryIds: List, + categoriesEmpty: Boolean, + startAmount: Double?, + endAmount: Double? + ): Flow> + + @Query( + """ SELECT trip.budget - IFNULL(SUM(expense.amount * expense.rate), 0) FROM trip LEFT JOIN expense ON expense.trip_id = trip.id WHERE trip.id = :tripId - """) + """ + ) fun budgetLeft(tripId: Int): Double @Delete diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/ExpenseRepository.kt b/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/ExpenseRepository.kt index bf77bb1..5212e00 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/ExpenseRepository.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/data/repository/ExpenseRepository.kt @@ -3,21 +3,16 @@ package cc.n0th1ng.tripmoney.data.repository import android.os.Build import androidx.annotation.RequiresApi import androidx.annotation.WorkerThread -import androidx.lifecycle.viewModelScope import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.PagingData +import cc.n0th1ng.tripmoney.Filter import cc.n0th1ng.tripmoney.data.dao.ExpenseDao -import cc.n0th1ng.tripmoney.data.dto.SummaryPerCategoryRaw import cc.n0th1ng.tripmoney.data.entity.Expense import cc.n0th1ng.tripmoney.data.entity.ExpenseDto import cc.n0th1ng.tripmoney.utils.Currencies -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch import javax.inject.Inject class ExpenseRepository @Inject constructor( @@ -39,15 +34,43 @@ class ExpenseRepository @Inject constructor( expenseDao.delete(expense) } - fun getExpensesDtoPaged(tripId: Int, filter: String): Flow> { + fun getExpensesDtoPaged( + tripId: Int, + search: String, + filter: Filter, + ): Flow> { return Pager( config = PagingConfig(pageSize = 50, enablePlaceholders = false), - pagingSourceFactory = { expenseDao.expenseDtoPaged(tripId, filter) } + pagingSourceFactory = { + val categoryIds = filter.categories.map { it.id } + expenseDao.expenseDtoPaged( + tripId = tripId, + search = search.takeIf { it.isNotBlank() }, + categoryIds = categoryIds, + categoriesEmpty = categoryIds.isEmpty(), + startAmount = filter.startAmount, + endAmount = filter.endAmount + ) + + } ).flow } - fun getExpensesDto(tripId: Int, filter: String = ""): Flow> { - return expenseDao.expenseDto(tripId, filter) + fun getExpensesDto( + tripId: Int, + search: String = "", + filter: Filter = Filter() + ): Flow> { + val categoryIds = filter.categories.map { it.id } + + return expenseDao.expenseDto( + tripId = tripId, + search = search.takeIf { it.isNotBlank() }, + categoryIds = categoryIds, + categoriesEmpty = categoryIds.isEmpty(), + startAmount = filter.startAmount, + endAmount = filter.endAmount + ) } @RequiresApi(Build.VERSION_CODES.O) diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/navigation/TopBar.kt b/app/src/main/java/cc/n0th1ng/tripmoney/navigation/TopBar.kt index dc2ae7c..b864b3f 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/navigation/TopBar.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/navigation/TopBar.kt @@ -2,25 +2,26 @@ package cc.n0th1ng.tripmoney.navigation import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Menu -import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilterChip import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextField -import androidx.compose.material3.TextFieldColors import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable @@ -33,12 +34,17 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController +import cc.n0th1ng.tripmoney.Filter import cc.n0th1ng.tripmoney.R +import cc.n0th1ng.tripmoney.data.entity.Category +import cc.n0th1ng.tripmoney.screens.addexpense.categoriesToPreview import cc.n0th1ng.tripmoney.theme.TripMoneyTheme import cc.n0th1ng.tripmoney.utils.AllPreviews import com.composables.icons.materialsymbols.outlined.R.drawable @@ -50,21 +56,27 @@ fun TopBar( onDrawerClick: () -> Unit, title: String = "", isSearchable: Boolean = false, - onFilterChange: (String) -> Unit + onSearchChange: (String) -> Unit, + filter: Filter, + onFilterChange: (Filter) -> Unit, + categories: List ) { - var isSearch by remember { mutableStateOf(false) } + var showSearch by remember { mutableStateOf(false) } + var showFilter by remember { mutableStateOf(false) } var value by remember { mutableStateOf("") } val focusRequester = remember { FocusRequester() } TopAppBar( title = { - if (isSearch && isSearchable) { + if (showSearch && isSearchable) { LaunchedEffect(Unit) { focusRequester.requestFocus() } OutlinedTextField( textStyle = MaterialTheme.typography.bodyMedium, shape = MaterialTheme.shapes.medium, - modifier = Modifier.fillMaxWidth(0.9f).focusRequester(focusRequester), + modifier = Modifier + .fillMaxWidth(0.9f) + .focusRequester(focusRequester), colors = TextFieldDefaults.colors( focusedContainerColor = Color.Transparent, unfocusedContainerColor = Color.Transparent, @@ -79,9 +91,9 @@ fun TopBar( trailingIcon = { Icon( modifier = Modifier.clickable(onClick = { - isSearch = false + showSearch = false value = "" - onFilterChange("") + onSearchChange("") }), imageVector = Icons.Default.Close, contentDescription = null @@ -90,7 +102,7 @@ fun TopBar( ) LaunchedEffect(key1 = value) { delay(1000) - onFilterChange(value) + onSearchChange(value) } } else { Text(title) @@ -102,7 +114,7 @@ fun TopBar( } }, actions = { - if (!isSearch && isSearchable) { + if (!showSearch && isSearchable) { Row( modifier = Modifier.padding(end = 13.dp), horizontalArrangement = Arrangement.spacedBy(15.dp) @@ -111,14 +123,16 @@ fun TopBar( tint = MaterialTheme.colorScheme.primary, painter = painterResource(drawable.materialsymbols_ic_filter_alt_outlined), contentDescription = null, - modifier = Modifier.clickable(onClick = {}) + modifier = Modifier.clickable(onClick = { + showFilter = true + }) ) Icon( tint = MaterialTheme.colorScheme.primary, painter = painterResource(drawable.materialsymbols_ic_search_outlined), contentDescription = null, modifier = Modifier.clickable(onClick = { - isSearch = true + showSearch = true }) ) @@ -127,13 +141,96 @@ fun TopBar( } ) + + if (showFilter) { + FilterDialog( + onDismiss = { showFilter = false }, + onSave = { newFilter -> + onFilterChange(newFilter) + showFilter = false + }, + categories = categories, + filter = filter + ) + + + } } +@Composable +fun FilterDialog( + onDismiss: () -> Unit, + onSave: (Filter) -> Unit, + categories: List, + filter: Filter +) { + var filter by remember { mutableStateOf(filter) } + var fromAmountString by remember { mutableStateOf(filter.startAmount.toString()) } + var toAmountString by remember { mutableStateOf(filter.endAmount.toString()) } + AlertDialog( + onDismiss, { + Button( + enabled = true, + onClick = { + onSave( + filter.withStartAmount(fromAmountString.safeToDouble()) + .withEndAmount( toAmountString.safeToDouble()) + ) + }) { Text(stringResource(R.string.save)) } + }, title = { Text("Filter") }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + Text(text = "Categories") + FlowRow(horizontalArrangement = Arrangement.spacedBy(10.dp)) { + categories.forEach { + FilterChip(selected = filter.categories.contains(it), onClick = { + filter = if (filter.categories.contains(it)) { + filter.without(it) + } else { + filter.with(it) + } + }, label = { Text(text = it.name) }) + } + } + AmountTextField(label = "from", onValueChange = { newText -> + fromAmountString = newText + }, value = fromAmountString) + AmountTextField(label = "to", onValueChange = { newText -> + toAmountString = newText + }, value = toAmountString) + + } + }) +} + +@Composable +fun AmountTextField(label: String, onValueChange: (String) -> Unit, value: String) { + var value by remember { mutableStateOf(value) } + OutlinedTextField( + label = { Text(label) }, + value = if (value == Double.MAX_VALUE.toString()) "∞" else value, + onValueChange = { newText -> + if (newText == Double.MAX_VALUE.toString()) { + value = "∞" + return@OutlinedTextField + } + val regex = Regex("^\\d*\\.?\\d{0,2}$") + if (regex.matches(newText)) { + value = newText + onValueChange(value) + } + }, + placeholder = { Text("0.00") }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Decimal, + imeAction = ImeAction.Done + ) + ) +} @OptIn(ExperimentalMaterial3Api::class) @Composable fun TopBarSettings(navController: NavHostController) { - TopAppBar( title = { Text(stringResource(R.string.settings)) }, navigationIcon = { @@ -151,7 +248,30 @@ fun PreviewTopBar() { TopBar( onDrawerClick = {}, title = "Essa", - onFilterChange = {} + onSearchChange = {}, + onFilterChange = {}, + isSearchable = true, + categories = categoriesToPreview, + filter = Filter() ) } } + +@AllPreviews +@Composable +fun PreviewFilterDialog() { + TripMoneyTheme { + FilterDialog( + onDismiss = {}, + onSave = {}, + categories = categoriesToPreview, + filter = Filter() + ) + } +} + +private fun String.safeToDouble(): Double { + if(this == "∞") return Double.MAX_VALUE + if(this.isEmpty()) return 0.0 + return this.toDouble() +} \ No newline at end of file diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/ListExpenseScreen.kt b/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/ListExpenseScreen.kt index 75b7431..d98b61e 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/ListExpenseScreen.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/screens/listexpense/ListExpenseScreen.kt @@ -55,6 +55,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.paging.PagingData import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.itemKey +import cc.n0th1ng.tripmoney.Filter import cc.n0th1ng.tripmoney.R.string import cc.n0th1ng.tripmoney.data.entity.Category import cc.n0th1ng.tripmoney.data.entity.Expense @@ -80,16 +81,19 @@ import kotlin.random.Random @RequiresApi(Build.VERSION_CODES.O) @Composable -fun ListExpenseScreen(filter: String, - initialAutoOpen: Boolean, - onAutoOpenConsumed: () -> Unit ) { +fun ListExpenseScreen( + filter: Filter, + search: String, + initialAutoOpen: Boolean, + onAutoOpenConsumed: () -> Unit +) { val settingsViewModel: SettingsViewModel = hiltViewModel() val tripViewModel: TripViewModel = hiltViewModel() val currentTripId by settingsViewModel.currentTrip.collectAsState() val currentTrip by tripViewModel.getTrip(currentTripId).collectAsState(Trip.DUMMY) val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel() val expensesFlow = - expenseAndCategoryViewModel.getExpensesWithHeadersPaged(currentTripId, filter) + expenseAndCategoryViewModel.getExpensesWithHeadersPaged(currentTripId, search, filter) val isRecalculatingRate by tripViewModel.isRecalculating.collectAsState() ListExpenseScreen( diff --git a/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/ExpenseAndCategoryViewModel.kt b/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/ExpenseAndCategoryViewModel.kt index 4b61a8f..3c5fba4 100644 --- a/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/ExpenseAndCategoryViewModel.kt +++ b/app/src/main/java/cc/n0th1ng/tripmoney/viewmodel/ExpenseAndCategoryViewModel.kt @@ -8,6 +8,7 @@ import androidx.paging.PagingData import androidx.paging.cachedIn import androidx.paging.insertSeparators import androidx.paging.map +import cc.n0th1ng.tripmoney.Filter import cc.n0th1ng.tripmoney.data.dto.SummaryPerCategory import cc.n0th1ng.tripmoney.data.entity.Category import cc.n0th1ng.tripmoney.data.entity.Expense @@ -44,16 +45,17 @@ open class ExpenseAndCategoryViewModel @Inject constructor( return expenseRepo.getBudgetLeft(tripId) } - fun getExpensesDtoPaged(tripId: Int, filter: String = ""): Flow> = - expenseRepo.getExpensesDtoPaged(tripId, filter).cachedIn(viewModelScope) + fun getExpensesDtoPaged(tripId: Int, search: String = "", filter: Filter = Filter()): Flow> = + expenseRepo.getExpensesDtoPaged(tripId, search, filter).cachedIn(viewModelScope) @RequiresApi(Build.VERSION_CODES.O) fun getExpensesWithHeadersPaged( tripId: Int, - filter: String = "" + search: String = "", + filter: Filter ): Flow> { - val pagingFlow = getExpensesDtoPaged(tripId, filter) - val sumsFlow = getDailySums(tripId, filter) + val pagingFlow = getExpensesDtoPaged(tripId, search, filter) + val sumsFlow = getDailySums(tripId, search, filter) val tripFlow = tripRepo.getTrip(tripId) return combine(pagingFlow, sumsFlow, tripFlow) { pagingData, sums, trip -> val currency = trip?.currency ?: "" @@ -85,8 +87,8 @@ open class ExpenseAndCategoryViewModel @Inject constructor( }.cachedIn(viewModelScope) } - fun getExpensesDto(tripId: Int, filter: String = ""): Flow> = - expenseRepo.getExpensesDto(tripId, filter) + fun getExpensesDto(tripId: Int, search: String = "", filter: Filter = Filter()): Flow> = + expenseRepo.getExpensesDto(tripId, search, filter) @RequiresApi(Build.VERSION_CODES.O) fun save(expense: Expense, trip: Trip) { @@ -143,8 +145,8 @@ open class ExpenseAndCategoryViewModel @Inject constructor( } @RequiresApi(Build.VERSION_CODES.O) - fun getDailySums(tripId: Int, filter: String): Flow> { - return getExpensesDto(tripId, filter) + fun getDailySums(tripId: Int, search: String, filter: Filter): Flow> { + return getExpensesDto(tripId, search, filter) .map { expenses -> expenses.groupBy { it.expense.datetime.toLocalDate() } .mapValues { (_, list) -> diff --git a/baselineprofile/build.gradle.kts b/baselineprofile/build.gradle.kts index 1f81fc5..bf95b1c 100644 --- a/baselineprofile/build.gradle.kts +++ b/baselineprofile/build.gradle.kts @@ -39,6 +39,7 @@ dependencies { implementation(libs.androidx.espresso.core) implementation(libs.androidx.uiautomator) implementation(libs.androidx.benchmark.macro.junit4) + implementation(libs.androidx.ui.test.junit4) } androidComponents { diff --git a/baselineprofile/src/main/java/cc/n0th1ng/baselineprofile/BaselineProfileGenerator.kt b/baselineprofile/src/main/java/cc/n0th1ng/baselineprofile/BaselineProfileGenerator.kt index 52d45e7..04bc79a 100644 --- a/baselineprofile/src/main/java/cc/n0th1ng/baselineprofile/BaselineProfileGenerator.kt +++ b/baselineprofile/src/main/java/cc/n0th1ng/baselineprofile/BaselineProfileGenerator.kt @@ -3,6 +3,7 @@ package cc.n0th1ng.baselineprofile import android.R.attr.contentDescription import androidx.benchmark.macro.MacrobenchmarkScope import androidx.benchmark.macro.junit4.BaselineProfileRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import androidx.test.platform.app.InstrumentationRegistry @@ -16,7 +17,6 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) @LargeTest class BaselineProfileGenerator { - @get:Rule val rule = BaselineProfileRule() @@ -29,12 +29,8 @@ class BaselineProfileGenerator { pressHome() startActivityAndWait() - - // Give Compose time to render - Thread.sleep(500) - - val listNav = device.wait(Until.findObject(By.desc("listExpenseScreen")), 10000) - listNav?.click() ?: throw RuntimeException("listExpenseScreen not found or not clickable") + device.waitForIdle() + device.wait(Until.hasObject(By.desc("list screen")), 10_000) } } -} \ No newline at end of file +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4c5ec5b..543126a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,6 @@ [versions] agp = "8.13.2" +datastorePreferences = "1.2.1" iconsMaterialSymbolsOutlinedAndroid = "2.2.1" kotlin = "2.2.21" coreKtx = "1.10.1" @@ -15,9 +16,11 @@ uiautomator = "2.3.0" benchmarkMacroJunit4 = "1.2.4" baselineprofile = "1.2.4" profileinstaller = "1.3.1" +uiTestJunit4 = "1.10.6" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } icons-material-symbols-outlined-android = { module = "com.composables:icons-material-symbols-outlined-android", version.ref = "iconsMaterialSymbolsOutlinedAndroid" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } @@ -37,6 +40,7 @@ androidx-compose-foundation-layout = { group = "androidx.compose.foundation", na androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomator", version.ref = "uiautomator" } androidx-benchmark-macro-junit4 = { group = "androidx.benchmark", name = "benchmark-macro-junit4", version.ref = "benchmarkMacroJunit4" } androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "profileinstaller" } +androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4", version.ref = "uiTestJunit4" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }