Aitor Viana
4 min readSep 27, 2020

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.

This is an image

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 plugins
  • menuItemName will return the name of the plugin
  • onMenuItemClicked 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 the MyMenuItem into a Set<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 the MenuItemModule

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 a Set of plugins and simply returns them as a list.
  • The MainMenuPluginProviderModule binds the MainMenuPluginProvider as PluginProvider<MainMenuPlugin>
  • @JvmSuppressWildcards annotation tells the Kotlin compiler to omit wildcards for type arguments and not adding it causes Dagger not to find the Set 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 a Set<? extends MainMenuPlugin> that is not exactly the same as Set<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.

No responses yet