{"record":{"id":"18b6435e987122eb","repo":"Tencent/QMUI_Android","slug":"call-the-method-must-be-in-main-thread-s","errorCode":null,"errorMessage":"Call the method must be in main thread: %s","messagePattern":"Call the method must be in main thread: (.+?)","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"arch/src/main/java/com/qmuiteam/qmui/arch/Utils.java","lineNumber":103,"sourceCode":"                }\n            }\n            @SuppressLint(\"PrivateApi\") Method convertToTranslucent = Activity.class.getDeclaredMethod(\"convertToTranslucent\",\n                    translucentConversionListenerClazz, ActivityOptions.class);\n            convertToTranslucent.setAccessible(true);\n            convertToTranslucent.invoke(activity, null, options);\n        } catch (Throwable ignore) {\n        }\n    }\n\n\n    public static void assertInMainThread() {\n        if (Looper.myLooper() != Looper.getMainLooper()) {\n            StackTraceElement[] elements = Thread.currentThread().getStackTrace();\n            String methodMsg = null;\n            if (elements != null && elements.length >= 4) {\n                methodMsg = elements[3].toString();\n            }\n            throw new IllegalStateException(\"Call the method must be in main thread: \" + methodMsg);\n        }\n    }\n\n    static void modifyOpForStartFragmentAndDestroyCurrent(FragmentManager fragmentManager,\n                                                                 final QMUIFragment fragment,\n                                                                 final boolean useNewTransitionConfigWhenPop,\n                                                                 final QMUIFragment.TransitionConfig transitionConfig){\n        findAndModifyOpInBackStackRecord(fragmentManager, -1, new Utils.OpHandler() {\n            @Override\n            public boolean handle(Object op) {\n                Field cmdField = null;\n                try {\n                    cmdField = Utils.getOpCmdField(op);\n                    cmdField.setAccessible(true);\n                    int cmd = (int) cmdField.get(op);\n                    if (cmd == 1) {\n                        if (useNewTransitionConfigWhenPop) {\n                            Field popEnterAnimField = Utils.getOpPopEnterAnimField(op);","sourceCodeStart":85,"sourceCodeEnd":121,"githubUrl":"https://github.com/Tencent/QMUI_Android/blob/026e7d486677d6593a96689cd590ec3561176717/arch/src/main/java/com/qmuiteam/qmui/arch/Utils.java#L85-L121","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Move the call to the main thread: runOnUiThread(...), activity.runOnUiThread, Handler(Looper.getMainLooper()).post(...), or withContext(Dispatchers.Main).","For RxJava, add .observeOn(AndroidSchedulers.mainThread()) before the QMUI call.","Ensure AsyncTask results manipulate fragments in onPostExecute/onProgressUpdate (which are main-thread).","Audit the method named in the exception message for off-main call sites and wrap it in a main-thread dispatcher."],"exampleFix":"// before\nexecutor.execute(() -> QMUISwipeBackActivityManager.getInstance().getCurrentActivity());\n// after\nexecutor.execute(() -> runOnUiThread(() ->\n    QMUISwipeBackActivityManager.getInstance().getCurrentActivity()));","handlingStrategy":"validation","validationCode":"if (Looper.myLooper() != Looper.getMainLooper()) {\n    // hop to main thread before invoking QMUI APIs\n    new Handler(Looper.getMainLooper()).post(() -> qmuiCall());\n    return;\n}\nqmuiCall();","typeGuard":"void runOnMain(Runnable r) {\n    if (Looper.myLooper() == Looper.getMainLooper()) r.run();\n    else new Handler(Looper.getMainLooper()).post(r);\n}","tryCatchPattern":"try {\n    qmuiCall();\n} catch (IllegalStateException e) {\n    if (e.getMessage() != null && e.getMessage().startsWith(\"Call the method must be in main thread\")) {\n        new Handler(Looper.getMainLooper()).post(this::qmuiCall);\n    } else throw e;\n}","preventionTips":["Wrap QMUI fragment/view APIs with runOnUiThread or Dispatchers.Main","Add .observeOn(AndroidSchedulers.mainThread()) on Rx chains touching QMUI","Never touch fragments from worker threads or binder callbacks"],"tags":["android","threading","main-thread","assertion"],"backgroundTag":"operation-not-supported","analyzedSha":"026e7d486677d6593a96689cd590ec3561176717","analyzedAt":"2026-09-06T13:32:24.816Z","contentChangedAt":"2026-09-06T13:32:24.816Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}