{"record":{"id":"5aed98e78db67015","repo":"Tencent/matrix","slug":"plugin-destroy-plugin-listener-is-null","errorCode":null,"errorMessage":"plugin destroy, plugin listener is null","messagePattern":"plugin destroy, plugin listener is null","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/plugin/Plugin.java","lineNumber":143,"sourceCode":"        if (pluginListener == null) {\n            throw new RuntimeException(\"plugin stop, plugin listener is null\");\n        }\n        pluginListener.onStop(this);\n    }\n\n    @Override\n    public void destroy() {\n        // stop first\n        if (isPluginStarted()) {\n            stop();\n        }\n        if (isPluginDestroyed()) {\n            throw new RuntimeException(\"plugin destroy, but plugin has been already destroyed\");\n        }\n        status = PLUGIN_DESTROYED;\n\n        if (pluginListener == null) {\n            throw new RuntimeException(\"plugin destroy, plugin listener is null\");\n        }\n        pluginListener.onDestroy(this);\n    }\n\n    @Override\n    public String getTag() {\n        return getClass().getName();\n    }\n\n    @Override\n    public void onForeground(boolean isForeground) {\n\n    }\n\n    public boolean isForeground() {\n        return ProcessUILifecycleOwner.INSTANCE.isProcessForeground();\n    }\n","sourceCodeStart":125,"sourceCodeEnd":161,"githubUrl":"https://github.com/Tencent/matrix/blob/3b8293bd65d47eeea7caf1f32a3a5d4d5eab60e7/matrix/matrix-android/matrix-android-lib/src/main/java/com/tencent/matrix/plugin/Plugin.java#L125-L161","documentation":"Plugin.destroy() throws this when pluginListener is null at destroy time (after the destroyed-state check passes). The listener's onDestroy callback must be invoked, and without one Matrix cannot complete destruction, so it throws a RuntimeException.","triggerScenarios":"Calling destroy() on a plugin that never received a non-null listener via setPluginListener(), where the plugin was also never started (so the stop()-path listener check never fired).","commonSituations":"Constructing plugins without registering PluginListener; custom plugin implementations forgetting to wire the listener; listener registration failing silently or being conditional on debug builds while destroy runs in all builds.","solutions":["Always call setPluginListener(listener) before adding the plugin to Matrix","Use Matrix.Builder to construct plugins so listener wiring is enforced","Null-check the listener before destroy() in your plugin management code","Provide a no-op PluginListener default instead of leaving it null"],"exampleFix":"// before\nMyPlugin plugin = new MyPlugin();\nplugin.destroy();\n\n// after\nMyPlugin plugin = new MyPlugin();\nplugin.setPluginListener(new PluginListener() { /* no-op impls */ });\nplugin.destroy();","handlingStrategy":"validation","validationCode":"if (plugin.getPluginListener() == null) {\n    throw new IllegalStateException(\"plugin listener must be set before destroy\");\n}\nplugin.destroy();","typeGuard":"if (plugin != null && plugin.getPluginListener() != null && !plugin.isPluginDestroyed()) { plugin.destroy(); }","tryCatchPattern":"try {\n    plugin.destroy();\n} catch (RuntimeException e) {\n    if (e.getMessage() != null && e.getMessage().contains(\"listener is null\")) {\n        Log.w(TAG, \"destroy skipped: no listener attached\", e);\n    } else { throw e; }\n}","preventionTips":["Attach a PluginListener (even a no-op) at plugin construction time","Keep listener registration unconditional, not build-type dependent","Add an assertion in debug builds that listeners are wired before lifecycle calls"],"tags":["android","lifecycle","null-listener","plugin"],"backgroundTag":"null-argument","analyzedSha":"3b8293bd65d47eeea7caf1f32a3a5d4d5eab60e7","analyzedAt":"2026-09-08T08:01:39.722Z","contentChangedAt":"2026-09-08T08:01:39.722Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}