Timer App: CounDownTimer – Android Kotlin Tutorial Part 2

Nessa continuação do nosso tutorial Timer Count Down vamos implementar a contagem dos intervalos (quando houver) entre as sessões de tempo assim como é feito na Técnica Pomodoro. Também usaremos o CountDownTimer nele, além de usarmos AlertDialog para realizar o controle de interrupção tanto dos intervalos quanto do Temporizador.

O que você vai aprender:

  1. Refatoração e “limpeza” de código
  2. Como utilizar AlerDialog
  3. Como criar String Resource
  4. Como retornar um resultado ao termino de outra Activity com a função “onActityResult”
  5. Como usar função “onClick” importada da classe “OnClickListener”
  6. Sobrescrever as funções: “onBackPressed” e “onSupportNavigateUp”
  7. Chamar TextView fora da classe Activity

Refatorar código é reestrutura-lo de uma forma que ele fique mais legível e limpo. Começaremos renomeando as variáveis na classe Timer. Basta clicar com botão direito em cima do nome da variável ir em Refactor -> Rename, dessa forma só precisaremos fazer isso uma vez que se refletirá em todos os lugares que essa variável aparecer:

Vamos colocar colocar os nomes mais adequados as variáveis e também deixar no singular as que tem número único para que quem leia entenda isso de cara!

var txt_breakCount: TextView? = null
var breakCount: Int = 0
var totalBreak: Int? = null
var ACTIVITY_BREAK = 0

Agora vamos criar um método para colocar todo que é necessário passar para quando a Activity iniciar! Basta selecionar todas as linhas de código desejadas, clicar com botão direito Refactor -> Extract -> Function. De o nome para a função de “initTimer” e aperte OK:

Também é importante deixar nosso métodos realizar a função que as descreve e somente ela, por isso precisamos tira da função “getData” todo que não esta relacionado com receber os dados e colocar no método “initTimer” (sobrescreveremos a função “onClick” da classe View.OnClickListener para receber o evento de click):

fun getData(){
        timerLength = intent.getIntExtra("TIME", 1)
        totalBreak = intent.getIntExtra("BREAKS", 0)
}
private fun initTimer() {
        img_play!!.setOnClickListener (this)
        img_replay!!.setOnClickListener(this)

        if (totalBreak  == 0){
            txt_breakCount!!.visibility = View.GONE
        }else{
            txt_breakCount!!.setText(breakCount.toString() + "/" + totalBreak.toString())
        }

        timeLeftInMillis = (timerLength!! * 60000).toLong()

        //start with time and progressbar complete
        setProgressBarValues()
        updateCountDownText()

        startTimer()
    }
    override fun onClick(view: View?) {
        when(view!!.id){
            R.id.img_play -> {
                if (timerStatus == TimerStatus.STARTED) {
                    pauseTimer()
                } else {
                    startTimer()
                }
            }

            R.id.img_replay -> resetTimer()
        }
    }

Bom, nossos intervalos (breaks) também terão contagem de tempo! Pensando nisso, seria bom não termos que repetir códigos que tanto o Timer quanto o Break usará, então vamos criar classes para podermos usa-la nos dois.
Primeiro vamos criar a classe TimerStatus:

enum class TimerStatus {
    STARTED, STOPPED
}

Excluía a classe TimerStatus da class Timer. É agora que poderíamos ter usado ela direto da classe Timer para a Break, mas por convenção de uma estrutura mais limpa a separamos.

Segundo vamos criar a classe FormatText e passar nossa função “updateCountDownText” para ela, excluindo-a do Timer. Nossa classe FormatText precisará pegar o Context e o id do TextView onde será atualizado nosso tempo da Activity que a instanciar, assim como a função precisará de receber o “timeLeftInMillis”:

import android.app.Activity
import android.content.Context
import android.widget.TextView
import java.util.*

class FormatText(context: Context, text_time: Int) {

    var txt_time:TextView = (context as Activity).findViewById(text_time)

    fun updateCountDownText(timeLeftInMillis:Long){
        val minutes: Int = (timeLeftInMillis.toInt() / 1000) / 60
        val seconds: Int = (timeLeftInMillis.toInt() / 1000) % 60
        val timeLeftFormatted: String =
            java.lang.String.format(
                Locale.getDefault(),
                "%02d:%02d", minutes, seconds)
        txt_time.setText(timeLeftFormatted)
    }

}

