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

Write your own Android Studio Plugin and automate everything.

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.

Marcos

April 23, 2019
Tweet

More Decks by Marcos

Other Decks in Programming

Transcript

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

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

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

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

    plugins = ['Kotlin', 'git4idea', 'android'] updateSinceUntilBuild false } version '2018.2.2' @Orbycius
  5. 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
  6. 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
  7. 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
  8. 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
  9. 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
  10. class DemoComponent } { : ApplicationComponent override fun initComponent() {

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

    super.initComponent() if (isANewVersion()) { // do something } } public var version = 1 public var localVersion = 0 @Orbycius
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. <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
  18. <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
  19. <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
  20. <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
  21. <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
  22. <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
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. If you are using RxJava… Completable.merge(completable) .subscribeOn(Schedulers.io()) .subscribe( { view.success(selectedItem,

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

    ticket) }, { error -> view.error(error) } ) .observeOn( AndroidSchedulers.mainThread()) @Orbycius
  30. 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
  31. 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
  32. @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
  33. @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
  34. class JiraSettings { private var userField: JTextField? = null private

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

    Boolean {} override fun getDisplayName(): String {} override fun apply() {} override fun createComponent(): JComponent? {} } @Orbycius
  36. 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
  37. 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
  38. 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
  39. 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
  40. 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
  41. 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
  42. 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
  43. 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
  44. 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
  45. 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
  46. 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
  47. 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
  48. /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
  49. │ ├── NewInstantFeatureModule │ ├── NewJavaLibrary │ └── common └──

    other ├── AidlFile ├── AidlFolder ├── AndroidAutoMediaService ├── AndroidAutoMessagingService ├── AndroidManifest ├── AppActionsResourceFile ├── AppWidget ├── AssetsFolder ├── BlankFragment ├── BroadcastReceiver ├── ContentProvider ├── CustomView ├── Daydream ├── FontFolder ├── IntentService ├── JavaFolder ├── JniFolder ├── LayoutResourceFile @Orbycius
  50. $ 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
  51. $ 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
  52. $ 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
  53. $ 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
  54. $ 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
  55. $ 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
  56. $ 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
  57. $ 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
  58. $ 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
  59. $ 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
  60. ~/Sandbox/demo-plugin/src/main/resources/androidTemplates/ class CopyTemplatesAction: AnAction() { override fun actionPerformed(event: AnActionEvent) {

    FileUtils.copyTemplates( "/androidTemplates/", "/.android/templates/other", event.project!!) } } @Orbycius
  61. @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
  62. @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
  63. 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
  64. 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
  65. 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
  66. Write your own Android Studio plugin and automate everything Icons

    made by Smashicons from www.flaticon.com @Orbycius Marcos Holgado