go/sysui-executors
In SystemUI, we are encouraging the use of Java's Executor over Android's Handler when shuffling a Runnable between threads or delaying the execution of a Runnable. We have an implementation of Executor available, as well as our own sub-interface, DelayableExecutor available. For test, FakeExecutor is available.
Executors make testing easier and are generally more flexible than Handlers. They are defined as an interface, making it easy to swap in fake implementations for testing. This also makes it easier to supply alternate implementations generally speaking - shared thread pools; priority queues; etc.
For testing, whereas a handler involves trying to directly control its underlying Looper (using things like Thread.sleep()
as well as overriding internal behaviors), an Executor implementation can be made to be directly controllable and inspectable.
See also go/executors-for-the-android-engineer
At present, there are two interfaces of Executor avaiable, each implemented, and each with two instances - @Background
and @Main
.
The simplest Executor available implements the interface directly, making available one method: Executor.execute()
. You can access an implementation of this Executor through Dependency Injection:
public class Foobar { @Inject public Foobar(@Background Executor bgExecutor) { bgExecutor.execute(new Runnable() { // ... }); } }
@Main
will give you an Executor that runs on the ui thread. @Background
will give you one that runs on a shared non-ui thread. If you ask for an non-annotated Executor, you will get the @Background
Executor.
We do not currently have support for creating an Executor on a new, virgin thread. We do not currently support any sort of shared pooling of threads. If you require either of these, please reach out.
DelayableExecutor is the closest analogue we provide to Handler. It adds executeDelayed(Runnable r, long delayMillis)
and executeAtTime(Runnable r, long uptimeMillis)
to the interface, just like Handler's postDelayed and postAtTime. It also adds the option to supply a TimeUnit as a third argument.
A DelayableExecutor can be accessed via Injection just like a standard Executor. In fact, at this time, it shares the same underlying thread as our basic Executor.
public class Foobar { @Inject public Foobar(@Background DelayableExecutor bgExecutor) { bgExecutor.executeDelayed(new Runnable() { // ... }, 1, TimeUnit.MINUTES); } }
Unlike Handler, the added methods return a Runnable that, when run, cancels the originally supplied Runnable if it has not yet started execution:
public class Foobar { @Inject public Foobar(@Background DelayableExecutor bgExecutor) { Runnable cancel = bgExecutor.executeDelayed(new Runnable() { // ... }, 1, TimeUnit.MINUTES); cancel.run(); // The supplied Runnable will (probably) not run. } }
Most use cases of Handlers can easily be handled by the above two interfaces above. A minor refactor makes the switch:
Handler | Executor | DelayableExecutor |
---|---|---|
post() | execute() | execute() |
postDelayed() | none | executeDelayed() |
postAtTime() | none | executeAtTime() |
There are some notable gaps in this implementation: Handler.postAtFrontOfQueue()
. If you require this method, or similar, please reach out. The idea of a PriorityQueueExecutor has been floated, but will not be implemented until there is a clear need.
Note also that "canceling" semantics are different. Instead of passing a token
object to Handler.postDelayed()
, you receive a Runnable that, when run, cancels the originally supplied Runnable.
Executors have no concept of message handling. This is an oft used feature of Handlers. There are (as of 2019-12-05) 37 places where we subclass Handler to take advantage of this. However, by-and-large, these subclases take the following form:
mHandler = new Handler(looper) { @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_A: handleMessageA(); break; case MSG_B: handleMessageB((String) msg.obj); break; case MSG_C: handleMessageC((Foobar) msg.obj); break; // ... } } }; // Elsewhere in the class void doSomething() { mHandler.obtainMessage(MSG_B, "some string"); mHandler.sendMessage(msg); }
This could easily be replaced by equivalent, more direct Executor code:
void doSomething() { mExecutor.execute(() -> handleMessageB("some string")); }
If you are posting Runnables frequently and you worry that the cost of creating anonymous Runnables is too high, consider creating pre-defined Runnables as fields in your class.
If you feel that you have a use case that this does not cover, please reach out.
One notable place where Handlers have been a requirement in the past is with ContentObserver, which takes a Handler as an argument. However, we have created ExecutorContentObserver, which is a hidden API that accepts an Executor in its constructor instead of a Handler, and is otherwise identical.
Handlers aren't going away. There are other Android APIs that still require them. Avoid Handlers when possible, but use them where necessary.
We have a FakeExecutor available. It implements DelayableExecutor (which in turn is an Executor). It takes a FakeSystemClock in its constructor that allows you to control the flow of time, executing supplied Runnables in a deterministic manner.
The implementation is well documented and tested. You are encouraged to read and reference it, but here is a quick overview:
If you advance the supplied FakeSystemClock directly, the FakeExecutor will execute pending Runnables accordingly. If you use the FakeExecutors advanceClockToNext()
and advanceClockToLast()
, this behavior will not be seen. You will need to tell the Executor to run its ready items. A quick example shows the difference:
Here we advance the clock directly:
FakeSystemClock clock = new FakeSystemClock(); FakeExecutor executor = new FakeExecutor(clock); executor.execute(() -> {}); // Nothing run yet. Runs at time-0 executor.executeDelayed(() -> {}, 100); // Nothing run yet. Runs at time-100. executor.executeDelayed(() -> {}, 500); // Nothing run yet. Runs at time-500. clock.synchronizeListeners(); // The clock just told the Executor it's time-0. // One thing run. clock.setUptimeMillis(500); // The clock just told the Executor it's time-500. // Two more items run.
Here we have more fine-grained control:
FakeSystemClock clock = new FakeSystemClock(); FakeExecutor executor = new FakeExecutor(clock); executor.execute(() -> {}); // Nothing run yet. Runs at time-0 executor.executeDelayed(() -> {}, 100); // Nothing run yet. Runs at time-100. executor.executeDelayed(() -> {}, 500); // Nothing run yet. Runs at time-500. executor.runNextReady(); // One thing run. executor.advanceClockToNext(); // One more thing ready to run. executor.runNextReady(); // One thing run. executor.runNextReady(); // Extra calls do nothing. (Returns false). executor.advanceClockToNext(); // One more thing ready to run. executor.runNextReady(); // Last item run.
One gotcha of direct-clock-advancement: If you have interleaved Runnables split between two executors like the following:
FakeSystemClock clock = new FakeSystemClock(); FakeExecutor executorA = new FakeExecutor(clock); FakeExecutor executorB = new FakeExecutor(clock); executorA.executeDelayed(() -> {}, 100); executorB.executeDelayed(() -> {}, 200); executorA.executeDelayed(() -> {}, 300); executorB.executeDelayed(() -> {}, 400); clock.setUptimeMillis(500);
The Runnables will not interleave. All of one Executor's callbacks will run, then all of the other's.
If a Handler is required because it is used by Android APIs, but is only used in simple ways (i.e. just Handler.post(Runnable)
), you may still want the benefits of FakeExecutor when writing your tests, which you can get by wrapping the Executor in a mock for testing. This can be done with com.android.systemui.util.concurrency.mockExecutorHandler
in MockExecutorHandler.kt
.
As long as you're using FakeExecutors in all the code under test (and no Handlers or Loopers) you don't need it. Get rid of it. No more TestableLooper; no more Looper at all, for that matter.