Home > Blockchain >  Not-blocking loop in Kotlin
Not-blocking loop in Kotlin

Time:08-17

Good morning everyone. I'm beginning to write Kotlin programs, so it's quite obvious there's much I don't know yet. But I hope I may improve in time. At the moment I'm writing a small app that should do two things:

  • play one sound for 2 seconds, then play another sound for 20 seconds.

  • countdown from 22 to 0 while the sounds are played. The countdown alone works. The sounds alone do work, as I wrote this:

      var player1 = MediaPlayer.create(this, com.example.dindon.R.raw.bell)
      var player2 = MediaPlayer.create(this, com.example.dindon.R.raw.pink)
      player1!!.setVolume(1F, 1F)
      player1!!.start()
      var timeNow = LocalDateTime.now()
      while (LocalDateTime.now().minusSeconds(2) < timeNow) {}
      player1!!.stop()
      player1.prepareAsync();
      player2!!.start()
      timeNow = LocalDateTime.now()
      while (LocalDateTime.now().minusSeconds(20) < timeNow) {}
      player2!!.stop()
      player2.prepareAsync();
      player1!!.release()
      player1 = null
      player2!!.release()
      player2 = null
    

The two cycles are needed in order to have the sounds played for the time I need; otherwise I would not hear anything, as the stop instructions immediately follow the start ones.

Problem is, the two cycles are also blocking everything else. Once they start I cannot see the countdown nor other changes in the app. I tried many different solutions, including running two threads, but the while-true cycle still blocks everything else. It even prevents a keyboard listener to execute (I need a listener because I want to input the number of times the two sounds are to be played). Thread.sleep has the same effects. It's like the smartphone freezes each time the while-true cycle is executed, although I know the freezing is not real: the countdown does not really stops, it just display zero once the loop is finished.

I've no idea how to do this, letting the sounds be played till the end without interfering with every other function, especially the countdown. Certainly there's much I need to learn, but after two days of browsing the Internet I could not find a solution that works.

CodePudding user response:

You can use Handler to get notifications when some time has elapsed, something like the following:

val handler: Handler = Handler(Looper.getMainLooper());

var player1 = MediaPlayer.create(this, com.example.dindon.R.raw.bell)
var player2 = MediaPlayer.create(this, com.example.dindon.R.raw.pink)
player1!!.setVolume(1F, 1F)
player1!!.start()

val runnable1 = Runnable { 
    player1!!.stop()
    player2!!.start()
}

val runnable2 = Runnable { 
    player2!!.stop()
    player1!!.release()
    player1 = null
    player2!!.release()
    player2 = null
}

handler.postDelayed(runnable1, 2000)
handler.postDelayed(runnable2, 22000)

CodePudding user response:

Pretty much any app or game with a UI (not just on Android) has something called the render loop that runs on a render thread. It is responsible for redrawing everything to the screen every time anything affects what should be displayed, and for collecting changes in input (screen taps, keypresses, etc.). If you ever block this thread, it prevents anything from being updated and freezes the app. The system clock is still running, but nothing can be drawn to screen and no user input can be collected.

On Android, the render thread is called the Main thread (or UI thread). The vast majority of code in a simple app is run on the main thread. For example, all the lifecycle functions in an Activity or Fragment, like onCreate(), onResume(), etc., are called on the main thread.

Android runs thread loops like the main thread using Handlers. So if you want to do something after a delay, you can post a delayed Runnable to a Main thread Handler, as shown in @Sergio's answer. The Handler will wait the specified delay without blocking the thread and then run the code in the Runnable on the main thread.

An alternative to using Handlers and Runnables is to use coroutines. Coroutines allow you to write code in sequential order that will not block the thread. When you call delay() in a coroutine, it suspends the coroutine for the given amount of time, so it doesn't block the thread. The next part of the coroutine will resume on a future loop of the main thread.

An example of launching a coroutine in a ViewModel below. I used elvis operators to log errors more clearly so it won't need all those assertion operators.

viewModelScope.launch {
  var player1 = MediaPlayer.create(this, com.example.dindon.R.raw.bell)
    ?: run { Log.e(TAG, "Failed to create player1"); return@launch }
  var player2 = MediaPlayer.create(this, com.example.dindon.R.raw.pink)
    ?: run { Log.e(TAG, "Failed to create player2"); return@launch }

  player1.setVolume(1F, 1F)
  player1.start()
  delay(2000)
  player1.stop()
  player1.prepareAsync()
  player2.start()
  delay(20_000)
  player2.stop()
  player2.prepareAsync()
  player1.release()
  player2.release()
}
  • Related