Navigating through runtime only modules and thinking outside of the box with androidx.startup and koin (decoupled multi-module projects)
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!
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.
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
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 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
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:
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
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 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
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.
Define navigation targets that we’ll use in various parts of the system:
Let’s hook up the Provider for each of our navigation targets in the appropriate feature module and register them in our dependency injector.
Let’s continue to define our DI modules
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.
Finally accessing a component from anywhere within the application 😋
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 😃
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