NRIネットコム Blog

NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

ChatGPTでTDDを加速させるAndroid Studio(IntelliJ IDEA)向けプラグインを作ってみた

はじめに

ChatGPTの登場により、AIアシストを上手く使えるのとそうでないのとでは大きく生産性に差が生まれるようになってきました。 既存の業務・開発プロセスにいかにChatGPTなどのAIアシストを組み込めるかあるいは変革できるか、今は様々な領域で試行錯誤されている状況かなと思います。

私は現在Androidアプリを開発しているので、Androidアプリ開発にChatGPTを組み込みたいなと思っているのですが、そもそもAndroidアプリの開発環境であるAndroid Studioのプラグインを開発する知識がありませんでした。 AIアシストの導入方法としてはIDE向けのプラグインだけが唯一の手段という訳ではないですが、最も開発作業に近い距離でアプローチできエンジニアの開発体験を大きく変えられるんじゃないかと思っています。

ということで、今回は練習がてらChatGPTを使用したAndroid Studio(IntelliJ IDEA)向けのプラグインを作ってみました。 この記事では、今回学んだChatGPTを使用したプラグインの作り方を共有しようと思いますので、より多くのAndroidアプリ開発向けAIアシストプラグインが世に出てくると嬉しいです。

作ったもの

今回練習として作ってみたものは、テストコードを選択してプラグインを実行すると、そのテストコードからプロダクトコードをChatGPTに生成してもらい、生成したコードとその説明を逐次IDEのツールウィンドウに表示するというものを作ってみました。 以前、TDDを採用してAndroidアプリ開発をしていたことがあるので、TDDという開発プロセスのうちテストコードを先に書いて「それを満たす最小限のプロダクトコードを書く」という部分をChatGPTで省力化してみようと思いました。

実装したソースコード全体は下記のリポジトリで公開しています。 github.com

下図、プラグインのデモです。複数のテストメソッドを選択してプラグインを実行しており、右側にChatGPTにより生成されたプロダクトコードとその説明が表示されています。

Android Studio(IntelliJ IDEA)向けプラグインの作り方

それでは、早速Android Studio(IntelliJ IDEA)向けプラグインの作り方を紹介していきます。 基本的にはIntelliJの公式に記載されているので、何かわからないことがあればこの辺りを見ると良いと思います。 …嘘です。先にChatGPTに聞いてみると良いと思います。

一応、上記の公式サイトで基本的なことは知ることができますが、より細かなトピックについて知るためには他のプラグインの実装を見てみるのが良いと紹介されています。
Required Experience | IntelliJ Platform Plugin SDK

IntelliJ IDEAのインストール

Android Studio(IntelliJ IDEA)向けのプラグインはIntelliJ IDEAを使うと開発しやすいため、まずは公式サイトからIntelliJ IDEAをダウンロードしインストールしておきます。 この記事ではIntelliJ IDEA 2022.2.5 (Community Edition)を使用しています。
IntelliJ IDEA - Java と Kotlin の最先端 IDE

IDEプラグインプロジェクトの作成

IntelliJ IDEAを起動し、「File > New > Project...」からプロジェクトの新規作成ウィザードを開きます。 左のGeneratorsからIDE Pluginを選択して必要な項目を入力し、新規プロジェクトを作成します。 今回はKotlinで開発したかったので、言語はKotlinを選択しています。

プロジェクトの構成

プロジェクト作成直後の構成ファイルは以下のようになっています。Gradleベースのプロジェクトになるため、Androidアプリ開発でも登場するGradle関連のファイルがあり、srcディレクトリ配下にプラグインの実装コードを含めるパッケージとプラグインの設定を記述する「plugin.xml」というファイルなどが作成されています。

root
├── .run
│   └── Run IDE with Plugin.run.xml
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── src
│   └── main
│       ├── kotlin
│       │   └── s.iwasaki.b.aiassistedtdd
│       └── resources
│           └── META-INF
│               ├── plugin.xml
│               └── pluginIcon.svg
├── .gitignore
├── build.gradle.kts
├── gradlew
├── gradlew.bat
└── settings.gradle.kts

