I use Handler for creating a timer in a Widget.
I use the recommended constructor, i.e. passing a Looper to it.
private val updateHandler = Handler(Looper.getMainLooper())
#RequiresApi(Build.VERSION_CODES.Q)
private val runnable = Runnable {
updateDisplay()
}
#RequiresApi(Build.VERSION_CODES.Q)
private fun updateDisplay () {
updateHandler?.postDelayed(runnable, TIMER_MS)
// some other code
}
The TIMER MS is set to 3000 ms.
The timer runs fine for a while and execute the given code. However after a random time elapsed the timer stops working and no more execution of the given code happens.
Please advise what the problem could be ond how to fix it.
Alternatively, can I use some other timer? (The timer should go off every few second - this is the reason why I use Handler)
Thank you for any advice in advance
You could always try using a Coroutine for something like this:
class TimedRepeater(var delayMs: Long,
var worker: (() -> Unit)) {
private var timerJob: Job? = null
suspend fun start() {
if (timerJob != null) throw IllegalStateException()
timerJob = launch {
while(isActive) {
delay(delayMs)
worker()
}
}
}
suspend fun stop() {
if (timerJob == null) return
timerJob.cancelAndJoin()
timerJob = null
}
}
suspend fun myStuff() {
val timer = Timer(1000) {
// Do my work
}
timer.start()
// Some time later
timer.stop()
}
I haven't tested the above, but it should work well enough.
You can use CountDownTimer from Android framework to achieve the same. It internally uses Handler for timer
val timer = object: CountDownTimer(1000,1000){
override fun onTick(millisUntilFinished: Long) {
}
override fun onFinish() {
}
}
timer.start()
Related
I want to call my function indefinitely every 1 second in a specific situation. I'm using AsyncTask to execute my API calls.
I'm using this type of call for some time but this is the first time when it actually blocked my UI Thread and I don't know why.
The handler in the code below is called inside onPostExecute.
protected fun purchaseCheck(transactionId: String){
app.sysLog("Wait for purchase...")
task = asyncTask({
api.checkPaymentStatus(transactionId)
}, taskName = "Purchase Status") { r ->
r.js?.let {
when(r.httpCode){
HTTP_PAYMENT_CHECK_PENDING -> {
App.log("purchaseCheck: response pending purchase - try again")
MainActivity.afterDelay(1000){
purchaseCheck(transactionId)
}
}
else -> {
App.log("purchaseCheck: response purchase success")
onPurchaseSuccessfullyCompleted()
}
}
}?:kotlin.run {
when(r.httpCode){
HTTP_PAYMENT_CARD_EXPIRED -> {
App.log("purchaseCheck: response card expired")
showApiErrorAndRetry(r, App.getString("err_purchase_card_expired"))
}
else -> {
App.log("purchaseCheck: response error (retry)")
MainActivity.afterDelay(1000){
purchaseCheck(transactionId)
}
}
}
}
}
}
Basically
MainActivity.afterDelay(1000){
purchaseCheck(transactionId)
}
is causing my ProgressBar animation to freeze. When I remove that delay it is working as intended.
Here is afterDelay function:
fun afterDelay(delay: Int, body: () -> Unit): Cancellable {
class DelayRun : Runnable, Cancellable {
override fun run() = body()
override fun cancel() {
removePost(this)
}
}
return DelayRun().also {
post(delay, it)
}
}
fun removePost(runnable: Runnable) {
App.handler.removeCallbacks(runnable)
}
fun post(delay: Int, runnable: Runnable){
App.handler.postDelayed(runnable, delay.toLong())
}
Handler in Application class:
class App : Application(), Application.ActivityLifecycleCallbacks{
companion object {
val handler = Handler()
}
...
}
Edit:
After suggestion from post below I implemented Handler like this:
class App : Application(), Application.ActivityLifecycleCallbacks{
companion object {
val handler: Handler by lazy {
HandlerThread("MyHandlerThread").let {
it.start()
Handler(it.looper)
}
}
}
...
}
but it is still freezing my UI Thread. (ProgressBar is lagging)
By default, Handler posts tasks on Main (UI) thread. Therefore any job/task you send to your handler will be executed on UI thread - that is the reason why UI freezes - it waits for job to finish before redrawing.
You want to make your handler using another thread. The most simple way is to create HandlerThread.
val handlerThread = new HandlerThread("MyHandlerThread")
handlerThread.start()
val looper = handlerThread.getLooper()
val handler = new Handler(looper)
After these four lines of code, handler will execute it jobs on another thread. But let's take a problem further - you are using AsyncTask, which is deprecated. You also do not want your delay to be counted by afterDelay function, handler can do it for you.
In your case you can just do something like this:
handler.postDelayed(1000, { ... your job ... }).
Getting it together:
protected fun purchaseCheck(transactionId: String){
app.sysLog("Wait for purchase...")
val runnable = {
val status = api.checkPaymentStatus(transactionId)
status.js?.let { ... }
}
handler.postDelayed(1000, runnable)
}
I also recommend you to declare handler on Activity level rather than Application. Since you usually don't want it to be global.
I have created a coroutine extension function to run a coroutine in the background periodically at fixed interval.
/**
* This method will receive a coroutine function block and will handle it according to the provided parameters.
*/
fun CoroutineScope.runWithCoroutineHandler(
intervalMils: Long,
startDelayMils: Long = 0L,
maxDurationMils: Long = 0L,
suspendFunctionBlock: suspend () -> Unit
): Job = launch(this.coroutineContext) {
delay(startDelayMils)
val startTime = System.nanoTime()
do {
suspendFunctionBlock()
delay(intervalMils)
} while ((maxDurationMils == 0L && isActive) || (isActive && ((System.nanoTime() - startTime) / 1000000 < maxDurationMils)))
}
Now, I run a coroutine as following in a repository:
fun initialize() {
externalScope.runWithCoroutineHandler(intervalMils = INTERVAL_MILLIS) {
process()
}
}
The issue now is, how do i correctly stop this coroutine from the background on demand?
I have tried to cancelAndJoin() the coroutine but how can I now refer to the specific coroutine that is running in the background?
fun terminate() {
// TODO how do I cancel the running coroutine?
}
Thanks in advance.
Your function does return a Job, all you have to do is keep that reference and cancel it (unless you're fine with cancelling externalScope but I assume not).
Also your while does not need to check isActive - delay does so internally so your loop won't reach condition check if it gets cancelled.
As #Pawel said - you should keep reference on job you are creating.
Simplest:
class MyExecutor(private val externalScope: CoroutineScope){
private var job: Job? = null
fun initialize() {
job = externalScope.runWithCoroutineHandler(intervalMils = INTERVAL_MILLIS) {
process()
}
}
fun terminate() {
job?.cancelAndJoin()
}
}
In my code, I have a time-out functionality and I want to use a countdown timer but after a lot of research, I couldn't find similar functionality as a countdown timer in Kotlin coroutine (able to start, cancel and catch finish callback). Then I decided to use GlobalScope.launch. I know this is a bad solution but my code is working perfectly.
Here is my code
viewModelScope.launch {
val timer = object: CountDownTimer(Constants.PAYMENT_TIMER, 1000) {
override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() {
GlobalScope.launch {
_eventFlow.emit(UIPaymentEvent.NavigateToBack)
}
}
}
timer.start()
collectPaymentIntentUseCase.invoke(currentPaymentIntent!!).onEach { result ->
when (result) {
is Resource.Success -> {
timer.cancel()
if (result.data?.exception == null) {
My question is how can find a 100% similar function to avoid using GlobalScope but be able to use the countdown timer (start, cancel,onComplete callback)?
Note: I am using GlobalScope.lanch to be able to emit UIPaymentEvent.NavigateToBack event to my view
You don't need a CountDownTimer here. Just use the delay() suspend function.
viewModelScope.launch {
val job = launch {
delay(Constants.PAYMENT_TIMER) // Wait for timeout
_eventFlow.emit(UIPaymentEvent.NavigateToBack)
}
collectPaymentIntentUseCase.invoke(currentPaymentIntent!!).onEach { result ->
when (result) {
is Resource.Success -> {
job.cancel() // Cancel the timer
if (result.data?.exception == null) {
You can use callbackFlow for listen your timer. I just code this editor. I hope it will be helpful.
fun timerFlow() = callbackFlow<UIPaymentEvent> {
val timer = object : CountDownTimer(10, 1000) {
override fun onTick(millisUntilFinished: Long) {}
override fun onFinish() {
CoroutineScope().launch {
_eventFlow.emit(UIPaymentEvent.NavigateToBack)
}
}
}
timer.start()
awaitClose()
}
Coroutines are launched inside a CoroutineScope which are similar to lifecycle for android. As such, Android automatically provide coroutine's scope for components like activity or fragment and bound them to theirs lifecycle.
While it's not recommended to use the global scope that starts and ends with android's process. There are no restriction on creating your own and limiting it to a specific view of time. Creating one starts its life and cancelling it stops all tasks inside.
In your case a countdown can be done with only coroutines. As stated in this answer.
But without changing too much of your existing code you could reuse the viewModelScope that launched your timer to emit your event.
viewModelScope.launch {
_eventFlow.emit(UIPaymentEvent.NavigateToBack)
}
Beware of the life of your scope. If the viewmodelScope is dead when the timer finish, the event will never be sent.
Here I am trying to add a view over main_layout for a specific time duration, for that I write this
main_layout.addView(linearLayout)
Handler(Looper.getMainLooper()).post(Runnable {
val timer = Timer()
timer.schedule(object : TimerTask() {
override fun run() {
main_layout.removeView(imageView)
timer.cancel()
}
}, 8000)
})
but app gets crash with the error :
java.lang.IllegalStateException: The current thread must have a looper!
kindly let me know, what else I have to do here.
I have resolved my problem by using HandleMessage() method in handler.
mainHandler = object :Handler(){
override fun handleMessage(msg: Message?) {
super.handleMessage(msg)
if (msg!!.what ==1){
removeView()
}
}
}
private fun removedata() {
main_layout.removeView(imageView)
}
also, to perform the desired operation, this method sends the required sets of instruction
mainHandler.sendEmptyMessageDelayed(1,5000)
so as you can see that to call remove(), it requires 1 to fullfil the condition and second parameter is the desired delay I wanted to set.
I am trying to code in kotlin android to move an image every second but I am not able to make it work. Right now I'm using a Timer to schecule a Timer Task every second but it is not working as expected.
Here is my code
class Actvt_Image<float> : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_actvt__image)
val pict_mario = findViewById<ImageView>(R.id.img_Mario)
val bt_down = findViewById<Button>(R.id.bt_down)
val frame = findViewById<LinearLayout>(R.id.frame)
val txt1=findViewById<TextView>(R.id.txt1)
var i =100
val timer = Timer()
val myTask = object : TimerTask() {
override fun run() {
txt1.text = (i+1).toString()
img_Mario.rotation=180f
img_Mario.translationX +=100
img_Mario.translationY +=20
}
}
bt_down.setOnClickListener {
i=0
timer.schedule(myTask, 1000, 1000)
}
}
}
You are trying to update the UI on a background thread which is not possible. UI can only be updated on the UI thread. Also, using a Timer and TimerTask to create and destroy a thread every 1 second is not the right way to use threads because creating a thread is a memory expensive operation.
What you need to do is to use a Handler and tell the UI Thread to run a Runnable after every desired interval. Remove Timer and TimerTask and use the following
val handler = Handler(Looper.getMainLooper())
handler.post(object : Runnable {
override fun run() {
txt1.text = (i+1).toString()
img_Mario.rotation=180f
img_Mario.translationX +=100
img_Mario.translationY +=20
handler.postDelayed(this, 1000)
}
})
Above code is using a handler and posting a task to the UI Thread message queue. The task itself is updating the UI and posting itself again to the UI Thread message queue using the same handler but this time after 1 second delay using the handler.postDelayed() methond
EDIT : How to stop runnable
If you want to stop a specific runnable you can use the following method and pass in the same runnable object that you passed in handler.post(). Surely you have to keep a reference to the runnable at all time to stop it. The above code doesn't keep a reference. See the Complete code below.
handler.removeCallbacks(runnable) //stops a specific runnable
To stop all remaining callbacks or runnable from the UI Thread message queue use this
handler.removeCallbacksAndMessages(null) //stops any pending callback in message queue
Complete code
NOTE: I have added a stop button click listener as an addition
class Actvt_Image<float> : AppCompatActivity() {
private lateinit var handler : Handler
private lateinit var runnable : Runnable // reference to the runnable object
private var i = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_actvt__image)
val pict_mario = findViewById<ImageView>(R.id.img_Mario)
val bt_down = findViewById<Button>(R.id.bt_down)
val bt_stop = findViewById<Button>(R.id.bt_stop)
val frame = findViewById<LinearLayout>(R.id.frame)
val txt1=findViewById<TextView>(R.id.txt1)
handler = Handler(Looper.getMainLooper())
runnable = Runnable {
i++
txt1.text = i.toString()
img_Mario.rotation=180f
img_Mario.translationX +=100
img_Mario.translationY +=20
handler.postDelayed(runnable, 1000)
}
bt_down.setOnClickListener {
handler.post(runnable)
}
bt_stop.setOnClickListener {
//Use this to stop all callbacks
//handler.removeCallbacksAndMessages(null)
handler.removeCallbacks(runnable)
}
}
}
Read more about processes, threads and handler here :
https://developer.android.com/guide/components/processes-and-threads
https://developer.android.com/reference/android/os/Handler
I have one code and it run as I expected
val t = object : Thread() {
override fun run() {
while (!isInterrupted) {
try {
Thread.sleep(1000) //1000ms = 1 sec
runOnUiThread {
i++
txt1.text = i.toString()
img_Mario.rotation=180f
img_Mario.translationX +=20
}
} catch (e: InterruptedException) {
e.printStackTrace()
}
}
}
}
bt_down.setOnClickListener {
i=0
t.start()
}