この記事は CyberAgent Developers Advent Calendar 2023 15日目の記事です。

 

株式会社AbemaTVでAndroidTVアプリを開発している@ameganeです。

サイバーエージェントでは2023の4月からGitHub Copilotの導入を開始し、GitHub Copilotの利用を促進しています。子会社であるAbemaTVでも積極的に導入され、プロダクト開発において欠かせないものとなっています。

また、そのGitHub Copilotを始めとしたコード生成AIが多く誕生しており、その1つにGoogleが提供するStudio Botがあります。本記事では、Studio Botによるコード生成の方法および精度の検討、GitHub Copilotとの比較や併用時の生産性について紹介します。

 

Studio Botとは

Studio BotはGoogleがGoogle I/O 2023で発表したAndroid開発向けのコーディング支援機能です。発表当初はアメリカでのみ利用可能でしたが、その後日本でも9/21より利用可能になりました。

Studio BotはAndroid Studioの一機能としてチャット機能として提供されています。ユーザーのAndroid開発に関する質問に対して自然言語で回答したり、コードを提示してくれたりします。また、Studio Botは会話のコンテキストを記憶しているため、Studio Botからの回答に対するフォローアップの質問でも回答できるとしています。ただし、まだ実験段階のため、不正確であったり誤解を招いたり嘘の情報を提供する場合があります。

余談ですが、Studio Bot自身に聞いたところ、かなり簡素な回答が返ってきました。

Q.What is Studio Bot? A.Studio Bot is powered by Codey, Google's foundation model for coding.

Studio Botに入力するプロンプトについて

公式の紹介ページでは、Studio BotはAndroid開発に関する質問を平易な英語で行うことができるとしています。しかし、実際には日本語で質問しても日本語で回答が返ってきます。これはStudio Botが用いているLLMのPaLM 2やCodeyが日本語に対応しているからだと考えられます。ただし、筆者が試した限りでは、生成される回答の質としては英語のほうが高かったため、以降本記事においてプロンプトは英語とします。

Studio Botからの回答を最適化するためのヒントとして、Googleは以下の3点をTipsとして述べています。

プロンプトは具体的にすること

ユーザーが使用したいライブラリやAPI、アプローチがある場合、それらを質問に含めます。

Do

How do I add camera support to my app using CameraX?

Don’t

How do I add camera support to my app?

質問時に特定の回答形式が欲しい場合はその指示をプロンプトに含めること

Studio Botによって生成されたコードをアプリに挿入して特定の形式でそのコードが必要な場合は、その手順を指定します。

Do

Insert comments into this code. Use double slashes for the comments, and put each comment above the line of code it describes.

Don’t

Write code comments for this code.

複雑なリクエストを一連の単純な質問に分割すること

特にコードを求める場合は、一連の単純な質問をすることで、全体としてより包括的な回答を導き出すことができます。

Do

1. Give me the `MainActivity.kt` file for an app with a login screen.
2. Can you add a class that represents a user profile?
3. Can you add code that supports multiple user profiles?

Don’t

How do I create an app with a login screen that supports multiple user profiles?

 

これらを踏まえると、Studio Botに対する質問を単純明快なものにすることによってより良い回答が得られることが伺えます。

 

Studio Botを実際に使ってみる

いよいよStudio Botを使用していきます。なお、回答全文を載せるとページが長くなってしまうので、生成されたコードや特筆すべきテキスト以外は折りたたみ表示とします。また、出力されたコードやテキストは同じプロンプトを10回入力して筆者が最も精度が高いと感じたものを掲載しています。また、回答の中にはReferencesとしてウェブサイトのページが載っている場合がありますが、リンクが不適切なものもあったため、今回はすべてリンク先のサイトに関する情報に置き換え、リンク自体は本ページには記載しません。

今回はこの3点について検証します。

  1. Jetpack Composeを用いたUIの作成
  2. 著名なライブラリの使用方法
  3. アプリ開発のアーキテクチャについて

 

