A 10,000-word deep dive into ComboLite: the 100% official API, Jetpack Compose-native framework designed to fix the stability crisis in dynamic Android apps and deliver an unparalleled developer experience.
Part 1: The Inevitable "Mid-life Crisis" of Modern Android Apps
Every successful Android app has a story of evolving from a small, elegant tool into a feature-packed giant. In the early days, the codebase is clean, the architecture is clear, and every team member knows every corner. But as business expands, new features pile up, and the team grows, a harsh reality sets in: our app is facing an inevitable "mid-life crisis."
The core of this crisis lies in a familiar yet frustrating pattern: the Monolithic Application. When an app becomes a monolith, developers experience clear symptoms:
- Chaotic Codebase Growth: The code easily exceeds millions of lines, with module boundaries blurring.
- Exponentially Increasing Maintenance Costs: Tightly coupled business logic means a small change can trigger a cascade of unexpected side effects.
- Painfully Long Build Times: Modifying a single line of code requires rebuilding the entire massive project, draining developers' energy.
- Huge Team Collaboration Friction: Teams from different business lines working in the same repository frequently face code conflicts and merging nightmares.
Is there an architecture that can free us from the shackles of the monolith, allowing us to regain development agility, much like building with LEGO bricks? The answer is a resounding yes: Plugin-based Architecture.
A monolithic Android app can be refactored into a lightweight "Host" and a series of independently developed, dynamically loadable "Plugins."
- Host: The foundation of the application. Its responsibilities are strictly limited, usually containing only the app's startup logic, basic common libraries, and, most importantly, the plugin lifecycle management mechanism.
- Plugin: A highly cohesive functional unit that encapsulates a specific business scenario (e.g., a user center, an e-commerce module, a video player). Each plugin has its own independent code, resources, and even lifecycle.
The key is that pluginization is not just about code organization (like Gradle Modules); it's about runtime dynamism. Plugins can be dynamically loaded, launched, updated, and even uninstalled by the host at runtime without reinstalling the entire app. This brings tangible advantages that can determine the success of your business:
- Dynamic Updates: Breaking Free from the App Store Release Cycle This is the most well-known and commercially valuable capability of pluginization. A critical online bug can take hours, or even days, to fix, from patching and packaging to submitting for app store review and finally reaching all users. With a plugin-based architecture, when a feature plugin has a bug, we only need to replace the faulty plugin package on the server. The host app on the user's device then pulls and dynamically loads the latest plugin at an appropriate time, making online bug fixes happen in "seconds."
- On-Demand Loading: Slimming Down Your App's Initial Install Size A bloated installation package undoubtedly raises the download barrier for new users. Pluginization offers an elegant solution: "on-demand loading." We can keep core, high-frequency features in the host to ensure an extremely lightweight initial package. Secondary or less-used features can be deployed as plugins in the cloud, downloaded and installed only when the user first tries to access them.
- Faster Compilation: Significantly Boosting Daily Development Efficiency In a monolithic app, developers' days are consumed by the "compile-wait-recompile" cycle. Pluginization dramatically improves this by providing physical isolation. When a developer focuses on a specific plugin, they only need to compile that module. The host and other business plugins can be included as pre-compiled binary dependencies, reducing incremental build times from minutes to seconds.
- Team Decoupling: Enabling True Parallel Development When multiple teams maintain a single monolithic app, architectural boundaries often rely on fragile conventions. Pluginization provides strong, architectural-level constraints, achieving a true "separation of concerns." Each business team can independently manage the entire lifecycle of one or more plugins, from requirements and development to testing and release.
Part 2: The "Side Effects" of Traditional Pluginization: A History of Black Magic
Once, the field of Android plugin technology flourished with frameworks like DroidPlugin, VirtualAPK, and RePlugin. In that era of explosive mobile internet growth, they creatively endowed Android apps with dynamic capabilities. They were the "dragon-slaying" techniques of their time, solving countless business emergencies.
However, as technology evolves, these once-mighty tools are showing their age and even becoming significant stability risks. This section delves into the root causes of these "side effects" and explains why we urgently need a new, future-proof plugin solution today.
The Original Sin: "Black Magic" Built on Quicksand
Most traditional plugin frameworks operate by "deceiving" the Android system. Since the four main components (Activity, Service, BroadcastReceiver, ContentProvider) in a plugin are not statically registered in the host's AndroidManifest.xml
, the system is unaware of their existence. To make these "unregistered" components work, frameworks must hook into the system's key services at runtime. This architecture, built on non-public APIs (Internal/Hidden APIs), laid the groundwork for its future instability.
1. The "Sword of Damocles": Hooking the System
This is the root of all problems. To bypass the system's static checks, frameworks commonly use similar techniques: runtime tampering of system service behavior through Java reflection and dynamic proxies.
-
Hooking
ActivityManagerService
(AMS): When an app callsstartActivity()
, it communicates with the system service AMS. The core operation of traditional frameworks is to replace the system-level proxy object with their own proxy. When a request to start an unregistered plugin Activity passes through this proxy, the framework secretly replaces theIntent
with one that starts a pre-registered, legitimate "placeholder" Activity in the host. After the placeholder starts, the framework creates the real plugin Activity instance within its lifecycle and forwards events. -
Hooking
PackageManagerService
(PMS): Similarly, to make the system believe a plugin APK has been "installed," frameworks hook PMS to deceive the system's queries for package information.
This approach was effective in earlier Android versions, but starting with Android 9.0 (Pie), Google began to strictly restrict non-SDK interfaces. Any attempt to call these interfaces via reflection now leads to warnings or even app crashes. This means the lifeline of hook-based plugin frameworks is firmly in Google's hands; every major Android update could be a devastating blow.
2. The "Chaotic Inheritance" of ClassLoader
To load plugin code and enable class sharing, many frameworks crudely interfere with the system's ClassLoader
hierarchy, for example, by forcibly inserting a plugin's DexPathList
into the host's BaseDexClassLoader
via reflection. This breaks the ClassLoader
's clear parent-delegation model, easily causing unpredictable class loading conflicts (ClassNotFoundException
, NoClassDefFoundError
) and relying on unstable internal class structures.
3. The "Tricks" of Resource Management
How can a plugin access its own resources while also accessing the host's? The traditional solution is typically to create a new AssetManager
instance via reflection, call its hidden addAssetPath
method to add the plugin APK's path, and then create a new Resources
object based on this aggregated AssetManager
. This process is cumbersome and prone to resource ID conflicts, a persistent headache in plugin development.
The New Challenge: The Paradigm Shift of Jetpack Compose
Even if we could overcome all the above compatibility issues, a new, more severe challenge has emerged: Jetpack Compose.
As Google's recommended declarative UI framework, Compose fundamentally changes how Android UIs are built. It no longer relies on parsing XML layout files and instantiating View
objects. @Composable
functions are managed by the Compose Runtime with their own lifecycle and state model.
This is a fatal blow to traditional plugin frameworks:
-
Hooking
LayoutInflater
is Obsolete: The old way of loading plugin Views by hookingLayoutInflater
is useless in the Compose world. -
Activity Lifecycle Hooking is Insufficient: Compose's lifecycle management is far more granular than an Activity's. Simply proxying an Activity's
onCreate
andonResume
methods is no longer sufficient to manage the state and recomposition of Composable functions. -
Resource Access Has Changed: The way resources are accessed in Compose (e.g.,
painterResource
,stringResource
) differs from the traditional View system, and old resource merging solutions may not be perfectly compatible.
In short, the design philosophy of traditional plugin frameworks is built on the underlying mechanisms of the Android View system. Jetpack Compose is a completely different paradigm. A deep chasm exists between them.
The era of pluginization that relied on hooking the system and walking the tightrope of compatibility is over. It's time to say goodbye to solutions fraught with "side effects." We need solutions that can coexist harmoniously with the modern Android ecosystem, not fight against it.
Part 3: Goodbye, Hooks! Introducing ComboLite
When "uncertainty" becomes a significant technical debt, we must return to first principles: seeking and building "certainty."
Today, we officially present ComboLite
, a new plugin framework that makes "certainty" its highest design principle. It's not a patch or an improvement on existing solutions but a complete reinvention based on official Android public APIs. Its core promise is simple:
A next-generation Android plugin framework, born for Jetpack Compose, 100% compliant with official APIs, and achieving 0 Hooks & 0 Reflection.
Core Philosophy: Coexisting with the Platform, Not Fighting It
The architectural philosophy of ComboLite
is a clean break from all the "black magic" of the past. We believe a framework's vitality comes from its harmonious coexistence with the platform ecosystem, not a continuous, fragile confrontation.
All features are rigorously built on the ClassLoader
delegation mechanism and the Component Proxy pattern, both explicitly recommended in the official Android documentation. This "return to the right path" offers unparalleled long-term value:
-
Forward Compatibility: By not relying on any non-public APIs (
@hide
/@UnsupportedAppUsage
),ComboLite
is naturally compatible with all Android versions from 7.0 (API 24) to future releases, completely eliminating the nightmare of compatibility issues caused by system upgrades. - Predictable Behavior: Every action of the framework is built on public, stable APIs. Developers can clearly predict its operational logic, making the entire lifecycle controllable and predictable.
A Modern Core for New-Era Android Development
ComboLite
not only achieves ultimate stability but also fully embraces modern Android development paradigms in its internal implementation.
1. A Reactive, Thread-Safe State Management Hub
The framework's core is the singleton PluginManager
. We've chosen a reactive architecture based on kotlinx.coroutines.flow.StateFlow
to manage the state of the entire plugin environment.
// in com/combo/core/runtime/PluginManager.kt
object PluginManager {
// Framework initialization state machine
private val _initState = MutableStateFlow(InitState.NOT_INITIALIZED)
val initStateFlow: StateFlow<InitState> = _initState.asStateFlow()
// Runtime info for loaded plugins, keyed by PluginId
private val _loadedPlugins = MutableStateFlow<Map<String, LoadedPluginInfo>>(emptyMap())
val loadedPluginsFlow: StateFlow<Map<String, LoadedPluginInfo>> = _loadedPlugins.asStateFlow()
// Instantiated plugin entry classes, keyed by PluginId
private val _pluginInstances = MutableStateFlow<Map<String, IPluginEntryClass>>(emptyMap())
val pluginInstancesFlow: StateFlow<Map<String, IPluginEntryClass>> = _pluginInstances.asStateFlow()
// ...
}
This design offers thread safety, data consistency, and declarative subscriptions, making it perfect for building highly responsive management UIs with modern frameworks like Jetpack Compose.
2. An Async-First Architecture with a Robust Coroutine Scope
Plugin installation, updates, and loading are I/O-intensive operations. PluginManager
maintains a dedicated coroutine scope for the framework's background tasks:
// in com/combo/core/runtime/PluginManager.kt
private val managerScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
The SupervisorJob
is crucial here. It ensures that if one plugin's loading task fails with an exception, it won't cancel the entire managerScope
, thus not affecting other ongoing or subsequent plugin operations.
3. Native, Seamless Support for Jetpack Compose
ComboLite
's support for Compose is not an afterthought; it's part of its core design.
-
UI Entry Point is a
@Composable
: The UI contract between a plugin and the framework,IPluginEntryClass.Content()
, is itself a@Composable
function, making the plugin's UI definition intuitive and pure. -
Transparent Merged Resources: This is the key to seamless Compose support.
PluginResourcesManager
creates aResources
object that aggregates the resource paths of the host and all loaded plugins. By overriding thegetResources()
method in the host'sBaseHostActivity
, the entireActivity
'sContext
environment uses this mergedResources
object by default.
// in com/combo/core/component/activity/BaseHostActivity.kt
override fun getResources(): Resources {
// Returns the merged Resources object managed by PluginResourcesManager
return PluginManager.resourcesManager.getResources() ?: super.getResources()
}
Therefore, when you call stringResource(R.string.some_string)
or painterResource(R.drawable.some_image)
in a plugin's Composable function, Compose's resource resolution mechanism can find it in the same merged Resources
object, regardless of its origin.
Production-Grade Reliability: Smart Fusing and Extensible Exception Handling
A production-grade framework must face various runtime exceptions head-on. ComboLite
not only provides powerful default protection mechanisms but also empowers developers to customize advanced handling strategies.
1. Default "Smart Fusing" Mechanism
An app getting stuck in an infinite crash loop due to a single plugin's defect is a nightmare. ComboLite
's "fusing" mechanism offers an elegant solution.
-
Precise Signal: When a
PluginClassLoader
can't find a class anywhere, it throws aPluginDependencyException
. This custom exception is the unique, precise signal to trigger the fuse, carrying theculpritPluginId
. -
Global Sentinel
PluginCrashHandler
: The framework registers itself as the application'sThread.defaultUncaughtExceptionHandler
. -
Accurate Target Identification:
PluginCrashHandler
recursively traverses the exception chain, specifically looking forPluginDependencyException
instances. -
Persistent "Self-Healing": Once the fusing signal is identified,
PluginManager.setPluginEnabled(..., false)
modifies theenabled
attribute of the corresponding plugin inplugins.xml
. This means that when the user restarts the app, the faulty plugin will be automatically skipped, allowing the app to start normally.
2. Customizable Crash Handling with IPluginCrashCallback
A one-size-fits-all fusing approach doesn't suit all business scenarios. ComboLite
addresses this with the IPluginCrashCallback
interface, allowing developers to completely take over the crash handling logic.
// in com/combo/core/security/crash/IPluginCrashCallback.kt
interface IPluginCrashCallback {
fun onClassCastException(info: PluginCrashInfo): Boolean = false
fun onDependencyException(info: PluginCrashInfo): Boolean = false
fun onResourceNotFoundException(info: PluginCrashInfo): Boolean = false
fun onApiIncompatibleException(info: PluginCrashInfo): Boolean = false
fun onOtherPluginException(info: PluginCrashInfo): Boolean = false
}
Developers can implement this interface and register it. When PluginCrashHandler
catches an exception, it will first call the developer-registered callback. Returning true
means the exception is handled, and the framework will not execute the default fusing logic. This transforms ComboLite
's exception handling from a simple "circuit breaker" into a highly programmable, intelligent "disaster recovery control center."
Part 4: Deconstructing the Ingenious Architecture Behind "0 Hooks"
This section will serve as ComboLite
's in-depth technical white paper, diving directly into the framework's source code to deconstruct the core pillars that support its stable operation.
High-Level Architecture
ComboLite
v2.0 adopts a concise and powerful micro-kernel design. An internal context (PluginFrameworkContext
) holds all the core managers, each with its own responsibilities, which are coordinated and exposed externally by a single commander, PluginManager
.
graph TD
subgraph "Host App & System"
HostApp[Host App Code] -- Calls API --> PM(PluginManager)
AndroidSystem[Android System] -- Interacts with --> HostProxies["Host Proxy Components<br>(HostActivity, HostService...)"]
end
subgraph "ComboLite Core Services"
PM -- Coordinates --> Installer(InstallerManager)
PM -- Coordinates --> ResManager(PluginResourcesManager)
PM -- Coordinates --> ProxyM(ProxyManager)
PM -- Coordinates --> DepManager(DependencyManager)
PM -- Coordinates --> Lifecycle(PluginLifecycleManager)
PM -- Coordinates --> Security(Security Managers)
end
subgraph "Runtime & Data State"
OnDiskState["On-Disk State<br>plugins.xml, APKs"]
InMemoryState["In-Memory State<br>Loaded Plugins, ClassLoaders, Instances"]
ClassIndex["Global Class Index<br>Map<Class, PluginID>"]
DepGraph["Dependency Graph<br>(Forward & Reverse)"]
MergedRes["Merged Resources"]
end
-
PluginManager
(Main Controller): The framework's supreme commander and sole singleton entry point. -
InstallerManager
(Installer): Manages the installation, update, and uninstallation processes, and maintains theplugins.xml
metadata registry. -
PluginLifecycleManager
(Lifecycle Manager): Responsible for core lifecycle operations like plugin loading, instantiation, launching, and unloading. -
ResourceManager
(Resource Manager): Provides a unifiedResources
object by merging the resources of all loaded plugins. -
ProxyManager
(Dispatcher): Manages the proxy components on the host side and correctly dispatches system intents to the corresponding plugin components. -
DependencyManager
(Dependency Manager): Dynamically analyzes inter-plugin dependencies, constructing a graph that provides data support for "chain restart" and cross-plugin class lookups. -
Security System
: A cluster of managers for signature validation, permission checks, and authorization.
Core Mechanism: Non-Invasive ClassLoader
Delegation & Dynamic Dependency Graph
This is the cornerstone of ComboLite
's architecture and the technical source of its "smart dependency" feature.
Step 1: Building a Global Class Index—Reducing O(n)
Class Search to O(1)
In traditional solutions, finding a class across plugins is an O(n)
linear search. ComboLite
fundamentally solves this. During plugin installation, the framework uses the powerful org.jf.dexlib2.DexFileFactory
library to efficiently parse the binary structure of DEX files and generate a class_index
file. At load time, this index is loaded into a global ConcurrentHashMap<ClassName, PluginId>
. This preprocessing step trades a one-time cost at load time for O(1)
complexity class location capability throughout the runtime.
Step 2: PluginClassLoader
—The Art of "Limited Responsibility" and "Proactive Delegation"
The PluginClassLoader
instance created for each plugin strictly follows Java's ClassLoader
parent-delegation model. Its findClass(name: String)
method's logic is a form of "directed, precise horizontal delegation after the standard process fails":
// in com/combo/core/runtime/loader/PluginClassLoader.kt
class PluginClassLoader(
private val pluginFinder: IPluginFinder?,
) : DexClassLoader(...) {
override fun findClass(name: String): Class<*> {
try {
// Step A: Strictly adhere to parent-delegation
return super.findClass(name)
} catch (e: ClassNotFoundException) {
// Step B: Only when the standard process fails, initiate horizontal delegation.
val result = pluginFinder?.findClass(name, this)
if (result != null) {
return result
}
// Step C: If delegation also fails, throw a specific exception for fusing
throw PluginDependencyException(...)
}
}
}
The brilliance of this design is that it doesn't pollute the core responsibility of the ClassLoader
. It outsources the complex problem of "where to find" to the DependencyManager
.
Step 3: DependencyManager
—The Smart Hub for "Arbitration, Recording, and Loading"
When DependencyManager
receives a class lookup delegation, it performs an atomic sequence of operations:
- Query Arbitration: It accesses the global
classIndex
to find the target class withO(1)
complexity. - Dynamic Dependency Recording: Once it determines that plugin
A
needs a class from pluginB
, it immediately updates its internal forward (dependencyGraph
) and reverse (dependentGraph
) dependency graphs. - Directed Loading: It gets plugin B's
PluginClassLoader
and calls an internal method that does not trigger delegation again to load the class.
This closed loop of "pre-indexing -> standard loading -> delegation on failure -> arbitration & recording -> directed loading" is built entirely on Java's ClassLoader
mechanism, achieving zero-hack, high-performance, fully dynamic dependency management.
Core Mechanism: "Chain Restart" Based on the Reverse Dependency Graph
Hot updates are essentially about replacing modules at runtime, which can easily lead to state inconsistencies. ComboLite
's "chain restart" provides a deterministic safety guarantee.
When a hot update is triggered for a plugin, the reloadPluginWithDependents
method is invoked:
- Query Reverse Dependencies: It calls
dependencyManager.findDependentsRecursive(pluginId)
. This method performs a Depth-First Search (DFS) on the dynamically builtdependentGraph
(reverse dependency graph) to find all plugins that directly or indirectly depend on the updated plugin. - Formulate Execution Plan: It merges the search result with the plugin itself to form a complete "restart set."
- Strict Reverse Unload and Forward Load: This is the core of ensuring state consistency.
PluginManager
first unloads all plugins in the "restart set" in reverse dependency order, cleaning up all their runtime resources. It then reloads them in the original dependency order, ensuring the entire dependency chain is updated consistently.
The v2.0 Security System: Permission & Authorization
This is the most significant architectural upgrade in v2.0, designed to provide enterprise-grade security.
- Annotation-based Declaration: The framework uses the
@RequiresPermission
annotation to declare the required permission level (HOST
orSELF
) for sensitive APIs. - Caller Tracking: When a plugin calls these APIs, the
checkApiCaller
extension function accurately identifies the calling plugin's ID by analyzing the call stack (Thread.currentThread().stackTrace
). - Static Permission Check:
AuthorizationManager
first performs a static check based on strict rules (e.g., does the caller's signature match the host's?). - Dynamic User Authorization: If the static check fails,
AuthorizationManager
forwards the request to anIAuthorizationHandler
implementation, which can launch a UI to display the operation details to the user and request their consent.
This closed-loop process of "Annotation Declaration -> Static Check -> Dynamic Authorization" builds a robust and flexible security defense for ComboLite
.
Part 5: From 0 to 1: A Complete Tutorial on Building a "Pluggable" App
Now, we'll guide you through a complete plugin development cycle from scratch, building a "pluggable" dynamic application with ComboLite
.
Step 1: Laying the Foundation — Project Initialization and Dependency Configuration
First, create a new Android project. We highly recommend using Version Catalog (libs.versions.toml
) to manage dependencies.
-
Define dependencies in
gradle/libs.versions.toml
:
[versions] combolite = "2.0.0" # We strongly recommend using the latest stable version aar2apk = "1.1.0" [libraries] combolite-core = { group = "io.github.lnzz123", name = "combolite-core", version.ref = "combolite" } [plugins] combolite-aar2apk = { id = "io.github.lnzz123.combolite-aar2apk", version.ref = "aar2apk" }
-
Apply the packaging plugin in the root
build.gradle.kts
:
// in your project's root /build.gradle.kts plugins { alias(libs.plugins.combolite.aar2apk) }
-
Add the core library to the host
:app
module'sbuild.gradle.kts
:
// in your :app/build.gradle.kts dependencies { implementation(libs.combolite.core) }
Step 2: Building a Solid Base — Configuring the "Shell" Host
-
Implement an auto-initializing
Application
:
ComboLite
offers an extremely simple initialization method. Just have yourApplication
class inherit fromBaseHostApplication
, and the framework will automatically handle all the tedious initialization work for you.
// in :app/src/main/java/.../HostApp.kt import com.combo.core.runtime.app.BaseHostApplication class MainApplication : BaseHostApplication() { override fun onFrameworkSetup(): suspend () -> Unit { return { // --- Perform all framework-related configurations here --- // Example: Set the plugin signature validation strategy PluginManager.setValidationStrategy(ValidationStrategy.Insecure) } } }
-
Configure the proxy
Activity
:
To allow plugins to correctly access merged resources, the host'sActivity
needs to inherit fromBaseHostActivity
.
// in :app/src/main/java/.../MainActivity.kt import com.combo.core.component.activity.BaseHostActivity class MainActivity : BaseHostActivity() { /* ... */ }
Step 3: Creating the First "Part" — Developing a Simple "Greeting" Plugin
Create a new plugin module:
In your project, selectFile > New > New Module...
, then chooseAndroid Library
, and name ithello-plugin
.-
Add dependencies for the plugin:
The plugin module's dependency on the core library should becompileOnly
, as these classes will be provided by the host at runtime.
// in :hello-plugin/build.gradle.kts dependencies { compileOnly(libs.combolite.core) }
-
Implement the plugin entry class
IPluginEntryClass
:
This is the core of the plugin. It implements theIPluginEntryClass
interface and is the sole bridge for interaction between the plugin and the framework.
// in :hello-plugin/src/main/java/.../HelloPluginEntry.kt package com.example.helloplugin import androidx.compose.material3.Text import androidx.compose.runtime.Composable import com.combo.core.api.IPluginEntryClass import com.combo.core.model.PluginContext class HelloPluginEntry : IPluginEntryClass { override fun onLoad(context: PluginContext) { println("Plugin [${context.pluginInfo.id}] has been loaded.") } override fun onUnload() { println("Plugin is being unloaded.") } @Composable override fun Content() { Text(text = "Hello from a dynamically loaded Plugin!") } }
-
Configure plugin metadata in the Manifest:
The framework learns the plugin's ID, version, and entry class through itsAndroidManifest.xml
.
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.myplugin" android:versionCode="1" android:versionName="1.0.0"> <application android:label="My First Plugin" android:icon="@drawable/plugin_icon"> <meta-data android:name="plugin.entryClass" android:value="com.example.helloplugin.HelloPluginEntry" /> </application> </manifest>
Step 4: Packaging & Debugging with aar2apk
's One-Click Magic
The aar2apk
Gradle plugin fully automates the complex conversion of an AAR into a functional APK. ComboLite
v2.0 introduces a revolutionary feature: seamless development-time debugging.
-
Declare Plugins in the Project Root
build.gradle.kts
:
This tells theaar2apk
plugin which modules to manage.
// in your project's root /build.gradle.kts aar2apk { modules { module(":hello-plugin") } signing { /* ... configure your signing information ... */ } }
-
Enable Integration in the Host App
build.gradle.kts
:
This turns on the automatic integration feature for debugging.
// in your :app/build.gradle.kts packagePlugins { enabled.set(true) buildType.set(PackageBuildType.DEBUG) pluginsDir.set("plugins") // The directory where APKs are stored within assets }
With this setup, every time you click "Run" or "Debug" in Android Studio, the plugin is automatically compiled, packaged, and embedded into the host's assets
directory, ready for immediate testing.
Step 5: Witness the Magic — Dynamically Loading and Running the Plugin
Now, let's write the host logic to load and run the plugin from the assets
folder during debug builds.
// In MainActivity.kt
import androidx.lifecycle.lifecycleScope
import com.combo.core.runtime.PluginManager
import com.combo.core.utils.installPluginsFromAssetsForDebug
import kotlinx.coroutines.launch
class MainActivity : BaseHostActivity() {
private val pluginId = "com.example.myplugin"
private var pluginEntry by mutableStateOf<IPluginEntryClass?>(null)
private var isLoading by mutableStateOf(true)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { /* ... render UI based on isLoading and pluginEntry ... */ }
initialize()
}
private fun initialize() {
lifecycleScope.launch {
if (BuildConfig.DEBUG) {
// Debug mode: Force reinstall on every launch to ensure the latest code
installPluginsFromAssetsForDebug()
PluginManager.loadEnabledPlugins()
PluginManager.launchPlugin(pluginId)
}
// Check for the plugin instance
pluginEntry = PluginManager.getPluginInstance(pluginId)
isLoading = false
}
}
}
Now, run your app
module. The app will launch, and “Hello from a dynamically loaded Plugin!” will magically appear on your screen!
Part 6: Mastering ComboLite - Advanced Guides and Best Practices
Packaging Strategies: compileOnly
is the Golden Rule
For plugin development, compileOnly
is the norm, implementation
is the exception. All common libraries you expect the host to provide (like comboLite-core
, Kotlin
, AndroidX
, OkHttp
, etc.) should use compileOnly
to keep plugins lightweight and avoid dependency conflicts.
While the aar2apk
plugin provides capabilities like includeAllDependencies()
to bundle dependencies, this should be considered a fallback for special cases, not a routine operation.
Full Support for the Four Major Components
ComboLite
provides comprehensive support for Android's four major components (Activity, Service, BroadcastReceiver, ContentProvider) through a powerful proxy pattern.
A crucial concept to understand is the Proxy Context. Plugin Activities
and Services
are not native components but regular objects. You cannot directly call standard Context
methods like this.finish()
. Instead, you must use the proxyActivity
or proxyService
property provided by the base classes (BasePluginActivity
, BasePluginService
) to access all standard Context
functionalities.
The setup for each component follows a similar pattern:
- Host-Side Setup: Create an empty proxy component (e.g.,
HostActivity
), register it in the host'sAndroidManifest.xml
, and inform thePluginManager
. - Plugin-Side Implementation: Inherit from the provided base class (e.g.,
BasePluginActivity
) and implement your logic, always using theproxy
object for context-related calls. - Final Invocation: Use the provided extension functions like
context.startPluginActivity(...)
to launch your plugin components.
A Tour of the Core APIs (PluginManager
)
The PluginManager
is the single public API entry point of the framework, providing comprehensive control. Key functions include:
-
Status Query & Listening:
initStateFlow
,loadedPluginsFlow
,getPluginInstance(pluginId)
. -
Runtime Lifecycle Control:
launchPlugin(pluginId)
,unloadPlugin(pluginId)
,loadEnabledPlugins()
. -
Service Discovery:
getInterface(interfaceClass, className)
for ultimate cross-plugin decoupling. -
Dependency Query:
getPluginDependentsChain(pluginId)
("Who depends on me?") andgetPluginDependenciesChain(pluginId)
("What do I depend on?").
Part 7: The Future is Now: The Game-Changer 2.0 Release
In the 1.0 era of ComboLite
, we accomplished our core mission: to provide a rock-solid, future-proof foundation. However, a truly modern framework cannot stop at being merely "stable and usable."
Two major roadblocks have plagued plugin developers: a hellish debugging experience and a fragile security model. ComboLite
2.0 is here to fix that, making the crucial leap from a "stable foundation" to an "efficient and secure productivity platform."
A Revolution in Developer Experience
The build-time automatic integration (packagePlugins
feature) described in the tutorial is the most exciting feature of ComboLite
2.0. It completely changes your plugin development workflow, reducing debugging cycles from minutes to seconds.
An Enterprise-Grade Security System
ComboLite
2.0 introduces a complete enterprise-grade security architecture, built on the three pillars of a permission system, validation strategies, and crash handling. This system is what allows ComboLite
to support an open ecosystem, like a "plugin store," with confidence.
-
Pillar 1: The Permission System (The Law): Through the
@RequiresPermission
annotation, we've set clear "access rules" for all sensitive APIs within the framework (PermissionLevel.HOST
andPermissionLevel.SELF
). -
Pillar 2: Validation Strategies (The Gatekeeper):
ComboLite
2.0 gives the decision-making power to you. ThroughPluginManager.setValidationStrategy()
, you can set three different "security check" strategies:-
ValidationStrategy.Strict
: Continues the 1.0 strategy; the signature must match the host's. -
ValidationStrategy.UserGrant
: If the signature doesn't match, it automatically launches an activity to ask the user for permission. -
ValidationStrategy.Insecure
: Disables all validation, for debugging only.
-
-
Pillar 3: The Crash Handler (The Fusebox): The
PluginCrashHandler
has been fully enhanced in 2.0 with a new crash UI with syntax highlighting and a tiered callback mechanism for fine-grained exception handling.
Part 8: The Road Ahead
ComboLite
is an active open-source project, and development doesn't stop at 2.0. While the core is more stable and feature-rich than ever, we are always looking for ways to improve.
Our immediate focus is on continuing to refine the framework, fix bugs, and enhance the existing features based on community feedback. One potential area of exploration for embracing an even more open ecosystem is the introduction of a Whitelist Mode for validation. This would allow a host to configure a set of trusted public key fingerprints, enabling the loading of third-party plugins from a pre-approved list without prompting the user every time.
We believe in community-driven development. If you have ideas, encounter pain points, or have suggestions for the future, your feedback is crucial.
Conclusion: It's Time to Build Dynamic Applications in a Modern Way
ComboLite
provides a "return to standards" option for the Android dynamic landscape. It proves that we can build a powerful, user-friendly, and truly future-proof plugin framework without resorting to any hacking. We firmly believe that a great framework should be like a fine Swiss Army knife: not only powerful and reliable but also a pleasure to use every time.
We've paved the way for you. Now, we invite you to embark on this journey and experience this unprecedented development bliss.
Call to Action
-
Project Source Code: https://github.com/lnzz123/ComboLite
- If you appreciate the design philosophy and engineering practices of
ComboLite
, please give us a Star! Your support is our greatest motivation for continuous iteration.
- If you appreciate the design philosophy and engineering practices of
-
Sample App Download: Click here to download the APK directly
- Install the sample app and experience what a "pluggable everything" application is like.
-
Communication & Contribution:
- Have any questions, suggestions, or found a bug? We look forward to in-depth technical discussions with you in GitHub Issues!
Top comments (0)