Tencent/QMUI_Android · error · IllegalStateException

Call the method must be in main thread

Error message

Call the method must be in main thread: %s

What it means

Utils.assertInMainThread() is a debugging guard used by QMUI APIs that touch view state or fragment transactions which are only safe on the UI thread. When Looper.myLooper() != Looper.getMainLooper(), it throws IllegalStateException naming the offending call site (extracted from the stack trace) so the developer can see which method was called off the main thread.

Solutions

  1. Move the call to the main thread: runOnUiThread(...), activity.runOnUiThread, Handler(Looper.getMainLooper()).post(...), or withContext(Dispatchers.Main).
  2. For RxJava, add .observeOn(AndroidSchedulers.mainThread()) before the QMUI call.
  3. Ensure AsyncTask results manipulate fragments in onPostExecute/onProgressUpdate (which are main-thread).
  4. Audit the method named in the exception message for off-main call sites and wrap it in a main-thread dispatcher.

Example fix

// before
executor.execute(() -> QMUISwipeBackActivityManager.getInstance().getCurrentActivity());
// after
executor.execute(() -> runOnUiThread(() ->
    QMUISwipeBackActivityManager.getInstance().getCurrentActivity()));
Defensive patterns

Strategy: validation

Validate before calling

if (Looper.myLooper() != Looper.getMainLooper()) {
    // hop to main thread before invoking QMUI APIs
    new Handler(Looper.getMainLooper()).post(() -> qmuiCall());
    return;
}
qmuiCall();

Type guard

void runOnMain(Runnable r) {
    if (Looper.myLooper() == Looper.getMainLooper()) r.run();
    else new Handler(Looper.getMainLooper()).post(r);
}

Try / catch

try {
    qmuiCall();
} catch (IllegalStateException e) {
    if (e.getMessage() != null && e.getMessage().startsWith("Call the method must be in main thread")) {
        new Handler(Looper.getMainLooper()).post(this::qmuiCall);
    } else throw e;
}

Prevention

When it happens

Trigger: Invoking any QMUI API that internally calls assertInMainThread() — e.g. fragment transition helpers or latest-visit operations — from a background thread, worker (HandlerThread/coroutine on Dispatchers.Default/IO), or a binder/callback thread.

Common situations: Updating fragments after a network callback on a background thread, running fragment operations inside coroutines without Dispatchers.Main, RxJava/AsyncTask callbacks that forgot to hop back to the main thread, and tests running logic on non-UI threads.

Related errors


AI-assisted analysis of Tencent/QMUI_Android@026e7d4866 (2026-09-06). Data as JSON: /api/errors/18b6435e987122eb. Report an issue: GitHub.

Appendix: source

Thrown at arch/src/main/java/com/qmuiteam/qmui/arch/Utils.java:103

                }
            }
            @SuppressLint("PrivateApi") Method convertToTranslucent = Activity.class.getDeclaredMethod("convertToTranslucent",
                    translucentConversionListenerClazz, ActivityOptions.class);
            convertToTranslucent.setAccessible(true);
            convertToTranslucent.invoke(activity, null, options);
        } catch (Throwable ignore) {
        }
    }


    public static void assertInMainThread() {
        if (Looper.myLooper() != Looper.getMainLooper()) {
            StackTraceElement[] elements = Thread.currentThread().getStackTrace();
            String methodMsg = null;
            if (elements != null && elements.length >= 4) {
                methodMsg = elements[3].toString();
            }
            throw new IllegalStateException("Call the method must be in main thread: " + methodMsg);
        }
    }

    static void modifyOpForStartFragmentAndDestroyCurrent(FragmentManager fragmentManager,
                                                                 final QMUIFragment fragment,
                                                                 final boolean useNewTransitionConfigWhenPop,
                                                                 final QMUIFragment.TransitionConfig transitionConfig){
        findAndModifyOpInBackStackRecord(fragmentManager, -1, new Utils.OpHandler() {
            @Override
            public boolean handle(Object op) {
                Field cmdField = null;
                try {
                    cmdField = Utils.getOpCmdField(op);
                    cmdField.setAccessible(true);
                    int cmd = (int) cmdField.get(op);
                    if (cmd == 1) {
                        if (useNewTransitionConfigWhenPop) {
                            Field popEnterAnimField = Utils.getOpPopEnterAnimField(op);

View on GitHub (pinned to 026e7d4866)