Jetpack Composeを用いたUIの作成

まずはJetpack ComposeによるUIの作成をStudio Botに質問してみます。

最初に質問するのはリスト型のUIです。リスト内のコンポーネントはTODOリストっぽく左側にチェックボックス、右側にテキストとします。どちらも基本的なUIですね。

Tipsをもとに、「リスト型のUIを作るのに必要な関数を尋ねる→リスト内のコンポーネントを作るのに必要な関数を尋ねる」といった形に分割して質問します。

プロンプト

What functions are required to display a list in Jetpack Compose?

生成されたコード

LazyColumn {
    items(listOf("Item 1", "Item 2", "Item 3")) { item ->
        Text(item)
    }
}
全文

To display a list in Jetpack Compose, you need to use the LazyColumn composable. The LazyColumn composable is a scrollable list that can be used to display a large number of items. It is more efficient than using a Column composable, which loads all of the items at once.
To use the LazyColumn composable, you need to provide it with a list of items to display. You can do this by using the items() modifier. The items() modifier takes a lambda function that returns the items to display.
For example, the following code shows how to display a list of strings in a LazyColumn:


LazyColumn {
    items(listOf("Item 1", "Item 2", "Item 3")) { item ->
        Text(item)
    }
}

The LazyColumn composable also has a number of other modifiers that you can use to customize its appearance. For example, you can use the verticalScroll() modifier to enable scrolling.
For more information on how to use the LazyColumn composable, see the Jetpack Compose documentation.

問題なく出力されています。次にリスト内のコンポーネントについてもチェックします。
プロンプト

How do I display a checkbox on the left side and text on the right side for a component in a list?

生成されたコード

data class Item(
    val checkboxState: Boolean,
    val text: String
)

@Composable
fun ItemRow(item: Item) {
    Row(
        modifier = Modifier
            .padding(horizontal = 16.dp, vertical = 8.dp)
    ) {
        Checkbox(
            checked = item.checkboxState,
            onCheckedChange = { item.checkboxState = it }
        )
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(start = 16.dp),
            text = item.text
        )
    }
}

@Composable
fun ListScreen() {
    LazyColumn {
        items(listOf(
            Item(true, "Item 1"),
            Item(false, "Item 2"),
            Item(true, "Item 3")
        )) { item ->
            ItemRow(item)
        }
    }
}
全文

To display a checkbox on the left side and text on the right side for a component in a list, you can use the following steps:

  1. Create a data class to represent the item in the list. The data class should have a property for the checkbox state and a property for the text.
  2. Create a composable function to render the item in the list. The composable function should take the item as an argument and render the checkbox and text.
  3. Use the LazyColumn composable to display the list of items. The LazyColumn composable will take care of scrolling and loading the items as needed.

Here is an example of how to display a checkbox on the left side and text on the right side for a component in a list:

data class Item(
    val checkboxState: Boolean,
    val text: String
)

@Composable
fun ItemRow(item: Item) {
    Row(
        modifier = Modifier
            .padding(horizontal = 16.dp, vertical = 8.dp)
    ) {
        Checkbox(
            checked = item.checkboxState,
            onCheckedChange = { item.checkboxState = it }
        )
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(start = 16.dp),
            text = item.text
        )
    }
}

@Composable
fun ListScreen() {
    LazyColumn {
        items(listOf(
            Item(true, "Item 1"),
            Item(false, "Item 2"),
            Item(true, "Item 3")
        )) { item ->
            ItemRow(item)
        }
    }
}

This code will display a list of three items. Each item will have a checkbox on the left side and text on the right side. The checkbox will be checked for the first and third items, and unchecked for the second item.

生成されたコードいくつか問題があります。まずビルドが通りません。これは`checkboxState`が定数なのに`onCheckedChange`で値を代入しようとしているためです。また、状態管理が行われていないので、チェックボックスをクリックしてもUIに反映されません。
というわけで状態管理を行うために質問してみます。

