Andro Idiots Podcast 17: Android Studio Plugins with Tushar Aeron
Android Studio was announced on May 16, 2013, at the Google I/O conference as an IDE for android development and we finally said Bye Bye to our beloved Eclipse Android Development Tools (ADT).
It quickly became the de facto tool for all future android development. It’s built on Jetbrains’ IntelliJ platform which comes with a host of features like File templates, Code navigation, and the one that we are going to discuss in this episode with Tusharaeron from Urbanclap, Plugins.
Show Notes
Plugin :
A software component that adds a specific feature to an existing computer program.
In Android Studio some of the commonly used plugins are ADB wifi, RoboPojoGenerator and KeyPromotorX
Why write your own?
We observe that in day to day development, there are a few tasks that are executed repeatedly and slowly eat up dev-productivity. To avoid these we can create custom plugins specific to our project or our organization. For example, we use Jenkins as our CI tool. To trigger a build on Jenkins you have to open a browser, log in, find your job, and then run it. What we did was create an Android Studio custom plugin which lets us do the same in 2 clicks without having to leave the IDE.
How to write your own?
Let's walk you through a step by step guide which takes you from knowing nothing about Android Studio plugins to publishing one of your own.
Prerequisites :
- Android Studio latest edition
- Intellij Community Edition
We need a platform to work on Android Studio was built on IntelliJ hence we create plugins for IntelliJ only
Note: IntelliJ CE should be compatible with the Android Studio build number.
Step 1: Create A Gradle project in Intellij
Understanding Project Architecture :
- Plugin.xml
This file is similar to AndroidManifest.xml used in Android development. It contains metadata about the plugin and here we can register different elements of our plugin like actions, components etc.
<idea-plugin>
<id>myPlugin.myPlugin</id>
<name>Hello Idiots action Project</name>
<version>0.0.2</version>
<vendor email="andro021idiots@gmail.com" url="androidiots.in">Androidiots</vendor> <extensions defaultExtensionNs="com.intellij">
<!-- Add your extensions here -->
</extensions> <actions>
<!-- Add your actions here -->
</actions><applicationcomponents>
<!-- Add your components here -->
</applicationcomponents>
</idea-plugin>
2. build.gradle
We can mention our repositories, dependencies inside this file. It is essential to mention the local android studio path as alternativeIdePath to run our plugin in Android Studio.
buildscript {
ext.kotlin_version = '1.2.31' repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}plugins {
id 'java'
id 'org.jetbrains.intellij' version '0.4.10'
}version '1.0-SNAPSHOT'apply plugin: 'kotlin'sourceCompatibility = 1.8repositories {
mavenCentral()
}dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
testCompile group: 'junit', name: 'junit', version: '4.12'
}compileKotlin {
kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
kotlinOptions.jvmTarget = "1.8"
}
intellij {
version '2019.1.4'
alternativeIdePath '/Applications/Android Studio.app' // add your local android studio path here
}
patchPluginXml {
sinceBuild = '191.8026'
untilBuild = '191.*'
}
Step 2: Define Actions and Components
Actions: Remember the toolbar at the top of your project, where you see the options like File, Edit, Tool, Build and Run, etc. When you click on one of these options every line item inside the drop-down is called an Action.
Actions are organized into groups, these groups can contain other groups and so on and so forth. We can create our own action by extending AnAction() class.
class IdiotAction: AnAction() { override fun actionPerformed(e: AnActionEvent) {
val noti = NotificationGroup("myFirstPlugin", NotificationDisplayType.BALLOON, true)
noti.createNotification("My Title",
"My Message",
NotificationType.INFORMATION,
null
).notify(e.project)
}
Components: Components control the life-cycle and manage the state of a plugin.
There are 3 variants of Component :
1. Application-level component: it is initialized when the IDE (Android Studio) starts up. Remember the Send Statistics to google dialog on the bottom right corner that pops up every single time, that’s an application-level component.
class ApplicationComponentExample : ApplicationComponent {
val LOG: Logger = Logger.getInstance(ApplicationComponentExample::class.java)
override fun initComponent() {
LOG.info("Initializing plugin. Project is not opened yet");
}
override fun disposeComponent() {
LOG.info("Disposing plugin. Intellij is closing");
}
override fun getComponentName(): String {
return "components.ApplicationComponentExample"
}
}
2. Project-Level Components: it is created for each project instance in the IDE.
class ProjectComponentExample : ProjectComponent {
val LOG: Logger = Logger.getInstance(ProjectComponentExample::class.java)
private var project: Project? = null
private var applicationComponent: ApplicationComponentExample? = null constructor (project: Project, applicationComponentExample: ApplicationComponentExample){
this.project = project
this.applicationComponent = applicationComponentExample
}
override fun initComponent() {
//called before projectOpened()
}
override fun disposeComponent() {
//called after projectClosed()
}
override fun projectOpened() {
LOG.info(String.format("Project '%s' has been opened, base dir '%s'", project?.getName(), project?.getBaseDir()?.getCanonicalPath()))
}
override fun projectClosed() {
LOG.info(String.format("Project '%s' has been closed.", project?.getName()))
}
override fun getComponentName(): String {
return "components.ApplicationComponentExample"
}
}
3. Module-level components: it is created for each module inside every project loaded in the IDE.
class ModuleLevelComponent : ModuleComponent {
val LOG: Logger = Logger.getInstance(ModuleLevelComponent::class.java)
private var module: Module? = null constructor (module: Module){
this.module = module
}
override fun projectOpened() {
LOG.info(String.format("Module '%s' has been opened'", module?.getName()))
}
override fun projectClosed() {
LOG.info(String.format("Module '%s' has been closed'", module?.getName()))
}
override fun moduleAdded(){
LOG.info(String.format("Module '%s' has been added'", module?.getName()))
}
}
JetBrains has introduced 3 new components namely Service, Extensions, and Listeners which make writing plugins even simpler and even save development time using Dynamic plugins.
Services: It is a plugin component loaded on demand when your plugin calls the getService() method of the ServiceManager class and the IntelliJ Platform ensures that only one instance of a service is loaded even though the service is called several times.
Service is also available at the application level, project level, and module level.
For example, say we are migrating our code from java to kotlin and need to find all the java files when the project loads. We can then use the project level service. When a project loads, we can call the getService() method of ServiceManager, get the instance of the service and calculate the file count.
Extensions: Extensions are the most common way for a plugin to extend the functionality of the IntelliJ Platform. It is not as straightforward as adding an action to a menu or toolbar.
Extensions(read Extension Points) allow us to add tool windows, the panels displayed at the sides of the IDE user interface, or to add pages to the Settings/Preferences dialog.
Listeners: Listeners allow plugins to declaratively subscribe to events delivered through the message bus. You can define both application- and project-level listeners.
Let’s take an example of a project level listener. The class implementing the listener interface can define a one-argument constructor accepting a Project, and it will receive the instance of the project for which the listener is created.
Refer below git projects, for the understanding of Actions, Components, Service, Extensions, and Listeners.
1. IntelliJ-Plugin Examples
2. Android Studio Jenkins Project
Step 3: Open the plugin.xml
<idea-plugin>
<id>myPlugin.myPlugin</id>
<name>Hello Idiots action Project</name>
<version>0.0.2</version>
<vendor email="andro021idiots@gmail.com" url="androidiots.in">Androidiots</vendor> <depends>com.intellij.modules.lang</depends>
<application-components> // Add application component here
<component>
<implementation- class>components.ApplicationComponentExample</implementation-class>
</component>
</application-components>
<actions> // Add your actions and group of actions here
<group // For grouping similar actions
id="IdiotPluginGroup"
text="Androidiots"
description="Idiot Menu">
<add-to-group // Add reference for where to place the group
group-id="MainMenu" anchor="last"/>
<action id="IdiotAction" // Add indivisual action
class="action.IdiotAction"
text="Idiot Action"
description="Hello Idiots">
<keyboard-shortcut first-keystroke="control alt k" keymap="$default"/> // Set shortcut for action
</action>
</group>
</actions>
</idea-plugin>
Declare your elements (i.e. actions, components, services, etc.) in plugin.xml to be picked when the studio is launched. Refer comments in the code above for understanding the usage of individual elements.
Step 4: Run the project
Either by Run button in IntelliJ or in the Gradle Tool Window, you can find the runIde task. Double click and run this.
Step 5: Wait for Android Studio to open
When we hit run in our IntelliJ, it will build the plugin, launch Android Studio, (since we have mentioned alternativeIdePath in plugin.xml), and install the plugin. We can now see our Action placed at the last position on our Main Menu along with the keyboard shortcut which we have set.
Debugging
Debugging plugins works exactly the same way as in android app development. Either you launch Android Studio while debugging or you attach the debugger once it’s already launched.
Testing
IntelliJ provides capabilities to do functional or integration testing of high-level functionality.
For example: We can use AssertJ and JUnit4 to create unit tests for our plugins. Please read the official docs on testing.
We need to add the respective dependency in build.gradle file.
dependencies {
testImplementation("org.assertj:assertj-core:3.11.1")
}
Publish
To locally share it with friends or colleagues, you can build a “zip file” of your Gradle plugin. They can install it via Android Studio’s plugin manager and start using it after restarting the IDE.
To publish your plugin visit the JetBrains website, signup, and upload your plugin. Jetbrains takes some time to review the plugin after which it will be available on the Plugins Marketplace.
References
You can check the codes of plugins you use in your day to day life in the Readme section.
We are inviting Android Developers to contribute back to the community via AndroIDIOTS. If you are interested in recording a podcast or writing tech articles, we would love to have you on board. Here is a small form you can fill so we can get back to you.
Follow us on Twitter for regular updates and feel free to DM the hosts (Anupam & Vivek) for any queries.
If you have any questions/suggestions/queries about the topic feel free to contact the speaker Tusharaeron for more info.
Cheers!!