Sem a função no Timer precisamos criar uma variável do tipo FormatText.

var formatText: FormatText? = null

Inicializa-la e chamar a função “updateCountDownTimer” (substitua em todos os lugares necessários) dela na função “initTimer”:

formatText = FormatText(this, R.id.txt_time_play)
formatText!!.updateCountDownText(timeLeftInMillis)

Vamos seguir as boas praticas e deixar todos os textos do app com strings resource. Abaixo estão todas que usaremos até a parte 2:

<resources>    
    <string name="message_finish_pomodoro">You finish your Pomorodo!</string>
    <string name="title_finish_pomodoro">Congratulations</string>
    <string name="title_atention">Attention</string>
    <string name="action_yes">YES</string>
    <string name="action_no">NO</string>
    <string name="message_finish_break">Are you sure you want to leave your break?</string>
    <string name="message_finis_timer">Are you sure you want to leave your pomodoro?</string>
    <string name="action_ok">OK</string>
    <string name="txt_timer">Timer</string>
    <string name="note_break">It\'s time to take a break. Don\'t forget hydrate!</string>
    <string name="leave_break">Leave Break</string>
    <string name="start_pomodoro">START POMODORO</string>
    <string name="title_breaks">Number of breaks:</string>
    <string name="title_time">Time (minute):</string>
    <string name="title_dialog">Set Time</string>
    <string name="action_start">START</string>
</resources>

Obs.: Na parte 1 desse tutorial não tínhamos colocados os textos de descrições (que não mutáveis) em string resource, mas isso já foi atualizado! Caso você ainda não tenha feito retorne a ele!

Agora que nosso código esta mais “cheiroso” vamos partir para a parte 2!
Começando criando o vector “restore” que indicará que nosso intervalo começou:

Crie o Activity empty Break. E vamos editar o layout activity_break:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical"
    tools:context=".Break">

    <ImageView
        android:layout_width="150dp"
        android:layout_height="150dp"
        android:layout_marginTop="42dp"
        android:src="@drawable/ic_restore"
        android:layout_gravity="center_horizontal"/>

    <TextView
        android:id="@+id/txt_time_break"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_margin="24dp"
        android:text="02:00"
        android:textAlignment="center"
        android:textSize="45sp"
        android:textStyle="bold"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginStart="24dp"
        android:layout_marginEnd="24dp"
        android:text="@string/note_break"
        android:textAlignment="center"
        android:textSize="24sp"/>

    <Button
        android:id="@+id/btn_leave_break"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="18dp"
        android:layout_marginStart="24dp"
        android:layout_marginEnd="24dp"
        android:layout_gravity="center_horizontal"
        android:background="@drawable/bg_btn"
        android:text="@string/leave_break"/>

</LinearLayout>

Agora implementar nossas funções “startBreak” e “pauseBreak”, além do botão “leave break” que usaremos a classe View.OnClickListener para receber o evento de click que criará um AlertDialog perguntando se o usuário realmente deseja interromper a contagem ou não:

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.CountDownTimer
import android.view.View
import android.widget.Button
import androidx.appcompat.app.AlertDialog

class Break : AppCompatActivity(), View.OnClickListener {

    var btn_leave_break: Button? = null
    var formatText: FormatText? = null

    var timerStatus = TimerStatus.STARTED
    lateinit var countDownTimer: CountDownTimer

