0
# Looper and Threading Control
1
2
Comprehensive control over Android's Looper and threading behavior with multiple modes for different testing scenarios, from legacy scheduler-based control to realistic paused execution and instrumentation test simulation.
3
4
## Capabilities
5
6
### Looper Mode Configuration
7
8
Primary annotation for controlling Robolectric's Looper and threading behavior.
9
10
```java { .api }
11
/**
12
* Configurer annotation for controlling Robolectric's Looper behavior.
13
* Currently defaults to PAUSED behavior, can be overridden at package/class/method level
14
* or via 'robolectric.looperMode' system property.
15
*/
16
@Documented
17
@Retention(RetentionPolicy.RUNTIME)
18
@Target({ElementType.PACKAGE, ElementType.TYPE, ElementType.METHOD})
19
public @interface LooperMode {
20
/** Set the Looper mode */
21
Mode value();
22
23
/** Supported Looper modes */
24
enum Mode {
25
/**
26
* @deprecated Robolectric's default threading model prior to 4.4
27
*
28
* Tasks posted to Loopers are managed via Scheduler.
29
* Only single Looper thread - tests and posted tasks execute on same thread.
30
*
31
* Problems with this mode:
32
* - Default UNPAUSED state executes tasks inline synchronously (differs from Android)
33
* - Scheduler list can get out of sync with MessageQueue causing deadlocks
34
* - Each Scheduler keeps own time value that can desync
35
* - Background Looper tasks execute in main thread causing thread enforcement errors
36
*/
37
@Deprecated
38
LEGACY,
39
40
/**
41
* Mode that accurately models real Android Looper behavior (default).
42
*
43
* Similar to LEGACY PAUSED in these ways:
44
* - Tests run on main looper thread
45
* - Main Looper tasks not executed automatically, need explicit ShadowLooper APIs
46
* - SystemClock time is frozen, manually advanced via Robolectric APIs
47
*
48
* Improvements over LEGACY:
49
* - Warns if test fails with unexecuted tasks in main Looper queue
50
* - Robolectric test APIs automatically idle main Looper
51
* - Each Looper has own thread, background loopers execute asynchronously
52
* - Loopers use real MessageQueue for pending tasks
53
* - Single clock value managed via ShadowSystemClock
54
*/
55
PAUSED,
56
57
/**
58
* Simulates instrumentation test threading model with separate test thread
59
* distinct from main looper thread.
60
*
61
* Similar to PAUSED mode but with separate test and main threads.
62
* Clock time still fixed, can use shadowLooper methods for control.
63
* Recommended for tests using androidx.test APIs.
64
* Most org.robolectric APIs that interact with UI will raise exception
65
* if called off main thread.
66
*/
67
INSTRUMENTATION_TEST
68
69
// RUNNING mode planned for future - free running threads with auto-increasing clock
70
}
71
}
72
```
73
74
**Usage Examples:**
75
76
```java
77
// Class-level configuration
78
@LooperMode(LooperMode.Mode.PAUSED)
79
public class MyTest {
80
// All tests use PAUSED mode
81
}
82
83
// Method-level override
84
@LooperMode(LooperMode.Mode.PAUSED)
85
public class MyTest {
86
@LooperMode(LooperMode.Mode.INSTRUMENTATION_TEST) // Overrides class setting
87
@Test
88
public void instrumentationStyleTest() {
89
// This test runs with separate test thread
90
}
91
}
92
93
// Package-level configuration in package-info.java
94
@LooperMode(LooperMode.Mode.PAUSED)
95
package com.example.tests;
96
```
97
98
### Legacy Scheduler Control (Deprecated)
99
100
Scheduler-based control for LEGACY looper mode. Strongly recommended to migrate to PAUSED mode.
101
102
```java { .api }
103
public class Robolectric {
104
/**
105
* @deprecated Use PAUSED Looper mode with ShadowLooper APIs instead
106
* Return foreground scheduler (UI thread scheduler).
107
*/
108
@Deprecated
109
public static Scheduler getForegroundThreadScheduler();
110
111
/**
112
* @deprecated Use ShadowLooper.runToEndOfTasks() instead
113
* Execute all runnables enqueued on foreground scheduler.
114
*/
115
@Deprecated
116
public static void flushForegroundThreadScheduler();
117
118
/**
119
* @deprecated Use PAUSED Looper mode instead
120
* Return background scheduler.
121
*/
122
@Deprecated
123
public static Scheduler getBackgroundThreadScheduler();
124
125
/**
126
* @deprecated Use ShadowLooper.runToEndOfTasks() instead
127
* Execute all runnables enqueued on background scheduler.
128
*/
129
@Deprecated
130
public static void flushBackgroundThreadScheduler();
131
}
132
```
133
134
```java { .api }
135
public class RuntimeEnvironment {
136
/**
137
* @deprecated Use PAUSED Looper mode instead
138
* Retrieves current master scheduler used by main Looper and optionally all Loopers.
139
*/
140
@Deprecated
141
public static Scheduler getMasterScheduler();
142
143
/**
144
* @deprecated Use PAUSED Looper mode instead
145
* Sets current master scheduler. Primarily for core setup, changing during test
146
* will have unpredictable results.
147
*/
148
@Deprecated
149
public static void setMasterScheduler(Scheduler masterScheduler);
150
}
151
```
152
153
### ShadowLooper Control (Recommended)
154
155
Modern Looper control APIs for PAUSED and INSTRUMENTATION_TEST modes.
156
157
```java { .api }
158
/**
159
* Shadow implementation providing control over Looper behavior.
160
* Recommended approach for controlling task execution in tests.
161
*/
162
public class ShadowLooper {
163
/** Get ShadowLooper for main Looper */
164
public static ShadowLooper shadowMainLooper();
165
166
/** Get ShadowLooper for specific Looper */
167
public static ShadowLooper shadowOf(Looper looper);
168
169
// Task execution control
170
171
/** Execute all currently queued tasks */
172
public void idle();
173
174
/** Execute tasks for specified duration, advancing clock */
175
public void idleFor(long time, TimeUnit timeUnit);
176
public void idleFor(Duration duration);
177
178
/** Execute only tasks scheduled before current time */
179
public void runToEndOfTasks();
180
181
/** Execute next queued task */
182
public boolean runOneTask();
183
184
/** Execute tasks until next delayed task or queue empty */
185
public void runToNextTask();
186
187
// Queue inspection
188
189
/** Check if Looper has queued tasks */
190
public boolean hasQueuedTasks();
191
192
/** Get next task execution time */
193
public long getNextScheduledTaskTime();
194
195
/** Get number of queued tasks */
196
public int size();
197
198
// Clock control
199
200
/** Get current Looper time */
201
public long getCurrentTime();
202
203
// Looper state control
204
205
/** Pause Looper (tasks won't execute automatically) */
206
public void pause();
207
208
/** Unpause Looper (tasks execute when posted) */
209
public void unPause();
210
211
/** Check if Looper is paused */
212
public boolean isPaused();
213
214
/** Reset Looper state */
215
public void reset();
216
}
217
```
218
219
**Usage Examples:**
220
221
```java
222
import static org.robolectric.Shadows.shadowOf;
223
224
// Main looper control
225
ShadowLooper mainLooper = shadowOf(Looper.getMainLooper());
226
227
// Execute all pending tasks
228
mainLooper.idle();
229
230
// Execute tasks for 5 seconds
231
mainLooper.idleFor(Duration.ofSeconds(5));
232
233
// Check for pending work
234
if (mainLooper.hasQueuedTasks()) {
235
mainLooper.runOneTask();
236
}
237
238
// Background looper control
239
HandlerThread backgroundThread = new HandlerThread("background");
240
backgroundThread.start();
241
ShadowLooper backgroundLooper = shadowOf(backgroundThread.getLooper());
242
backgroundLooper.idle();
243
```
244
245
### SystemClock Control
246
247
Clock manipulation for time-sensitive testing in PAUSED and INSTRUMENTATION_TEST modes.
248
249
```java { .api }
250
/**
251
* Shadow for SystemClock providing clock control in tests.
252
*/
253
public class ShadowSystemClock {
254
/** Advance system clock by specified amount */
255
public static void advanceBy(Duration duration);
256
public static void advanceBy(long time, TimeUnit timeUnit);
257
258
/** Set absolute system time */
259
public static void setCurrentTimeMillis(long millis);
260
261
/** Get current system time */
262
public static long currentTimeMillis();
263
264
/** Sleep current thread for specified time without advancing clock */
265
public static void sleep(Duration duration);
266
public static void sleep(long time, TimeUnit timeUnit);
267
}
268
```
269
270
**Usage Example:**
271
272
```java
273
// Start with known time
274
ShadowSystemClock.setCurrentTimeMillis(1000000000L);
275
276
// Advance time by 1 hour
277
ShadowSystemClock.advanceBy(Duration.ofHours(1));
278
279
// Verify time-based behavior
280
assertThat(System.currentTimeMillis()).isEqualTo(1000000000L + 3600000L);
281
```
282
283
### Handler and Message Control
284
285
Fine-grained control over Handler and Message execution.
286
287
```java { .api }
288
/**
289
* Shadow for Handler providing message control.
290
*/
291
public class ShadowHandler {
292
/** Get ShadowHandler for specific Handler */
293
public static ShadowHandler shadowOf(Handler handler);
294
295
// Message queue inspection
296
297
/** Check if Handler has pending messages */
298
public boolean hasMessages(int what);
299
public boolean hasMessages(int what, Object object);
300
301
/** Get queued messages */
302
public List<Message> getMessages();
303
304
// Message execution control
305
306
/** Execute all queued messages */
307
public void flush();
308
309
/** Execute specific message */
310
public void handleMessage(Message message);
311
}
312
```
313
314
## Threading Patterns by Mode
315
316
### PAUSED Mode (Recommended)
317
318
```java
319
@LooperMode(LooperMode.Mode.PAUSED)
320
public class PausedModeTest {
321
@Test
322
public void testAsyncOperation() {
323
// Start async operation
324
myService.performAsyncOperation();
325
326
// Tasks are queued but not executed automatically
327
ShadowLooper mainLooper = shadowOf(Looper.getMainLooper());
328
assertThat(mainLooper.hasQueuedTasks()).isTrue();
329
330
// Explicitly execute tasks
331
mainLooper.idle();
332
333
// Verify result
334
assertThat(myService.isOperationComplete()).isTrue();
335
}
336
337
@Test
338
public void testDelayedOperation() {
339
// Post delayed task (5 seconds)
340
Handler handler = new Handler(Looper.getMainLooper());
341
handler.postDelayed(() -> myCallback.onComplete(), 5000);
342
343
// Advance time and execute
344
ShadowLooper mainLooper = shadowOf(Looper.getMainLooper());
345
mainLooper.idleFor(Duration.ofSeconds(5));
346
347
// Verify callback executed
348
verify(myCallback).onComplete();
349
}
350
}
351
```
352
353
### INSTRUMENTATION_TEST Mode
354
355
```java
356
@LooperMode(LooperMode.Mode.INSTRUMENTATION_TEST)
357
public class InstrumentationTest {
358
@Test
359
public void testWithSeparateTestThread() {
360
// Test runs on separate thread from main Looper
361
// Can use androidx.test APIs that expect this pattern
362
363
// Use CountDownLatch for synchronization
364
CountDownLatch latch = new CountDownLatch(1);
365
366
// Post to main thread
367
new Handler(Looper.getMainLooper()).post(() -> {
368
// UI operations on main thread
369
myActivity.updateUI();
370
latch.countDown();
371
});
372
373
// Wait for completion
374
latch.await(5, TimeUnit.SECONDS);
375
376
// Verify result
377
assertThat(myActivity.getDisplayText()).isEqualTo("Updated");
378
}
379
}
380
```
381
382
### Legacy Mode (Not Recommended)
383
384
```java
385
@SuppressWarnings("deprecation")
386
@LooperMode(LooperMode.Mode.LEGACY)
387
public class LegacyModeTest {
388
@Test
389
public void testWithScheduler() {
390
// Use deprecated scheduler APIs
391
Scheduler scheduler = Robolectric.getForegroundThreadScheduler();
392
393
// Control execution
394
scheduler.pause();
395
myService.performAsyncOperation();
396
397
// Manually advance
398
scheduler.advanceToLastPostedRunnable();
399
400
// Verify result
401
assertThat(myService.isOperationComplete()).isTrue();
402
}
403
}
404
```
405
406
## Best Practices
407
408
### Migration from LEGACY to PAUSED
409
410
1. **Replace Scheduler APIs**:
411
```java
412
// Old (LEGACY)
413
Robolectric.getForegroundThreadScheduler().advanceToLastPostedRunnable();
414
415
// New (PAUSED)
416
shadowOf(Looper.getMainLooper()).idle();
417
```
418
419
2. **Handle Background Threads**:
420
```java
421
// Old (LEGACY) - everything on main thread
422
Robolectric.getBackgroundThreadScheduler().advanceToLastPostedRunnable();
423
424
// New (PAUSED) - real background threads
425
HandlerThread thread = new HandlerThread("background");
426
thread.start();
427
shadowOf(thread.getLooper()).idle();
428
```
429
430
3. **Time Management**:
431
```java
432
// Old (LEGACY)
433
scheduler.advanceBy(5000);
434
435
// New (PAUSED)
436
ShadowSystemClock.advanceBy(Duration.ofSeconds(5));
437
shadowOf(Looper.getMainLooper()).idle();
438
```
439
440
### Test Cleanup
441
442
Always ensure proper cleanup to avoid test pollution:
443
444
```java
445
@After
446
public void cleanup() {
447
// Reset looper state
448
shadowOf(Looper.getMainLooper()).reset();
449
450
// Clean up background threads
451
if (backgroundThread != null) {
452
backgroundThread.quitSafely();
453
}
454
}
455
```
456
457
### Debugging Tips
458
459
1. **Check for Unexecuted Tasks**:
460
```java
461
ShadowLooper mainLooper = shadowOf(Looper.getMainLooper());
462
if (mainLooper.hasQueuedTasks()) {
463
System.out.println("Unexecuted tasks: " + mainLooper.size());
464
}
465
```
466
467
2. **Inspect Next Task Time**:
468
```java
469
long nextTaskTime = mainLooper.getNextScheduledTaskTime();
470
System.out.println("Next task at: " + nextTaskTime);
471
```
472
473
3. **Use Incremental Execution**:
474
```java
475
// Execute one task at a time for debugging
476
while (mainLooper.runOneTask()) {
477
// Inspect state after each task
478
}
479
```
480
481
## Common Issues and Solutions
482
483
1. **"Main looper has queued unexecuted runnables"**: Add `shadowOf(getMainLooper()).idle()` calls in your test
484
485
2. **Background tasks not executing**: Ensure you're controlling the correct Looper for background threads
486
487
3. **Time-based tests flaky**: Use `ShadowSystemClock.advanceBy()` instead of `Thread.sleep()`
488
489
4. **UI updates not reflecting**: Call `shadowOf(Looper.getMainLooper()).idle()` after UI operations