依存関係の追加

今回ChatGPTのクライアントにOpenAI APIで紹介されていた下記のライブラリを使用しようと思います。
github.com

また、プラグインのGUIにはJavaのSwing UIが採用されており、Swing UIでKotlinのコルーチンを使用して非同期処理を行うためには下記のライブラリを入れておく必要があります。
kotlinlang.org

そのため、「build.gradle.kts」には以下のような依存関係を追加しておきます。下記を記述できたらGradle Syncを実行して、依存関係を解決しておきます。

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.6.4")
    implementation("com.aallam.openai:openai-client:3.1.1")
    implementation("io.ktor:ktor-client-okhttp:2.1.3")
}

その他のGradle Taskについてはデフォルトのままで問題ないです。どんな内容が定義されているかは下記公式サイトをご確認ください。
Creating a Plugin Gradle Project | IntelliJ Platform Plugin SDK

プラグインのクラス構成

今回開発するプラグインの実装クラスの構成について説明しておきます。 基本的にプラグインはAnActionクラスを継承したActionクラスを実装し、それをplugin.xmlでユーザのIDE上のアクションに紐づけてプラグインの機能を呼び出せるようにします。 簡単なプラグインであれば、このActionクラスのみ実装すれば良いことになりますが、今回は以下のような理由からその他のクラスについても実装していきます。

Configurableクラス

ChatGPTをAPI経由で利用する場合、そのリクエストにOpenAI上で生成したAPI Keyというものを付与してリクエストする必要があります。 このAPI Keyはユーザによって異なるため、プラグインの利用者が自分のAPI Keyを設定してプラグインを利用できるようにしなければいけません。 そのため、プラグインの設定画面を実装し、その設定画面上でAPI Keyを入力して保存できるようにしておきたいです。 そこで使えるのがこのConfigurableクラスになり、これを実装してplugin.xmlでIDE上の設定と紐づけることで自分のプラグイン用の設定画面をIDEに組み込むことができます。

Serviceクラス

これはプラグインのビジネスロジックをカプセル化するためのクラスになります。 これにより、Actionクラスからビジネスロジックを分離することができ、より複雑なプラグインを開発しようとしたときにActionクラスが肥大化することを防止できます。 今回はわざわざServiceクラスにするほどでもないですが、練習目的で実装してみます。

ToolWindowFactoryクラス

ChatGPTから返却された結果をGUIで表示する最も簡単な方法はDialogを使用することです。 ですが、この方法はダイアログを閉じるまでIDE上のユーザ操作を阻害してしまうため、ChatGPTの結果を見ながら開発することができません。 そこで、ユーザ操作を阻害しないToolWindowというコンポーネントを使用して、IDEの子ウィンドウ上に結果を表示させることにします。 このToolWindow上にどんなコンテンツを表示させるかを実装するために、ToolWindowFactoryクラスが必要になります。

Configurableクラスの実装

下図のようなAPI Keyと使用するModelのIDを入力できる簡単な設定画面を作ります。

ソースコード全体は下記になりますが、大事なところだけ抜粋して紹介します。
chatgpt-idea-plugin/AIAssistedTDDPluginSettings.kt at main · s-iwasaki-dev/chatgpt-idea-plugin · GitHub

設定画面のGUIを構成する

下記のメソッドは設定画面上に描画するコンポーネントを作るときに呼び出されます。 このメソッド内でSwing UIを構成するのですが、今回は簡単にするためにAPI KeyとModelIDを入力するTextFieldを並べたUIとしています。

    override fun createComponent(): JComponent? {
        apiKeyTextField = JTextField(apiKey).also { field ->
            field.setBounds(10, 30, 300, 40)
        }
        modelIdTextField = JTextField(modelId).also { field ->
            field.setBounds(10, 80, 300, 40)
        }
        return JPanel().also {
            it.layout = null
            it.add(apiKeyTextField)
            it.add(modelIdTextField)
        }
    }
