DEV Community

Cover image for The "0 Hook" Android Architecture Your Team Has Been Dreaming Of (Especially for Jetpack Compose)
本性
本性

Posted on

The "0 Hook" Android Architecture Your Team Has Been Dreaming Of (Especially for Jetpack Compose)

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:

  1. 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."
  2. 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.
  3. 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.
  4. 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 calls startActivity(), 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 the Intent 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 hooking LayoutInflater 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 and onResume 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()
    // ...
}
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

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 a Resources object that aggregates the resource paths of the host and all loaded plugins. By overriding the getResources() method in the host's BaseHostActivity, the entire Activity's Context environment uses this merged Resources 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()
}
Enter fullscreen mode Exit fullscreen mode

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 a PluginDependencyException. This custom exception is the unique, precise signal to trigger the fuse, carrying the culpritPluginId.
  • Global Sentinel PluginCrashHandler: The framework registers itself as the application's Thread.defaultUncaughtExceptionHandler.
  • Accurate Target Identification: PluginCrashHandler recursively traverses the exception chain, specifically looking for PluginDependencyException instances.
  • Persistent "Self-Healing": Once the fusing signal is identified, PluginManager.setPluginEnabled(..., false) modifies the enabled attribute of the corresponding plugin in plugins.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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • PluginManager (Main Controller): The framework's supreme commander and sole singleton entry point.
  • InstallerManager (Installer): Manages the installation, update, and uninstallation processes, and maintains the plugins.xml metadata registry.
  • PluginLifecycleManager (Lifecycle Manager): Responsible for core lifecycle operations like plugin loading, instantiation, launching, and unloading.
  • ResourceManager (Resource Manager): Provides a unified Resources 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(...)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Query Arbitration: It accesses the global classIndex to find the target class with O(1) complexity.
  2. Dynamic Dependency Recording: Once it determines that plugin A needs a class from plugin B, it immediately updates its internal forward (dependencyGraph) and reverse (dependentGraph) dependency graphs.
  3. 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:

  1. Query Reverse Dependencies: It calls dependencyManager.findDependentsRecursive(pluginId). This method performs a Depth-First Search (DFS) on the dynamically built dependentGraph (reverse dependency graph) to find all plugins that directly or indirectly depend on the updated plugin.
  2. Formulate Execution Plan: It merges the search result with the plugin itself to form a complete "restart set."
  3. 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.

  1. Annotation-based Declaration: The framework uses the @RequiresPermission annotation to declare the required permission level (HOST or SELF) for sensitive APIs.
  2. 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).
  3. Static Permission Check: AuthorizationManager first performs a static check based on strict rules (e.g., does the caller's signature match the host's?).
  4. Dynamic User Authorization: If the static check fails, AuthorizationManager forwards the request to an IAuthorizationHandler 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.

  1. 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" }
    
  2. Apply the packaging plugin in the root build.gradle.kts:

    // in your project's root /build.gradle.kts
    plugins {
        alias(libs.plugins.combolite.aar2apk)
    }
    
  3. Add the core library to the host :app module's build.gradle.kts:

    // in your :app/build.gradle.kts
    dependencies {
        implementation(libs.combolite.core)
    }
    

Step 2: Building a Solid Base — Configuring the "Shell" Host

  1. Implement an auto-initializing Application:
    ComboLite offers an extremely simple initialization method. Just have your Application class inherit from BaseHostApplication, 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)
            }
        }
    }
    
  2. Configure the proxy Activity:
    To allow plugins to correctly access merged resources, the host's Activity needs to inherit from BaseHostActivity.

    // 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

  1. Create a new plugin module:
    In your project, select File > New > New Module..., then choose Android Library, and name it hello-plugin.

  2. Add dependencies for the plugin:
    The plugin module's dependency on the core library should be compileOnly, as these classes will be provided by the host at runtime.

    // in :hello-plugin/build.gradle.kts
    dependencies {
        compileOnly(libs.combolite.core)
    }
    
  3. Implement the plugin entry class IPluginEntryClass:
    This is the core of the plugin. It implements the IPluginEntryClass 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!")
        }
    }
    
  4. Configure plugin metadata in the Manifest:
    The framework learns the plugin's ID, version, and entry class through its AndroidManifest.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.

  1. Declare Plugins in the Project Root build.gradle.kts:
    This tells the aar2apk plugin which modules to manage.

    // in your project's root /build.gradle.kts
    aar2apk {
        modules {
            module(":hello-plugin")
        }
        signing { /* ... configure your signing information ... */ }
    }
    
  2. 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
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Host-Side Setup: Create an empty proxy component (e.g., HostActivity), register it in the host's AndroidManifest.xml, and inform the PluginManager.
  2. Plugin-Side Implementation: Inherit from the provided base class (e.g., BasePluginActivity) and implement your logic, always using the proxy object for context-related calls.
  3. 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?") and getPluginDependenciesChain(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 and PermissionLevel.SELF).
  • Pillar 2: Validation Strategies (The Gatekeeper): ComboLite 2.0 gives the decision-making power to you. Through PluginManager.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.
  • 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)