プロンプト

Can you give me the code to be able to manage the CheckBox state in Jetpack Compose?

生成されたコード

import androidx.compose.material.Checkbox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue

@Composable
fun CheckBoxExample() {
    // Create a state variable to track the checkbox status.
    var checkedState by mutableStateOf(false)

    // Create a Checkbox composable and pass the state variable as the checked property.
    Checkbox(
        checked = checkedState,
        onCheckedChange = { checkedState = it }
    )
}
全文

Sure, here is the code to be able to manage the CheckBox status in Jetpack Compose:

import androidx.compose.material.Checkbox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue

@Composable
fun CheckBoxExample() {
    // Create a state variable to track the checkbox status.
    var checkedState by mutableStateOf(false)

    // Create a Checkbox composable and pass the state variable as the checked property.
    Checkbox(
        checked = checkedState,
        onCheckedChange = { checkedState = it }
    )
}

This code will render a checkbox that can be checked or unchecked by the user. The state of the checkbox will be tracked in the state variable checkedState.
References
(Workerに関するStackOverFlowの記事)

ここまで出てきたコードを良い感じにまとめたものを出力できると初学者はありがたいと思いますが、筆者が試した限りではうまくいきませんでした。
また、回答にはReferencesとしてStackOverFlowが載っていましたが、内容はWorkerに関するもので本質問とは無関係でした。

著名なライブラリの使用方法

次に、Androidアプリ開発でよく使われているRetrofitとDagger Hiltの使用方法について尋ねてみます。
最初はRetrofitです。

プロンプト

How can I use Retrofit to get data from the API?

生成されたコード

// Create a new project in Android Studio.
// Add the Retrofit library to your project.
// Create a data class to represent the data you want to get from the API.

data class MarsProperty(
    val id: Int,
    val name: String,
    val type: String,
    val price: Double,
    val image: String
)

// Create an interface that defines the methods you want to use to get data from the API.

interface MarsApiService {
    @GET("realestate")
    suspend fun getProperties(): List
}

// Create a Retrofit service object that implements the interface.

object MarsApi {
    val retrofitService: MarsApiService by lazy {
        val retrofit = Retrofit.Builder()
            .baseUrl("https://android-kotlin-fun-mars-server.appspot.com/")
            .addConverterFactory(MoshiConverterFactory.create())
            .build()

        retrofit.create(MarsApiService::class.java)
    }
}

// Use the Retrofit service object to get data from the API.

suspend fun getMarsProperties(): List {
    return MarsApi.retrofitService.getProperties()
}
全文

To use Retrofit to get data from an API, you can follow these steps:

  1. Create a new project in Android Studio.
  2. Add the Retrofit library to your project.
  3. Create a data class to represent the data you want to get from the API.
  4. Create an interface that defines the methods you want to use to get data from the API.
  5. Create a Retrofit service object that implements the interface.
  6. Use the Retrofit service object to get data from the API.

Here is an example of how to use Retrofit to get data from the Mars API:

// Create a new project in Android Studio.
// Add the Retrofit library to your project.
// Create a data class to represent the data you want to get from the API.

data class MarsProperty(
    val id: Int,
    val name: String,
    val type: String,
    val price: Double,
    val image: String
)

// Create an interface that defines the methods you want to use to get data from the API.

interface MarsApiService {
    @GET("realestate")
    suspend fun getProperties(): List
}

// Create a Retrofit service object that implements the interface.

object MarsApi {
    val retrofitService: MarsApiService by lazy {
        val retrofit = Retrofit.Builder()
            .baseUrl("https://android-kotlin-fun-mars-server.appspot.com/")
            .addConverterFactory(MoshiConverterFactory.create())
            .build()

        retrofit.create(MarsApiService::class.java)
    }
}

// Use the Retrofit service object to get data from the API.

suspend fun getMarsProperties(): List {
    return MarsApi.retrofitService.getProperties()
}

