티스토리 뷰

반응형

개발을 하면서 문득 그런 생각을 하였습니다. 반복적으로 생성하는 클래스를 한번에 생성할 수는 없을까?
개발을 하다보면 여러 프로젝트를 경험할테고 반복적으로 생성되는 클래스들이 많을 것입니다. 또한 협업을 하다보면 동일한 화면을 구현한다고 해도 개발자 각자의 스타일로 개발하다보면 보일러 플레이트 코드가 발생하게 될 수 있습니다.
이러한 문제점을 해결하기 위해서 Template을 만들어 정형화 한다면, 개발자간의 소통 부재와 각자의 스타일로 만들어서 발생할 수 있는 보일러플레이트 코드들을 최소화 할 수 있을 것입니다.
이번 시간은 Android Studio 에서 Template 을 만들 수 있는 방법에 대해서 공유하려고 합니다.

 

반응형

플러그인 만들기

Android Studio Dolphin(213.7172.25) 을 활용해서 플러그인을 만들었습니다.
Android Studio 버전은 Android Studio > About Android Studio 에서 확인 할 수 있습니다.

플로그인을 만들기 앞써 IntelliJ Platform Plugin Tempalte GitHub Repository을 개인 GitHub 에서 사용할 수 있도록 Use this template 해야합니다.
IntelliJ Platform Plugin Tempalte 에 접속하면 아래와 같이 Use this template 가 노출될 것입니다.

해당 버튼을 클릭하여 개인 GitHub의 Repository로 설정합니다.

 

gradle.properties

  • StudioCompilePath, StudioRunPath는 IDE 실행 시 Intellij 대신 Android Studio를 사용하기 위해 경로를 설정합니다.
  • platformPlugins 에서 사용 할 java, android, kotlin 을 세팅합니다.
# IntelliJ Platform Artifacts Repositories -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html

pluginGroup = com.github.faithdeveloper.testplugintemplate
pluginName = TestPlugInTemplate // 플러그인 이름
# SemVer format -> https://semver.org
pluginVersion = 0.0.3  // 플러그인 버전

# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
pluginSinceBuild = 213
pluginUntilBuild = 222.*

# IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#intellij-extension
platformType = IC
platformVersion = 2021.3.3

# Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
# Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22
platformPlugins = java, com.intellij.java, org.jetbrains.android, android, org.jetbrains.kotlin

# Gradle Releases -> https://github.com/gradle/gradle/releases
gradleVersion = 7.5.1

# Opt-out flag for bundling Kotlin standard library -> https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library
# suppress inspection "UnusedProperty"
kotlin.stdlib.default.dependency = false

StudioCompilePath = /Applications/Android Studio.app/Contents

StudioRunPath=/Applications/Android Studio.app/Contents

 

Build gradle.kts 설정하기

  • build.gradle.kts
  • intellij.localPathStudioRunPath를 설정합니다. 위와 같이 설정하는 이유는 나중에 runIde 시 Intellij가 아닌 Android Studio가 열리게 하기 위해서입니다.
import org.jetbrains.changelog.markdownToHTML

fun properties(key: String) =project.findProperty(key).toString()

plugins{
// Java support
    id("java")
    // Kotlin support
    id("org.jetbrains.kotlin.jvm")version"1.7.10"
    // Gradle IntelliJ Plugin
    id("org.jetbrains.intellij")version"1.0"
    // Gradle Changelog Plugin
    id("org.jetbrains.changelog")version"1.3.1"
    // Gradle Qodana Plugin
    id("org.jetbrains.qodana")version"0.1.13"
    // detekt linter - read more: https://detekt.github.io/detekt/gradle.html
    id("io.gitlab.arturbosch.detekt")version"1.17.1"
    // ktlint linter - read more: https://github.com/JLLeitschuh/ktlint-gradle
    id("org.jlleitschuh.gradle.ktlint")version"10.0.0"
}

...

// Configure Gradle IntelliJ Plugin - read more: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html
intellij{
pluginName.set(properties("pluginName"))
    version.set(properties("platformVersion"))
    type.set(properties("platformType"))

intellij.localPath.set(properties("StudioRunPath"))
    // Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file.
    plugins.set(properties("platformPlugins").split(',').map(String::trim).filter(String::isNotEmpty))
}

...

