Write your own Android Studio Plugin and automate everything.

8123b9ca408d9b35d0cf955feb32cfb8?s=47 Marcos
April 23, 2019

Write your own Android Studio Plugin and automate everything.

Let’s face it, we waste a lot of time on things that don't add any value. Maybe you use the same pattern in different places and copy&paste the same files over and over. Probably the same happens whenever you create a new app or module. What about all the time that you spend in Jira or other tools on tasks like moving tickets or logging time?. What if you could automate all that? Well, you can, and I’m going to show you how by writing your own Android Studio Plugin.

In this talk you will learn how to create a new Android Studio Plugin using Kotlin, create templates to generate code automatically, write live templates to insert frequently-used constructors in your code, easily integrate Android Studio with Jira or other tools, create a brand new full App in minutes using feature modules and much more.

8123b9ca408d9b35d0cf955feb32cfb8?s=128

Marcos

April 23, 2019
Tweet

Transcript

  1. Write your own Android Studio plugin and automate everything @Orbycius

    Marcos Holgado (AndroidMakers version)
  2. Stop wasting time on things that don’t add value @Orbycius

  3. Writing a plugin is easy… @Orbycius

  4. …and you can do it in kotlin! @Orbycius

  5. https://github.com/marcosholgado/demo-plugin https://github.com/marcosholgado/plugin-app @Orbycius

  6. https://github.com/JetBrains/gradle-intellij-plugin @Orbycius

  7. File > New... > Project @Orbycius

  8. @Orbycius

  9. @Orbycius

  10. @Orbycius

  11. dependencies { "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 'com.squareup.retrofit2:retrofit:2.5.0' 'com.squareup.retrofit2:adapter-rxjava2:2.5.0' 'com.squareup.retrofit2:converter-gson:2.5.0' 'com.google.dagger:dagger:2.20' 'com.google.dagger:dagger-compiler:2.20' 'io.reactivex.rxjava2:rxjava:2.2.5' 'com.github.akarnokd:rxjava2-swing:0.3.0'

    } compile compile compile compile compile kapt compile compile @Orbycius
  12. dependencies { "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 'com.squareup.retrofit2:retrofit:2.5.0' 'com.squareup.retrofit2:adapter-rxjava2:2.5.0' 'com.squareup.retrofit2:converter-gson:2.5.0' 'com.google.dagger:dagger:2.20' 'com.google.dagger:dagger-compiler:2.20' 'io.reactivex.rxjava2:rxjava:2.2.5' 'com.github.akarnokd:rxjava2-swing:0.3.0'

    } compile compile compile compile compile kapt compile compile @Orbycius
  13. publishPlugin { token yourToken password yourPassword channels 'nightly' } @Orbycius

  14. intellij { pluginName 'demo-plugin' plugins = ['Kotlin', 'git4idea', 'android'] updateSinceUntilBuild

    false localPath ASRunPath } @Orbycius
  15. intellij { pluginName 'demo-plugin' plugins = ['Kotlin', 'git4idea', 'android'] updateSinceUntilBuild

    false ASRunPath = /Applications/Android Studio.app/Contents gradle.properties localPath ASRunPath } @Orbycius
  16. localPath ASRunPath intellij { pluginName 'demo-plugin' plugins = ['Kotlin', 'git4idea',

    'android'] updateSinceUntilBuild false localPath ASRunPath } @Orbycius
  17. localPath ASRunPath // alternativeIdePath '/Applications/Android Studio.app' intellij { pluginName 'demo-plugin'

    plugins = ['Kotlin', 'git4idea', 'android'] updateSinceUntilBuild false } version '2018.2.2' @Orbycius
  18. localPath ASRunPath // alternativeIdePath '/Applications/Android Studio.app' intellij { pluginName 'demo-plugin'

    plugins = ['Kotlin', 'git4idea', 'android'] updateSinceUntilBuild false } version '2018.2.2' @Orbycius
  19. https://developer.android.com/studio/releases/ @Orbycius

  20. intellij { pluginName 'demo-plugin' updateSinceUntilBuild false localPath ASRunPath } plugins

    = ['Kotlin', 'git4idea', 'android'] @Orbycius
  21. intellij { pluginName 'demo-plugin' updateSinceUntilBuild false localPath ASRunPath } plugins

    = ['Kotlin', 'git4idea', 'android'] @Orbycius
  22. @Orbycius

  23. <depends>org.jetbrains.kotlin</depends> <depends>Git4Idea</depends> <depends>org.jetbrains.android</depends> @Orbycius

  24. <depends>org.jetbrains.kotlin</depends> <depends>Git4Idea</depends> <depends>org.jetbrains.android</depends> intellij { pluginName 'demo-plugin' plugins = ['Kotlin',

    'git4idea', ] updateSinceUntilBuild false 'android' localPath ASRunPath } @Orbycius
  25. localPath ASRunPath <depends>org.jetbrains.kotlin</depends> <depends>Git4Idea</depends> <depends>org.jetbrains.android</depends> intellij { pluginName 'demo-plugin' plugins

    = ['Kotlin', 'git4idea', ] updateSinceUntilBuild false 'android' localPath ASRunPath } @Orbycius
  26. localPath ASRunPath <depends>org.jetbrains.kotlin</depends> <depends>Git4Idea</depends> <depends>org.jetbrains.android</depends> intellij { pluginName 'demo-plugin' plugins

    = ['Kotlin', 'git4idea', ] updateSinceUntilBuild false 'android' version 'IC-2018.2.2' alternativeIdePath '/Applications/Android Studio.app' // } @Orbycius
  27. intellij { pluginName 'demo-plugin' plugins = ['Kotlin', 'git4idea', 'android'] updateSinceUntilBuild

    false // localPath ASRunPath } version 'IC-2017.3.3' alternativeIdePath '/Applications/Android Studio.app' Android Studio 3.1.4 @Orbycius
  28. intellij { pluginName 'demo-plugin' plugins = ['Kotlin', 'git4idea', 'android'] updateSinceUntilBuild

    false // localPath ASRunPath } version 'IC-2017.3.3' alternativeIdePath '/Applications/Android Studio.app' Android Studio 3.1.4 @Orbycius
  29. intellij { pluginName 'demo-plugin' plugins = ['Kotlin', 'git4idea', 'android'] updateSinceUntilBuild

    false // localPath ASRunPath } version 'IC-2017.3.3' alternativeIdePath '/Applications/Android Studio.app' Android Studio 3.1.4 Android Studio 3.3.1 intellij { pluginName 'demo-plugin' plugins = ['Kotlin', 'git4idea', 'android'] updateSinceUntilBuild false // localPath ASRunPath } version 'IC-2018.2.2' alternativeIdePath '/Applications/Android Studio.app' @Orbycius
  30. intellij { pluginName 'demo-plugin' plugins = ['Kotlin', 'git4idea', 'android'] updateSinceUntilBuild

    false // localPath ASRunPath } version 'IC-2017.3.3' alternativeIdePath '/Applications/Android Studio.app' Android Studio 3.1.4 Android Studio 3.3.1 intellij { pluginName 'demo-plugin' plugins = ['Kotlin', 'git4idea', 'android'] updateSinceUntilBuild false // localPath ASRunPath } version 'IC-2018.2.2' alternativeIdePath '/Applications/Android Studio.app' @Orbycius
  31. <depends>org.jetbrains.kotlin</depends> <depends>Git4Idea</depends> <depends>org.jetbrains.android</depends> intellij { pluginName 'droidcon18' plugins = ['Kotlin',

    ‘git4idea', ] updateSinceUntilBuild false } 'android' localPath ASRunPath @Orbycius
  32. Plugin Structure @Orbycius

  33. Plugin Components @Orbycius

  34. Application level components @Orbycius

  35. Application level components Project level components @Orbycius

  36. Application level components Project level components Module level components @Orbycius

  37. Application level components Project level components Module level components @Orbycius

  38. class DemoComponent } { @Orbycius

  39. class DemoComponent } { : ApplicationComponent @Orbycius

  40. class DemoComponent } { : ApplicationComponent override fun initComponent() {

    super.initComponent() if (isANewVersion()) { // do something } } @Orbycius
  41. class DemoComponent } { : ApplicationComponent override fun initComponent() {

    super.initComponent() if (isANewVersion()) { // do something } } public var version = 1 public var localVersion = 0 @Orbycius
  42. class DemoComponent } { : ApplicationComponent override fun initComponent() {

    super.initComponent() if (isANewVersion()) { // do something } } public var version = 1 public var localVersion = 0 private fun isANewVersion() = localVersion < version private fun updateVersion() { localVersion = version } @Orbycius
  43. class DemoComponent } { : ApplicationComponent override fun initComponent() {

    super.initComponent() if (isANewVersion()) { // do something public var version = 1 public var localVersion = 0 private fun isANewVersion() = localVersion < version private fun updateVersion() { localVersion = version } } } updateVersion() @Orbycius
  44. class DemoComponent } { : ApplicationComponent override fun initComponent() {

    super.initComponent() if (isANewVersion()) { // do something public var version = 1 public var localVersion = 0 private fun isANewVersion() = localVersion < version private fun updateVersion() { localVersion = version } } } updateVersion() override fun getState(): DemoComponent? = this override fun loadState(state: DemoComponent) = , PersistentStateComponent<DemoComponent> XmlSerializerUtil.copyBean(state, this) @Orbycius
  45. class DemoComponent { : ApplicationComponent public var version = 1

    public var localVersion = 0 private fun isANewVersion() = localVersion < version private fun updateVersion() { localVersion = version } override fun getState(): DemoComponent? = this override fun loadState(state: DemoComponent) = , PersistentStateComponent<DemoComponent>, Serializable XmlSerializerUtil.copyBean(state, this) } } } updateVersion() override fun initComponent() { super.initComponent() if (isANewVersion()) { // do something @Orbycius
  46. class DemoComponent { : ApplicationComponent public var version = 1

    public var localVersion = 0 private fun isANewVersion() = localVersion < version private fun updateVersion() { localVersion = version } override fun getState(): DemoComponent? = this override fun loadState(state: DemoComponent) = , PersistentStateComponent<DemoComponent>, Serializable XmlSerializerUtil.copyBean(state, this) } } } updateVersion() override fun initComponent() { super.initComponent() if (isANewVersion()) { // do something @State(name = "DemoConfiguration", storages = [ Storage(value = "demoConfiguration.xml") ]) @Orbycius
  47. <application-components> <component> <implementation-class> com.marcosholgado.demo.plugin.components.DemoComponent </implementation-class> </component> </application-components> @Orbycius

  48. Actions @Orbycius

  49. @Orbycius

  50. class JiraMoveAction : AnAction() { override fun actionPerformed(event: AnActionEvent) {

    // do something } } @Orbycius
  51. <actions> </actions> <action id="JiraMoveTicket" class=".JiraMoveAction" text="Move Ticket" description="Move Ticket in

    Jira"> </action> <keyboard-shortcut first-keystroke="control alt k" keymap="$default"/> <add-to-group group-id="CutCopyPasteGroup" anchor="last"/> @Orbycius
  52. <actions> </actions> <action id="JiraMoveTicket" class=".JiraMoveAction" text="Move Ticket" description="Move Ticket in

    Jira"> </action> <keyboard-shortcut first-keystroke="control alt k" keymap="$default"/> <add-to-group group-id="CutCopyPasteGroup" anchor="last"/> @Orbycius
  53. <actions> </actions> <action id="JiraMoveTicket" class=".JiraMoveAction" text="Move Ticket" description="Move Ticket in

    Jira"> </action> <keyboard-shortcut first-keystroke="control alt k" keymap="$default"/> <add-to-group group-id="CutCopyPasteGroup" anchor="last"/> @Orbycius
  54. <actions> </actions> <action id="JiraMoveTicket" class=".JiraMoveAction" text="Move Ticket" description="Move Ticket in

    Jira"> </action> <keyboard-shortcut first-keystroke="control alt k" keymap="$default"/> <add-to-group group-id="CutCopyPasteGroup" anchor="last"/> control == cmd @Orbycius
  55. <actions> </actions> <action id="JiraMoveTicket" class=".JiraMoveAction" text="Move Ticket" description="Move Ticket in

    Jira"> </action> <keyboard-shortcut first-keystroke="control alt k" keymap="$default"/> <add-to-group group-id="CutCopyPasteGroup" anchor="last"/> @Orbycius
  56. <actions> </actions> <action id="JiraMoveTicket" class=".JiraMoveAction" text="Move Ticket" description="Move Ticket in

    Jira"> </action> <keyboard-shortcut first-keystroke="control alt k" keymap="$default"/> <add-to-group group-id="CutCopyPasteGroup" anchor="last"/> @Orbycius
  57. http://www.jetbrains.org/intellij/sdk/docs/basics/action_system.html @Orbycius

  58. Extensions and extension points @Orbycius

  59. Show me the code @Orbycius

  60. You are still writing code! @Orbycius

  61. JiraMoveAction @Orbycius

  62. JiraMoveAction JiraMoveDialog JiraMoveDialogPresenter Model @Orbycius

  63. JiraMoveAction JiraMoveDialog JiraMoveDialogPresenter Model JiraMovePanel @Orbycius

  64. JiraMoveAction JiraMoveDialog JiraMoveDialogPresenter Model JiraMovePanel @Orbycius

  65. Java Swing Forms @Orbycius

  66. None
  67. None
  68. @orbycius JiraMoveAction JiraMoveDialog JiraMoveDialogPresenter Model JiraMovePanel @Orbycius

  69. class JiraMovePanel: JPanel() { private val comboTransitions = ComboBox<Transition>() val

    txtJiraTicket = JTextField() val txtComment = JTextArea() init { initComponents() } private fun initComponents() { layout = null val lblJiraTicket = JLabel(message("jira.move.label.ticket")) lblJiraTicket.setBounds(25, 33, 77, 16) add(lblJiraTicket) ... } } @Orbycius
  70. JiraMoveAction JiraMoveDialog JiraMoveDialogPresenter Model JiraMovePanel @Orbycius

  71. class JiraMoveDialog constructor(val project: Project): { @Inject lateinit var presenter:

    JiraMoveDialogPresenter init { DaggerJiraComponent.builder() .jiraModule(JiraModule(this, project)) .build().inject(this) isModal = true presenter.load() init() } override fun doOKAction() = presenter.doTransition(...) } private val panel: JiraMovePanel = JiraMovePanel() DialogWrapper(true) override fun createCenterPanel(): JComponent = panel @Orbycius
  72. class JiraMoveDialog constructor(val project: Project): { @Inject lateinit var presenter:

    JiraMoveDialogPresenter init { DaggerJiraComponent.builder() .jiraModule(JiraModule(this, project)) .build().inject(this) isModal = true presenter.load() init() } override fun doOKAction() = presenter.doTransition(...) } private val panel: JiraMovePanel = JiraMovePanel() DialogWrapper(true) override fun createCenterPanel(): JComponent = panel @Orbycius
  73. class JiraMoveDialog constructor(val project: Project): { @Inject lateinit var presenter:

    JiraMoveDialogPresenter init { DaggerJiraComponent.builder() .jiraModule(JiraModule(this, project)) .build().inject(this) isModal = true presenter.load() init() } override fun doOKAction() = presenter.doTransition(...) } private val panel: JiraMovePanel = JiraMovePanel() DialogWrapper(true) override fun createCenterPanel(): JComponent = panel @Orbycius
  74. DialogWrapper @Orbycius

  75. DialogWrapper Panel @Orbycius

  76. @Orbycius

  77. class JiraMoveDialog constructor(val project: Project): DialogWrapper(true) { @Inject lateinit var

    presenter: JiraMoveDialogPresenter private val panel: JiraMovePanel = JiraMovePanel() init { DaggerJiraComponent.builder() .jiraModule(JiraModule(this, project)) .build().inject(this) isModal = true presenter.load() init() } override fun createCenterPanel(): JComponent = panel } override fun doOKAction() = presenter.doTransition(...) @Orbycius
  78. If you are using RxJava… Completable.merge(completable) .subscribeOn(Schedulers.io()) .subscribe( { view.success(selectedItem,

    ticket) }, { error -> view.error(error) } ) .observeOn( AndroidSchedulers.mainThread()) @Orbycius
  79. If you are using RxJava… Completable.merge(completable) .subscribeOn(Schedulers.io()) .subscribe( { view.success(selectedItem,

    ticket) }, { error -> view.error(error) } ) .observeOn( AndroidSchedulers.mainThread()) @Orbycius
  80. If you are using RxJava… Completable.merge(completable) .subscribeOn(Schedulers.io()) .subscribe( { view.success(selectedItem,

    ticket) }, { error -> view.error(error) } ) .observeOn( SwingSchedulers.edt()) compile "com.github.akarnokd:rxjava2-swing:0.3.0" @Orbycius
  81. If you are using RxJava… Completable.merge(completable) .subscribeOn(Schedulers.io()) .subscribe( { view.success(selectedItem,

    ticket) }, { error -> view.error(error) } ) .observeOn( SwingSchedulers.edt()) compile "com.github.akarnokd:rxjava2-swing:0.3.0" @Orbycius
  82. Settings @Orbycius

  83. class JiraComponent: ProjectComponent { } @Orbycius

  84. @State(name = "JiraConfiguration", storages = [ Storage(value = "jiraConfiguration.xml") ])

    class JiraComponent: ProjectComponent, Serializable, PersistentStateComponent<JiraComponent> { override fun getState(): JiraComponent? = this override fun loadState(state: JiraComponent) = XmlSerializerUtil.copyBean(state, this) companion object { fun getInstance(project: Project): JiraComponent = project.getComponent(JiraComponent::class.java) } } var jiraUrl: String = "" var username: String = "" var password: String = "" var regex: String = "" @Orbycius
  85. @State(name = "JiraConfiguration", storages = [ Storage(value = "jiraConfiguration.xml") ])

    class JiraComponent: ProjectComponent, Serializable, PersistentStateComponent<JiraComponent> { override fun getState(): JiraComponent? = this override fun loadState(state: JiraComponent) = XmlSerializerUtil.copyBean(state, this) companion object { fun getInstance(project: Project): JiraComponent = project.getComponent(JiraComponent::class.java) } } var jiraUrl: String = "" var username: String = "" var password: String = "" var regex: String = "" @Orbycius
  86. <project-components> <component> <implementation-class> com.marcosholgado.demo.plugin.components.JiraComponent </implementation-class> </component> </project-components> @Orbycius

  87. class JiraSettings { private var userField: JTextField? = null private

    var passwordField: JPasswordField? = null private var jiraURLField: JTextField? = null … } @Orbycius
  88. class JiraSettings(private val project: Project): Configurable { } @Orbycius

  89. class JiraSettings(private val project: Project): Configurable { override fun isModified():

    Boolean {} override fun getDisplayName(): String {} override fun apply() {} override fun createComponent(): JComponent? {} } @Orbycius
  90. class JiraSettings(private val project: Project): Configurable { override fun getDisplayName():

    String {} override fun apply() {} override fun createComponent(): JComponent? {} private var modified = false override fun isModified(): Boolean = modified } @Orbycius
  91. class JiraSettings(private val project: Project): Configurable { private var modified

    = false override fun isModified(): Boolean = modified override fun apply() {} override fun createComponent(): JComponent? {} override fun getDisplayName(): String = "Jira Settings" } @Orbycius
  92. class JiraSettings(private val project: Project): Configurable { private var modified

    = false override fun isModified(): Boolean = modified override fun createComponent(): JComponent? {} override fun getDisplayName(): String = "Jira Settings" override fun apply() {} } @Orbycius
  93. class JiraSettings(private val project: Project): Configurable { … private var

    modified = false override fun apply() { val config = JiraComponent.getInstance(project) config.jiraUrl = jiraURLField!!.text config.username = userField!!.text config.password = String(passwordField!!.password) config.regex = regExField!!.text modified = false } override fun createComponent(): JComponent? {} } @Orbycius
  94. class JiraSettings(private val project: Project): Configurable { … private var

    modified = false override fun apply() { val config = JiraComponent.getInstance(project) config.jiraUrl = jiraURLField!!.text config.username = userField!!.text config.password = String(passwordField!!.password) config.regex = regExField!!.text modified = false } override fun createComponent(): JComponent? {} } @Orbycius
  95. class JiraSettings(private val project: Project): Configurable { … private var

    modified = false override fun createComponent(): JComponent? { val config = JiraComponent.getInstance(project) userField?.text = config.username passwordField?.text = config.password jiraURLField?.text = config.jiraUrl regExField?.text = config.regex return mainPanel } } @Orbycius
  96. class JiraSettings(private val project: Project): Configurable { … private var

    modified = false override fun isModified(): Boolean = modified override fun createComponent(): JComponent? val config = JiraComponent.getInstance(project) userField?.text = config.username passwordField?.text = config.password jiraURLField?.text = config.jiraUrl regExField?.text = config.regex return mainPanel } } { @Orbycius
  97. class JiraSettings(private val project: Project): … private var modified =

    false override fun isModified(): Boolean = modified override fun createComponent(): JComponent? val config = JiraComponent.getInstance(project) userField?.text = config.username passwordField?.text = config.password jiraURLField?.text = config.jiraUrl regExField?.text = config.regex return mainPanel } jiraURLField?.document?.addDocumentListener(this) userField?.document?.addDocumentListener(this) passwordField?.document?.addDocumentListener(this) regExField?.document?.addDocumentListener(this) } { DocumentListener Configurable, { @Orbycius
  98. class JiraSettings(private val project: Project): Configurable, DocumentListener override fun changedUpdate(e:

    DocumentEvent?) { modified = true } override fun insertUpdate(e: DocumentEvent?) { modified = true } override fun removeUpdate(e: DocumentEvent?) { modified = true } } override fun changedUpdate(e: DocumentEvent?) { modified = true } override fun insertUpdate(e: DocumentEvent?) { modified = true } override fun removeUpdate(e: DocumentEvent?) { modified = true } DocumentListener { @Orbycius
  99. <extensions defaultExtensionNs="com.intellij"> <defaultProjectTypeProvider type="Android"/> <projectConfigurable instance=".JiraSettings"> </projectConfigurable> </extensions> @Orbycius

  100. Live Templates @Orbycius

  101. @Orbycius

  102. @Orbycius

  103. @Orbycius

  104. How do you share them? • Export to a .jar

    file and import • Copy & paste template xml https://www.jetbrains.com/help/idea/2018.1/sharing-live-templates.html @Orbycius
  105. How do you share them? $ mv ~/Library/Preferences/AndroidStudio3.3/templates/Demo.xml ~/Sandbox/demo-plugin/src/main/resources/liveTemplates/Demo.xml @Orbycius

  106. How do you share them? $ mv ~/Library/Preferences/AndroidStudio3.3/templates/Demo.xml ~/Sandbox/demo-plugin/src/main/resources/liveTemplates/Demo.xml class

    DemoLiveTemplateProvider: DefaultLiveTemplatesProvider { override fun getDefaultLiveTemplateFiles(): Array<String> = arrayOf("liveTemplates/Demo") override fun getHiddenLiveTemplateFiles(): Array<String>? = null } @Orbycius
  107. How do you share them? $ mv ~/Library/Preferences/AndroidStudio3.3/templates/Demo.xml ~/Sandbox/demo-plugin/src/main/resources/liveTemplates/Demo.xml class

    DemoLiveTemplateProvider: DefaultLiveTemplatesProvider { override fun getDefaultLiveTemplateFiles(): Array<String> = arrayOf("liveTemplates/Demo") override fun getHiddenLiveTemplateFiles(): Array<String>? = null } <extensions defaultExtensionNs="com.intellij"> <defaultLiveTemplatesProvider implementation=".DemoLiveTemplateProvider"/> </extensions> @Orbycius
  108. Templates @Orbycius

  109. @Orbycius

  110. @Orbycius

  111. @Orbycius

  112. /Applications/Android\ Studio.app/Contents/plugins/android/lib/templates/ @Orbycius

  113. /Applications/Android\ Studio.app/Contents/plugins/android/lib/templates/ $ tree -L 2 -d ├── activities │

    ├── AndroidTVActivity │ ├── AndroidThingsActivity │ ├── AndroidThingsPeripheralActivity │ ├── BasicActivity │ ├── BlankWearActivity │ ├── BottomNavigationActivity │ ├── EmptyActivity │ ├── FullscreenActivity │ ├── GoogleAdMobAdsActivity │ ├── GoogleMapsActivity │ ├── GoogleMapsWearActivity │ ├── LoginActivity │ ├── MasterDetailFlow │ ├── NavigationDrawerActivity │ ├── ScrollActivity │ ├── SettingsActivity │ ├── TabbedActivity │ ├── ViewModelActivity @Orbycius
  114. │ ├── NewInstantFeatureModule │ ├── NewJavaLibrary │ └── common └──

    other ├── AidlFile ├── AidlFolder ├── AndroidAutoMediaService ├── AndroidAutoMessagingService ├── AndroidManifest ├── AppActionsResourceFile ├── AppWidget ├── AssetsFolder ├── BlankFragment ├── BroadcastReceiver ├── ContentProvider ├── CustomView ├── Daydream ├── FontFolder ├── IntentService ├── JavaFolder ├── JniFolder ├── LayoutResourceFile @Orbycius
  115. template.xml (UI) @Orbycius

  116. template.xml (UI) .ftl files @Orbycius

  117. template.xml (UI) .ftl files globals @Orbycius

  118. recipe.xml template.xml (UI) .ftl files globals @Orbycius

  119. recipe.xml template.xml (UI) .ftl files globals code @Orbycius

  120. $ cat template.xml <?xml version="1.0"?> <globals file="globals.xml.ftl" /> <execute file="recipe.xml.ftl"

    /> </template> <template format="5" revision="1" name="MVP" minApi="23" minBuildApi="23" description="MVP pattern for fragments in modules"> <category value="Demo" /> <parameter id="name" name="Name" type="string" constraints="nonempty" default="MyClass" help="This is the name of the main class." /> @Orbycius
  121. $ cat template.xml <?xml version="1.0"?> <globals file="globals.xml.ftl" /> <execute file="recipe.xml.ftl"

    /> </template> <template format="5" revision="1" name="MVP" minApi="23" minBuildApi="23" description="MVP pattern for fragments in modules"> <category value="Demo" /> <parameter id="name" name="Name" type="string" constraints="nonempty" default="MyClass" help="This is the name of the main class." /> @Orbycius
  122. $ cat template.xml <?xml version="1.0"?> <globals file="globals.xml.ftl" /> <execute file="recipe.xml.ftl"

    /> </template> <template format="5" revision="1" name="MVP" minApi="23" minBuildApi="23" description="MVP pattern for fragments in modules"> <category value="Demo" /> <parameter id="name" name="Name" type="string" constraints="nonempty" default="MyClass" help="This is the name of the main class." /> @Orbycius
  123. $ cat template.xml <?xml version="1.0"?> <globals file="globals.xml.ftl" /> <execute file="recipe.xml.ftl"

    /> </template> <template format="5" revision="1" name="MVP" minApi="23" minBuildApi="23" description="MVP pattern for fragments in modules"> <category value="Demo" /> <parameter id="name" name="Name" type="string" constraints="nonempty" default="MyClass" help="This is the name of the main class." /> @Orbycius
  124. $ cat template.xml <?xml version="1.0"?> <globals file="globals.xml.ftl" /> <execute file="recipe.xml.ftl"

    /> </template> <template format="5" revision="1" name="MVP" minApi="23" minBuildApi="23" description="MVP pattern for fragments in modules"> <category value="Demo" /> <parameter id="name" name="Name" type="string" constraints="nonempty" default="MyClass" help="This is the name of the main class." /> @Orbycius
  125. $ cat recipe.xml.ftl <?xml version="1.0"?> <recipe> </recipe> @Orbycius

  126. $ cat recipe.xml.ftl <?xml version="1.0"?> <recipe> </recipe> <instantiate from="root/src/app_package/di/DaggerComponent.${ktOrJavaExt}.ftl" to="${escapeXmlAttribute(srcOut)}/di/${name}Component.${ktOrJavaExt}"

    /> @Orbycius
  127. $ cat recipe.xml.ftl <?xml version="1.0"?> <recipe> </recipe> <instantiate from="root/src/app_package/di/DaggerComponent.${ktOrJavaExt}.ftl" to="${escapeXmlAttribute(srcOut)}/di/${name}Component.${ktOrJavaExt}"

    /> <copy from="root://common/gitignore" to="${escapeXmlAttribute(projectOut)}/.gitignore" /> @Orbycius
  128. $ cat recipe.xml.ftl <?xml version="1.0"?> <recipe> </recipe> <instantiate from="root/src/app_package/di/DaggerComponent.${ktOrJavaExt}.ftl" to="${escapeXmlAttribute(srcOut)}/di/${name}Component.${ktOrJavaExt}"

    /> <copy from="root://common/gitignore" to="${escapeXmlAttribute(projectOut)}/.gitignore" /> <mkdir at="${escapeXmlAttribute(resOut)}/drawable" /> @Orbycius
  129. $ cat recipe.xml.ftl <?xml version="1.0"?> <recipe> </recipe> <instantiate from="root/src/app_package/di/DaggerComponent.${ktOrJavaExt}.ftl" to="${escapeXmlAttribute(srcOut)}/di/${name}Component.${ktOrJavaExt}"

    /> <copy from="root://common/gitignore" to="${escapeXmlAttribute(projectOut)}/.gitignore" /> <mkdir at="${escapeXmlAttribute(resOut)}/drawable" /> <#include "root://common/proguard_recipe.xml.ftl"/> @Orbycius
  130. $ cat recipe.xml.ftl <?xml version="1.0"?> <recipe> </recipe> <instantiate from="root/src/app_package/di/DaggerComponent.${ktOrJavaExt}.ftl" to="${escapeXmlAttribute(srcOut)}/di/${name}Component.${ktOrJavaExt}"

    /> <copy from="root://common/gitignore" to="${escapeXmlAttribute(projectOut)}/.gitignore" /> <mkdir at="${escapeXmlAttribute(resOut)}/drawable" /> <#include "root://common/proguard_recipe.xml.ftl"/> <merge from="root/build.gradle.ftl" to="${escapeXmlAttribute(projectOut)}/build.gradle" /> @Orbycius
  131. $ cat recipe.xml.ftl <?xml version="1.0"?> <recipe> </recipe> <instantiate from="root/src/app_package/di/DaggerComponent.${ktOrJavaExt}.ftl" to="${escapeXmlAttribute(srcOut)}/di/${name}Component.${ktOrJavaExt}"

    /> <copy from="root://common/gitignore" to="${escapeXmlAttribute(projectOut)}/.gitignore" /> <mkdir at="${escapeXmlAttribute(resOut)}/drawable" /> <#include "root://common/proguard_recipe.xml.ftl"/> <merge from="root/build.gradle.ftl" to="${escapeXmlAttribute(projectOut)}/build.gradle" /> <open file="${escapeXmlAttribute(srcOut)}/${name}Fragment.kt" /> @Orbycius
  132. $ cat globals.xml.ftl <globals> <global id="srcOut" value="${srcDir}/${slashedPackageName(packageName)}" /> <global id="resOut"

    value="${resDir}" /> <#assign generateKotlin= (((includeKotlinSupport!false) || (language!'Java')?string == 'Kotlin'))> <global id="generateKotlin" type="boolean" value="${generateKotlin?string}" /> <global id="ktOrJavaExt" type="string" value="${generateKotlin?string('kt','java')}" /> </globals> @Orbycius
  133. $ cat FeatureContract.kt.ftl package ${escapeKotlinIdentifiers(packageName)} interface ${name}Contract { interface View

    { } interface Presenter { } } @Orbycius
  134. What happens when you upgrade to Android Studio 3.5? @Orbycius

  135. https://issuetracker.google.com/issues/37105193 Rebecca Franks @riggaroo @Orbycius

  136. $ mv $ASPATH/Contents/plugins/android/lib/templates/other/myTemplate ~/.android/templates/other @Orbycius

  137. How do you share them? @Orbycius

  138. ~/Sandbox/demo-plugin/src/main/resources/androidTemplates/ @Orbycius

  139. ~/Sandbox/demo-plugin/src/main/resources/androidTemplates/ class CopyTemplatesAction: AnAction() { override fun actionPerformed(event: AnActionEvent) {

    FileUtils.copyTemplates( "/androidTemplates/", "/.android/templates/other", event.project!!) } } @Orbycius
  140. @State(name = "DemoConfiguration", storages = [ Storage(value = "demoConfiguration.xml") ])

    class DemoComponent: ApplicationComponent, Serializable, PersistentStateComponent<DemoComponent> { override fun initComponent() { super.initComponent() if (isANewVersion()) { // do something updateVersion() … } } } @Orbycius
  141. @State(name = "DemoConfiguration", storages = [ Storage(value = "demoConfiguration.xml") ])

    class DemoComponent: ApplicationComponent, Serializable, PersistentStateComponent<DemoComponent> { override fun initComponent() { super.initComponent() if (shouldUpdateTemplates()) { FileUtils.copyTemplates( "/androidTemplates/", "/.android/templates/other", event.project!!) … } } } @Orbycius
  142. @Orbycius

  143. <extensions defaultExtensionNs="com.android"> <moduleDescriptionProvider implementation=".DemoModuleProvider"/> </extensions> @Orbycius

  144. class DemoModuleProvider: ModuleDescriptionProvider { override fun getDescriptions(): Collection<ModuleTemplateGalleryEntry> { }

    } @Orbycius
  145. class DemoModuleProvider: ModuleDescriptionProvider { override fun getDescriptions(): Collection<ModuleTemplateGalleryEntry> { val

    res = ArrayList<ModuleTemplateGalleryEntry>() val manager = TemplateManager.getInstance() val templateDirectories = TemplateManager.getExtraTemplateRootFolders() for (dir in templateDirectories) { if (dir.parent.endsWith(".android")) { } } } } return res @Orbycius
  146. class DemoModuleProvider: ModuleDescriptionProvider { override fun getDescriptions(): Collection<ModuleTemplateGalleryEntry> { val

    res = ArrayList<ModuleTemplateGalleryEntry>() val manager = TemplateManager.getInstance() val templateDirectories = TemplateManager.getExtraTemplateRootFolders() for (dir in templateDirectories) { if (dir.parent.endsWith(".android")) { } } } } return res val applicationTemplates = TemplateManager .getTemplatesFromDirectory(dir, true) for (templateFile in applicationTemplates) { val metadata = manager.getTemplateMetadata(templateFile) if (metadata == null || metadata.category != "Demo") continue res.add(DemoModuleEntry(…)) } break @Orbycius
  147. private class DemoModuleEntry(…): ModuleTemplateGalleryEntry { } @Orbycius

  148. private class DemoModuleEntry(…): ModuleTemplateGalleryEntry { … override fun createStep(model: NewModuleModel):

    SkippableWizardStep<*> { val basePackage = NewProjectModel .getSuggestedProjectPackage(model.project.value, false) return ConfigureAndroidModuleStep( model, formFactor, minSdkLevel, basePackage, isLibrary, false, name) } } @Orbycius
  149. Write your own Android Studio plugin and automate everything Icons

    made by Smashicons from www.flaticon.com @Orbycius Marcos Holgado
  150. None
  151. (plugin) https://github.com/marcosholgado/demo-plugin (demo) https://github.com/marcosholgado/plugin-app (blog) http://bit.ly/plugin-blog Write your own Android

    Studio plugin and automate everything @Orbycius Marcos Holgado