You can then use the data you get from the API to display it in your app.

少し前のCodelabに近いコードが出力されました。MarsPropertyのプロパティは適切ではないですが、方法を説明するだけならそれほど問題ないでしょう。
同じプロンプトで複数回実施した場合、他の公開されているAPIのエンドポイントや例示用のURLを使用したり、CallBackやLiveDataを用いているコードが出力されたり、build.gradleに依存関係を追加するところも出力してくれているケースもあります。
依存関係の追加は今回の回答に含まれていませんが、回答の後に質問することで依存関係の追加方法も答えてくれます。
プロンプト

How do I put the libraries needed to make the above code work in build.gradle.kts?

回答

To use Retrofit in your Android app, you need to add the following dependencies to your build.gradle.kts file:

dependencies {
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
    implementation("com.squareup.retrofit2:converter-moshi:2.9.0")
}

(以下省略)

次にDagger Hiltで同様に行ってみます。

プロンプト

How can I introduce Dependency Injection into my Android application with Dagger Hilt?

生成されたコード

// Add the Hilt dependencies to your application's build.gradle file.

dependencies {
    implementation 'com.google.dagger:hilt-android:2.38.1'
    kapt 'com.google.dagger:hilt-android-compiler:2.38.1'
}

// Create an application class that is annotated with @HiltAndroidApp.

@HiltAndroidApp
class MyApplication : Application() {

}

// Use the @AndroidEntryPoint annotation on your Android classes that need to be injected with dependencies.

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var myViewModel: MyViewModel

}

// Use the @Inject annotation on the fields of your classes that need to be injected.

class MyViewModel @Inject constructor(private val myRepository: MyRepository) {

}

全文

To introduce dependency injection into your Android application with Dagger Hilt, you can follow these steps:

  1. Add the Hilt dependencies to your application’s build.gradle file.
  2. Create an application class that is annotated with @HiltAndroidApp.
  3. Use the @AndroidEntryPoint annotation on your Android classes that need to be injected with dependencies.
  4. Use the @Inject annotation on the fields of your classes that need to be injected.
  5. Hilt will automatically generate the necessary code to provide the dependencies to your classes.

Here is an example of how to use Dagger Hilt in an Android application:

// Add the Hilt dependencies to your application's build.gradle file.

dependencies {
    implementation 'com.google.dagger:hilt-android:2.38.1'
    kapt 'com.google.dagger:hilt-android-compiler:2.38.1'
}

// Create an application class that is annotated with @HiltAndroidApp.

@HiltAndroidApp
class MyApplication : Application() {

}

// Use the @AndroidEntryPoint annotation on your Android classes that need to be injected with dependencies.

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var myViewModel: MyViewModel

}

// Use the @Inject annotation on the fields of your classes that need to be injected.

class MyViewModel @Inject constructor(private val myRepository: MyRepository) {

}

Hilt will automatically generate the necessary code to provide the dependencies to your classes. In this example, Hilt will generate a component called MyApplicationComponent that will provide the MyViewModel to the MainActivity. Hilt will also generate a factory class called MyViewModel_Factory that can be used to create instances of the MyViewModel.
You can learn more about Dagger Hilt by reading the official documentation.

Daggerのバージョンが古かったりViewModelがAACのViewModelを用いていなかったりしますが、Kotlinで実装する箇所に限れば問題ないです。実際にはAndroidManifestやbuild.gradleに変更を加える必要があります。
今回の出力ではKotlin側で大きな問題はないですが、同じプロンプトに対する他の回答だとアノテーションの位置がおかしかったり存在しない関数を参照していたりとそもそも動かないコードを生成していることがほとんどでした。
また、今回は著名なライブラリとしてRetrofitとDagger Hiltを例として挙げましたが、ライブラリによって回答精度はかなり変わります。例えばCoilやRoomでは適切なコードや説明が出力された一方、mockkやturbineの精度は微妙でした。
やはりライブラリの使用方法は公式ドキュメントを読むのが一番良いですね。