設定内容を永続化する

下記はユーザに入力された設定値を永続化するための実装です。今回は.propertiesファイルをローカルに作成しそこに保存するためにProperties (Java Platform SE 8)を使用しています。 apply()は設定値が変更されて決定されたときに呼び出されるので、その中で.propertiesファイルの内容を更新しておきます。 注意ですが、API Keyを含めた.propertiesファイルは機密情報ですので、リポジトリにプッシュしないようにしましょう。

    private val properties: Properties = Properties()

    var apiKey: String?
        get() = properties.getProperty("apiKey")
        set(value) { properties.setProperty("apiKey", value) }

    var modelId: String
        get() = properties.getProperty("modelId", DEFAULT_MODEL_ID)
        set(value) { properties.setProperty("modelId", value) }

    fun loadProject(project: Project) {
        this.project = project
        val propsFile = File(getConfigFilePath())
        if (propsFile.exists()) {
            FileInputStream(propsFile).use { properties.load(it) }
        } else {
            newProperties(propsFile)
        }
    }

    private fun newProperties(file: File) {
        file.parentFile.mkdirs()
        file.createNewFile()
    }

    private fun saveProperties() {
        FileOutputStream(File(getConfigFilePath())).use { properties.store(it, null) }
    }

    override fun apply() {
        apiKey = apiKeyTextField?.text.orEmpty()
        modelId = modelIdTextField?.text.orEmpty()
        saveProperties()
    }
plugin.xmlに設定を追加する

実装したConfigurableクラスをIDEの設定に組み込むために、plugin.xml<extensions>内に以下のような設定を追加します。 parentIdにはどのグループの設定に含ませたいかを指定しています。

<extensions defaultExtensionNs="com.intellij">
    <applicationConfigurable
        parentId="tools"
        instance="s.iwasaki.b.aiassistedtdd.AIAssistedTDDPluginSettings"
        id="s.iwasaki.b.aiassistedtdd.AIAssistedTDDPluginSettings"
        displayName="AI Assisted TDD Plugin" />
</extensions>

Configurableクラスの詳細については、下記をご確認ください。
Settings Guide | IntelliJ Platform Plugin SDK

Serviceクラスの実装

このクラスではIDE上で選択されているテストコードに所定のプロンプトを付与してChatGPTにリクエストを送信し、その結果をKotlin Coroutines Flowを使って返却する機能を実装します。

ソースコード全体は下記になります。
chatgpt-idea-plugin/AIAssistedTDDPluginService.kt at main · s-iwasaki-dev/chatgpt-idea-plugin · GitHub

所定のプロンプトを付与する

引数で渡されたテストコードの文字列に所定のプロンプトを先頭に加えます。 これにより、生成されるコードがKotlin言語で記述されるようにし、またその説明を加えるように指示をしておきます。 ちなみに"簡潔に"という言葉が無いと、生成されたコード内にコメントがつくようになります。

    private fun generatePrompt(testCode: String): String {
        return """
            以下のテストコードを満たすメソッドをKotlinを使用して簡潔に作成し、作成したソースコードの説明を箇条書きでお願いします。
            なお、回答はソースコードの記述から始めてください。
            ```
            $testCode
            ```
        """.trimIndent()
    }
ChatGPTにリクエストする

ライブラリのサンプルコードを参考にChatGPTにリクエストするコードを書きます。

    fun generateProductCode(testCode: String, apiKey: String, modelId: String): Flow<ChatCompletionChunk> {
        val request = ChatCompletionRequest(
            model = ModelId(modelId),
            messages = listOf(
                ChatMessage(
                    role = ChatRole.User,
                    content = generatePrompt(testCode)
                )
            )
        )
        return OpenAI(apiKey).chatCompletions(request)
    }

