Navigating through runtime only modules and thinking outside of the box with androidx.startup and koin (decoupled multi-module projects)

androix.startup is a new addition to the Jetpack suite of libraries. Read more about it here

Koin is a pragmatic lightweight dependency injection framework for Kotlin developers. Written in pure Kotlin using functional resolution only: no proxy, no code generation, no reflection!

Introduction

I’m sure you’ve read a lot about writing good code using best practises like first five object-oriented design(OOD) by Uncle Bob, if you haven’t now would be a good time to quickly touch up on the top S.O.L.I.D: The First 5 Principles of Object Oriented Design as we will make some references to some of these principles.

Problem Statement

Let’s assume you’re working on a project with several modules, either using dynamic feature modules or a runtimeOnly. In both cases the features modules code will not be made available at compile time but instead at runtime (this just means we can’t reference any code defined in the our dynamic or runtime only modules). Either way at some point you have to solve a certain number of challenges:

  • Accessing components from your feature modules (dynamic or runtime only)
  • Initialization of sub-components of your modules without explicitly managing this yourself

Sample Structure

There are a few ways to expose or interact with modules you may have at runtime, let’s briefly discuss each of them using a sample project with the following structure:

Let’s set up a few case scenarios which we need solutions to, at the very least we want to be able to get a static fragment class definition and a bunch of intents to launch activities or services .e.t.c

We’ll only focus on the following modules:

  • discover-movie (feature module that is only available at run time)
  • discover-series (feature module that is only available at run time)
  • navigation (module for handling how to navigate between modules)

Reflection

Reflection is commonly used by programs which require the ability to examine or modify the runtime behaviour of applications running in the Java virtual machine. This is a relatively advanced feature and should be used only by developers who have a strong grasp of the fundamentals of the language. With that caveat in mind, reflection is a powerful technique and can enable applications to perform operations which would otherwise be impossible. Source

While reflection is powerful it does have some drawbacks and limitations which you can read more about at the source above and here

Example:

Let’s define some extensions to manage some class loading stuff (full code extensions code can be found here):

Now to define our components targets (full code extensions code can be found here):

Each of our target have to know the fully qualified package name of the component we need to access, as you can imagine if we change class names, or minify then we might end up mismatching class package names.

Accessing our components:

Model to hold fragment definition and params
Accessing navigation targets

In the sample above we’re calling NavigationTargets and getting specific components specifically in this case only fragments in the form of Class<out Fragment> because I’m using the default fragment factory as the constructors for each of the feature fragments are empty

Service Loader

A service is a well-known set of interfaces and (usually abstract) classes. A service provider is a specific implementation of a service. The classes in a provider typically implement the interfaces and subclass the classes defined in the service itself. Service providers can be installed in an implementation of the Java platform in the form of extensions, that is, jar files placed into any of the usual extension directories. Providers can also be made available by adding them to the application’s class path or by some other platform-specific means. Source

Typically using a service loader in android as is introduces some performance penalties unless you use R8 shrinker to optimize/rewrite the implementation to avoid the performance penalties. Since service loader is no longer supported by R8 for dynamic feature modules we won’t be covering this topic

Dependency Injection

Dependency injection is a programming technique that makes a class independent of its dependencies. It achieves that by decoupling the usage of an object from its creation. This helps you to follow SOLID’s dependency inversion and single responsibility principles. Source

Example:

Applying the Interface Segregation rule we can define an interface in our navigation module that we’ll use to expose fragments or intents similar to our Reflection approach.

Notice that we’re not defining more than we need in this contract, this is because we’d rather define what we need and extend this interface if we need to access more modules. Full code

Define navigation targets that we’ll use in various parts of the system:

Contract for navigation targets, we don’t really need to define invoke at this point, it could be an extension function or whatever you want to use. Full code
Take not that each of the defined navigation targets here extend the INavigationFeature. Full code
An extension that we’ll showcase later to get fragment definitions

Let’s hook up the Provider for each of our navigation targets in the appropriate feature module and register them in our dependency injector.

discover-show feature implements contract from navigation module and provides details of implementation. Full code

Let’s continue to define our DI modules

Note how we’re setting our featureModule is set to provide a type of NavShow.Provider. Full code

DynamicFeatureModuleHelper is just a helper class that implements the detail of how how the koin should load or unload modules

Registering the specific module to the application global dependency injection registry, since this is a run time only module we can leverage androidx.startup to load this features modules dependencies.

Feature initializer to initialize our declared DI modules 💁 Full code
Manifest for FeatureInitializer, this exists in our feature module manifest. Full code

Finally accessing a component from anywhere within the application 😋

Full code

Conclusion

What we’ve built here is something that enforces good separation of concerns and low cohesion, by not only having separate feature modules but by also exposing contracts of components.

I believe there’s nothing new about any of the approaches demonstrated here, but simply an alternative look approach specifically tailored to Koin in the DI example.

The concept should be the same regardless of whichever DI engine you use, but obviously constraints may exist with your selected library of choice which will influence your design. I hope this was informative and will help you in future, feel free to leave some suggestions, thoughts or criticism 😃

Extras

I personally have not jumped onto the dynamic feature delivery band wagon yet, so I’m not sure if androidx.startup would automatically invoke the the provider intializer for a module that is installed at a later point in the application lifecycle

Freelancer and open source developer, growing my skills one byte a time :)