Reputation: 23
I am trying to create a tabata timer. I managed to take the user input from an editText and to start one timer, which represents the preparation time.
When that preparation time is over I want to start the work time and then the resting time. Later on I need to repeat the Worktime and Resttime for x times as the user gives the input. But I am not able to figure it out.
MainActivity.kt:
btn_Start_Timer.setOnClickListener() {
val prepTimeMillis = Integer.parseInt(eT_PrepTime.text.toString().trim()) * 1000L;
val workTimeMillis = Integer.parseInt(eT_PrepTime.text.toString().trim()) * 1000L;
val restTimeMillis = Integer.parseInt(eT_PrepTime.text.toString().trim()) * 1000L;
val numberOfRepetitions = Integer.parseInt(eT_Number_Repetitions.text.toString().trim());
val Timer = object : CountDownTimer(prepTimeMillis, 1000) {
override fun onTick(millisUntilFinished: Long) {
tV_Total_Duration.setText("Preparation 00:00: " + millisUntilFinished / 1000)
}
override fun onFinish() {
tV_Total_Duration.setText("Preparation done!")
}
}
timer.start()
}
XML:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<ScrollView
android:layout_width="0dp"
android:layout_height="0dp"
android:fillViewport="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tV_Workout_Name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:editable="false"
android:text="Name of the Workout"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tV_Total_Repetitions" />
<EditText
android:id="@+id/eT_WorkoutName"
android:layout_width="290dp"
android:layout_height="40dp"
android:layout_marginTop="16dp"
android:ems="10"
android:inputType="textPersonName"
android:text="Name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.495"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tV_Workout_Name" />
<TextView
android:id="@+id/tV_Prepare"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:editable="false"
android:text="Preparation"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/eT_WorkoutName" />
<EditText
android:id="@+id/eT_PrepTime"
android:layout_width="290dp"
android:layout_height="40dp"
android:layout_marginTop="16dp"
android:ems="10"
android:inputType="number"
android:text="0"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tV_Prepare" />
<Button
android:id="@+id/btn_Decrement_PrepTime"
android:layout_width="50dp"
android:layout_height="40dp"
android:layout_marginTop="56dp"
android:text="-"
app:layout_constraintEnd_toStartOf="@+id/eT_PrepTime"
app:layout_constraintTop_toBottomOf="@+id/eT_WorkoutName" />
<Button
android:id="@+id/btn_Increment_PrepTime"
android:layout_width="50dp"
android:layout_height="40dp"
android:layout_marginTop="56dp"
android:text="+"
app:layout_constraintStart_toEndOf="@+id/eT_PrepTime"
app:layout_constraintTop_toBottomOf="@+id/eT_WorkoutName" />
<TextView
android:id="@+id/tV_WorkTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:editable="false"
android:text="Working"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/eT_PrepTime" />
<EditText
android:id="@+id/eT_Work_Time"
android:layout_width="290dp"
android:layout_height="40dp"
android:layout_marginTop="16dp"
android:ems="10"
android:text="0"
android:inputType="number"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tV_WorkTime" />
<Button
android:id="@+id/btn_Decrement_WorkTime"
android:layout_width="50dp"
android:layout_height="40dp"
android:layout_marginTop="56dp"
android:text="-"
app:layout_constraintEnd_toStartOf="@+id/eT_Work_Time"
app:layout_constraintTop_toBottomOf="@+id/btn_Decrement_PrepTime" />
<Button
android:id="@+id/btn_Increment_WorkTime"
android:layout_width="50dp"
android:layout_height="40dp"
android:layout_marginTop="56dp"
android:text="+"
app:layout_constraintStart_toEndOf="@+id/eT_Work_Time"
app:layout_constraintTop_toBottomOf="@+id/btn_Increment_PrepTime" />
<TextView
android:id="@+id/tv_Repetitions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:editable="false"
android:text="Number of Repetitions"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/eT_Rest_Time" />
<Button
android:id="@+id/btn_Decrement_Repetitions"
android:layout_width="50dp"
android:layout_height="40dp"
android:layout_marginTop="56dp"
android:text="-"
app:layout_constraintEnd_toStartOf="@+id/eT_Number_Repetitions"
app:layout_constraintTop_toBottomOf="@+id/btn_Decrement_Rest" />
<Button
android:id="@+id/btn_Increment_Repetitions"
android:layout_width="50dp"
android:layout_height="40dp"
android:layout_marginTop="56dp"
android:text="+"
app:layout_constraintStart_toEndOf="@+id/eT_Number_Repetitions"
app:layout_constraintTop_toBottomOf="@+id/btn_Increment_Rest" />
<EditText
android:id="@+id/eT_Number_Repetitions"
android:layout_width="290dp"
android:layout_height="40dp"
android:layout_marginTop="16dp"
android:ems="10"
android:inputType="number"
android:text="1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.495"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_Repetitions" />
<TextView
android:id="@+id/tV_Rest_Time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:editable="false"
android:text="Resting"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/eT_Work_Time" />
<Button
android:id="@+id/btn_Decrement_Rest"
android:layout_width="50dp"
android:layout_height="40dp"
android:layout_marginTop="56dp"
android:text="-"
app:layout_constraintEnd_toStartOf="@+id/eT_Rest_Time"
app:layout_constraintTop_toBottomOf="@+id/btn_Decrement_WorkTime" />
<EditText
android:id="@+id/eT_Rest_Time"
android:layout_width="290dp"
android:layout_height="40dp"
android:layout_marginTop="16dp"
android:ems="10"
android:text="0"
android:inputType="number"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.495"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tV_Rest_Time" />
<Button
android:id="@+id/btn_Increment_Rest"
android:layout_width="50dp"
android:layout_height="40dp"
android:layout_marginTop="56dp"
android:text="+"
app:layout_constraintStart_toEndOf="@+id/eT_Rest_Time"
app:layout_constraintTop_toBottomOf="@+id/btn_Increment_WorkTime" />
<TextView
android:id="@+id/tV_Total_Duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:editable="false"
android:text="Duration 00:00:00"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tV_Total_Repetitions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:editable="false"
android:text="Repeated for x times"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tV_Total_Duration" />
<Button
android:id="@+id/btn_Start_Timer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="52dp"
android:layout_marginEnd="110dp"
android:layout_marginBottom="52dp"
android:text="Start now"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/eT_Number_Repetitions" />
<Button
android:id="@+id/btn_Stop_Timer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="110dp"
android:layout_marginTop="52dp"
android:layout_marginBottom="52dp"
android:text="STOP"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/eT_Number_Repetitions" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
EDIT:
I managed to import the library.
But I get the following errors now:
Suspend function 'countDown' should be called only from a coroutine or another suspend function
Unresolved reference: lifecycleScope
Cannot inline bytecode built with JVM target 1.8 into bytecode that is being built with JVM target 1.6. Please specify proper 'jvm-target' option
No value passed for parameter 'handler'
Code:
btn_Start_Timer.setOnClickListener() {
val prepTimeMillis = Integer.parseInt(eT_PrepTime.text.toString().trim()) * 1000L;
val workTimeMillis = Integer.parseInt(eT_Work_Time.text.toString().trim()) * 1000L;
val restTimeMillis = Integer.parseInt(eT_Rest_Time.text.toString().trim()) * 1000L;
val numberOfRepetitions = Integer.parseInt(eT_Number_Repetitions.text.toString().trim());
countdownJob = lifecycleScope.launch {
repeat(numberOfRepetitions) {
countDown(prepTimeMillis, 1000L) { millisUntilFinished ->
tV_Total_Duration.setText("Preparation 00:00: " + millisUntilFinished / 1000)
}
countDown(workTimeMillis, 1000L) { millisUntilFinished ->
tV_Total_Duration.setText("Work 00:00: " + millisUntilFinished / 1000)
}
countDown(restTimeMillis, 1000L) { millisUntilFinished ->
tV_Total_Duration.setText("Rest 00:00: " + millisUntilFinished / 1000)
}
}
}
}
btn_Stop_Timer.setOnClickListener(){
}
}
suspend inline fun countDown(millisInFuture: Long, countDownInterval: Long, crossinline onTick: (Long) -> Unit) = withContext(Dispatchers.Main)
{
suspendCancellableCoroutine<Unit>
{ continuation ->
val timer = object: CountDownTimer(millisInFuture, countDownInterval)
{
override fun onTick(millisUntilFinished: Long) = onTick(millisUntilFinished)
override fun onFinish() = continuation.resume(Unit)
}.start()
continuation.invokeOnCancellation()
{
timer.cancel()
}
}
}
EDIT: build.gradle (Module.app):
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 29
buildToolsVersion "29.0.3"
defaultConfig {
applicationId "com.example.instafollow"
minSdkVersion 24
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation("androidx.lifecycle:lifecycle-viewmodel:2.4.0-rc01")
implementation("androidx.lifecycle:lifecycle-livedata:2.4.0-rc01")
implementation("androidx.lifecycle:lifecycle-runtime:2.4.0-rc01")
implementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.4.0-rc01")
annotationProcessor("androidx.lifecycle:lifecycle-compiler:2.4.0-rc01")
implementation("androidx.lifecycle:lifecycle-common-java8:2.4.0-rc01")
implementation("androidx.lifecycle:lifecycle-service:2.4.0-rc01")
implementation("androidx.lifecycle:lifecycle-process:2.4.0-rc01")
implementation("androidx.lifecycle:lifecycle-reactivestreams:2.4.0-rc01")
testImplementation("androidx.arch.core:core-testing:2.1.0")
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
implementation 'com.google.android.material:material:1.4.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
}
build.gradle(project)
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.3.72'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.6.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
EDIT:
I managed to change the target JVM to 1.8, and I have no more errors in my code. But when I try to start the project, I Get a series of errors like this:
Upvotes: 0
Views: 776
Reputation: 93581
The way you would do this with the existing class is to nest another countdown timer inside the onFinish()
of the first one and use that nested one to count down the work time. And then for the rest time you would nest another timer inside the onFinish()
of that timer. But also, since you want to repeat the whole thing, you would have to move this into a separate function so you can call it repeatedly. So that function that starts the three timers would also need a countdown parameter. Very complicated, it would look something like this:
btn_Start_Timer.setOnClickListener() {
val prepTimeMillis = Integer.parseInt(eT_PrepTime.text.toString().trim()) * 1000L;
val workTimeMillis = Integer.parseInt(eT_PrepTime.text.toString().trim()) * 1000L;
val restTimeMillis = Integer.parseInt(eT_PrepTime.text.toString().trim()) * 1000L;
val numberOfRepetitions = Integer.parseInt(eT_Number_Repetitions.text.toString().trim());
doRep(prepTimeMillis, workTimeMillis, restTimeMillis, numberOfRepetitions)
}
private fun doRep(prepTimeMillis: Long, workTimeMillis: Long, restTimeMillis: Long, times: Int) {
object : CountDownTimer(prepTimeMillis, 1000) {
override fun onTick(millisUntilFinished: Long) {
tV_Total_Duration.setText("Preparation 00:00: " + millisUntilFinished / 1000)
}
override fun onFinish() {
object : CountDownTimer(workTimeMillis, 1000) {
override fun onTick(millisUntilFinished: Long) {
tV_Total_Duration.setText("Work 00:00: " + millisUntilFinished / 1000)
}
override fun onFinish() {
object : CountDownTimer(restTimeMillis, 1000) {
override fun onTick(millisUntilFinished: Long) {
tV_Total_Duration.setText("Rest 00:00: " + millisUntilFinished / 1000)
}
override fun onFinish() {
if (times == 1) {
tV_Total_Duration.setText("All done!")
} else {
doRep(prepTimeMillis, workTimeMillis, restTimeMillis, times - 1)
}
}
}.start()
}
}.start()
}
}.start()
}
This is what is nicknamed "callback hell", where you have to nest code deeply and it is hard to follow, so it is a prime candidate for simplifying with coroutines. Here is a suspend function version of CountdownTimer that lets you use it sequentially instead of by nesting code. When you call this function in a coroutine, you pass it the action you would have normally put in the onTick
function. It automatically starts the timer, and then the coroutine suspends until onFinish()
happens, so your coroutine code can be written sequentially. If the coroutine that called it is cancelled, it will cancel the timer so onTick()
stops getting called.
suspend inline fun countDown(
millisInFuture: Long,
countDownInterval: Long,
crossinline onTick: (Long) -> Unit
) = withContext(Dispatchers.Main) {
suspendCancellableCoroutine<Unit>{ continuation ->
val timer = object: CountDownTimer(millisInFuture, countDownInterval) {
override fun onTick(millisUntilFinished: Long) = onTick(millisUntilFinished)
override fun onFinish() = continuation.resume(Unit)
}.start()
continuation.invokeOnCancellation { timer.cancel() }
}
}
I would also create a simple helper function to get your user input from the edit text and avoid code duplication, like this:
fun TextView.inputToInt(): Long = text.toString().trim().toIntOrNull() ?: 0
With these two functions, you can write your code in the button listener sequentially using a launched coroutine:
btn_Start_Timer.setOnClickListener() {
val prepTimeMillis = eT_PrepTime.inputToInt() * 1000L
val workTimeMillis = eT_PrepTime.inputToInt() * 1000L // TODO pick correct ET
val restTimeMillis = eT_PrepTime.inputToInt() * 1000L // TODO pick correct ET
val numberOfRepetitions = eT_Number_Repetitions.inputToInt()
lifecycleScope.launch {
repeat(numberOfRepetitions) {
countDown(prepTimeMillis, 1000L) { millisUntilFinished ->
tV_Total_Duration.setText("Preparation 00:00: " + millisUntilFinished / 1000)
}
countDown(workTimeMillis, 1000L) { millisUntilFinished ->
tV_Total_Duration.setText("Work 00:00: " + millisUntilFinished / 1000)
}
countDown(restTimeMillis, 1000L) { millisUntilFinished ->
tV_Total_Duration.setText("Rest 00:00: " + millisUntilFinished / 1000)
}
}
}
}
Note that your design is susceptible to the timer being reset every time the screen rotates, because the Activity will be destroyed and recreated. I suggest putting the timer inside a ViewModel that updates a LiveData<String>
with a value that can be observed in the Activity to apply to the TextView. But that is a huge topic. You can read up in the documentation about how to use ViewModel and LiveData.
Edit: To support cancellation, you need a property to hold the coroutine job.
private var countdownJob: Job? = null
When you want to cancel a current countdown if one exists, use null-safe cancel:
countdownJob?.cancel()
When you launch the coroutine, assign its Job to this variable:
countdownJob = lifecycleScope.launch {
// ... code from above example
}
Upvotes: 3