Why is posting & cancelled a runnable on a View and Handler result in different bahviour?
Asked Answered
L

3

8

I've been playing about with Runnables and have discovered that if you postDelayed a Runnable on a View then removing the callback won't work, however if you do the same but post the Runnable on a Handler then removing the callback does work.

Why does this work (Runnable run() code never gets executed):

Runnable runnable = new Runnable() {
    @Override
    public void run() {
        // execute some code
    }
};

Handler handler = new Handler();
handler.postDelayed(runnable, 10000);
handler.removeCallbacks(runnable);

where as this doesn't (Runnable run() code always gets executed)?:

Runnable runnable = new Runnable() {
    @Override
    public void run() {
        // execute some code
    }
};

View view = findViewById(R.id.some_view);
view.postDelayed(runnable, 10000);
view.removeCallbacks(runnable);
Livvi answered 19/3, 2012 at 10:40 Comment(5)
Have you been checking the return value from removeCallbacks()?Bromberg
I hadn't seen this, can you explain how this can help? I've read the documentation but don't see how this can help in my above View example.Livvi
View.removeCallbacks() will always return true; (at least on ICS - rest probably too) see hereEqual
@zapi: Oops, yeah, sorry, hadn't thought that all the way through.Bromberg
@Bromberg View not behaving as the docs say isn't really your fault :)Equal
B
5

If the View is not attached to a window, I can see this happening, courtesy of what looks like a bug in Android. Tactically, therefore, it may be a question of timing, making sure that you do not post or remove the Runnable until after the View is attached to the window.

If you happen to have a sample project lying around that replicates this problem, I'd like to take a look at it. Otherwise, I will try making my own, so I can have something I can use to report my presumed bug.


UPDATE

As mentioned in the comments, removeCallbacks() on more ordinary widgets works, so it appears this is a WebView-specific problem, per the OP's sample code.

Bromberg answered 19/3, 2012 at 11:25 Comment(10)
@Martyn: Yeah, as I suspected, the WebView is not attached to the window at that point (e.g., getWindowToken() returns null), and so you are going to trip over this bug in Android. I will file an issue on this soonish. In the meantime, you will need to use Handler to reliably removeCallbacks().Bromberg
@Martyn: It appears the problem is more subtle than I thought. I tried reproducing your problem using a Button, and removeCallbacks() succeeds, even though the Button is not attached to the window at the time of the postDelayed(). I am now guessing that your difficulty might be more peculiar to WebView. Regardless, you probably should just use a Handler for now.Bromberg
Thanks Mark - I also got the removeCallback() working with a TextView and was trying to investigate why the WebView wasn't working but didn't get anywhere - if you find out, I'd love to know.Livvi
For what it's worth, Handler.removeCallbacks() always works. Due to the unpredictable nature of View.removeCallbacks(), I avoid using View.post...() when the callback needs to be managed.Dunford
"you do not post or remove the Runnable until after the View is attached to the window" - If I call post on the view which I just added by calling addView, is there any chance the Runnable will not be posted? I'm using this technique to get size of the view or something like that.Prelacy
Recently I ran my project on both on my old Samsung Note 2 (Android 4.4.2) and software emulator HTC One (4.3) and noticed some unexpected issues which I've never faced with the current device (Android 8.0). I suspect the issue has something to do with calling on Views which are not yet added (but addView was called). I was to reproduce the issue with new empty project but failed. But what I can tell you for sure is, Runnables never get called which are posted just after calling addView 7 out of 10 on the Note 2, and 2 out of 10 on the HTC One.Prelacy
The reason why it works so randomly is, maybe because my project keeps pushing a lot of Runnables from other worker threads at the same time. And today I saw one comment from the link I added above which says "this technique works wonderfully for most devices, but fails on Nexus S (running 2.2, and 4.0)". And also saw your answer here so I'm asking if there's any known bugs related to View's post.Prelacy
@Jenix: "If I call post on the view which I just added by calling addView, is there any chance the Runnable will not be posted?" -- I would not expect that to be a problem. You could always call post() on a Handler, or on some View that is stable (e.g., android.R.id.content). AFAIK, the View is merely a gateway for posting things.Bromberg
Yes, you're right, most of the time I wouldn't need to do this way. But in my recent project, the reason why I'm calling specific Views' post is, I need to know those sizes which are determined after their first layout pass. And also there are subsequent jobs needed to be executed after initialization done by those first Runnables. So I posted such jobs through specific Views in advance. But in many many test runs (I mean in my project only. As I said above, I couldn't reproduce the same problem with a new simple project)Prelacy
turned out first few Runnables never get called which were posted just after calling addView. So I tried to delay posting those Runnables about 3 seconds and this time no issues at all. As I said, I never had such problem at all with recent devices. So I'm not sure if it's the old Android OS or just some specific old devices' issue. Or maybe my project is pushing too many Runnables to UI Thread and slow old ones fail to keep up with. Anyway I'm sure some of the first Runnables are missing when I call post just after calling addView.Prelacy
S
0

For various reasons, the View's handler (view.getHandler()) may not be ready when you want to initiate the animation.

Therefor you should probably wait before assigning the runnable to the view.

Assuming you are trying to do that from within an Activity, here is a code that waits for the handler to be available before posting the runnable:

private void assignRunnable(final View view, final Runnable runnable, final int delay)
{
  if (view.getHandler() == null) {
    // View is not ready, postpone assignment
    this.getView().postDelayed(new Runnable() {
      @Override
      public void run() {
        assignRunnable(view, runnable, delay);
      }
    }, 100);

    //Abort
    return;
  }

  //View is ready, assign the runnable
  view.postDelayed(runnable, delay);
}
Seeto answered 2/3, 2014 at 11:37 Comment(0)
H
0

Looking at ViewRootImpl.java, the semantics of View.removeCallbacks() seem unclear to say the least.

RunQueue.removeCallbacks just removes the Runnables from an ArrayList. See here.

If RunQueue.executeActions is called before removeCallbacks, then the ArrayList is cleared in all cases making removeCallbacks a no-op. See here.

RunQueue.executeActions is called for every traversal.... See here.

So unless I miss something, View.removeCallbacks will not work if a traversal has happened since you called View.post.

I'll stick to @james-wald comment above and not use View.post

Hygrometry answered 7/4, 2016 at 20:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.