tasks{
instrumentCode{
compilerVersion.set("213.7172.25") // Android Studio Version
}

...

 

Plugin.xml 설정하기

  • src/main/resource/META-INF/plugin.xml
  • gradle.properties 에서 정의한 pluginGroup, pluginName을 id와 name에 설정합니다.
  • 프로젝트 구조를 pluginGroup 과 동일하게 설정합니다.
  • vendor는 faithdeveloper 으로 설정하였습니다. custom 하게 하시면 됩니다.
<!-- Plugin Configuration File. Read more: https://plugins.jetbrains.com/docs/intellij/plugin-configuration-file.html -->
<idea-plugin>
    <id>com.github.faithdeveloper.testplugintemplate</id>
    <name>TestPlugInTemplate</name>
    <vendor>faithdeveloper</vendor>

    <depends>com.intellij.modules.platform</depends>
    <!-- 우리가 사용할 디펜던시를 정의합니다. -->
    <depends>org.jetbrains.android</depends>
    <depends>org.jetbrains.kotlin</depends>
    <depends>com.intellij.modules.java</depends>
    <depends>com.intellij.modules.platform</depends>
    <depends>com.intellij.modules.androidstudio</depends>

    <extensions defaultExtensionNs="com.intellij">
        <applicationService serviceImplementation="com.github.faithdeveloper.testplugintemplate.services.MyApplicationService"/>
        <projectService serviceImplementation="com.github.faithdeveloper.testplugintemplate.services.MyProjectService"/>
    </extensions>

     <!-- WizardTemplateProviderImpl 파일을 생성해줍니다. 해당 클래스에서 실행될 template 을 정의할 예정입니다. -->
    <extensions defaultExtensionNs="com.android.tools.idea.wizard.template">
        <wizardTemplateProvider implementation="com.github.faithdeveloper.testplugintemplate.WizardTemplateProviderImpl" />
    </extensions>

    <applicationListeners>
        <listener class="com.github.faithdeveloper.testplugintemplate.listeners.MyProjectManagerListener"
                  topic="com.intellij.openapi.project.ProjectManagerListener"/>
    </applicationListeners>
</idea-plugin>

 

MyProjectManagerListener 설정하기

MyProjectManagerListener.kt
Project Open 과 Close에 따른 리스너 처리를 합니다.

internal class MyProjectManagerListener : ProjectManagerListener {

    //Android Studio에서 프로젝트를 열 때마다 해당 function 호출
    override fun projectOpened(project: Project) {
        println("######### WellCommon Project Name : ${project.name} #########")

        // 만약 특정 prefix Name 만 동작하고 싶을 시 주석 삭제
                // project.name.startsWith("Test", ignoreCase = true)
        projectInstance = project
                // }
        project.service<MyProjectService>()
    }

    //Android Studio에서 프로젝트를 닫을 때마다 해당 function 호출
    override fun projectClosing(project: Project) {
        // 만약 특정 prefix Name 만 동작하고 싶을 시 주석 삭제
                // project.name.startsWith("Test", ignoreCase = true)
        projectInstance = null
                // }
        super.projectClosing(project)
    }

    companion object {
        var projectInstance: Project? = null
    }
}

 

WizardTemplateProviderImpl 설정하기

WizardTemplateProviderImpl.kt
listOf 로 Template 리스트를 정의합니다.
아래 코드는 recyclerActivitySetupTemplate 라는 Template 을 선언한 것입니다.
만약, scrollActivitySetupTemplate 도 추가한다면 listOf(recyclerActivitySetupTemplate scrollActivitySetupTemplate) 이렇게 구성하면 됩니다.

class WizardTemplateProviderImpl : WizardTemplateProvider() {
   override fun getTemplates(): List<Template> =listOf(recyclerActivitySetupTemplate)
}

 

RecyclerActivitySetupTemplate 설정하기

RecyclerActivitySetupTemplate.kt
생성할 Template 의 기본적인 정보를 정의합니다.
Template 을 선택하면 사용자에게 보여질 화면에서 입력 받을 파라미터와 Recipe를 정의합니다.

valrecyclerActivitySetupTemplate
get() = template{
                name = "MVVM RecyclerView Activity"
        description = "This Template make RecyclerView Template with MVVM Architecture."
        minApi = 21
        category = Category.Other// Check other categories
        formFactor = FormFactor.Mobile
                screens =listOf(
            WizardUiContext.FragmentGallery, WizardUiContext.MenuEntry,
            WizardUiContext.NewProject, WizardUiContext.NewModule
                )

        val packageNameParam =defaultPackageNameParameter
                val className = stringParameter{
                name = "Class Name"
                            default = "" //ex) default = "RecyclerViewActivity"
                            help = "Please, Input Class Name."
                            constraints =listOf(Constraint.NONEMPTY)
                    }

val activityLayoutName = stringParameter{
                        name = "Activity Layout Name."
            default = "" //ex) default = "RecyclerView"
            help = "Please, Input Layout Name"
            constraints =listOf(Constraint.LAYOUT, Constraint.UNIQUE, Constraint.NONEMPTY)
            suggest ={activityToLayout(className.value.toSnakeCase())}
        }

            widgets(
                        TextFieldWidget(className),
                        TextFieldWidget(activityLayoutName),
                        PackageNameWidget(packageNameParam)
                    )

        recipe = {
                        data: TemplateData->
                        // Template 생성
                        mvvmRecyclerActivitySetup(
                data as ModuleTemplateData,
                packageNameParam.value,
                className.value,
                activityLayoutName.value
            )
                }
    }

 

RecyclerActivitySetupRecipe 설정

RecyclerActivitySetupRecipe.kt

mvvmRecyclerActivitySetup 이름의 RecipeExecutor 확장 함수 정의를 합니다.

Template 생성 할 위치 설정 및 파일을 생성하게 됩니다.

fun RecipeExecutor.mvvmRecyclerActivitySetup(
    moduleData: ModuleTemplateData,
    packageName: String,
    className: String,
    activityLayoutName: String,
) {
    val (projectData, _, _, manifestOut) = moduleData
    val project = projectInstance ?: run {
        println("projectInstance is null")
        return
    }

    addAllKotlinDependencies(moduleData)

    val virtualFiles = ProjectRootManager.getInstance(project).contentSourceRoots
    val virtSrc = virtualFiles.firstOrNull { it.path.contains("app/src/main/java") } ?: return
    val virtRes = virtualFiles.firstOrNull { it.path.contains("app/src/main/res") } ?: return
    val directorySrc = PsiManager.getInstance(project).findDirectory(virtSrc) ?: return
    val directoryRes = PsiManager.getInstance(project).findDirectory(virtRes) ?: return

    val activityClass = "${className}Activity".replaceFirstChar { it }
    val adapterClass = "${className}RecyclerAdapter".replaceFirstChar { it }
    val viewHolderClass = "${className}ItemViewHolder".replaceFirstChar { it }
    val viewModelClass = "${className}ViewModel".replaceFirstChar { it }

    println("[check]packageName = $packageName")

    // Activity 추가 시 Manifest 에 추가를 원할 시 주석제거 (개선필요)
//    mergeXml(
//        manifestTemplateXml(projectData = projectData, packageName = packageName, activityClassName = "${className}Activity"),
//        manifestOut.resolve("AndroidManifest.xml")
//    )

    createRecyclerActivity(packageName, className, activityLayoutName, projectData)
        .save(directorySrc, packageName, "$activityClass.kt")

    createRecyclerAdapter(packageName, className)
        .save(directorySrc, "$packageName.adapter", "$adapterClass.kt")

    createViewHolder(packageName, className)
        .save(directorySrc, "$packageName.viewholder", "$viewHolderClass.kt")

    createViewModel(packageName, className)
        .save(directorySrc, "$packageName.viewmodel", "$viewModelClass.kt")

    createRecyclerActivityLayout(packageName, className)
        .save(directoryRes, "layout", "${activityLayoutName}.xml")

    createViewHolderLayout()
        .save(directoryRes, "layout", "item_${className.toSnakeCase()}.xml")
}

 

Template 생성 시 각 파일 정의하기

Activity.kt
Adapter.kt
AndroidManifest.kt
ViewHolder.kt
ViewModel.kt

선언된 String 이 그대로 생성되므로 import나 ClassName 등의 Template 생성 시 구성하려는 것을 정확히 입력합니다.

fun createRecyclerActivity(
    packageName: String,
    className: String,
    activityLayoutName: String,
    projectData: ProjectTemplateData
) = """
    package $packageName

    import ${projectData.applicationPackage}.R
    import androidx.appcompat.app.AppCompatActivity
    import android.os.Bundle

    class ${className}Activity : AppCompatActivity() {
    }
""".trimIndent()

 

Plugin Install

Gradle Run Plugin을 실행하면 Build/libs 안에 jar 파일을 볼 수 있습니다.

Gradle Run Plugin 하는 방법은 여러방법이 있는데 쉽게 할 수 있는 방법은 Android Studio의 Build 를 Run Plugin 로 설정하면됩니다.

생성된 jar 파일을 Android Studio Plugins에서 세팅하면 지금 만든 Template을 사용할 수 있습니다.

 

PlugIn 사용 예시

 

마무리

이미 저와 같은 Template 을 만드는 것에 대해 고민 한 사람들을 쉽게 발견할 수 있었으며, 개발 포스트로 다양한 예제 소스를 제공 있었습니다. 이번 포스트는 개발 포스트에 올라온 것을 참고하여 공유하였는데요.
Template을 자신의 프로젝트의 특성에 맞게 구성한다면 보일러플레이트 코드를 최소화 할 수 있으며, 개발 속도도 향상될 것으로 예상됩니다.

 

참고

스마트한 개발을 위한 Android Studio 플러그인 템플릿
https://github.com/JetBrains/intellij-platform-plugin-template
https://github.com/FaithDeveloper/TestPlugInTemplate 

반응형
댓글