53

I want to create a simple countdown for my game, when the game starts I want this function to be called every second:

fun minusOneSecond(){
  if secondsLeft > 0{
     secondsLeft -= 1
     seconds_thegame.text = secondsLeft.toString()
  }
}

I tried this:

var secondsLeft = 15

timer.scheduleAtFixedRate(
   object : TimerTask() {

      override fun run() {
         minusOneSecond()
      }

    },0, 1000
)   // 1000 Millisecond  = 1 second

But the app unfortunately stops, the 2nd time the run function is called

I just started with android development and Kotlin 3 weeks ago and so far I understand the most out of it.

With swift in Xcode I use this line and I thought something similar would work with Kotlin

setTimer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(minusOneSecond), userInfo: nil, repeats: true)
Alex Karapanos
  • 895
  • 2
  • 7
  • 20
  • Possible duplicate of [How to call a function after delay in Kotlin?](https://stackoverflow.com/questions/43348623/how-to-call-a-function-after-delay-in-kotlin) – nologin Apr 08 '19 at 10:14
  • The runOnUiThread is necessary because the UI can only be manipulated from a UI thread, and the callback happens in a temporary background thread. – Eugene Petrenko Apr 08 '19 at 10:15
  • What about using an [AsyncTask](https://developer.android.com/reference/android/os/AsyncTask.html#onProgressUpdate(Progress...)) ? With a background task composed of a loop, that wait a sec, then update progress. And a onProgressUpdate() method that update your UI. – vincrichaud Apr 08 '19 at 10:17

9 Answers9

81

Problem: Timer class uses a background thread with a queue to queue and execute all tasks sequentially. From your code, because you update UI (changing TextView content in minusOneSecond function). That why the app throws the following exception and make your app crash.

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

Solution: There are many ways to achieve your task, but I prefer using post() and postDelayed() method from Handler class. Because it's simple and easy to understand.

val mainHandler = Handler(Looper.getMainLooper())

mainHandler.post(object : Runnable {
    override fun run() {
        minusOneSecond()
        mainHandler.postDelayed(this, 1000)
    }
})

Update: From author's comment about how to pause/resume the task from Handler. Here is an example.

class MainActivityKt : AppCompatActivity() {

    lateinit var mainHandler: Handler

    private val updateTextTask = object : Runnable {
        override fun run() {
            minusOneSecond()
            mainHandler.postDelayed(this, 1000)
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // Your logic code
        ...
        mainHandler = Handler(Looper.getMainLooper())
    }

    override fun onPause() {
        super.onPause()
        mainHandler.removeCallbacks(updateTextTask)
    }

    override fun onResume() {
        super.onResume()
        mainHandler.post(updateTextTask)
    }

    fun minusOneSecond() {
        if secondsLeft > 0 {
            secondsLeft -= 1
            seconds_thegame.text = secondsLeft.toString()
        }
    }
}
Son Truong
  • 13,661
  • 5
  • 32
  • 58
21

I am using this code to update a clock every minute

 fixedRateTimer("timer", false, 0L, 60 * 1000) {
     this@FullscreenActivity.runOnUiThread {
         tvTime.text = SimpleDateFormat("dd MMM - HH:mm", Locale.US).format(Date())
     }
 }

so you have to run it with paratemer 1000 instead of 60*1000

gmetax
  • 3,853
  • 2
  • 31
  • 45
9

please use

inline fun Timer.schedule(
    time: Date, 
    period: Long, 
    crossinline action: TimerTask.() -> Unit
): TimerTask

reference: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.concurrent/java.util.-timer/schedule.html

marstran
  • 26,413
  • 5
  • 61
  • 67
Sandy Genedy
  • 186
  • 8
9
val timer = object: CountDownTimer(10000, 1000) {
    override fun onTick(millisUntilFinished: Long) {
        // do something
    }
    override fun onFinish() {
        // do something
    }
}
timer.start()

You can also use CountDownTimer for this purpose. As this takes two parameters (the total time and the interval time)
Plus it also provides an on finish method to perform any task when the total time is finished.

  • This is not good. It limits you to having to know the maximum amount of time in advance and will terminate when that time is reached. – Johann Apr 29 '21 at 16:08
4

I am calling my function every second like this

val handler = Handler()
  handler.postDelayed(object : Runnable {
      override fun run() {
            //Call your function here
            handler.postDelayed(this, 1000)//1 sec delay
        }
}, 0)
Aftab Alam
  • 1,969
  • 17
  • 17
3

My solution

viewModelScope.launch(Dispatchers.IO) {
            while(isActive) {
                when(val response = repository.getApi()) {
                    is NetworkState.Success -> {
                        getAllData.postValue(response.data)
                    }
                    is NetworkState.Error -> this@MainViewModel.isActive = false
                }

                delay(API_CALL_DELAY)
            }
        }
Thiago
  • 12,778
  • 14
  • 93
  • 110
  • 1
    This does not execute every second. It executes every second plus the amount of time to perform the code inside the while statement. – Johann Apr 29 '21 at 16:05
3

if you use any background task or background service try this code

val timer = Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate({
            Log.d("RUNNING ","Thread")
        },0,10,TimeUnit.SECONDS)

if you work with UI thers like update UI layout try this code

val timer = Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate({
            Log.d("RUNNING ","BACKGROUN Thread")
            runOnUiThread {
                Log.d("RUNNING ","Update UI Thread")
                btnUpdate.setText(System.currentTimeMillis().toString())
            }
        },0,1,TimeUnit.SECONDS)
milan pithadia
  • 840
  • 11
  • 16
0

I'm using recursion with Coroutine its very simple

  private fun loop() {
    CoroutineScope(IO).launch {
        delay(5000)
        CoroutineScope(Main).launch {
            ManagerToWorker()
            loop()
        }
    }
}
  • 11
    Wouldn't that grow the stack until overflow ? – Antzi Aug 06 '20 at 07:57
  • 1
    @Antzi it will overflow, but the stack is large enough that the commenter couldn't see it with 5 second delay. – Dominik Murzynowski Jul 07 '21 at 07:32
  • Recursion often leads to unexpected stack overflows. While this solution may "work" in limited situations, it will eventually crash (perhaps after many hours or even days given the 5000ms delay). – Matt Mar 28 '22 at 15:45
  • As I think about this further, this may not technically cause a stack overflow, since each call to CoroutineScope(Main).launch would create a separate runnable. However, it would still lead to a memory leak in most cases, since Stacktrace Recovery would remember each call. (If ManagerToWorker() threw an exception, the stack trace could be enormous!) https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/topics/debugging.md#stacktrace-recovery – Matt Apr 15 '22 at 16:21
0
var isActionAchieved = false
var secondsPassed = 0

fun cDTimer(){
    if (!isActionAchieved && secondsPassed < 10){  // repeat check if Action NOT Achieved for max of 10 seconds 
        Handler(Looper.getMainLooper()).postDelayed({
            repeatThisFunction()
            repeater()
            secondsPassed++
        }, 1000) //one second till next execution
    }
}

fun repeater(){
    cDTimer()
}
Juan José Melero Gómez
  • 2,742
  • 2
  • 19
  • 36
arkfalak
  • 1
  • 1
  • 1
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Nov 18 '21 at 23:28