init #48
@@ -30,6 +30,19 @@ android {
|
|||||||
"proguard-rules.pro"
|
"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 {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
@@ -95,7 +108,7 @@ dependencies {
|
|||||||
|
|
||||||
implementation("androidx.paging:paging-runtime:3.4.2")
|
implementation("androidx.paging:paging-runtime:3.4.2")
|
||||||
implementation("androidx.paging:paging-compose: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)
|
implementation(libs.icons.material.symbols.outlined.android)
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.TripMoney">
|
android:theme="@style/Theme.TripMoney">
|
||||||
<activity
|
<activity
|
||||||
|
android:screenOrientation="portrait"
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@style/Theme.TripMoney">
|
android:theme="@style/Theme.TripMoney">
|
||||||
|
|||||||
@@ -19,16 +19,12 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
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.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.navigation.NavType
|
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import androidx.navigation.navArgument
|
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||||
import androidx.paging.compose.collectAsLazyPagingItems
|
|
||||||
import cc.n0th1ng.tripmoney.data.entity.Trip
|
import cc.n0th1ng.tripmoney.data.entity.Trip
|
||||||
import cc.n0th1ng.tripmoney.navigation.BottomNavigation
|
import cc.n0th1ng.tripmoney.navigation.BottomNavigation
|
||||||
import cc.n0th1ng.tripmoney.navigation.CustomNavigationDrawer
|
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.SettingsViewModel
|
||||||
import cc.n0th1ng.tripmoney.viewmodel.TripViewModel
|
import cc.n0th1ng.tripmoney.viewmodel.TripViewModel
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
@@ -69,6 +63,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
fun NavigationDrawer() {
|
fun NavigationDrawer() {
|
||||||
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
||||||
val tripViewModel: TripViewModel = hiltViewModel()
|
val tripViewModel: TripViewModel = hiltViewModel()
|
||||||
|
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
|
||||||
|
val categories by expenseAndCategoryViewModel.getCategories().collectAsState(emptyList())
|
||||||
val currentTripId by settingsViewModel.currentTrip.collectAsState()
|
val currentTripId by settingsViewModel.currentTrip.collectAsState()
|
||||||
val currentTrip by tripViewModel.getTrip(currentTripId).collectAsState(Trip.DUMMY)
|
val currentTrip by tripViewModel.getTrip(currentTripId).collectAsState(Trip.DUMMY)
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
@@ -76,7 +72,8 @@ fun NavigationDrawer() {
|
|||||||
val current = navBackStack?.destination?.route
|
val current = navBackStack?.destination?.route
|
||||||
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
||||||
val scope = rememberCoroutineScope()
|
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()
|
val autoOpenPref by settingsViewModel.autoOpenStartupPref.collectAsState()
|
||||||
var hasHandledStartupOpen by rememberSaveable { mutableStateOf(false) }
|
var hasHandledStartupOpen by rememberSaveable { mutableStateOf(false) }
|
||||||
val shouldTriggerAutoOpen = autoOpenPref == true && !hasHandledStartupOpen
|
val shouldTriggerAutoOpen = autoOpenPref == true && !hasHandledStartupOpen
|
||||||
@@ -98,7 +95,11 @@ fun NavigationDrawer() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
isSearchable = current == Screens.LIST_EXPENSE,
|
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 ->
|
bottomBar = { BottomNavigation(navController) }) { innerPadding ->
|
||||||
@@ -109,7 +110,7 @@ fun NavigationDrawer() {
|
|||||||
) {
|
) {
|
||||||
composable(Screens.LIST_EXPENSE) {
|
composable(Screens.LIST_EXPENSE) {
|
||||||
ListExpenseScreen(
|
ListExpenseScreen(
|
||||||
filter,
|
filter = filter, search = search,
|
||||||
initialAutoOpen = shouldTriggerAutoOpen,
|
initialAutoOpen = shouldTriggerAutoOpen,
|
||||||
onAutoOpenConsumed = { hasHandledStartupOpen = true })
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,32 +6,50 @@ import androidx.room.Delete
|
|||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.Transaction
|
import androidx.room.Transaction
|
||||||
import androidx.room.Upsert
|
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.Expense
|
||||||
import cc.n0th1ng.tripmoney.data.entity.ExpenseDto
|
import cc.n0th1ng.tripmoney.data.entity.ExpenseDto
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface ExpenseDao {
|
interface ExpenseDao {
|
||||||
|
|
||||||
@Upsert
|
@Upsert
|
||||||
suspend fun insert(expense: Expense)
|
suspend fun insert(expense: Expense)
|
||||||
|
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""
|
"""
|
||||||
SELECT expense.*, category.*
|
SELECT * FROM expense
|
||||||
FROM expense
|
|
||||||
JOIN category ON expense.category_id = category.id
|
JOIN category ON expense.category_id = category.id
|
||||||
WHERE expense.trip_id = :tripId
|
WHERE trip_id = :tripId
|
||||||
AND
|
AND (
|
||||||
(
|
:search IS NULL
|
||||||
(:filter IS NULL OR category.name LIKE '%' || :filter || '%')
|
OR category.name LIKE '%' || :search || '%'
|
||||||
OR (:filter IS NULL OR expense.note LIKE '%' || :filter || '%')
|
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
|
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
|
@Transaction
|
||||||
@Query(
|
@Query(
|
||||||
@@ -39,22 +57,42 @@ interface ExpenseDao {
|
|||||||
SELECT * FROM expense
|
SELECT * FROM expense
|
||||||
JOIN category ON expense.category_id = category.id
|
JOIN category ON expense.category_id = category.id
|
||||||
WHERE trip_id = :tripId
|
WHERE trip_id = :tripId
|
||||||
AND
|
AND (
|
||||||
(
|
:search IS NULL
|
||||||
(:filter IS NULL OR category.name LIKE '%' || :filter || '%')
|
OR category.name LIKE '%' || :search || '%'
|
||||||
OR (:filter IS NULL OR expense.note LIKE '%' || :filter || '%')
|
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
|
||||||
)
|
)
|
||||||
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)
|
SELECT trip.budget - IFNULL(SUM(expense.amount * expense.rate), 0)
|
||||||
FROM trip
|
FROM trip
|
||||||
LEFT JOIN expense ON expense.trip_id = trip.id
|
LEFT JOIN expense ON expense.trip_id = trip.id
|
||||||
WHERE trip.id = :tripId
|
WHERE trip.id = :tripId
|
||||||
""")
|
"""
|
||||||
|
)
|
||||||
fun budgetLeft(tripId: Int): Double
|
fun budgetLeft(tripId: Int): Double
|
||||||
|
|
||||||
@Delete
|
@Delete
|
||||||
|
|||||||
@@ -3,21 +3,16 @@ package cc.n0th1ng.tripmoney.data.repository
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import androidx.paging.Pager
|
import androidx.paging.Pager
|
||||||
import androidx.paging.PagingConfig
|
import androidx.paging.PagingConfig
|
||||||
import androidx.paging.PagingData
|
import androidx.paging.PagingData
|
||||||
|
import cc.n0th1ng.tripmoney.Filter
|
||||||
import cc.n0th1ng.tripmoney.data.dao.ExpenseDao
|
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.Expense
|
||||||
import cc.n0th1ng.tripmoney.data.entity.ExpenseDto
|
import cc.n0th1ng.tripmoney.data.entity.ExpenseDto
|
||||||
import cc.n0th1ng.tripmoney.utils.Currencies
|
import cc.n0th1ng.tripmoney.utils.Currencies
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ExpenseRepository @Inject constructor(
|
class ExpenseRepository @Inject constructor(
|
||||||
@@ -39,15 +34,43 @@ class ExpenseRepository @Inject constructor(
|
|||||||
expenseDao.delete(expense)
|
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(
|
return Pager(
|
||||||
config = PagingConfig(pageSize = 50, enablePlaceholders = false),
|
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
|
).flow
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getExpensesDto(tripId: Int, filter: String = ""): Flow<List<ExpenseDto>> {
|
fun getExpensesDto(
|
||||||
return expenseDao.expenseDto(tripId, filter)
|
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)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
|||||||
@@ -2,25 +2,26 @@ package cc.n0th1ng.tripmoney.navigation
|
|||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
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.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.Menu
|
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.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.FilterChip
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextField
|
import androidx.compose.material3.TextField
|
||||||
import androidx.compose.material3.TextFieldColors
|
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
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.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
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.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
|
import cc.n0th1ng.tripmoney.Filter
|
||||||
import cc.n0th1ng.tripmoney.R
|
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.theme.TripMoneyTheme
|
||||||
import cc.n0th1ng.tripmoney.utils.AllPreviews
|
import cc.n0th1ng.tripmoney.utils.AllPreviews
|
||||||
import com.composables.icons.materialsymbols.outlined.R.drawable
|
import com.composables.icons.materialsymbols.outlined.R.drawable
|
||||||
@@ -50,21 +56,27 @@ fun TopBar(
|
|||||||
onDrawerClick: () -> Unit,
|
onDrawerClick: () -> Unit,
|
||||||
title: String = "",
|
title: String = "",
|
||||||
isSearchable: Boolean = false,
|
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("") }
|
var value by remember { mutableStateOf("") }
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
if (isSearch && isSearchable) {
|
if (showSearch && isSearchable) {
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
}
|
}
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
textStyle = MaterialTheme.typography.bodyMedium,
|
textStyle = MaterialTheme.typography.bodyMedium,
|
||||||
shape = MaterialTheme.shapes.medium,
|
shape = MaterialTheme.shapes.medium,
|
||||||
modifier = Modifier.fillMaxWidth(0.9f).focusRequester(focusRequester),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(0.9f)
|
||||||
|
.focusRequester(focusRequester),
|
||||||
colors = TextFieldDefaults.colors(
|
colors = TextFieldDefaults.colors(
|
||||||
focusedContainerColor = Color.Transparent,
|
focusedContainerColor = Color.Transparent,
|
||||||
unfocusedContainerColor = Color.Transparent,
|
unfocusedContainerColor = Color.Transparent,
|
||||||
@@ -79,9 +91,9 @@ fun TopBar(
|
|||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier.clickable(onClick = {
|
modifier = Modifier.clickable(onClick = {
|
||||||
isSearch = false
|
showSearch = false
|
||||||
value = ""
|
value = ""
|
||||||
onFilterChange("")
|
onSearchChange("")
|
||||||
}),
|
}),
|
||||||
imageVector = Icons.Default.Close,
|
imageVector = Icons.Default.Close,
|
||||||
contentDescription = null
|
contentDescription = null
|
||||||
@@ -90,7 +102,7 @@ fun TopBar(
|
|||||||
)
|
)
|
||||||
LaunchedEffect(key1 = value) {
|
LaunchedEffect(key1 = value) {
|
||||||
delay(1000)
|
delay(1000)
|
||||||
onFilterChange(value)
|
onSearchChange(value)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Text(title)
|
Text(title)
|
||||||
@@ -102,7 +114,7 @@ fun TopBar(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
if (!isSearch && isSearchable) {
|
if (!showSearch && isSearchable) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.padding(end = 13.dp),
|
modifier = Modifier.padding(end = 13.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(15.dp)
|
horizontalArrangement = Arrangement.spacedBy(15.dp)
|
||||||
@@ -111,14 +123,16 @@ fun TopBar(
|
|||||||
tint = MaterialTheme.colorScheme.primary,
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
painter = painterResource(drawable.materialsymbols_ic_filter_alt_outlined),
|
painter = painterResource(drawable.materialsymbols_ic_filter_alt_outlined),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.clickable(onClick = {})
|
modifier = Modifier.clickable(onClick = {
|
||||||
|
showFilter = true
|
||||||
|
})
|
||||||
)
|
)
|
||||||
Icon(
|
Icon(
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
painter = painterResource(drawable.materialsymbols_ic_search_outlined),
|
painter = painterResource(drawable.materialsymbols_ic_search_outlined),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.clickable(onClick = {
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun TopBarSettings(navController: NavHostController) {
|
fun TopBarSettings(navController: NavHostController) {
|
||||||
|
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text(stringResource(R.string.settings)) },
|
title = { Text(stringResource(R.string.settings)) },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
@@ -151,7 +248,30 @@ fun PreviewTopBar() {
|
|||||||
TopBar(
|
TopBar(
|
||||||
onDrawerClick = {},
|
onDrawerClick = {},
|
||||||
title = "Essa",
|
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()
|
||||||
|
}
|
||||||
@@ -55,6 +55,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|||||||
import androidx.paging.PagingData
|
import androidx.paging.PagingData
|
||||||
import androidx.paging.compose.collectAsLazyPagingItems
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
import androidx.paging.compose.itemKey
|
import androidx.paging.compose.itemKey
|
||||||
|
import cc.n0th1ng.tripmoney.Filter
|
||||||
import cc.n0th1ng.tripmoney.R.string
|
import cc.n0th1ng.tripmoney.R.string
|
||||||
import cc.n0th1ng.tripmoney.data.entity.Category
|
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||||
import cc.n0th1ng.tripmoney.data.entity.Expense
|
import cc.n0th1ng.tripmoney.data.entity.Expense
|
||||||
@@ -80,16 +81,19 @@ import kotlin.random.Random
|
|||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
@Composable
|
@Composable
|
||||||
fun ListExpenseScreen(filter: String,
|
fun ListExpenseScreen(
|
||||||
|
filter: Filter,
|
||||||
|
search: String,
|
||||||
initialAutoOpen: Boolean,
|
initialAutoOpen: Boolean,
|
||||||
onAutoOpenConsumed: () -> Unit ) {
|
onAutoOpenConsumed: () -> Unit
|
||||||
|
) {
|
||||||
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
val settingsViewModel: SettingsViewModel = hiltViewModel()
|
||||||
val tripViewModel: TripViewModel = hiltViewModel()
|
val tripViewModel: TripViewModel = hiltViewModel()
|
||||||
val currentTripId by settingsViewModel.currentTrip.collectAsState()
|
val currentTripId by settingsViewModel.currentTrip.collectAsState()
|
||||||
val currentTrip by tripViewModel.getTrip(currentTripId).collectAsState(Trip.DUMMY)
|
val currentTrip by tripViewModel.getTrip(currentTripId).collectAsState(Trip.DUMMY)
|
||||||
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
|
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
|
||||||
val expensesFlow =
|
val expensesFlow =
|
||||||
expenseAndCategoryViewModel.getExpensesWithHeadersPaged(currentTripId, filter)
|
expenseAndCategoryViewModel.getExpensesWithHeadersPaged(currentTripId, search, filter)
|
||||||
val isRecalculatingRate by tripViewModel.isRecalculating.collectAsState()
|
val isRecalculatingRate by tripViewModel.isRecalculating.collectAsState()
|
||||||
|
|
||||||
ListExpenseScreen(
|
ListExpenseScreen(
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import androidx.paging.PagingData
|
|||||||
import androidx.paging.cachedIn
|
import androidx.paging.cachedIn
|
||||||
import androidx.paging.insertSeparators
|
import androidx.paging.insertSeparators
|
||||||
import androidx.paging.map
|
import androidx.paging.map
|
||||||
|
import cc.n0th1ng.tripmoney.Filter
|
||||||
import cc.n0th1ng.tripmoney.data.dto.SummaryPerCategory
|
import cc.n0th1ng.tripmoney.data.dto.SummaryPerCategory
|
||||||
import cc.n0th1ng.tripmoney.data.entity.Category
|
import cc.n0th1ng.tripmoney.data.entity.Category
|
||||||
import cc.n0th1ng.tripmoney.data.entity.Expense
|
import cc.n0th1ng.tripmoney.data.entity.Expense
|
||||||
@@ -44,16 +45,17 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
|
|||||||
return expenseRepo.getBudgetLeft(tripId)
|
return expenseRepo.getBudgetLeft(tripId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getExpensesDtoPaged(tripId: Int, filter: String = ""): Flow<PagingData<ExpenseDto>> =
|
fun getExpensesDtoPaged(tripId: Int, search: String = "", filter: Filter = Filter()): Flow<PagingData<ExpenseDto>> =
|
||||||
expenseRepo.getExpensesDtoPaged(tripId, filter).cachedIn(viewModelScope)
|
expenseRepo.getExpensesDtoPaged(tripId, search, filter).cachedIn(viewModelScope)
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
fun getExpensesWithHeadersPaged(
|
fun getExpensesWithHeadersPaged(
|
||||||
tripId: Int,
|
tripId: Int,
|
||||||
filter: String = ""
|
search: String = "",
|
||||||
|
filter: Filter
|
||||||
): Flow<PagingData<ExpenseListItemUi>> {
|
): Flow<PagingData<ExpenseListItemUi>> {
|
||||||
val pagingFlow = getExpensesDtoPaged(tripId, filter)
|
val pagingFlow = getExpensesDtoPaged(tripId, search, filter)
|
||||||
val sumsFlow = getDailySums(tripId, filter)
|
val sumsFlow = getDailySums(tripId, search, filter)
|
||||||
val tripFlow = tripRepo.getTrip(tripId)
|
val tripFlow = tripRepo.getTrip(tripId)
|
||||||
return combine(pagingFlow, sumsFlow, tripFlow) { pagingData, sums, trip ->
|
return combine(pagingFlow, sumsFlow, tripFlow) { pagingData, sums, trip ->
|
||||||
val currency = trip?.currency ?: ""
|
val currency = trip?.currency ?: ""
|
||||||
@@ -85,8 +87,8 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
|
|||||||
}.cachedIn(viewModelScope)
|
}.cachedIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getExpensesDto(tripId: Int, filter: String = ""): Flow<List<ExpenseDto>> =
|
fun getExpensesDto(tripId: Int, search: String = "", filter: Filter = Filter()): Flow<List<ExpenseDto>> =
|
||||||
expenseRepo.getExpensesDto(tripId, filter)
|
expenseRepo.getExpensesDto(tripId, search, filter)
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
fun save(expense: Expense, trip: Trip) {
|
fun save(expense: Expense, trip: Trip) {
|
||||||
@@ -143,8 +145,8 @@ open class ExpenseAndCategoryViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
fun getDailySums(tripId: Int, filter: String): Flow<Map<LocalDate, Double>> {
|
fun getDailySums(tripId: Int, search: String, filter: Filter): Flow<Map<LocalDate, Double>> {
|
||||||
return getExpensesDto(tripId, filter)
|
return getExpensesDto(tripId, search, filter)
|
||||||
.map { expenses ->
|
.map { expenses ->
|
||||||
expenses.groupBy { it.expense.datetime.toLocalDate() }
|
expenses.groupBy { it.expense.datetime.toLocalDate() }
|
||||||
.mapValues { (_, list) ->
|
.mapValues { (_, list) ->
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ dependencies {
|
|||||||
implementation(libs.androidx.espresso.core)
|
implementation(libs.androidx.espresso.core)
|
||||||
implementation(libs.androidx.uiautomator)
|
implementation(libs.androidx.uiautomator)
|
||||||
implementation(libs.androidx.benchmark.macro.junit4)
|
implementation(libs.androidx.benchmark.macro.junit4)
|
||||||
|
implementation(libs.androidx.ui.test.junit4)
|
||||||
}
|
}
|
||||||
|
|
||||||
androidComponents {
|
androidComponents {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package cc.n0th1ng.baselineprofile
|
|||||||
import android.R.attr.contentDescription
|
import android.R.attr.contentDescription
|
||||||
import androidx.benchmark.macro.MacrobenchmarkScope
|
import androidx.benchmark.macro.MacrobenchmarkScope
|
||||||
import androidx.benchmark.macro.junit4.BaselineProfileRule
|
import androidx.benchmark.macro.junit4.BaselineProfileRule
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.filters.LargeTest
|
import androidx.test.filters.LargeTest
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
@@ -16,7 +17,6 @@ import org.junit.runner.RunWith
|
|||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@LargeTest
|
@LargeTest
|
||||||
class BaselineProfileGenerator {
|
class BaselineProfileGenerator {
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val rule = BaselineProfileRule()
|
val rule = BaselineProfileRule()
|
||||||
|
|
||||||
@@ -29,12 +29,8 @@ class BaselineProfileGenerator {
|
|||||||
pressHome()
|
pressHome()
|
||||||
startActivityAndWait()
|
startActivityAndWait()
|
||||||
|
|
||||||
|
device.waitForIdle()
|
||||||
// Give Compose time to render
|
device.wait(Until.hasObject(By.desc("list screen")), 10_000)
|
||||||
Thread.sleep(500)
|
|
||||||
|
|
||||||
val listNav = device.wait(Until.findObject(By.desc("listExpenseScreen")), 10000)
|
|
||||||
listNav?.click() ?: throw RuntimeException("listExpenseScreen not found or not clickable")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "8.13.2"
|
agp = "8.13.2"
|
||||||
|
datastorePreferences = "1.2.1"
|
||||||
iconsMaterialSymbolsOutlinedAndroid = "2.2.1"
|
iconsMaterialSymbolsOutlinedAndroid = "2.2.1"
|
||||||
kotlin = "2.2.21"
|
kotlin = "2.2.21"
|
||||||
coreKtx = "1.10.1"
|
coreKtx = "1.10.1"
|
||||||
@@ -15,9 +16,11 @@ uiautomator = "2.3.0"
|
|||||||
benchmarkMacroJunit4 = "1.2.4"
|
benchmarkMacroJunit4 = "1.2.4"
|
||||||
baselineprofile = "1.2.4"
|
baselineprofile = "1.2.4"
|
||||||
profileinstaller = "1.3.1"
|
profileinstaller = "1.3.1"
|
||||||
|
uiTestJunit4 = "1.10.6"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
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" }
|
icons-material-symbols-outlined-android = { module = "com.composables:icons-material-symbols-outlined-android", version.ref = "iconsMaterialSymbolsOutlinedAndroid" }
|
||||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||||
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
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-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-benchmark-macro-junit4 = { group = "androidx.benchmark", name = "benchmark-macro-junit4", version.ref = "benchmarkMacroJunit4" }
|
||||||
androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "profileinstaller" }
|
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]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|||||||
Reference in New Issue
Block a user