Serviceクラスの詳細については、下記をご確認ください。
Services | IntelliJ Platform Plugin SDK

ToolWindowFactoryクラスの実装

このクラスではToolWindowに表示するUIを構成します。

ソースコード全体は下記になります。
chatgpt-idea-plugin/AIAssistedTDDPluginToolWindow.kt at main · s-iwasaki-dev/chatgpt-idea-plugin · GitHub

初期表示の実装

ToolWindowがユーザによって開かれたときcreateToolWindowContent()が呼び出されるため、このメソッド内でSwing UIを使用して初期表示を作成しToolWindowに表示させます。 今回の表示内容はChatGPTからの結果を表示するためのTextAreaのみになっているので、初期表示としては固定文言をそこにセットしておきます。

    override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
        updateContent(toolWindow, "Select your test code and run \"Generate Product Code from Test Code\" in context menu.")
    }
表示の更新

表示の更新は何度も呼び出されるため別メソッド化しておきます。引数で渡されたテキストを用いてSwing UIコンポーネントを再生成し、ToolWindowインスタンスにセットし直すことで更新された内容を表示しています。

    fun updateContent(toolWindow: ToolWindow, text: String) {
        val textArea = JBTextArea().also {
            it.isEditable = false
            it.text = text
        }
        val scrollPane = JBScrollPane(textArea, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER)
        val panel = SimpleToolWindowPanel(true, true)
        panel.setContent(scrollPane)

        val content = toolWindow.contentManager.factory.createContent(panel, null, false)
        toolWindow.contentManager.removeAllContents(true)
        toolWindow.contentManager.addContent(content)
    }
plugin.xmlに設定を追加する

実装したToolWindowFactoryクラスをIDEに組み込むために、plugin.xml<extensions>内に以下のような設定を追加します。 これにより、IDEの右端GradleNotificationsなどのToolWindowタブが並んでいる場所に、自分のプラグインのタブが並ぶようになります。

<extensions defaultExtensionNs="com.intellij" >
    <toolWindow
        id="AIAssistedTDDPluginToolWindow"
        anchor="right"
        factoryClass="s.iwasaki.b.aiassistedtdd.AIAssistedTDDPluginToolWindow" />
</extensions>

ToolWindowFactoryクラスの詳細については、下記をご確認ください。
Tool Windows | IntelliJ Platform Plugin SDK

Actionクラスの実装

このクラスでは所定の操作がIDE上で行われたときに実行するプラグインの処理を実装します。 AnActionクラスを継承し、actionPerformed()メソッドをオーバーライドすることで実装できます。

ソースコード全体は下記になります。
chatgpt-idea-plugin/AIAssistedTDDPluginAction.kt at main · s-iwasaki-dev/chatgpt-idea-plugin · GitHub

IDE上で選択されているコードを取得する

引数で渡されるAnActionEventから、以下のようなコードでIDE上で選択されている文字列を取得することができます。

val editor = event.getData(CommonDataKeys.EDITOR) ?: return
val selectedText = editor.selectionModel.selectedText ?: return
API Keyが設定されていないときはIDEの設定画面を表示する

API Keyが設定されていないとプラグインを利用できないため、その場合はIDEの設定画面を開くように以下のような実装をしておきます。 また、設定画面を開くときにAIAssistedTDDPluginSettings::class.javaのように指定することで、自分のプラグインの設定画面を直接開くことができます。

val project = event.project ?: return
val settings = AIAssistedTDDPluginSettings.also { it.loadProject(project) }
if (settings.apiKey.isNullOrEmpty()) {
    ShowSettingsUtil.getInstance().showSettingsDialog(project, AIAssistedTDDPluginSettings::class.java)
}
Serviceクラスの実行と結果の表示

API Keyが設定されていたら、Serviceクラスを実行してChatGPTに選択されているテストコードを引き渡し、生成されたプロダクトコードをFlowを使用して逐次表示させます。