アプリ開発のアーキテクチャについて

最後はアプリ開発のアーキテクチャについてStudio Botに質問してみます。

プロンプト

What is the architecture usually used in Android application development?

回答

The most common architecture used in Android application development is the MVVM architecture. MVVM stands for Model-View-ViewModel.  In this architecture, the Model represents the data in the application, the View represents the UI, and the ViewModel acts as a bridge between the Model and the View. The ViewModel is responsible for transforming the data from the Model into a format that can be used by the View. This architecture helps to keep the UI and the data separate, which makes it easier to maintain and test the application.

案の定MVVMアーキテクチャという回答が得られました。ここからMVVMアーキテクチャについて質問すればより詳細な答えが返ってきます。

プロンプト

What is the MVVM architecture?
詳細な回答例

The MVVM architecture is a software design pattern that is used in Android application development. It is a variation of the Model-View-Controller (MVC) pattern, and it is designed to make it easier to develop and maintain complex applications.
In the MVVM architecture, the Model represents the data in the application. The View represents the UI, and the ViewModel acts as a bridge between the Model and the View. The ViewModel is responsible for transforming the data from the Model into a format that can be used by the View. This architecture helps to keep the UI and the data separate, which makes it easier to maintain and test the application.
Here is a diagram that shows the MVVM architecture:
[Image of the MVVM architecture]
The Model is the source of truth for the application. It contains all of the data that is used by the application. The Model is typically implemented as a database or a data repository.
The View is the UI of the application. It is responsible for displaying the data from the Model to the user. The View is typically implemented using a layout file and a set of Views.
The ViewModel is the bridge between the Model and the View. It is responsible for transforming the data from the Model into a format that can be used by the View. The ViewModel also handles the logic for the application, such as handling user input and updating the Model.
The MVVM architecture is a powerful tool that can be used to develop complex and maintainable Android applications. It is a good choice for applications that have a lot of data and a complex UI.
Here are some of the benefits of using the MVVM architecture:
It helps to keep the UI and the data separate, which makes it easier to maintain and test the application.
It makes it easier to reuse code, because the ViewModel can be used in multiple Views.
It helps to improve the performance of the application, because the ViewModel can cache data and only update the View when necessary.
If you are developing an Android application, I highly recommend that you consider using the MVVM architecture. It is a powerful tool that can help you to develop complex and maintainable applications.

さて、MVVMアーキテクチャについて説明を行った後はコードで例を出してもらいましょう。

プロンプト

Can you give me an example of the MVVM architecture code?

生成されたコード

// Model
class User {
    val name: String
    val age: Int

    constructor(name: String, age: Int) {
        this.name = name
        this.age = age
    }
}

// ViewModel
class UserViewModel(private val repository: UserRepository) : ViewModel() {
    private val _user = MutableLiveData()
    val user: LiveData = _user

    init {
        viewModelScope.launch {
            try {
                val user = repository.getUserById(userId)
                _user.value = user
            } catch (error: Exception) {
                // Show error message to user
            }
        }
    }
}

// Repository
class UserRepository {
    suspend fun getUserById(userId: String): User {
        // Get user from database
        return User("John Doe", 30)
    }
}

// UI Controller
class UserFragment : Fragment() {
    private val viewModel: UserViewModel by viewModels()

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_user, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewModel.user.observe(viewLifecycleOwner) { user ->
            // Update UI with user data
        }
    }
}
全文

Sure, here is an example of the MVVM architecture code:

// Model
class User {
    val name: String
    val age: Int

    constructor(name: String, age: Int) {
        this.name = name
        this.age = age
    }
}

// ViewModel
class UserViewModel(private val repository: UserRepository) : ViewModel() {
    private val _user = MutableLiveData()
    val user: LiveData = _user

