user8775221
user8775221

Reputation: 23

start a sequence of timer in kotlin

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:

enter image description here

Upvotes: 0

Views: 776

Answers (1)

Tenfour04
Tenfour04

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

Related Questions