Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Write your own Android Studio plugin and automate everything

8123b9ca408d9b35d0cf955feb32cfb8?s=47 Marcos
October 25, 2018

Write your own Android Studio plugin and automate everything

Let’s face it, you spend a lot of time writing the same code over and over again. Maybe you are using the same pattern in different places.

Maybe you copy&paste some classes and layouts when you create a new Activity using MVP or MVVM. Maybe you “reuse” the same files when you create a new App. What if you could generate that code automatically? 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, create templates to generate code automatically, write live templates to insert frequently-used constructors in your code and how you can create a brand new full app in minutes putting together feature modules and templates.

8123b9ca408d9b35d0cf955feb32cfb8?s=128

Marcos

October 25, 2018
Tweet

Transcript

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

    Marcos Holgado
  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/droidcon18-plugin https://github.com/marcosholgado/droidcon18 @orbycius

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

  7. File > New... > Project @orbycius

  8. @orbycius

  9. @orbycius

  10. @orbycius

  11. dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" compile "org.jetbrains.kotlin:kotlin-runtime:$kotlin_version" compile 'com.squareup.retrofit2:retrofit:2.4.0' compile 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'

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

    compile 'com.squareup.retrofit2:converter-gson:2.4.0' compile 'com.google.dagger:dagger:2.17' kapt 'com.google.dagger:dagger-compiler:2.17' compile 'io.reactivex.rxjava2:rxjava:2.1.10' compile "com.github.akarnokd:rxjava2-swing:0.3.0" } compile compile compile compile compile compile kapt compile compile @orbycius
  13. publishPlugin { token yourToken password yourPassword channels 'nightly' } @orbycius

  14. intellij { pluginName 'droidcon18' plugins = ['Kotlin', 'git4idea', 'android'] updateSinceUntilBuild

    false localPath ASRunPath } @orbycius
  15. ASRunPath = /Applications/Android Studio.app/Contents gradle.properties intellij { pluginName 'droidcon18' plugins

    = ['Kotlin', 'git4idea', 'android'] updateSinceUntilBuild false localPath ASRunPath } localPath ASRunPath @orbycius
  16. intellij { pluginName 'droidcon18' plugins = ['Kotlin', 'git4idea', 'android'] updateSinceUntilBuild

    false // localPath ASRunPath version 2018.1.6' alternativeIdePath ‘/Applications/Android Studio.app' } // localPath ASRunPath version '2018.1.6' alternativeIdePath '/Applications/Android Studio.app' @orbycius
  17. intellij { pluginName 'droidcon18' plugins = ['Kotlin', 'git4idea', 'android'] updateSinceUntilBuild

    false // localPath ASRunPath version 2018.1.6' alternativeIdePath '/Applications/Android Studio.app' } version '2018.1.6' @orbycius
  18. https://developer.android.com/studio/releases/ @orbycius

  19. intellij { pluginName 'droidcon18' plugins = ['Kotlin', 'git4idea', 'android'] updateSinceUntilBuild

    false localPath ASRunPath } @orbycius
  20. intellij { pluginName 'droidcon18' plugins = ['Kotlin', 'git4idea', 'android'] updateSinceUntilBuild

    false localPath ASRunPath } plugins = ['Kotlin', 'git4idea', 'android'] @orbycius
  21. @orbycius

  22. <depends>org.jetbrains.android</depends> <depends>org.jetbrains.kotlin</depends> <depends>Git4Idea</depends> @orbycius

  23. <depends>org.jetbrains.android</depends> <depends>org.jetbrains.kotlin</depends> <depends>Git4Idea</depends> <depends>org.jetbrains.android</depends> intellij { pluginName 'droidcon18' plugins =

    ['Kotlin', 'git4idea', 'android'] updateSinceUntilBuild false localPath ASRunPath } 'android' @orbycius
  24. <depends>org.jetbrains.android</depends> <depends>org.jetbrains.kotlin</depends> <depends>Git4Idea</depends> <depends>org.jetbrains.android</depends> intellij { pluginName 'droidcon18' plugins =

    ['Kotlin', 'git4idea', 'android'] updateSinceUntilBuild false localPath ASRunPath } 'android' localPath ASRunPath @orbycius
  25. <depends>org.jetbrains.android</depends> <depends>org.jetbrains.kotlin</depends> <depends>Git4Idea</depends> <depends>org.jetbrains.android</depends> intellij { pluginName 'droidcon18' plugins =

    ['Kotlin', 'git4idea', 'android'] updateSinceUntilBuild false // localPath ASRunPath version 2018.1.6' alternativeIdePath ‘/Applications/Android Studio.app' } version 'IC-2018.1.6' alternativeIdePath '/Applications/Android Studio.app' @orbycius
  26. intellij { pluginName 'droidcon18' plugins = ['Kotlin', 'git4idea', 'android'] updateSinceUntilBuild

    false // localPath ASRunPath version alternativeIdePath ‘/Applications/Android Studio.app' } version 'IC-2017.3.3' alternativeIdePath '/Applications/Android Studio.app' Android Studio 3.1.4 @orbycius
  27. intellij { pluginName 'droidcon18' plugins = ['Kotlin', 'git4idea', 'android'] updateSinceUntilBuild

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

    false // localPath ASRunPath version alternativeIdePath ‘/Applications/Android Studio.app' } version 'IC-2017.3.3' alternativeIdePath '/Applications/Android Studio.app' Android Studio 3.1.4 Android Studio 3.2 intellij { pluginName 'droidcon18' plugins = ['Kotlin', 'git4idea', 'android'] updateSinceUntilBuild false // localPath ASRunPath version alternativeIdePath ‘/Applications/Android Studio.app' } version 'IC-2018.1.6' alternativeIdePath '/Applications/Android Studio.app' @orbycius
  29. intellij { pluginName 'droidcon18' plugins = ['Kotlin', 'git4idea', 'android'] updateSinceUntilBuild

    false // localPath ASRunPath version alternativeIdePath ‘/Applications/Android Studio.app' } version 'IC-2017.3.3' alternativeIdePath '/Applications/Android Studio.app' Android Studio 3.1.4 Android Studio 3.2 intellij { pluginName 'droidcon18' plugins = ['Kotlin', 'git4idea', 'android'] updateSinceUntilBuild false // localPath ASRunPath version alternativeIdePath ‘/Applications/Android Studio.app' } version 'IC-2018.1.6' alternativeIdePath '/Applications/Android Studio.app' @orbycius
  30. <depends>org.jetbrains.android</depends> <depends>org.jetbrains.kotlin</depends> <depends>Git4Idea</depends> <depends>org.jetbrains.android</depends> intellij { pluginName 'droidcon18' plugins =

    ['Kotlin', 'git4idea', 'android'] updateSinceUntilBuild false localPath ASRunPath } 'android' localPath ASRunPath @orbycius
  31. Plugin Structure @orbycius

  32. Plugin Components @orbycius

  33. Application level components @orbycius

  34. Application level components Project level components @orbycius

  35. Application level components Project level components Module level components @orbycius

  36. class DroidconComponent { } @orbycius

  37. class DroidconComponent: ApplicationComponent { } @orbycius

  38. class DroidconComponent: ApplicationComponent { override fun initComponent() { super.initComponent() if

    (isANewVersion()) { // do something } } } @orbycius
  39. class DroidconComponent: ApplicationComponent { private var version = 1 private

    var localVersion = 0 override fun initComponent() { super.initComponent() if (isANewVersion()) { // do something } } } @orbycius
  40. class DroidconComponent: ApplicationComponent { private var version = 1 private

    var localVersion = 0 private fun isANewVersion() = localVersion < version private fun updateVersion() { localVersion = version } override fun initComponent() { super.initComponent() if (isANewVersion()) { // do something updateVersion() } } } @orbycius
  41. class DroidconComponent: ApplicationComponent, PersistentStateComponent<DroidconComponent> { private var version = 1

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

    1 private var localVersion = 0 override fun getState(): DroidconComponent? = this override fun loadState(state: DroidconComponent) = XmlSerializerUtil.copyBean(state, this) private fun isANewversion() = localVersion < version private fun updateVersion() { localVersion = version } override fun initComponent() { super.initComponent() if (isANewVersion()) { // do something updateVersion() } } } @orbycius
  43. @State(name = "DroidconConfiguration", storages = [ Storage(value = "droidconConfiguration.xml") ])

    class DroidconComponent: ApplicationComponent, PersistentStateComponent<DroidconComponent>, Serializable { private var version = 1 private var localVersion = 0 override fun getState(): DroidconComponent? = this override fun loadState(state: DroidconComponent) = XmlSerializerUtil.copyBean(state, this) private fun isANewVersion() = localVersion < version private fun updateVersion() { localVersion = version } override fun initComponent() { super.initComponent() if (isANewVersion()) { // do something updateVersion() } } } @orbycius
  44. <application-components> <component> <implementation-class> com.marcosholgado.droidcon18.plugin.components.DroidconComponent </implementation-class> </component> </application-components> @orbycius

  45. Actions @orbycius

  46. @orbycius

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

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

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

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

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

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

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

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

  55. Extensions and extension points @orbycius

  56. Show me the code @orbycius

  57. You are still writing code! @orbycius

  58. @orbycius

  59. @orbycius

  60. @orbycius

  61. @orbycius

  62. Java Swing Forms @orbycius

  63. None
  64. None
  65. @orbycius

  66. 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
  67. @orbycius

  68. 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
  69. 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(...) } DialogWrapper(true) @orbycius
  70. 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(...) } private val panel: JiraMovePanel = JiraMovePanel() override fun createCenterPanel(): JComponent = panel } @orbycius
  71. DialogWrapper Panel @orbycius

  72. @orbycius

  73. 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(...) } override fun doOKAction() = presenter.doTransition(...) } @orbycius
  74. If you are using RxJava… Completable.merge(completable) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( {

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

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

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

  78. class JiraComponent: ProjectComponent { } @orbycius

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

    class JiraComponent: ProjectComponent, Serializable, PersistentStateComponent<JiraComponent> { var jiraUrl: String = "" var username: String = "" var password: String = "" var regex: String = "" 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) } } @orbycius
  80. @State(name = "JiraConfiguration", storages = [ Storage(value = "jiraConfiguration.xml") ])

    class JiraComponent: ProjectComponent, Serializable, PersistentStateComponent<JiraComponent> { var jiraUrl: String = "" var username: String = "" var password: String = “ var regex: String = "" 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
  81. <project-components> <component> <implementation-class> com.marcosholgado.droidcon18.plugin.components.JiraComponent </implementation-class> </component> </project-components> @orbycius

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

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

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

    Boolean {} override fun getDisplayName(): String {} override fun apply() {} override fun createComponent(): JComponent? {} } @orbycius
  85. class JiraSettings(private val project: Project): Configurable { private var modified

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

    = false override fun isModified(): Boolean = modified override fun getDisplayName(): String = "Jira Settings" override fun apply() {} override fun createComponent(): JComponent? {} } override fun getDisplayName(): String = "Jira Settings" @orbycius
  87. 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? {} } 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 } @orbycius
  88. 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 } } 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
  89. class JiraSettings(private val project: Project): Configurable { … private var

    modified = false override fun isModified(): Boolean = modified } private var modified = false override fun isModified(): Boolean = modified @orbycius
  90. class JiraSettings(private val project: Project): Configurable, DocumentListener { override fun

    createComponent(): JComponent? { jiraURLField?.document?.addDocumentListener(this) userField?.document?.addDocumentListener(this) passwordField?.document?.addDocumentListener(this) regExField?.document?.addDocumentListener(this) return mainPanel } } DocumentListener { override fun createComponent(): JComponent? { jiraURLField?.document?.addDocumentListener(this) userField?.document?.addDocumentListener(this) passwordField?.document?.addDocumentListener(this) regExField?.document?.addDocumentListener(this) return mainPanel } @orbycius
  91. 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 } @orbycius
  92. <extensions defaultExtensionNs="com.intellij"> <defaultProjectTypeProvider type="Android"/> <projectConfigurable instance=".JiraSettings"> </projectConfigurable> </extensions> @orbycius

  93. Live Templates @orbycius

  94. @orbycius

  95. @orbycius

  96. @orbycius

  97. @orbycius

  98. How do you share them? @orbycius

  99. • Export to a .jar file and import • Copy

    & paste template xml https://www.jetbrains.com/help/idea/2018.1/sharing-live-templates.html How do you share them? @orbycius
  100. How do you share them? $ mv ~/Library/Preferences/AndroidStudio3.2/templates/Droidcon.xml ~/Sandbox/droidcon18-plugin/src/main/resources/liveTemplates/Droidcon.xml @orbycius

  101. How do you share them? $ mv ~/Library/Preferences/AndroidStudio3.2/templates/Droicon.xml ~/Sandbox/droidcon18-plugin/src/main/resources/liveTemplates/Droidcon.xml class

    DroidconLiveTemplateProvider: DefaultLiveTemplatesProvider { override fun getDefaultLiveTemplateFiles(): Array<String> = arrayOf("liveTemplates/Droidcon") override fun getHiddenLiveTemplateFiles(): Array<String>? = null } @orbycius
  102. How do you share them? $ mv ~/Library/Preferences/AndroidStudio3.2/templates/Droicon.xml ~/Sandbox/droidcon18-plugin/src/main/resources/liveTemplates/Droidcon.xml class

    DroidconLiveTemplateProvider: DefaultLiveTemplatesProvider { override fun getDefaultLiveTemplateFiles(): Array<String> = arrayOf("liveTem plates/Droidcon") override fun getHiddenLiveTemplateFiles(): Array<String>? = null } <extensions defaultExtensionNs="com.intellij"> <defaultLiveTemplatesProvider implementation=".DroidconLiveTemplateProvider"/> </extensions> @orbycius
  103. Templates @orbycius

  104. @orbycius

  105. @orbycius

  106. @orbycius

  107. /Applications/Android\ Studio.app/Contents/plugins/android/lib/templates/ @orbycius

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

    ├── AndroidTVActivity │ ├── AndroidThingsActivity │ ├── AndroidThingsPeripheralActivity │ ├── BasicActivity │ ├── BlankWearActivity │ ├── BottomNavigationActivity │ ├── EmptyActivity │ ├── FullscreenActivity │ ├── GoogleAdMobAdsActivity │ ├── GoogleMapsActivity │ ├── GoogleMapsWearActivity │ ├── LoginActivity │ ├── MasterDetailFlow │ ├── NavigationDrawerActivity │ ├── ScrollActivity │ ├── SettingsActivity │ ├── TabbedActivity │ ├── ViewModelActivity @orbycius
  109. $ tree -L 2 -d /Applications/Android\ Studio.app/Contents/plugins/android/lib/templates/ └── other ├──

    AidlFile ├── AidlFolder ├── AndroidAutoMediaService ├── AndroidAutoMessagingService ├── AndroidManifest ├── AppActionsResourceFile ├── AppWidget ├── AssetsFolder ├── BlankFragment ├── BroadcastReceiver ├── ContentProvider ├── CustomView ├── Daydream ├── FontFolder ├── IntentService ├── JavaFolder ├── JniFolder ├── LayoutResourceFile @orbycius
  110. recipe.xml template.xml (UI) .ftl files globals code @orbycius

  111. $ cat template.xml <?xml version="1.0"?> <template format="5" revision="1" name="MVP" minApi="23"

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

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

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

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

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

    </recipe> @orbycius
  117. $ cat recipe.xml.ftl <?xml version="1.0"?> <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" /> </recipe> @orbycius
  118. $ cat recipe.xml.ftl <?xml version="1.0"?> <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" /> </recipe> @orbycius
  119. $ cat recipe.xml.ftl <?xml version="1.0"?> <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"/> </recipe> @orbycius
  120. $ cat recipe.xml.ftl <?xml version="1.0"?> <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" /> </recipe> @orbycius
  121. $ cat recipe.xml.ftl <?xml version="1.0"?> <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" /> </recipe> @orbycius
  122. $ 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
  123. $ cat FeatureContract.kt.ftl package ${escapeKotlinIdentifiers(packageName)} interface ${name}Contract { interface View

    { } interface Presenter { } } @orbycius
  124. What happens when you upgrade to Android Studio 3.3? @orbycius

  125. https://issuetracker.google.com/issues/37105193 Rebecca Franks @riggaroo @orbycius

  126. $ mv $ASPATH/Contents/plugins/android/lib/templates/other/myTemplate ~/.android/templates/other @orbycius

  127. How do you share them? @orbycius

  128. ~/Sandbox/droidcon18-plugin/src/main/resources/androidTemplates/ @orbycius

  129. class CopyTemplatesAction: AnAction() { override fun actionPerformed(event: AnActionEvent) { FileUtils.copyTemplates(

    "/androidTemplates/", "/.android/templates/other", event.project!!) } } ~/Sandbox/droidcon18-plugin/src/main/resources/androidTemplates/ @orbycius
  130. @State(name = "DroidconConfiguration", storages = [ Storage(value = "droidconConfiguration.xml") ])

    class DroidconComponent: ApplicationComponent, Serializable, PersistentStateComponent<DroidconComponent> { … override fun initComponent() { super.initComponent() if (shouldUpdateTemplates()) { // update templates and notify user } } } @orbycius
  131. @State(name = "DroidconConfiguration", storages = [ Storage(value = "droidconConfiguration.xml") ])

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

  133. <extensions defaultExtensionNs="com.android"> <moduleDescriptionProvider implementation=".DroidconModuleProvider"/> </extensions> @orbycius

  134. class DroidconModuleProvider: ModuleDescriptionProvider { override fun getDescriptions(): Collection<ModuleTemplateGalleryEntry> { }

    } @orbycius
  135. 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
  136. 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")) { val applicationTemplates = TemplateManager .getTemplatesFromDirectory(dir, true) for (templateFile in applicationTemplates) { val metadata = manager.getTemplateMetadata(templateFile) if (metadata == null || metadata.category != "Droidcon") continue res.add(DroidconModuleEntry(…)) } break } } return res } @orbycius
  137. private class DroidconModuleEntry(…): ModuleTemplateGalleryEntry { } @orbycius

  138. private class DroidconModuleEntry(…): 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
  139. Write your own Android Studio plugin and automate everything Icons

    made by Smashicons from www.flaticon.com @Orbycius Marcos Holgado
  140. None
  141. Write your own Android Studio plugin and automate everything @Orbycius

    Marcos Holgado (plugin) https://github.com/marcosholgado/droidcon18-plugin (demo) https://github.com/marcosholgado/droidcon18