Mobile Plugin Architecture

This document provides a comprehensive analysis of the pluginization of the Open edX mobile application for iOS and Android platforms.

Motivation

The primary goal of introducing a plugin system into our architecture is to enhance extendability and eliminate the necessity of integrating company-specific features directly into the base app repository.
By modularizing functionalities such as analytics, push notifications, and eCommerce into plugins, developers gain the flexibility to configure their own versions of the application.
Furthermore, this architecture allows for the development of custom plugins for analytics, eCommerce, and other features, thus promoting a more adaptable and scalable mobile application framework.

Key Objectives

  1. Extendability: Facilitate the easy addition of new features without altering the core application.

  2. Customization: Allow companies to tailor the application to their specific needs by configuring or developing their own plugins.

  3. Separation of Concerns: Isolate different functionalities into plugins to simplify maintenance and improve code clarity.

  4. Scalability: Support the growth of the application by enabling the seamless integration of new plugins as requirements evolve.

Architecture Overview

Plugin System Design

  • Plugin Interface: Defines the standard methods and properties that all plugins must implement.

  • Plugin Manager: Handles the loading, initialization, and management of plugins within the application.

  • Core Application: The base application that remains lightweight, focusing on essential functionalities and delegating extended features to plugins.

Plugin Types

  1. Analytics Plugins: Enable developers to integrate different analytics providers based on their preferences.

  2. Push Notification Plugins: Allow the use of various push notification services.

  3. eCommerce Plugins: Provide customizable eCommerce solutions tailored to a specific framework and business needs.

Implementation Options

iOS

The initial idea was to use the existing Core module from the mobile apps as a foundation and toolkit for creating plugins. However, extensive research revealed that this approach is problematic when the Core module is included as a file dependency in the project and as an external dependency in the plugin.

When integrating the plugin into the main project, the Core module/dependency conflicts arise due to non-binary compatibility, as they are built from different sources.
To resolve this issue, the Core module must be moved out of the project to a separate repository and distributed with strict versioning.

While this solution ensures consistency, it complicates the development of new functionality for the Base App. Adding a small utility needed by two other modules would require making changes in a different repository and building a release.

To avoid complicating Base App development, we propose creating a new type of module that will be lower than Core in the hierarchy. This module will reside in its own separate repository and contain only the necessary tools, abstractions, and dependencies for creating plugins.


ToolingLib and Abstraction

Here is an example of a new entity/module that can serve these purposes.

https://github.com/volodymyr-chekyrta/iOSReusableToolingLib

It is published on GitHub and built as a Swift Package, which allows easy versioning via GitHub Releases and plugging it into other projects (plugins) via Swift Package Manager.

This Swift Package provides basic dependencies that are needed for the development of any feature.

image-20240711-104409.png

And its own set of abstractions and tools.

image-20240711-104433.png


After applying these changes, the application architecture will look like this:

Before

 

After

 

Since Core with Base App and MyPlugin use the same version of ToolingModule connected via Swift Package Manager, this does not cause duplication or conflict in dependencies.

Example of a framework(plugin) based on abstraction

Here is an example of how plugins can use abstractions and tools from the Tooling package:

This plugin provides itself as a Swift Package with implementation and depends on ToolingLib.

Plugin Integration

Now, let’s integrate our tooling and plugin into the project.

XCode → Project → Package Dependencies.

We can see that XCode has extracted all dependencies and there is no duplication of the ReusableToolingLib.

Let's test this by creating some entity that requires dependency on abstractions.

And provide implementation from the plugin (In a real application, this should be done by dependency injection).

XCode → Build → Run

Logs:

Required effort

To apply this approach, we need to do some refactoring:

  1. Changes in dependencies:
    Alamofire - Core -> ToolingFramework
    KeychainSwift - Core -> OpenEdX (app module)
    SwiftUIIntrospect - Core -> ToolingFramework
    Kingfisher - Core -> ToolingFramework
    Swinject - Core -> ToolingFramework
    YoutubePlayerKit - Core -> Course
    GoogleSignIn - Core -> Authorization
    Facebook - Core -> Authorization
    BranchSDK - Core -> Course or OpenEdX (app module)

  2. Move part of Core/Extensions to the ToolingFramework

  3. Move part of Core/Network to the ToolingFramework

  4. Move AnalyticsManager.AnalyticsService to the ToolingFramework

  5. Publish ToolingFramework

  6. Add ToolingFramework to the Core

  7. Fix imports

  8. Fix errors and warnings

Drawbacks of this approach:

  • We must maintain another repository and keep track of its dependencies and releases.

Advantages of this approach:

  • We get to create plugins and keep them in separate repositories.

iOS (alternative/straightforward solution)

To simplify the pluginization process, we can continue using the current monorepo approach.
This allows us to add plugins to the project without integrating them directly into the main app module, this way the main module will be lightweight but extensible if desired.

For example:

This approach is convenient because we can still rely on our Core module and manage everything in 1 repository.

But this approach has some disadvantages:

  • We must accept and maintain ALL community plugins to the repo or add and maintain only “official“ ones developed by Axim; in this case, community members will have to build their own plugins in their forks, making it extremely difficult for other community members to use them.

  • This can increase the size of the repository and make it harder to work with it.

Android

The Android developer tools provide a more straightforward method for publishing modules as separate libraries, followed by their subsequent publication. However, to maintain consistency between our applications, let’s follow a similar approach and explore how to implement a plugin system on Android.

ToolingLib and Abstraction

Here is an example of a new Android entity/module containing abstractions and some basic dependencies that we would like to reuse in each module/plugin.

Now it contains two implementation interfaces and one fake network manager.

Let's publish it to the Maven Repository so everyone can easily pull and use this dependency.

Maven Repository is a storage location where project artifacts and dependencies are stored and retrieved. It can be local (on the developer's machine), central (hosted by the Maven community), or remote (hosted by third parties or within an organization). Maven uses these repositories to manage project dependencies during the build process.

Official Android documentation provides us with clear instructions how to publish libraries:

Step 1. Add the maven-publish plugin.

Step 2. Paste publishing instructions.

Step 3. (Optional) Add credentials for your remote Maven Repository.
We'll skip this step and use MavenLocal for this article, but it should be published to Maven Central in a real scenario.

Step 4. Publish.

Run the publish script added by the maven-publish plugin.

Example of a library(plugin) based on abstraction

Here is an example of how plugins can use abstractions and tools from the Tooling lib:

Step 1. Add base dependency to the plugin.

Step 2. Provide implementation for the abstraction.

Step 3. Publish.

Plugin Integration

Now, let’s integrate our tooling and plugin into the project.

Step 1. Add base dependency to the Core module.

Step 2. Add plugin dependency to the App module.

Step 3. Provide implementation for the abstraction.

Step 4. Build & Run.

Logs:

Required effort

To apply this approach, we need to do some refactoring:

  1. Changes in dependencies:
    Compose - Core → ToolingLib
    Retrofit - Core → ToolingLib
    Koin - Core → ToolingLib
    Coil - Core → ToolingLib
    Jetpack Lifecycle - Core → ToolingLib

  2. Move part of Core/Extensions to the ToolingLib

  3. Move org.openedx.app.analytics.Analytics abstraction to the ToolingLib.

  4. Publish ToolingLib on GitHub and Maven

  5. Add ToolingLib to the Core

  6. Fix imports

  7. Fix errors and warnings