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);
Martyn
  • 16,432
  • 24
  • 71
  • 104
  • 1
    Have you been checking the return value from `removeCallbacks()`? – CommonsWare Mar 19 '12 at 10:47
  • 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. – Martyn Mar 19 '12 at 10:58
  • 2
    `View.removeCallbacks()` will always `return true;` (at least on ICS - rest probably too) [see here](http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.0.3_r1/android/view/View.java#8786) – zapl Mar 19 '12 at 11:05
  • @zapi: Oops, yeah, sorry, hadn't thought that all the way through. – CommonsWare Mar 19 '12 at 11:18
  • @CommonsWare View not behaving as the docs say isn't really your fault :) – zapl Mar 19 '12 at 12:03

3 Answers3

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.

CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
  • @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()`. – CommonsWare Mar 19 '12 at 12:40
  • @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. – CommonsWare Mar 19 '12 at 12:49
  • 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. – Martyn Mar 19 '12 at 13:20
  • 2
    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. – James Wald Jan 24 '14 at 23:31
  • "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](https://stackoverflow.com/questions/3602026/linearlayout-height-in-oncreate-is-0/3602144#3602144) to get size of the view or something like that. – Jenix Oct 31 '18 at 21:09
  • 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, `Runnable`s 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. – Jenix Oct 31 '18 at 21:09
  • The reason why it works so randomly is, maybe because my project keeps pushing a lot of `Runnable`s 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`. – Jenix Oct 31 '18 at 21:09
  • @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. – CommonsWare Oct 31 '18 at 21:50
  • 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 `Runnable`s. 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) – Jenix Oct 31 '18 at 22:35
  • turned out first few `Runnable`s never get called which were posted just after calling `addView`. So I tried to delay posting those `Runnable`s 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 `Runnable`s 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`. – Jenix Oct 31 '18 at 22:35
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);
}
Tom Susel
  • 3,397
  • 1
  • 24
  • 25
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

mbonnin
  • 6,893
  • 3
  • 39
  • 55