This commit is contained in:
Rafal Wisniewski
2026-04-08 13:31:38 +02:00
parent 767d54e8f6
commit 3847e311a5
11 changed files with 308 additions and 85 deletions

View File

@@ -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)

View File

@@ -13,6 +13,7 @@
android:supportsRtl="true"
android:theme="@style/Theme.TripMoney">
<activity
android:screenOrientation="portrait"
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.TripMoney">

View File

@@ -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<Category> = 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)
}
}

View File

@@ -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<Int, ExpenseDto>
fun expenseDtoPaged(
tripId: Int,
search: String?,
categoryIds: List<Int>,
categoriesEmpty: Boolean,
startAmount: Double?,
endAmount: Double?
): PagingSource<Int, ExpenseDto>
@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<List<ExpenseDto>>
@Query("""
ORDER BY expense.datetime DESC
"""
)
fun expenseDto(
tripId: Int,
search: String?,
categoryIds: List<Int>,
categoriesEmpty: Boolean,
startAmount: Double?,
endAmount: Double?
): Flow<List<ExpenseDto>>
@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

View File

@@ -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<PagingData<ExpenseDto>> {
fun getExpensesDtoPaged(
tripId: Int,
search: String,
filter: Filter,
): Flow<PagingData<ExpenseDto>> {
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<List<ExpenseDto>> {
return expenseDao.expenseDto(tripId, filter)
fun getExpensesDto(
tripId: Int,
search: String = "",
filter: Filter = Filter()
): Flow<List<ExpenseDto>> {
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)

View File

@@ -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<Category>
) {
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<Category>,
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()
}

View File

@@ -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(

View File

@@ -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<PagingData<ExpenseDto>> =
expenseRepo.getExpensesDtoPaged(tripId, filter).cachedIn(viewModelScope)
fun getExpensesDtoPaged(tripId: Int, search: String = "", filter: Filter = Filter()): Flow<PagingData<ExpenseDto>> =
expenseRepo.getExpensesDtoPaged(tripId, search, filter).cachedIn(viewModelScope)
@RequiresApi(Build.VERSION_CODES.O)
fun getExpensesWithHeadersPaged(
tripId: Int,
filter: String = ""
search: String = "",
filter: Filter
): Flow<PagingData<ExpenseListItemUi>> {
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<List<ExpenseDto>> =
expenseRepo.getExpensesDto(tripId, filter)
fun getExpensesDto(tripId: Int, search: String = "", filter: Filter = Filter()): Flow<List<ExpenseDto>> =
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<Map<LocalDate, Double>> {
return getExpensesDto(tripId, filter)
fun getDailySums(tripId: Int, search: String, filter: Filter): Flow<Map<LocalDate, Double>> {
return getExpensesDto(tripId, search, filter)
.map { expenses ->
expenses.groupBy { it.expense.datetime.toLocalDate() }
.mapValues { (_, list) ->

View File

@@ -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 {

View File

@@ -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)
}
}
}
}

View File

@@ -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" }