Recently, I have spent some time playing around with Anvil. It is a Kotlin compiler plugin to make dependency injection with Dagger a bit easier.
In this blog post I will showcase — building a simple plugin system — how Dagger+Anvil make a great couple to easily decouple your code.
What is a plugin system?
According to wikipedia, a plugin “is a software component that adds a specific feature to an existing computer program”. And that is not a bad definition. In the scope of this post, I would say that “[…] it adds a specific feature to an existing software component”.
One of the desired requirements of a plugin system is that to add a plugin, one does not need to touch any code outside your plugin. And to show case our Dagger-powered plugin system, we are going to build a simple app that will have an Activity with an options menu, where its items can be added as plugins.
Building the plugin system
How to define a plugin
Plugins are generally grouped by type, defined by a common contract, this is the one we’ll define.
interface MainMenuPlugin {
fun menuItemId(): Int
fun menuItemName(): String
fun onMenuItemClicked()
}
where:
menuItemId
will return the ID of the menu item, this should be unique for all pluginsmenuItemName
will return the name of the pluginonMenuItemClicked
will be called when the user taps in the menu item
How to get a Plugin
Plugins of a certain type will be registered and held inside a plugin provider, that we will define as follows.
interface PluginProvider<T> {
/**
* @return the list of plugins of type <T>
*/
fun getPlugins(): List<T>
}
And so in our case, we’ll have a plugin provider of type PluginProvider<MainMenuPlugin>
that will return the list of MainMenuPlugin
plugins registered with it.
How to use a plugin
We will see later on how we can register the plugins into its provider, but first, let’s take a look at how we will use the plugins in the context of our little app.
class MainActivity : AppCompatActivity() { @Inject lateinit var menuItems: PluginProvider<MainMenuPlugin>
override fun onCreate(savedInstanceState: Bundle?) {...}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menu?.let {
menuItems.getPlugins().forEach { item ->
menu.add(Menu.NONE, item.menuItemId(), Menu.NONE, item.menuItemName())
}
} return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
menuItems.getPlugins().firstOrNull { it.menuItemId() == item.itemId }?.onMenuItemClicked() return super.onOptionsItemSelected(item)
}
- Our
Activity
will depend on the plugin provider,PluginProvider<MainMenuPlugin>
. - When the
menu
is created, we will get all plugins from the provider and programmatically add them to the options menu. - Similarly, when the user taps on a options menu item, we just find the plugin by item ID and call its
onMenuItemClicked
callback on it.
How to register a plugin
So far we have seen how plugins are defined, provided and how we will use them in our sample app. But we have left the meat of this post to the end. And that is how to register the plugins with its provider.
Remember in the beginning we said it is highly desired to have a plugin system that does not require modifying any code outside the plugin itself.
Let’s start by looking at the implementation of our menu item plugin and how to register it. For that, we will use Dagger multibindings, that basically allows to bind multiple objects into a collection. In our case the collection will be a Set<MainMenuPlugin>
.
private class MyMenuItem(...) : MainMenuPlugin {...}
@Module
@ContributesTo(MainScope::class)
class MyMenuItemModule { @Provides
@MainComponentScope
@IntoSet
fun provideMenuItem(...): MainMenuPlugin = MyMenuItem(...)
}
@IntoSet
tells Dagger to bind theMyMenuItem
into aSet<MainMenuPlugin>
collection instead of just providing the individual type.ContributesTo
is part of Anvil, and adds to the magic by avoiding needing to touch any component to add theMenuItemModule
By now, Dagger will take our MyMenuItem
plugin and collect it inside a Set<MainMenuPlugin>
. The PluginProvider
will be the one that depends on the plugins. It is time to see its implementation.
class MainMenuPluginProvider(
private val plugins: Set<MainMenuPlugin>
) : PluginProvider<MainMenuPlugin> { override fun getPlugins(): List<MainMenuPlugin> {
return plugins.toList()
}
}
@Module
@ContributesTo(MainScope::class)
class MainMenuPluginProviderModule { @Provides
@MainComponentScope
fun provideMainMenuPluginProvider(
plugins: Set<@JvmSuppressWildcards MainMenuPlugin>
): PluginProvider<MainMenuPlugin> = MainMenuPluginProvider(plugins)
}
- the
MainMenuPluginProvider
implementation is very straight-forward, it just depends on aSet
of plugins and simply returns them as a list. - The
MainMenuPluginProviderModule
binds theMainMenuPluginProvider
asPluginProvider<MainMenuPlugin>
@JvmSuppressWildcards
annotation tells the Kotlin compiler to omit wildcards for type arguments and not adding it causes Dagger not to find theSet
of plugins. This is a topic for another post, but basically in order for Kotlin to work with Java, in this cases and without the annotation, Kotlin would generate aSet<? extends MainMenuPlugin>
that is not exactly the same asSet<MainMenuPlugin>
. You can read more about this here.
And that’s pretty much it. I personally find this a very simple pattern that opens plenty of opportunities to keep your app decoupled. Notice how adding/removing plugins does not require to touch any code outside the plugin implementation itself — ie. I could remove MyMenuItem
just removing it along with its Dagger module MainMenuPluginProviderModule
.
I hope it was useful. You can find the code of this post in this github.
Note: I believe one could do the same with Hilt’s @InstallIn
feature, but I haven't tried it myself.
Found it interesting? The 👏 👏 👏 andFollow
me to receive more stories.
Don’t forget to leave your 🗨️ bellow with your favourite tip.