    var timeLeftInMillis: Long = (5 * 60000).toLong()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_break)

        initBreak()
        startBreak()
    }

    private fun initBreak() {
        btn_leave_break = findViewById(R.id.btn_leave_break)
        btn_leave_break!!.setOnClickListener(this)
        formatText = FormatText(this, R.id.txt_time_break)
        formatText!!.updateCountDownText(timeLeftInMillis)
    }


    override fun onClick(view: View?) {
        when(view!!.id){
            R.id.btn_leave_break -> {
                if (timerStatus == TimerStatus.STARTED){
                    pauseBreak()

                    val builder = AlertDialog.Builder(this)
                    builder.setTitle(getString(R.string.title_atention))
                    builder.setMessage(getString(R.string.message_finish_break))
                    builder.setPositiveButton(getText(R.string.action_yes)){ _, _ -> finish()}
                    builder.setNegativeButton(getString(R.string.action_no)){ _, _ -> startBreak()}

                    val dialog = builder.create()
                    dialog.show()
                }else{
                    finish()
                }
            }
        }
    }

    private fun startBreak() {
        countDownTimer = object  : CountDownTimer(timeLeftInMillis, 1000){

            override fun onTick(millisUntilFinished: Long) {
                timeLeftInMillis = millisUntilFinished
                formatText!!.updateCountDownText(timeLeftInMillis)
            }

            override fun onFinish() {
                finish()
            }
        }.start()
        timerStatus = TimerStatus.STARTED
    }

    fun pauseBreak(){
        countDownTimer.cancel()
        timerStatus = TimerStatus.STOPPED
    }

}

Falta acrescentar algumas funções ainda ao Timer:

  • O “statusBreak” para monitorar quando os intervalos ainda existem para chamar a Activity Break e quando eles acabam para que o Pomodoro termine (daremos o aviso com o AlerDialog). E depois coloca-lo na função onFinish do CountDownTimer;
  • O “onActivityResult” que fará o controle de acrescentar mais “um” a variável “breakCount” sempre que a contagem da Activity Break finalizar ou ser interrompida.
  • O “onBackPressed” que criará um AlertDialog sempre que o usuário apertar o botão voltar no celular perguntando se realmente deseja encerrar todo o Pomodoro.
  • O “onSupportNavigateUp” que fará o mesmo que o “onBackPressed“, mas no botão “Home Up” que vamos inserir no “onCreate” por com o código:
    supportActionBar!!.setDisplayHomeAsUpEnabled(true)
private fun startTimer() {
        countDownTimer = object : CountDownTimer(timeLeftInMillis, 1000){

            override fun onTick(millisUntilFinished: Long) {
                timeLeftInMillis = millisUntilFinished
                img_play!!.setImageResource(R.drawable.ic_pause)
                formatText!!.updateCountDownText(timeLeftInMillis)
                progressbar!!.setProgress((millisUntilFinished / 1000).toInt())
            }

            override fun onFinish() {
                timerStatus = TimerStatus.STOPPED
                resetTimer()
                statusBreak()
            }

        }.start()
        timerStatus = TimerStatus.STARTED
    }
    override fun onBackPressed() {
        pauseTimer()

        val builder = AlertDialog.Builder(this)
        builder.setTitle(getString(R.string.title_atention))
        builder.setMessage(getString(R.string.message_finis_timer))
        builder.setPositiveButton(getString(R.string.action_yes)){ _, _ -> finish()}
        builder.setNegativeButton(getString(R.string.action_no)){ _, _ -> startTimer()}

        val dialog = builder.create()
        dialog.show()
    }
  override fun onSupportNavigateUp(): Boolean {
        onBackPressed()
        return true
    }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        if(requestCode == 0){
            breakCount++
            txt_breakCount!!.text = breakCount.toString() + "/" + totalBreak.toString()
            startTimer()
        }
    }

Assim fica toda nossa classe Timer:

import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.CountDownTimer
import android.view.View
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import androidx.appcompat.app.AlertDialog

class Timer : AppCompatActivity(), View.OnClickListener {

    var txt_breakCount: TextView? = null
    var img_play:ImageView? = null
    var img_replay: ImageView? = null
    var progressbar: ProgressBar? = null
    var timerLength: Int? = null
    var breakCount: Int = 0
    var totalBreak: Int? = null
    var formatText: FormatText? = null
    var ACTIVITY_BREAK = 0

    //CountDownTimer Control

    //When start activity start playing
    private var timerStatus = TimerStatus.STARTED

