Compare commits

...

7 Commits

Author SHA1 Message Date
Rafal Wisniewski
bfbb1056d7 init 2026-04-30 09:58:14 +02:00
Rafal Wisniewski
43aec61c75 init 2026-04-29 15:59:17 +02:00
Rafal Wisniewski
e6c8cf5cd3 init 2026-04-29 15:58:20 +02:00
Rafal Wisniewski
2ab7ef3f65 init 2026-04-29 15:42:57 +02:00
Rafal Wisniewski
664df1e5a1 init 2026-04-27 19:58:38 +02:00
Rafal Wisniewski
0518da44d7 init 2026-04-27 19:29:19 +02:00
Rafal Wisniewski
795ce9812a init 2026-04-25 13:32:46 +02:00
30 changed files with 9206 additions and 326 deletions

View File

@@ -0,0 +1 @@
-dontobfuscate

View File

@@ -10,7 +10,9 @@ plugins {
android {
namespace = "cc.n0th1ng.tripmoney"
compileSdk = 36
buildFeatures {
buildConfig = true
}
defaultConfig {
applicationId = "cc.n0th1ng.tripmoney"
minSdk = 24
@@ -19,10 +21,16 @@ android {
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments["androidx.compose.ui.test.tagsAsResourceId"] = "true"
}
buildTypes {
debug {
applicationIdSuffix = ".debug"
isDebuggable = true
}
release {
signingConfig = signingConfigs.getByName("debug")
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
@@ -31,25 +39,21 @@ android {
)
}
create("benchmark") {
initWith(getByName("release"))
// 🔑 Critical settings for Macrobenchmark
isDebuggable = false
isMinifyEnabled = false
isShrinkResources = false
// Use release signing if needed (optional for local)
initWith(buildTypes.getByName("release"))
signingConfig = signingConfigs.getByName("debug")
matchingFallbacks += listOf("release")
isDebuggable = false
proguardFiles("baseline-profiles-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "11"
jvmTarget = "17"
freeCompilerArgs = listOf("-XXLanguage:+PropertyParamAnnotationDefaultTargetMode")
}
buildFeatures {
@@ -75,49 +79,35 @@ dependencies {
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
"baselineProfile"(project(":baselineprofile"))
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.ui.test.manifest)
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
val room_version = "2.8.4"
coreLibraryDesugaring(libs.tools.desugar.jdk.libs)
implementation("androidx.room:room-runtime:$room_version")
implementation(libs.androidx.room.runtime)
// If this project uses any Kotlin source, use Kotlin Symbol Processing (KSP)
// See Add the KSP plugin to your project
ksp("androidx.room:room-compiler:$room_version")
ksp(libs.androidx.room.compiler)
// optional - Kotlin Extensions and Coroutines support for Room
implementation("androidx.room:room-ktx:$room_version")
// optional - RxJava2 support for Room
implementation("androidx.room:room-rxjava2:$room_version")
// optional - RxJava3 support for Room
implementation("androidx.room:room-rxjava3:$room_version")
// optional - Guava support for Room, including Optional and ListenableFuture
implementation("androidx.room:room-guava:$room_version")
// optional - Test helpers
testImplementation("androidx.room:room-testing:$room_version")
// optional - Paging 3 Integration
implementation("androidx.room:room-paging:$room_version")
implementation("androidx.paging:paging-runtime:3.4.2")
implementation("androidx.paging:paging-compose:3.4.2")
implementation(libs.androidx.room.ktx)
implementation(libs.androidx.room.rxjava2)
implementation(libs.androidx.room.rxjava3)
implementation(libs.androidx.room.guava)
testImplementation(libs.androidx.room.testing)
implementation(libs.androidx.room.paging)
implementation(libs.androidx.paging.runtime)
implementation(libs.androidx.paging.compose)
implementation(libs.androidx.datastore.preferences)
implementation(libs.icons.material.symbols.outlined.android)
implementation(libs.icons.material.symbols.outlined.filled.android)
implementation("com.google.dagger:hilt-android:2.57.1")
ksp("com.google.dagger:hilt-android-compiler:2.57.1")
implementation("androidx.hilt:hilt-navigation-compose:1.3.0")
implementation(libs.hilt.android)
ksp(libs.hilt.android.compiler)
implementation(libs.androidx.hilt.navigation.compose)
implementation("io.ktor:ktor-client-core:3.4.1")
implementation("io.ktor:ktor-client-okhttp:3.4.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.10.0")
implementation("org.apache.commons:commons-csv:1.5")
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.okhttp)
implementation(libs.kotlinx.serialization.json.jvm)
implementation(libs.commons.csv)
}

View File

@@ -19,3 +19,4 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-dontwarn edu.umd.cs.findbugs.annotations.SuppressFBWarnings

View File

@@ -12,16 +12,22 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.TripMoney">
<profileable
android:shell="true"
tools:targetApi="29" />
<activity
android:screenOrientation="portrait"
android:name=".MainActivity"
android:exported="true"
android:screenOrientation="portrait"
android:theme="@style/Theme.TripMoney">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
@@ -33,5 +39,4 @@
</provider>
</application>
</manifest>

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@ package cc.n0th1ng.tripmoney
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.ReportDrawnWhen
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.annotation.RequiresApi
@@ -11,6 +12,7 @@ import androidx.compose.material3.DrawerValue
import androidx.compose.material3.Scaffold
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -55,6 +57,7 @@ class MainActivity : ComponentActivity() {
NavigationDrawer()
}
}
}
}
@@ -77,7 +80,7 @@ fun NavigationDrawer() {
val autoOpenPref by settingsViewModel.autoOpenStartupPref.collectAsState()
var hasHandledStartupOpen by rememberSaveable { mutableStateOf(false) }
val shouldTriggerAutoOpen = autoOpenPref == true && !hasHandledStartupOpen
ReportDrawnWhen { !categories.isEmpty() }
CustomNavigationDrawer(navController, drawerState) {
Scaffold(
topBar = {
@@ -150,4 +153,8 @@ data class Filter(
fun without(category: Category): Filter {
return this.copy(categories = categories - category)
}
fun isDefault(): Boolean {
return this.categories.isEmpty() && startAmount == 0.0 && endAmount == Double.MAX_VALUE
}
}

View File

@@ -7,6 +7,7 @@ import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import cc.n0th1ng.tripmoney.BuildConfig
import cc.n0th1ng.tripmoney.data.dao.CategoryDao
import cc.n0th1ng.tripmoney.data.dao.ExchangeRateDao
import cc.n0th1ng.tripmoney.data.dao.ExpenseDao
@@ -14,9 +15,7 @@ import cc.n0th1ng.tripmoney.data.dao.TripDao
import cc.n0th1ng.tripmoney.data.entity.Category
import cc.n0th1ng.tripmoney.data.entity.ExchangeRate
import cc.n0th1ng.tripmoney.data.entity.Expense
import cc.n0th1ng.tripmoney.data.entity.ExpenseDto
import cc.n0th1ng.tripmoney.data.entity.Trip
import cc.n0th1ng.tripmoney.screens.listexpense.toEpochMilli
import cc.n0th1ng.tripmoney.utils.Currencies
import cc.n0th1ng.tripmoney.utils.Icons
import cc.n0th1ng.tripmoney.utils.colors
@@ -26,9 +25,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Delay
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.time.Instant
import java.time.LocalDate
@@ -36,11 +33,9 @@ import java.time.LocalDateTime
import java.time.ZoneOffset
import javax.inject.Singleton
import kotlin.random.Random
import kotlin.random.nextInt
@Database(
entities = [Trip::class, Expense::class, Category::class, ExchangeRate::class],
version = 1
entities = [Trip::class, Expense::class, Category::class, ExchangeRate::class], version = 1
)
@TypeConverters(Converters::class)
abstract class TripDatabase : RoomDatabase() {
@@ -62,16 +57,19 @@ object DatabaseModule {
fun provideTripDatabase(
@ApplicationContext context: Context
): TripDatabase {
val db: TripDatabase = Room.inMemoryDatabaseBuilder(
// val db: TripDatabase = Room.databaseBuilder(
// name = "tripmoney_db",
val builder = if (BuildConfig.DEBUG) Room.inMemoryDatabaseBuilder(
context = context, klass = TripDatabase::class.java
) else Room.databaseBuilder(
name = "tripmoney_db",
context = context,
klass = TripDatabase::class.java,
)
.allowMainThreadQueries() // TODO Remove in production!
.fallbackToDestructiveMigration() // TODO Handle schema changes during dev
val db: TripDatabase =
builder.fallbackToDestructiveMigration() // TODO Handle schema changes during dev
.build()
if (BuildConfig.DEBUG) {
CoroutineScope(Dispatchers.IO).launch {
DatabasePrepopulator(
tripDao = db.tripDao(),
@@ -79,6 +77,8 @@ object DatabaseModule {
expenseDao = db.expenseDao()
).prepopulate()
}
}
return db
}
@@ -152,29 +152,46 @@ private class DatabasePrepopulator(
val sampleCategories = listOf(
Category(
name = "Hotel",
icon = Icons.HOTEL,
color = colors.random()
name = "Hotel", icon = Icons.HOTEL, color = colors.random()
),
Category(
name = "Jedzenie",
icon = Icons.RESTAURANT,
color = colors.random()
name = "Jedzenie", icon = Icons.RESTAURANT, color = colors.random()
),
Category(
name = "Transport",
icon = Icons.FLIGHT,
color = colors.random()
name = "Transport", icon = Icons.FLIGHT, color = colors.random()
),
Category(
name = "Rozrywka",
icon = Icons.ATTRACTION,
color = colors.random()
name = "Rozrywka", icon = Icons.ATTRACTION, color = colors.random()
),
Category(
name = "Zakupy",
icon = Icons.GROCERIES,
color = colors.random()
name = "Zakupy", icon = Icons.GROCERIES, color = colors.random()
),
Category(
name = "Zakupy1", icon = Icons.GROCERIES, color = colors.random()
),
Category(
name = "Zakupy2", icon = Icons.GROCERIES, color = colors.random()
),
Category(
name = "Zakupy3", icon = Icons.GROCERIES, color = colors.random()
),
Category(
name = "Zakupy4", icon = Icons.GROCERIES, color = colors.random()
),
Category(
name = "Zakupy5", icon = Icons.GROCERIES, color = colors.random()
),
Category(
name = "Zakupy6", icon = Icons.GROCERIES, color = colors.random()
),
Category(
name = "Zakupy7", icon = Icons.GROCERIES, color = colors.random()
),
Category(
name = "Zakupy8", icon = Icons.GROCERIES, color = colors.random()
),
Category(
name = "Zakupy9", icon = Icons.GROCERIES, color = colors.random()
),
)
@@ -193,15 +210,14 @@ private class DatabasePrepopulator(
val expense = Expense(
categoryId = Random.nextInt(1, 5),
categoryId = Random.nextInt(1, sampleCategories.size),
tripId = 1,
amount = Random.nextDouble(0.1, 300.0),
currency = Currencies.entries.random().name,
note = if (i % 3 == 0) "Some note" else "",
datetime = datetime,
rate = if (Random.nextBoolean()) Random.nextDouble(
0.1,
5.0
0.1, 5.0
) else 1.0
)
expense

View File

@@ -4,9 +4,9 @@ import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Query
import androidx.room.RewriteQueriesToDropUnusedColumns
import androidx.room.Transaction
import androidx.room.Upsert
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
@@ -18,6 +18,8 @@ interface ExpenseDao {
suspend fun insert(expense: Expense)
@Transaction
@RewriteQueriesToDropUnusedColumns
@Query(
"""
SELECT * FROM expense
@@ -87,13 +89,17 @@ interface ExpenseDao {
@Query(
"""
SELECT trip.budget - IFNULL(SUM(expense.amount * expense.rate), 0)
SELECT
CASE
WHEN trip.budget = 0 THEN NULL
ELSE trip.budget - IFNULL(SUM(expense.amount * expense.rate), 0)
END
FROM trip
LEFT JOIN expense ON expense.trip_id = trip.id
WHERE trip.id = :tripId
"""
)
fun budgetLeft(tripId: Int): Double
fun budgetLeft(tripId: Int): Flow<Double?>
@Delete
suspend fun delete(expense: Expense)

View File

@@ -5,6 +5,7 @@ import androidx.room.ColumnInfo
import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
import androidx.room.Relation
import java.time.LocalDateTime
@@ -17,7 +18,8 @@ import java.time.LocalDateTime
childColumns = arrayOf("category_id"),
onUpdate = ForeignKey.CASCADE,
onDelete = ForeignKey.CASCADE
)]
)],
indices = [Index(value = ["category_id"])]
)
@Immutable
data class Expense(

View File

@@ -20,7 +20,7 @@ class ExpenseRepository @Inject constructor(
private val exchangeRateRepository: ExchangeRateRepository
) {
fun getBudgetLeft(tripId: Int): Double {
fun getBudgetLeft(tripId: Int): Flow<Double?> {
return expenseDao.budgetLeft(tripId)
}

View File

@@ -14,6 +14,7 @@ import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon
@@ -21,7 +22,6 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
@@ -38,7 +38,6 @@ 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
@@ -121,7 +120,11 @@ fun TopBar(
) {
Icon(
tint = MaterialTheme.colorScheme.primary,
painter = painterResource(drawable.materialsymbols_ic_filter_alt_outlined),
painter = painterResource(
if (filter.isDefault())
drawable.materialsymbols_ic_filter_alt_outlined
else com.composables.icons.materialsymbols.outlinedfilled.R.drawable.materialsymbols_ic_filter_alt_outlined_filled
),
contentDescription = null,
modifier = Modifier.clickable(onClick = {
showFilter = true
@@ -150,7 +153,11 @@ fun TopBar(
showFilter = false
},
categories = categories,
filter = filter
filter = filter,
onClear = {
onFilterChange(Filter())
showFilter = false
}
)
@@ -161,6 +168,7 @@ fun TopBar(
fun FilterDialog(
onDismiss: () -> Unit,
onSave: (Filter) -> Unit,
onClear: () -> Unit,
categories: List<Category>,
filter: Filter
) {
@@ -168,7 +176,15 @@ fun FilterDialog(
var fromAmountString by remember { mutableStateOf(filter.startAmount.toString()) }
var toAmountString by remember { mutableStateOf(filter.endAmount.toString()) }
AlertDialog(
onDismiss, {
onDismissRequest = onDismiss,
dismissButton = {
Button(
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary),
enabled = true,
onClick = onClear
) { Text(stringResource(R.string.clear)) }
},
confirmButton = {
Button(
enabled = true,
onClick = {
@@ -181,7 +197,7 @@ fun FilterDialog(
text = {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text(text = "Categories")
FlowRow(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
FlowRow(horizontalArrangement = Arrangement.spacedBy(7.dp)) {
categories.forEach {
FilterChip(selected = filter.categories.contains(it), onClick = {
filter = if (filter.categories.contains(it)) {
@@ -189,7 +205,12 @@ fun FilterDialog(
} else {
filter.with(it)
}
}, label = { Text(text = it.name) })
}, label = {
Row(horizontalArrangement = Arrangement.spacedBy(5.dp)) {
Icon(painterResource(it.icon.resource), contentDescription = null)
Text(text = it.name)
}
})
}
}
AmountTextField(label = "from", onValueChange = { newText ->
@@ -265,7 +286,8 @@ fun PreviewFilterDialog() {
onDismiss = {},
onSave = {},
categories = categoriesToPreview,
filter = Filter()
filter = Filter(),
onClear = {}
)
}
}

View File

@@ -487,7 +487,7 @@ fun NumberKeyboard(
onLongClick = onLongBackspaceClick
)
"+", "/", "-", "*" -> KeyboardButton(
"+", "÷", "", "×" -> KeyboardButton(
text = key,
onClick = { onOperatorClick(key) },
modifier = Modifier.weight(1f),
@@ -531,7 +531,7 @@ fun KeyboardButton(
) {
when {
text != null -> Text(
text,
text = text,
style = MaterialTheme.typography.headlineMedium
)
@@ -541,7 +541,7 @@ fun KeyboardButton(
}
val keyboard = listOf(
listOf("+", "-", "*", "/"),
listOf("+", "", "×", "÷"),
listOf("1", "2", "3"),
listOf("4", "5", "6"),
listOf("7", "8", "9"),

View File

@@ -2,6 +2,7 @@ package cc.n0th1ng.tripmoney.screens.listexpense
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerDialog
@@ -20,13 +21,15 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import cc.n0th1ng.tripmoney.R.*
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
import cc.n0th1ng.tripmoney.utils.AllPreviews
import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.ZoneId
import java.util.Calendar
@RequiresApi(Build.VERSION_CODES.O)
@OptIn(ExperimentalMaterial3Api::class)
@@ -38,8 +41,10 @@ fun DateRangePicker(
onConfirm: (LocalDate, LocalDate) -> Unit
) {
val datePickerState =
rememberDateRangePickerState(initialSelectedStartDateMillis = startDate.toEpochMilli(),
initialSelectedEndDateMillis = endDate.toEpochMilli())
rememberDateRangePickerState(
initialSelectedStartDateMillis = startDate.toEpochMilli(),
initialSelectedEndDateMillis = endDate.toEpochMilli()
)
DatePickerDialog(
onDismissRequest = onDismiss,
@@ -64,7 +69,8 @@ fun DateRangePicker(
TextButton(onClick = onDismiss) { Text(stringResource(string.cancel)) }
}
) {
DateRangePicker(state = datePickerState, showModeToggle = false,
DateRangePicker(
state = datePickerState, showModeToggle = false,
title = {})
}
}
@@ -73,16 +79,22 @@ fun DateRangePicker(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DatePicker(
dateTime: LocalDate = LocalDate.now(),
date: LocalDate = LocalDate.now(),
onDismiss: () -> Unit,
onConfirm: (LocalDate) -> Unit
) {
val datePickerState =
rememberDatePickerState(initialSelectedDateMillis = dateTime.toEpochMilli())
rememberDatePickerState(initialSelectedDateMillis = date.toEpochMilli())
DatePickerDialog(
onDismissRequest = onDismiss,
confirmButton = {
Row() {
TextButton(onClick = {
onConfirm(LocalDate.now().minusDays(1))
}) {
Text(stringResource(string.yesterday))
}
TextButton(onClick = {
val selectedMillis = datePickerState.selectedDateMillis
if (selectedMillis != null) {
@@ -94,6 +106,8 @@ fun DatePicker(
}) {
Text("OK")
}
}
},
dismissButton = {
TextButton(onClick = onDismiss) { Text(stringResource(string.cancel)) }
@@ -103,13 +117,17 @@ fun DatePicker(
}
}
@RequiresApi(Build.VERSION_CODES.O)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TimePicker(onDismiss: () -> Unit, onConfirm: (TimePickerState) -> Unit) {
val currentTime = Calendar.getInstance()
fun TimePicker(
onDismiss: () -> Unit,
onConfirm: (TimePickerState) -> Unit,
time: LocalTime = LocalTime.now()
) {
val timePickerState = rememberTimePickerState(
initialHour = currentTime.get(Calendar.HOUR_OF_DAY),
initialMinute = currentTime.get(Calendar.MINUTE),
initialHour = time.hour,
initialMinute = time.minute,
is24Hour = true
)
@@ -141,7 +159,9 @@ fun DateTimePicker(
var date by remember { mutableStateOf(dateTime.toLocalDate()) }
if (showDatePicker) {
DatePicker(onDismiss = { showDatePicker = false }, onConfirm = { newDate ->
DatePicker(
date = dateTime.toLocalDate(),
onDismiss = { showDatePicker = false }, onConfirm = { newDate ->
date = newDate
showDatePicker = false
showTimePicker = true
@@ -157,7 +177,7 @@ fun DateTimePicker(
showDatePicker = true
val newTime = LocalTime.of(timePickerState.hour, timePickerState.minute)
onChange(LocalDateTime.of(date, newTime))
})
}, time = dateTime.toLocalTime())
}
}
@@ -167,4 +187,13 @@ fun LocalDateTime.toEpochMilli(): Long =
@RequiresApi(Build.VERSION_CODES.O)
fun LocalDate.toEpochMilli(): Long =
this.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()
this.atStartOfDay().atZone(ZoneId.of("UTC")).toInstant().toEpochMilli()
@RequiresApi(Build.VERSION_CODES.O)
@AllPreviews
@Composable
fun DatePickerPreview() {
TripMoneyTheme {
DatePicker(LocalDate.now(), {}, {})
}
}

View File

@@ -47,8 +47,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import androidx.core.graphics.toColorInt
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
@@ -143,7 +146,9 @@ fun ListExpenseScreen(
{
Box {
LazyColumn(
modifier = Modifier.fillMaxSize(),
modifier = Modifier.fillMaxSize().semantics {
contentDescription = "expensesList"
},
horizontalAlignment = Alignment.CenterHorizontally,
state = listState
) {
@@ -363,7 +368,7 @@ fun ExpenseCard(
colors = CardDefaults.elevatedCardColors()
.copy(containerColor = MaterialTheme.colorScheme.surfaceContainer),
modifier = Modifier
.fillMaxWidth(0.9f)
.fillMaxWidth(0.95f)
.height(70.dp)
.combinedClickable(
enabled = true,

View File

@@ -1,6 +1,5 @@
package cc.n0th1ng.tripmoney.screens.settings
import android.content.Intent
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
@@ -36,34 +35,23 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.FileProvider
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import cc.n0th1ng.tripmoney.R.*
import cc.n0th1ng.tripmoney.data.entity.Category
import cc.n0th1ng.tripmoney.R.string
import cc.n0th1ng.tripmoney.data.entity.Trip
import cc.n0th1ng.tripmoney.data.repository.AppTheme
import cc.n0th1ng.tripmoney.navigation.Screens
import cc.n0th1ng.tripmoney.screens.listexpense.CategorySelectionDialog
import cc.n0th1ng.tripmoney.screens.listexpense.CurrencySelectionDialog
import cc.n0th1ng.tripmoney.screens.statistics.categories
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
import cc.n0th1ng.tripmoney.utils.AllPreviews
import cc.n0th1ng.tripmoney.utils.Currencies
import cc.n0th1ng.tripmoney.utils.Icons
import cc.n0th1ng.tripmoney.utils.saveCsv
import cc.n0th1ng.tripmoney.utils.shareCsv
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel
import cc.n0th1ng.tripmoney.viewmodel.SettingsViewModel
import cc.n0th1ng.tripmoney.viewmodel.TripViewModel
import com.composables.icons.materialsymbols.outlined.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.nio.file.Files
@RequiresApi(Build.VERSION_CODES.S)
@Composable
@@ -168,7 +156,7 @@ fun SettingsScreen(
iconResource = R.drawable.materialsymbols_ic_label_outlined
)
SettingsListItem(
onClick = onCategoriesClick,
onClick = {},
stringResource(string.add_expense),
supportingText = stringResource(string.add_expense_settings),
iconResource = R.drawable.materialsymbols_ic_payments_outlined,

View File

@@ -13,7 +13,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.Icon
@@ -32,7 +34,6 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.graphics.toColorInt
import androidx.core.graphics.toColorLong
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import cc.n0th1ng.tripmoney.data.dto.SummaryPerCategory
import cc.n0th1ng.tripmoney.data.entity.Category
@@ -59,11 +60,12 @@ fun StatisticsScreen() {
.collectAsState(emptyList())
val summaryAmount by expenseAndCategoryViewModel.getSummaryAmount(currentTripId)
.collectAsState(0.0)
val moneyLeft by expenseAndCategoryViewModel.getBudgetLeft(currentTripId).collectAsState(null)
StatisticsScreen(
summaryPerCategoryList,
summaryAmount,
Currencies.valueOf(currentTrip?.currency ?: Currencies.default().name),
expenseAndCategoryViewModel.getBudgetLeft(currentTripId)
moneyLeft
)
}
@@ -73,7 +75,7 @@ fun StatisticsScreen(
summaryPerCategoryList: List<SummaryPerCategory>,
summaryAmount: Double,
tripCurrency: Currencies,
moneyLeft: Double
moneyLeft: Double?
) {
Column(
modifier = Modifier
@@ -104,7 +106,7 @@ fun StatisticsScreen(
@Composable
fun Summary(
modifier: Modifier = Modifier,
amount: Double,
amount: Double?,
currency: String,
text: String,
icon: Int,
@@ -117,7 +119,8 @@ fun Summary(
) {
Column(
modifier = Modifier.padding(10.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
verticalArrangement = Arrangement.spacedBy(10.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically,
@@ -141,6 +144,7 @@ fun Summary(
}
Text(
if (amount == null) "" else
"%.2f %s".format(amount, currency),
style = MaterialTheme.typography.titleLarge,
)
@@ -156,7 +160,10 @@ fun SummaryPerCategoryCard(summaryPerCategoryList: List<SummaryPerCategory>) {
.copy(containerColor = MaterialTheme.colorScheme.surfaceContainer)
) {
Column(
modifier = Modifier.padding(15.dp),
modifier = Modifier
.padding(15.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(5.dp)
) {
summaryPerCategoryList.forEach {
@@ -204,7 +211,7 @@ fun CategoryCard(modifier: Modifier = Modifier, summaryPerCategory: SummaryPerCa
) {
Box(
modifier = Modifier
.height(40.dp)
.height(30.dp)
.fillMaxWidth(0.12f + (0.90f - 0.12f) * summaryPerCategory.percent)
.clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.primary)
@@ -213,7 +220,7 @@ fun CategoryCard(modifier: Modifier = Modifier, summaryPerCategory: SummaryPerCa
verticalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxSize()
.padding(11.dp)
.padding(vertical = 5.dp, horizontal = 10.dp)
) {
Text(
"%d%%".format((summaryPerCategory.percent * 100).toInt()),

View File

@@ -104,7 +104,7 @@ fun AddTripBottomSheet(
var endDate by remember {
mutableStateOf(
tripToEdit?.startDate ?: LocalDate.now()
tripToEdit?.endDate ?: LocalDate.now()
)
}

View File

@@ -3,7 +3,8 @@ package cc.n0th1ng.tripmoney.utils
enum class Currencies {
PLN,
EUR,
USD;
USD,
RON;
companion object {
fun default(): Currencies {

View File

@@ -30,7 +30,6 @@ import org.apache.commons.csv.CSVPrinter
import java.io.File
import java.time.LocalDate
import javax.inject.Inject
import kotlin.collections.mapValues
@HiltViewModel
@@ -41,7 +40,7 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
private val tripRepo: TripRepository
) : ViewModel() {
fun getBudgetLeft(tripId: Int): Double {
fun getBudgetLeft(tripId: Int): Flow<Double?> {
return expenseRepo.getBudgetLeft(tripId)
}

View File

@@ -39,4 +39,6 @@
<string name="budget">Budget</string>
<string name="money_left">Money left</string>
<string name="add_expense_settings">Open add expense form on startup</string>
<string name="yesterday">Yesterday</string>
<string name="clear">Clear</string>
</resources>

View File

@@ -1,53 +0,0 @@
plugins {
alias(libs.plugins.android.test)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.baselineprofile)
}
android {
namespace = "cc.n0th1ng.baselineprofile"
compileSdk = 36
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
defaultConfig {
minSdk = 28
targetSdk = 36
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
targetProjectPath = ":app"
}
// This is the configuration block for the Baseline Profile plugin.
// You can specify to run the generators on a managed devices or connected devices.
baselineProfile {
useConnectedDevices = true
}
dependencies {
implementation(libs.androidx.junit)
implementation(libs.androidx.espresso.core)
implementation(libs.androidx.uiautomator)
implementation(libs.androidx.benchmark.macro.junit4)
implementation(libs.androidx.ui.test.junit4)
}
androidComponents {
onVariants { v ->
val artifactsLoader = v.artifacts.getBuiltArtifactsLoader()
v.instrumentationRunnerArguments.put(
"targetAppId",
v.testedApks.map { artifactsLoader.load(it)?.applicationId }
)
}
}

View File

@@ -1,36 +0,0 @@
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
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Direction
import androidx.test.uiautomator.Until
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class BaselineProfileGenerator {
@get:Rule
val rule = BaselineProfileRule()
@Test
fun generate() {
rule.collect(
packageName = "cc.n0th1ng.tripmoney",
includeInStartupProfile = true
) {
pressHome()
startActivityAndWait()
device.waitForIdle()
device.wait(Until.hasObject(By.desc("list screen")), 10_000)
}
}
}

View File

@@ -1,76 +0,0 @@
package cc.n0th1ng.baselineprofile
import androidx.benchmark.macro.BaselineProfileMode
import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.StartupTimingMetric
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
/**
* This test class benchmarks the speed of app startup.
* Run this benchmark to verify how effective a Baseline Profile is.
* It does this by comparing [CompilationMode.None], which represents the app with no Baseline
* Profiles optimizations, and [CompilationMode.Partial], which uses Baseline Profiles.
*
* Run this benchmark to see startup measurements and captured system traces for verifying
* the effectiveness of your Baseline Profiles. You can run it directly from Android
* Studio as an instrumentation test, or run all benchmarks for a variant, for example benchmarkRelease,
* with this Gradle task:
* ```
* ./gradlew :baselineprofile:connectedBenchmarkReleaseAndroidTest
* ```
*
* You should run the benchmarks on a physical device, not an Android emulator, because the
* emulator doesn't represent real world performance and shares system resources with its host.
*
* For more information, see the [Macrobenchmark documentation](https://d.android.com/macrobenchmark#create-macrobenchmark)
* and the [instrumentation arguments documentation](https://d.android.com/topic/performance/benchmarking/macrobenchmark-instrumentation-args).
**/
@RunWith(AndroidJUnit4::class)
@LargeTest
class StartupBenchmarks {
@get:Rule
val rule = MacrobenchmarkRule()
@Test
fun startupCompilationNone() =
benchmark(CompilationMode.None())
@Test
fun startupCompilationBaselineProfiles() =
benchmark(CompilationMode.Partial(BaselineProfileMode.Require))
private fun benchmark(compilationMode: CompilationMode) {
// The application id for the running build variant is read from the instrumentation arguments.
rule.measureRepeated(
packageName = InstrumentationRegistry.getArguments().getString("targetAppId")
?: throw Exception("targetAppId not passed as instrumentation runner arg"),
metrics = listOf(StartupTimingMetric()),
compilationMode = compilationMode,
startupMode = StartupMode.COLD,
iterations = 10,
setupBlock = {
pressHome()
},
measureBlock = {
startActivityAndWait()
// TODO Add interactions to wait for when your app is fully drawn.
// The app is fully drawn when Activity.reportFullyDrawn is called.
// For Jetpack Compose, you can use ReportDrawn, ReportDrawnWhen and ReportDrawnAfter
// from the AndroidX Activity library.
// Check the UiAutomator documentation for more information on how to
// interact with the app.
// https://d.android.com/training/testing/other-components/ui-automator
}
)
}
}

View File

@@ -0,0 +1,54 @@
import com.android.build.api.dsl.ManagedVirtualDevice
kotlin {
jvmToolchain(21)
}
plugins {
alias(libs.plugins.android.test)
alias(libs.plugins.kotlin.android)
}
android {
namespace = "cc.n0th1ng.benchmark"
compileSdk = 36
defaultConfig {
minSdk = 24
targetSdk = 36
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments["androidx.compose.ui.test.tagsAsResourceId"] = "true"
}
testOptions.managedDevices.devices {
create<ManagedVirtualDevice>("pixel6Api31") {
device = "Pixel 6"
apiLevel = 31
systemImageSource = "aosp"
}
}
buildTypes {
create("benchmark") {
isDebuggable = true
signingConfig = getByName("debug").signingConfig
matchingFallbacks += listOf("release")
}
}
targetProjectPath = ":app"
experimentalProperties["android.experimental.self-instrumenting"] = true
}
dependencies {
implementation(libs.androidx.junit)
implementation(libs.androidx.espresso.core)
implementation(libs.androidx.uiautomator)
implementation(libs.androidx.benchmark.macro.junit4)
}
androidComponents {
beforeVariants(selector().all()) {
it.enable = it.buildType == "benchmark"
}
}

View File

@@ -0,0 +1,46 @@
package cc.n0th1ng.benchmark
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.benchmark.macro.junit4.BaselineProfileRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Direction
import androidx.test.uiautomator.Until
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4ClassRunner::class)
class BaselineProfileGenerator {
@RequiresApi(Build.VERSION_CODES.P)
@get:Rule
val rule = BaselineProfileRule()
@RequiresApi(Build.VERSION_CODES.P)
@Test
fun startup() = rule.collect(
maxIterations = 1,
packageName = "cc.n0th1ng.tripmoney",
profileBlock = {
device.executeShellCommand(
"rm -rf /data/data/cc.n0th1ng.tripmoney/files/datastore/"
)
startActivityAndWait()
device.wait(Until.hasObject(By.text("Włochy")), 10_000)
device.findObject(By.text("Włochy")).click()
val isVisible = device.wait(Until.hasObject(By.desc("expensesList")), 10_000)
assert(isVisible)
val expensesList = device.findObject(By.desc("expensesList"))
expensesList.setGestureMargin(device.displayWidth / 5)
expensesList.fling(Direction.DOWN)
expensesList.fling(Direction.UP)
expensesList.fling(Direction.DOWN)
expensesList.fling(Direction.UP)
}
)
}

View File

@@ -0,0 +1,43 @@
package cc.n0th1ng.benchmark
import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.StartupTimingMetric
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Until
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
/**
* This is an example startup benchmark.
*
* It navigates to the device's home screen, and launches the default activity.
*
* Before running this benchmark:
* 1) switch your app's active build variant in the Studio (affedaggcts Studio runs only)
* 2) add `<profileable android:shell="true" />` to your app's manifest, within the `<application>` tag
*
* Run this benchmark from Studio to see startup measurements, and captured system traces
* for investigating your app's performance.
*/
@RunWith(AndroidJUnit4::class)
class ExampleStartupBenchmark {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun startup() = benchmarkRule.measureRepeated(
packageName = "cc.n0th1ng.tripmoney",
metrics = listOf(StartupTimingMetric()),
iterations = 5,
startupMode = StartupMode.COLD
) {
pressHome()
startActivityAndWait()
device.wait(Until.hasObject(By.pkg("cc.n0th1ng.tripmoney")), 10_000)
device.waitForIdle()
}
}

View File

@@ -1,27 +1,78 @@
[versions]
agp = "8.13.2"
commonsCsv = "1.14.1"
commonsCsvVersion = "1.14.1"
datastorePreferences = "1.2.1"
desugar_jdk_libsVersion = "2.1.5"
hiltAndroid = "2.59.2"
hiltAndroidCompiler = "2.57.1"
hiltNavigationCompose = "1.3.0"
hiltNavigationComposeVersion = "1.3.0"
iconsMaterialSymbolsOutlinedAndroid = "2.2.1"
kotlin = "2.2.21"
coreKtx = "1.10.1"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
kotlinxSerializationJsonJvm = "1.11.0"
ktorClientCore = "3.4.3"
ktorClientOkhttp = "3.4.3"
lifecycleRuntimeKtx = "2.6.1"
activityCompose = "1.8.0"
composeBom = "2024.09.00"
navigationCompose = "2.9.7"
foundationLayout = "1.10.5"
pagingCompose = "3.4.2"
pagingComposeVersion = "3.4.2"
pagingRuntime = "3.4.2"
roomCompiler = "2.8.4"
roomCompilerVersion = "2.8.4"
roomGuava = "2.8.4"
roomKtx = "2.8.4"
roomPaging = "2.8.4"
roomRuntime = "2.8.4"
roomRxjava2 = "2.8.4"
roomRxjava3 = "2.8.4"
roomTesting = "2.8.4"
uiautomator = "2.3.0"
benchmarkMacroJunit4 = "1.2.4"
baselineprofile = "1.2.4"
profileinstaller = "1.3.1"
uiTestJunit4 = "1.10.6"
benchmarkMacroJunit4 = "1.4.1"
baselineprofile = "1.4.1"
profileinstaller = "1.4.1"
uiTestJunit4 = "1.11.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
#androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
#androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "pagingCompose" }
#androidx-paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "pagingCompose" }
#androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomCompiler" }
#androidx-room-guava = { module = "androidx.room:room-guava", version.ref = "roomCompiler" }
#androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomCompiler" }
#androidx-room-paging = { module = "androidx.room:room-paging", version.ref = "roomCompiler" }
#androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomCompiler" }
#androidx-room-rxjava3 = { module = "androidx.room:room-rxjava3", version.ref = "roomCompiler" }
#androidx-room-rxjava2 = { module = "androidx.room:room-rxjava2", version.ref = "roomCompiler" }
#androidx-room-testing = { module = "androidx.room:room-testing", version.ref = "roomCompiler" }
#commons-csv = { module = "org.apache.commons:commons-csv", version.ref = "commonsCsv" }
#hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" }
#hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroid" }
androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationComposeVersion" }
androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "pagingComposeVersion" }
androidx-paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "pagingRuntime" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomCompilerVersion" }
androidx-room-guava = { module = "androidx.room:room-guava", version.ref = "roomGuava" }
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomKtx" }
androidx-room-paging = { module = "androidx.room:room-paging", version.ref = "roomPaging" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomRuntime" }
androidx-room-rxjava2 = { module = "androidx.room:room-rxjava2", version.ref = "roomRxjava2" }
androidx-room-rxjava3 = { module = "androidx.room:room-rxjava3", version.ref = "roomRxjava3" }
androidx-room-testing = { module = "androidx.room:room-testing", version.ref = "roomTesting" }
commons-csv = { module = "org.apache.commons:commons-csv", version.ref = "commonsCsvVersion" }
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroidCompiler" }
hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroidCompiler" }
icons-material-symbols-outlined-android = { module = "com.composables:icons-material-symbols-outlined-android", version.ref = "iconsMaterialSymbolsOutlinedAndroid" }
icons-material-symbols-outlined-filled-android = { module = "com.composables:icons-material-symbols-outlined-filled-android", version.ref = "iconsMaterialSymbolsOutlinedAndroid" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
@@ -41,6 +92,10 @@ androidx-uiautomator = { group = "androidx.test.uiautomator", name = "uiautomato
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" }
kotlinx-serialization-json-jvm = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-jvm", version.ref = "kotlinxSerializationJsonJvm" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktorClientCore" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktorClientOkhttp" }
tools-desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libsVersion" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }

View File

@@ -21,4 +21,4 @@ dependencyResolutionManagement {
rootProject.name = "tripMoney"
include(":app")
include(":baselineprofile")
include(":benchmark")