Implement timer logic
This commit is contained in:
@@ -47,6 +47,7 @@ dependencies {
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.lifecycle.viewmodel)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.ui)
|
||||
|
||||
@@ -4,23 +4,20 @@ import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.activity.viewModels
|
||||
import org.nsh07.pomodoro.ui.AppScreen
|
||||
import org.nsh07.pomodoro.ui.theme.PomodoroTheme
|
||||
import org.nsh07.pomodoro.ui.viewModel.UiViewModel
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
val viewModel: UiViewModel by viewModels<UiViewModel>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
PomodoroTheme {
|
||||
AppScreen()
|
||||
AppScreen(viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,135 +1,21 @@
|
||||
package org.nsh07.pomodoro.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.text.TextAutoSize
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.FilledIconButton
|
||||
import androidx.compose.material3.FilledTonalIconButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||
import androidx.compose.material3.MaterialTheme.typography
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import org.nsh07.pomodoro.R
|
||||
import org.nsh07.pomodoro.ui.theme.AppFonts.interDisplayBlack
|
||||
import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.nsh07.pomodoro.ui.timerScreen.TimerScreen
|
||||
import org.nsh07.pomodoro.ui.viewModel.UiViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun AppScreen(modifier: Modifier = Modifier) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
"Focus",
|
||||
style = TextStyle(
|
||||
fontFamily = interDisplayBlack,
|
||||
fontSize = 32.sp,
|
||||
lineHeight = 32.sp,
|
||||
color = colorScheme.onPrimaryContainer
|
||||
)
|
||||
)
|
||||
},
|
||||
subtitle = {},
|
||||
titleHorizontalAlignment = Alignment.CenterHorizontally
|
||||
)
|
||||
},
|
||||
modifier = modifier.fillMaxSize()
|
||||
) { insets ->
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(insets)
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator(
|
||||
progress = { 0.3f },
|
||||
modifier = Modifier.size(350.dp),
|
||||
strokeWidth = 32.dp,
|
||||
gapSize = 32.dp
|
||||
)
|
||||
Box(Modifier.width(220.dp)) {
|
||||
Text(
|
||||
text = "08:34",
|
||||
style = TextStyle(
|
||||
fontFamily = openRundeClock,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 76.sp,
|
||||
letterSpacing = (-2).sp
|
||||
),
|
||||
maxLines = 1,
|
||||
autoSize = TextAutoSize.StepBased()
|
||||
)
|
||||
}
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
FilledTonalIconButton(
|
||||
onClick = { /*TODO*/ },
|
||||
shapes = IconButtonDefaults.shapes(),
|
||||
modifier = Modifier.size(96.dp)
|
||||
) {
|
||||
Icon(
|
||||
painterResource(R.drawable.restart_large),
|
||||
contentDescription = "Restart"
|
||||
)
|
||||
}
|
||||
FilledIconButton(
|
||||
onClick = { /*TODO*/ },
|
||||
shapes = IconButtonDefaults.shapes(),
|
||||
modifier = Modifier.size(width = 128.dp, height = 96.dp)
|
||||
) {
|
||||
Icon(
|
||||
painterResource(R.drawable.pause_large),
|
||||
contentDescription = "Pause"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
fun AppScreen(
|
||||
viewModel: UiViewModel,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
Spacer(Modifier.height(32.dp))
|
||||
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text("Up next", style = typography.titleSmall)
|
||||
Text(
|
||||
"5:00",
|
||||
style = TextStyle(
|
||||
fontFamily = openRundeClock,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
color = colorScheme.tertiary
|
||||
)
|
||||
)
|
||||
Text("Short break", style = typography.titleMediumEmphasized)
|
||||
}
|
||||
}
|
||||
}
|
||||
TimerScreen(uiState = uiState, resetTimer = viewModel::resetTimer, toggleTimer = viewModel::toggleTimer, modifier = modifier)
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
package org.nsh07.pomodoro.ui.timerScreen
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.FilledIconToggleButton
|
||||
import androidx.compose.material3.FilledTonalIconButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||
import androidx.compose.material3.MaterialTheme.motionScheme
|
||||
import androidx.compose.material3.MaterialTheme.typography
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import org.nsh07.pomodoro.R
|
||||
import org.nsh07.pomodoro.ui.theme.AppFonts.interDisplayBlack
|
||||
import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock
|
||||
import org.nsh07.pomodoro.ui.viewModel.TimerMode
|
||||
import org.nsh07.pomodoro.ui.viewModel.UiState
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun TimerScreen(
|
||||
uiState: UiState,
|
||||
resetTimer: () -> Unit,
|
||||
toggleTimer: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val color by animateColorAsState(
|
||||
if (uiState.timerMode == TimerMode.FOCUS) colorScheme.primary
|
||||
else colorScheme.tertiary,
|
||||
animationSpec = motionScheme.slowEffectsSpec()
|
||||
)
|
||||
val onColor by animateColorAsState(
|
||||
if (uiState.timerMode == TimerMode.FOCUS) colorScheme.onPrimary
|
||||
else colorScheme.onTertiary,
|
||||
animationSpec = motionScheme.slowEffectsSpec()
|
||||
)
|
||||
val colorContainer by animateColorAsState(
|
||||
if (uiState.timerMode == TimerMode.FOCUS) colorScheme.secondaryContainer
|
||||
else colorScheme.tertiaryContainer,
|
||||
animationSpec = motionScheme.slowEffectsSpec()
|
||||
)
|
||||
val onColorContainer by animateColorAsState(
|
||||
if (uiState.timerMode == TimerMode.FOCUS) colorScheme.onPrimaryContainer
|
||||
else colorScheme.onTertiaryContainer,
|
||||
animationSpec = motionScheme.slowEffectsSpec()
|
||||
)
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
when (uiState.timerMode) {
|
||||
TimerMode.FOCUS -> "Focus"
|
||||
TimerMode.SHORT_BREAK -> "Short Break"
|
||||
TimerMode.LONG_BREAK -> "Long Break"
|
||||
},
|
||||
style = TextStyle(
|
||||
fontFamily = interDisplayBlack,
|
||||
fontSize = 32.sp,
|
||||
lineHeight = 32.sp,
|
||||
color = onColorContainer
|
||||
)
|
||||
)
|
||||
},
|
||||
subtitle = {},
|
||||
titleHorizontalAlignment = Alignment.CenterHorizontally
|
||||
)
|
||||
},
|
||||
modifier = modifier.fillMaxSize()
|
||||
) { insets ->
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(insets)
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator(
|
||||
progress = { (uiState.totalTime.toFloat() - uiState.remainingTime) / uiState.totalTime },
|
||||
modifier = Modifier.size(350.dp),
|
||||
color = color,
|
||||
trackColor = colorContainer,
|
||||
strokeWidth = 16.dp,
|
||||
gapSize = 16.dp
|
||||
)
|
||||
// Box {
|
||||
Text(
|
||||
text = uiState.timeStr,
|
||||
style = TextStyle(
|
||||
fontFamily = openRundeClock,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 76.sp,
|
||||
letterSpacing = (-2).sp
|
||||
),
|
||||
maxLines = 1
|
||||
// autoSize = TextAutoSize.StepBased(stepSize = 24.sp)
|
||||
)
|
||||
// }
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
FilledTonalIconButton(
|
||||
onClick = resetTimer,
|
||||
colors = IconButtonDefaults.filledTonalIconButtonColors(containerColor = colorContainer),
|
||||
shapes = IconButtonDefaults.shapes(),
|
||||
modifier = Modifier.size(96.dp)
|
||||
) {
|
||||
Icon(
|
||||
painterResource(R.drawable.restart_large),
|
||||
contentDescription = "Restart"
|
||||
)
|
||||
}
|
||||
FilledIconToggleButton(
|
||||
onCheckedChange = { toggleTimer() },
|
||||
checked = uiState.timerRunning,
|
||||
colors = IconButtonDefaults.filledIconToggleButtonColors(
|
||||
checkedContainerColor = color,
|
||||
checkedContentColor = onColor
|
||||
),
|
||||
shapes = IconButtonDefaults.toggleableShapes(),
|
||||
modifier = Modifier.size(width = 128.dp, height = 96.dp)
|
||||
) {
|
||||
if (uiState.timerRunning) {
|
||||
Icon(
|
||||
painterResource(R.drawable.pause_large),
|
||||
contentDescription = "Pause"
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
painterResource(R.drawable.play_large),
|
||||
contentDescription = "Play"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(32.dp))
|
||||
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text("Up next", style = typography.titleSmall)
|
||||
Text(
|
||||
uiState.nextTimeStr,
|
||||
style = TextStyle(
|
||||
fontFamily = openRundeClock,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
color = if (uiState.nextTimerMode == TimerMode.FOCUS) colorScheme.primary else colorScheme.tertiary
|
||||
)
|
||||
)
|
||||
Text(
|
||||
when (uiState.nextTimerMode) {
|
||||
TimerMode.FOCUS -> "Focus"
|
||||
TimerMode.SHORT_BREAK -> "Short Break"
|
||||
TimerMode.LONG_BREAK -> "Long Break"
|
||||
},
|
||||
style = typography.titleMediumEmphasized
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showSystemUi = true)
|
||||
@Composable
|
||||
fun TimerScreenPreview() {
|
||||
val uiState = UiState(
|
||||
timeStr = "08:34", nextTimeStr = "5:00"
|
||||
)
|
||||
TimerScreen(uiState, {}, {})
|
||||
}
|
||||
15
app/src/main/java/org/nsh07/pomodoro/ui/viewModel/UiState.kt
Normal file
15
app/src/main/java/org/nsh07/pomodoro/ui/viewModel/UiState.kt
Normal file
@@ -0,0 +1,15 @@
|
||||
package org.nsh07.pomodoro.ui.viewModel
|
||||
|
||||
data class UiState(
|
||||
val timerMode: TimerMode = TimerMode.FOCUS,
|
||||
val timeStr: String = "25:00",
|
||||
val totalTime: Int = 25 * 60,
|
||||
val remainingTime: Int = 25 * 60,
|
||||
val timerRunning: Boolean = false,
|
||||
val nextTimerMode: TimerMode = TimerMode.SHORT_BREAK,
|
||||
val nextTimeStr: String = "5:00"
|
||||
)
|
||||
|
||||
enum class TimerMode {
|
||||
FOCUS, SHORT_BREAK, LONG_BREAK
|
||||
}
|
||||
114
app/src/main/java/org/nsh07/pomodoro/ui/viewModel/UiViewModel.kt
Normal file
114
app/src/main/java/org/nsh07/pomodoro/ui/viewModel/UiViewModel.kt
Normal file
@@ -0,0 +1,114 @@
|
||||
package org.nsh07.pomodoro.ui.viewModel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Locale
|
||||
|
||||
class UiViewModel : ViewModel() {
|
||||
val focusTime = 10
|
||||
val shortBreakTime = 5
|
||||
val longBreakTime = 20
|
||||
|
||||
private val _uiState = MutableStateFlow(
|
||||
UiState(
|
||||
totalTime = focusTime,
|
||||
remainingTime = focusTime,
|
||||
timeStr = secondsToStr(focusTime),
|
||||
nextTimeStr = secondsToStr(shortBreakTime)
|
||||
)
|
||||
)
|
||||
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
|
||||
var timerJob: Job? = null
|
||||
|
||||
var time = focusTime
|
||||
var cycles = 0
|
||||
|
||||
fun resetTimer() {
|
||||
time = focusTime
|
||||
cycles = 0
|
||||
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
timerMode = TimerMode.FOCUS,
|
||||
timeStr = secondsToStr(time),
|
||||
totalTime = time,
|
||||
remainingTime = time,
|
||||
nextTimerMode = TimerMode.SHORT_BREAK,
|
||||
nextTimeStr = secondsToStr(shortBreakTime)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleTimer() {
|
||||
if (uiState.value.timerRunning) {
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(timerRunning = false)
|
||||
}
|
||||
timerJob?.cancel()
|
||||
} else {
|
||||
_uiState.update { it.copy(timerRunning = true) }
|
||||
timerJob = viewModelScope.launch {
|
||||
while (true) {
|
||||
if (!uiState.value.timerRunning) break
|
||||
time--;
|
||||
|
||||
if (time < 0) {
|
||||
cycles++;
|
||||
|
||||
if (cycles % 2 == 0) {
|
||||
time = focusTime
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
timerMode = TimerMode.FOCUS,
|
||||
timeStr = secondsToStr(time),
|
||||
totalTime = time,
|
||||
remainingTime = time,
|
||||
nextTimerMode = if (cycles % 6 == 0) TimerMode.LONG_BREAK else TimerMode.SHORT_BREAK,
|
||||
nextTimeStr = if (cycles % 6 == 0) secondsToStr(longBreakTime) else secondsToStr(
|
||||
shortBreakTime
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val long = cycles % 7 == 0
|
||||
time = if (long) longBreakTime else shortBreakTime
|
||||
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
timerMode = if (long) TimerMode.LONG_BREAK else TimerMode.SHORT_BREAK,
|
||||
timeStr = secondsToStr(time),
|
||||
totalTime = time,
|
||||
remainingTime = time,
|
||||
nextTimerMode = TimerMode.FOCUS,
|
||||
nextTimeStr = secondsToStr(focusTime)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
timeStr = secondsToStr(time),
|
||||
remainingTime = time
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun secondsToStr(t: Int): String {
|
||||
val min = t / 60
|
||||
val sec = t % 60
|
||||
return String.format(locale = Locale.getDefault(), "%02d:%02d", min, sec)
|
||||
}
|
||||
}
|
||||
13
app/src/main/res/drawable/play_large.xml
Normal file
13
app/src/main/res/drawable/play_large.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="40dp"
|
||||
android:height="40dp"
|
||||
android:autoMirrored="true"
|
||||
android:tint="#000000"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M301.33,706.67v-458q0,-17.86 12.67,-29.77 12.67,-11.9 30,-11.9 4.67,0 10.33,1.33 5.67,1.34 11.34,5l360.66,229q9.84,6.67 15.09,16 5.25,9.34 5.25,19.83 0,10.49 -5.5,19.66 -5.5,9.18 -14.84,15.18L365.67,742q-5.67,4.33 -11.56,5.67 -5.88,1.33 -10.44,1.33 -17,0 -29.67,-11.9 -12.67,-11.91 -12.67,-30.43Z" />
|
||||
|
||||
</vector>
|
||||
@@ -11,6 +11,7 @@ composeBom = "2025.06.01"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "lifecycleRuntimeKtx" }
|
||||
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" }
|
||||
|
||||
Reference in New Issue
Block a user