    var timeLeftInMillis:Long = 0
    lateinit var countDownTimer: CountDownTimer


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_timer)
        supportActionBar!!.setDisplayHomeAsUpEnabled(true)

        init()
        getData()
        initTimer()

    }


    fun init(){

        txt_breakCount = findViewById(R.id.txt_total_breaks)
        progressbar = findViewById(R.id.progressBar)
        img_play = findViewById(R.id.img_play)
        img_replay = findViewById(R.id.img_replay)

    }


    fun getData(){

        timerLength = intent.getIntExtra("TIME", 1)
        totalBreak = intent.getIntExtra("BREAKS", 0)

    }

    private fun initTimer() {
        img_play!!.setOnClickListener (this)
        img_replay!!.setOnClickListener(this)

        if (totalBreak  == 0){
            txt_breakCount!!.visibility = View.GONE
        }else{
            txt_breakCount!!.setText(breakCount.toString() + "/" + totalBreak.toString())
        }

        timeLeftInMillis = (timerLength!! * 60000).toLong()


        //start with time and progressbar complete
        setProgressBarValues()
        formatText = FormatText(this, R.id.txt_time_play)
        formatText!!.updateCountDownText(timeLeftInMillis)

        startTimer()
    }

    override fun onClick(view: View?) {
        when(view!!.id){
            R.id.img_play -> {
                if (timerStatus == TimerStatus.STARTED) {
                    pauseTimer()
                } else {
                    startTimer()
                }
            }

            R.id.img_replay -> resetTimer()
        }
    }


    private fun setProgressBarValues(){
        progressbar!!.setMax(timeLeftInMillis.toInt() / 1000)
        progressbar!!.setProgress(timeLeftInMillis.toInt() / 1000)
    }

    fun statusBreak(){
        if(breakCount == totalBreak){

            val builder = AlertDialog.Builder(this)
            builder.setTitle(getString(R.string.title_finish_pomodoro))
            builder.setMessage(getString(R.string.message_finish_pomodoro))
            builder.setPositiveButton(getString(R.string.action_ok)){ _, _ -> finish()}

            val dialog = builder.create()
            dialog.show()
        }else{
            val intent = Intent(this@Timer, Break::class.java)
            startActivityForResult(intent, ACTIVITY_BREAK)
        }
    }

    private fun startTimer() {
        countDownTimer = object : CountDownTimer(timeLeftInMillis, 1000){

            override fun onTick(millisUntilFinished: Long) {
                timeLeftInMillis = millisUntilFinished
                img_play!!.setImageResource(R.drawable.ic_pause)
                formatText!!.updateCountDownText(timeLeftInMillis)
                progressbar!!.setProgress((millisUntilFinished / 1000).toInt())
            }

            override fun onFinish() {
                timerStatus = TimerStatus.STOPPED
                resetTimer()
                statusBreak()
            }

        }.start()
        timerStatus = TimerStatus.STARTED
    }

    private fun pauseTimer(){
        countDownTimer.cancel()
        timerStatus = TimerStatus.STOPPED
        img_play!!.setImageResource(R.drawable.ic_play)
    }

    private fun resetTimer(){
        pauseTimer()
        timeLeftInMillis = (timerLength!! * 60000).toLong()
        setProgressBarValues()
        formatText!!.updateCountDownText(timeLeftInMillis)
    }

    override fun onSupportNavigateUp(): Boolean {
        onBackPressed()
        return true
    }

    override fun onBackPressed() {
        pauseTimer()

        val builder = AlertDialog.Builder(this)
        builder.setTitle(getString(R.string.title_atention))
        builder.setMessage(getString(R.string.message_finis_timer))
        builder.setPositiveButton(getString(R.string.action_yes)){ _, _ -> finish()}
        builder.setNegativeButton(getString(R.string.action_no)){ _, _ -> startTimer()}

        val dialog = builder.create()
        dialog.show()
    }


    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        if(requestCode == ACTIVITY_BREAK){
            breakCount++
            txt_breakCount!!.text = breakCount.toString() + "/" + totalBreak.toString()
            startTimer()
        }
    }

}

Nosso app estilo Pomodoro esta pronto!!! Para ficar igual a técnica basta colocar o seekbar de tempo com o minimo de 25 (para iniciar o Timer com no minimo 25 minutos)!
Obrigada por acompanhar o conteúdo e caso tenha dúvidas ainda você pode assistir os vídeos com passo-a-passo dos tutoriais no nosso canal Dev Mundi.

O vídeo da parte 2 se encontra abaixo:

Video Tutorial Timer app CountDownTimer Android – Parte 2

Deixe um comentário