Dynamic Plugin System Architecture
Overview
The Kototoro application features a fully dynamic, decoupled plugin system for parsing content sources. Previously, parsers were directly embedded into the app at compile-time as standard Kotlin extensions (kototoro-parsers and kotatsu-parsers-redo). This created massive compilation overhead, tightly coupled the app release cycle to parser changes, and bloated the APK size.
The new Dynamic Plugin System extracts these parsers into standalone .jar (Dex) files that are loaded at runtime. This allows users to hot-swap extensions, develop custom forks, and import external parser collections without needing an app update or modifying the core Kototoro codebase.
Core Concepts
1. parser-api Module
A shared, lightweight Android library module that defines all the foundational interfaces (ContentParser, MangaParser, ContentSource, ContentLoaderContext, etc.).
- Both the host app (Kototoro) and the parser plugins depend on
parser-apias heavily version-controlled contracts. - Plugins depend on this as
compileOnlyso they do not bundle the interfaces themselves, minimizing duplicate classes and guaranteeing runtime compatibility.
2. Zero-Overhead ClassLoading (PluginClassLoader)
When evaluating plugin solutions, Android’s standard DexClassLoader isolates the loaded classes but requires cumbersome java.lang.reflect.Proxy wrappers to map Host interfaces to Plugin interfaces, causing massive reflection overhead on every method call.
To achieve zero runtime performance overhead, Kototoro introduces a custom parent-delegation strategy:
- The
PluginClassLoaderforcefully intercepts requests for classes in theparser-apinamespaces (org.koitharu.kotatsu.parsers.*api classes) and delegates them directly to the Host App’s ClassLoader. - By sharing the exact same
Class<?>reference in memory for the interface definitions, the Host App can simply cast the instantiated plugin objects (e.g.,val parser = pluginInstance as ContentParser), negating the need for any reflection during runtime execution.
3. Namespace Isolation & Dual Architecture
Each imported .jar file is loaded into its own distinct PluginClassLoader instance. This completely avoids class conflicts for identical paths (e.g., two different plugins can both have org.koitharu.kotatsu.parsers.model.MangaParserSource enum or a specific site parser).
- Dual Architecture: Kototoro supports loading both legacy Kotatsu parser collections (
MangaParserFactoryKt) and native Kototoro content collections (ContentParserFactoryKt) interchangeably.
4. GlobalExtensionManager (Single Source of Truth)
The app state, UI, and Database (Data Layer) now interact solely with GlobalExtensionManager.
- Aggregation: It aggregates all discovered
ContentSources fromJarExtensionLoader(JAR plugins) andApkExtensionLoader(Mihon/Aniyomi/IReader APKs). - Deduplication: Sources are namespace-prefixed internally.
- Reactive Layout: The manager uses Kotlin Coroutines and
MutableStateFlowto broadcast state changes across the application whenever a user imports or deletes a plugin via the settings menu, automatically refreshing the exploration views.
User Workflow
- Users navigate to Settings > Remote Sources > Installed Plugins.
- They tap Import Plugin (.jar) to select a compatible
plugin.jar(which must be a Dalvik/Dex compatible bytecode archive). - The app copies the JAR to its private
files/plugins/directory and triggersGlobalExtensionManager.initialize(context). - The plugins are immediately available, and the UI reacts seamlessly.
Plugin Build Pipeline
A standard JAR cannot be loaded directly on Android. The generation of these plugins follows a specific pipeline orchestrated via GitHub Actions:
- The parser code remains in separate repositories (e.g.,
skepsun/kototoro-parsers). - Standard Kotlin compilation produces a standard
.jar. - The Android SDK's
d8(Dexer) tool converts the.classfiles into Dalvikclasses.dexand packages it into an Android-compatibleplugin.jar. - Manifest Generation: The GitHub Actions runner executes
generate_index.pyduring release, automatically creating anindex.min.jsonmapping all hosted.jarversions. - These artifacts (
index.min.jsonand the corresponding versions) are distributed to a specificrepobranch to mirror standard extension repos (like Mihon).
5. JAR ABI Compatibility Constraints
Because the plugin APK operates on the ClassLoader boundary by deferring to the Host App's parser-api, the ABI (method signatures and return types) MUST match exactly. If the Host changes an interface (e.g., converting val source: ContentParserSource to val source: ContentSource), the plugin's ContentParserSource enum classloader boundary must be forcefully contained by modifying JarExtensionLoader to allow the plugin to class-load its own bytecode instead of deferring to a non-existent Host implementation.