var result = ""
service<AIAssistedTDDPluginService>().generateProductCode(
    testCode = selectedText,
    apiKey = settings.apiKey!!,
    modelId = settings.modelId
).onEach { completion ->
    result += completion.choices.mapNotNull { it.delta?.content }.joinToString("")
    AIAssistedTDDPluginToolWindow.updateContent(toolWindow, result)
}.launchIn(CoroutineScope(Dispatchers.Main))
plugin.xmlに設定を追加する

実装したActionクラスをIDE上の操作と紐づけるために、plugin.xml<actions>内に以下のような設定を追加します。 今回は右クリックしたときに表示されるコンテキストメニューの先頭にGenerate Product Code From Test Codeというメニューを追加し、それが選択されたときにActionクラスを実行するように設定しています。

<actions>
    <action id="AIAssistedTDDPluginAction"
            class="s.iwasaki.b.aiassistedtdd.AIAssistedTDDPluginAction"
            text="Generate Product Code From Test Code">
        <add-to-group group-id="EditorPopupMenu" anchor="first" />
    </action>
</actions>

Actionクラスの詳細については、下記をご確認ください。
Actions | IntelliJ Platform Plugin SDK

動作確認

これで必要なクラスは実装できたので、動作確認していきます。 Gradle Taskの一覧からRun Pluginを実行することで、動作確認用にプラグインがすでにインストールされた状態のIntelliJ IDEAが立ち上がります。

以下のような適当なテストコードを実装して動作確認してみます。 今回はAndroidアプリ開発でよくある、ビジネスロジック(UseCase)の結果を使って画面の状態(ViewState)を更新する処理を想定してテストを書いてみました。
chatgpt-idea-plugin/ViewStateConverterTest.kt at main · s-iwasaki-dev/chatgpt-idea-plugin · GitHub

実際に生成されたプロダクトコードを実装して、テストを実行してみると全て問題なく通りました。

プラグイン配布ファイルのビルド

プラグインの動作確認ができたので、配布用のファイルをビルドしていきます。 Gradle TaskのbuildPluginを実行することで.zip形式の配布ファイルが/build/distributions内に作成されます。

Android Studioで開発したプラグインをインストール

Android Stuidoに開発したプラグインをインストールしていきます。 「File > Settings... > Plugins」からプラグイン一覧画面を開き、歯車アイコンをクリックして「Install Plugin from Disk...」を選択します。

先ほど生成した.zip形式のプラグイン配布ファイルを選択しAndroid Studioを再起動すると使えるようになっています。

おわり

今回初めてAndroid Studioのプラグイン開発をしてみましたが、意外に簡単に作れました。 プラグイン開発について全く知識が無い状態でChatGPTとペアプロしながら作ってみたのですが、1日くらいで形にできたのと結構楽しかったのでそのときの様子はまた別の記事で紹介できたらと思います!

将来的には基本的なAIアシスト機能はツール側が標準で提供してくれるものになりそうですが、自分で開発できるようになっておくと思いついたアイデアをすぐに実践できるし面白いと思います。 Androidデベロッパーな皆さんでAIを活用してAndroidアプリ開発を加速させるプラグインをどんどん作っていきましょう!

すでにリリースされているChatGPTを使用したプラグインは下記で確認できます。
Search Results | JetBrains Marketplace

ちなみに、個人的に今一番欲しいのは、ソースコードや設計書をインプットにして、アプリの既存仕様を自然言語で問い合わせたり改修案の提案を行ってくれるチャットです(プラグイン関係ないですが…)。 どの現場でも企画側から仕様の問い合わせや要求を受け、設計書やソースコードをエンジニアが確認・調査して方針検討するというシーンが多くあると思うので、これがあればすごい便利だと思います。

それを実現してくれそうなGitHub Copilot Xに期待!! github.com

執筆者岩崎 聖夜

モバイルアプリ開発者です。AWSでデータ分析基盤を作ったりもしています。