    init {
        viewModelScope.launch {
            try {
                val user = repository.getUserById(userId)
                _user.value = user
            } catch (error: Exception) {
                // Show error message to user
            }
        }
    }
}

// Repository
class UserRepository {
    suspend fun getUserById(userId: String): User {
        // Get user from database
        return User("John Doe", 30)
    }
}

// UI Controller
class UserFragment : Fragment() {
    private val viewModel: UserViewModel by viewModels()

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_user, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewModel.user.observe(viewLifecycleOwner) { user ->
            // Update UI with user data
        }
    }
}

This code shows how the MVVM architecture can be used to separate the UI, data, and business logic of an Android app. The UI controller (UserFragment) observes the data in the ViewModel (UserViewModel), which in turn gets the data from the repository (UserRepository). This allows the UI controller to be decoupled from the data and business logic, making it easier to maintain and test.

References

  1. (リンクからスパムサイトへ遷移するページ)
  2. (ローカルデータベースに関するStackOverFlowの記事)
  3. (Androidアプリアーキテクチャに関する記事)

Userクラスがdata classではなくJavaっぽい書き方になっていたり、userIdが使われていなかったりと突っ込みどころはありますが、アーキテクチャのコード例としては悪くないと思います。
ただ、一部実装がおかしな回答が出力がされるケースもあったので、このくらいの抽象的な質問の仕方だと回答の信頼性は下がりそうです。

Studio BotとGitHub Copilotを組み合わせて開発してみる

Studio Botの使用例を確認したところで、Studio BotとGitHub Copilotを組み合わせて開発を行ってみます。なお、Studio Botはこの記事を執筆している時点では所属部署で使用できないため、個人の開発環境で行います。
今回は簡易的な口コミアプリを作成します。画面は「投稿一覧」、「投稿詳細」、「投稿画面」の3種類で、投稿する場合はGoogleアカウントでの登録およびログインを必要とします。
開発する際、環境構築を除き、Studio BotとGitHub Copilot以外のツール(Google検索・ChatGPTなど)は使用せずに行いました。環境構築のみ許可したのは生成AIがサジェストしてくるライブラリのバージョンが古いことが多く、ライブラリとKotlinやcompileSdkのバージョンの互換性が合わず、環境構築ができなくなることを防ぐためです。
実際に、開発をしてみた結果、すでに実装している箇所を考慮して何かしらを実装する際はGitHub Copilot、新しく何か実装したいけど実装方法や必要なライブラリを忘れたり、GitHub Copilotでのサジェストが微妙だったり、書いたコードを再度精査する際はStudio Botを使うことが多かったです。個人的にはAndroid Studio内ですべてが完結するため、他のウィンドウに目をやる必要がないのはいいですね。ChatGPTとGitHub Copilot、GitHub Copilot Chatを組み合わせて開発している方もすでにいらっしゃると思いますが、同じような使い方でコーディング支援を行ってくれます。ただ、Android開発に特化しているからChatGPTよりもStudio Botのほうが精度が良い…とは必ずしも言えず、筆者の主観ではそこまで差はない印象です。今後、より精度が上がればかなり強力な支援ツールとなるでしょう。
なお、実際に書いたコードはこちらです。コメントでStudio Botと書いてある箇所はStudio Botを参考にして書いたコード、GitHub Copilotと書いてある箇所はGitHub Copilotを参考にして書いたコードです。

まとめ

本記事ではAndroid開発においてStudio BotとGitHub Copilotを用いた開発を紹介しました。Studio Bot、GitHub Copilotは補完的な関係があり、双方を活用することで、より効率的な開発が行えます。普段、ChatGPTを用いて開発しているのであれば、ほぼ移行コストなく開発できると思います。まだ初期の段階ということもあり精度はそこまで高くないですが、基本的な実装に関しては使える範疇であり、今後の精度向上によって強力な支援ツールとなることを期待したいです。

2023年新卒入社のAndroidエンジニアです。現在はABEMAでAndroid TVアプリの開発を行っています。