Chat-GEMINI: Google Gemini AI を使用した Android チャット アプリの構築

<h2> はじめに </h2>
はじめまして、Hridoy Chandra Das(リド)（@ihridoydas）です。

 #### Google Gemini AI Model を使用してAndroidの簡単なアプリケーションを作成する方法を学びたいと思います。

 Androidアプリのアーキテクチャ（MVVM）を使用したGemini AI搭載のAIチャットボットの構築方法について、丁寧にご紹介いたします。

 <h2>Gemini AI モデルとは</h2>
Google Gemini AI モデルは、Googleが開発した自然言語処理のための先進的なモデルです。Geminiは、大規模なデータセットを用いて訓練され、人間のような文章を生成することができます。Geminiは、様々なタスクに応用できる汎用的なモデルであり、チャットボット、テキスト生成、質問応答システムなどのアプリケーションに使用されています。

「Gemini」は、当社の1.0 Proモデルを特に調整したバージョンと共に、現在40以上の言語で利用可能であり、230以上の国と地域で利用できます。
日本語で質問すれば日本語で答えてくれます。

 <h2>Gemini Proの価格設定</h2>
現在、開発者はGoogle AI Studioを通じて、最大60リクエスト/分まで無料でGemini ProとGemini Pro Visionにアクセスできます。これにより、ほとんどのアプリ開発ニーズに適しています。Vertex AIの開発者は、来年初めの一般提供まで、同じモデルと同じレート制限で、無料で試すことができます。その後、Google AI StudioとVertex AIの両方で、1,000文字または1枚の画像あたりに料金が発生します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619500/8b15696c-04b2-ef8b-7ae6-0de250a7a9f6.png)



<h2>新プロジェクトを作成</h2>

アプリ テンプレート：[BaseTemplateAndroidApp](https://github.com/ihridoydas/BaseTemplateAndroidApp) を使用して新しいプロジェクトを作成しました。

https://qiita.com/ihridoydas/items/704fd3fd484c9867d5d8

 ```PackageStructure.md
 # パッケージ構造

com.hridoy.chatgemini   # ルートパッケージ
   |
   ├── common              # 共通パッケージ～すべての重要なコンポーネントとユーティリティ
   |   └── utils           # ユーティリティクラス / Kotlin拡張
   |
   ├── data                # データ処理用
   |   └── ChatData        # APIキーの提供と画像付きおよび画像なしのチャット応答の取得
   |
   ├── domain              # ドメイン
   │   └── model           # モデル（チャット）
   │         └── Chat      # チャットデータクラス
   |
   ├── navigation          # ナビゲーション
   |
   ├── presentation        # UI / View、state、およびeventレイヤー
   |   ├── event           # イベント
   |   ├── screens         # すべての画面がここに配置されます
   │   ├── state           # UIの状態
   │   └── viewmodel       # ビューモデル
   |
   ├── theme               # プロジェクトのテーマ
   |
   ├── BaseApp             # プロジェクトベースアプリケーションクラス
   └── MainActivity        # メインアクティビティ
 ```

<h2> プロジェクトに使うデペンデンシーライブラリー:</h2>

```build.gradle.kts
  // Gemini AI
  implementation("com.google.ai.client.generativeai:generativeai:0.2.0")
  implementation("io.coil-kt:coil-compose:2.5.0")
  implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
  implementation("androidx.compose.material:material-icons-extended:1.6.1")
```

以下のリンクから取得したAPIキーを使用してください：
https://aistudio.google.com/app/prompts/new_chat

このAPIキーを使用することで、プロジェクトがGemini AIと通信できるようになります。

プロジェクトを構築するには、```local.properties```ファイルに次の行を追加する必要があります：
```local.properties
API_KEY=YOUR_API_KEY
```

<h1>Chat データクラスを作成</h1>

```Chat.kt
/**
 * Chat data class
 *
 * @param prompt: String
 * @param bitmap: Bitmap?
 * @param isFromUser: Boolean
 */
data class Chat(
    val prompt: String,
    val bitmap: Bitmap?,
    val isFromUser: Boolean,
)
```
<h1>データレスポンスの取得</h1>

:::note warn
警告
今日は例外処理を改善しませんでしたが必要であれば、これを処理してください。
:::

```ChatData.kt
object ChatData {
    // Provide your API key here(local.properties) -> API_KEY=YOUR_API_KEY
    val api_key = BuildConfig.API_KEY      //You can use directly

    //画像なしで応答を取得する
    suspend fun getResponse(prompt: String): Chat {
        val generativeModel = GenerativeModel(
            modelName = "gemini-pro",
            apiKey = api_key,
        )

        try {
            val response = withContext(Dispatchers.IO) {
                generativeModel.generateContent(prompt)
            }

            return Chat(
                prompt = response.text ?: "error",
                bitmap = null,
                isFromUser = false,
            )
        } catch (e: Exception) {
            return Chat(
                prompt = e.message ?: "error",
                bitmap = null,
                isFromUser = false,
            )
        }
    }

    /**
     * Get response with image
     *
     * @param prompt: String
     * @param bitmap: Bitmap
     * @return Chat
     */
     
   //画像付きのレスポンスを取得する
    suspend fun getResponseWithImage(
        prompt: String,
        bitmap: Bitmap,
    ): Chat {
        val generativeModel = GenerativeModel(
            modelName = "gemini-pro-vision",
            apiKey = api_key,
        )

        try {
            val inputContent = content {
                image(bitmap)
                text(prompt)
            }

            val response = withContext(Dispatchers.IO) {
                generativeModel.generateContent(inputContent)
            }

            return Chat(
                prompt = response.text ?: "error",
                bitmap = null,
                isFromUser = false,
            )
        } catch (e: Exception) {
            return Chat(
                prompt = e.message ?: "error",
                bitmap = null,
                isFromUser = false,
            )
        }
    }
}
```
<h1>UIイベントを作成</h1>

```ChatUiEvent.kt
/**
 * Chat UI event
 */
sealed class ChatUiEvent {
    data class UpdatePrompt(val newPrompt: String) : ChatUiEvent()

    data class SendPrompt(
        val prompt: String,
        val bitmap: Bitmap?,
    ) : ChatUiEvent()

    // show indicator
    data object ShowIndicator : ChatUiEvent()
}
```
<h1>UI State(状態管理)</h1>

```ChatState.kt
/**
 * Chat state class
 *
 * @param chatList: MutableList<Chat>
 * @param prompt: String
 * @param bitmap: Bitmap?
 * @param showIndicator: Boolean
 */
data class ChatState(
    val chatList: MutableList<Chat> = mutableListOf(),
    var prompt: String = "",
    val bitmap: Bitmap? = null,
    val showIndicator: Boolean = false,
)
```
<h1>viewModel</h1>
このデモンストレーションでは、画像なしと画像付きの応答を取得する方法を使用します。

```ChatViewModel.kt
/**
 * Chat view model
 */
class ChatViewModel : ViewModel() {
    private val _chatState = MutableStateFlow(ChatState())
    val chatState = _chatState.asStateFlow()

    /**
     * Handle chat UI event
     *
     * @param event: ChatUiEvent
     */
    fun onEvent(event: ChatUiEvent) {
        when (event) {
            is ChatUiEvent.SendPrompt -> {
                if (event.prompt.isNotEmpty()) {
                    addPrompt(event.prompt, event.bitmap)
                    // Trigger the loading indicator when sending prompt
                    showIndicator()

                    if (event.bitmap != null) {
                        getResponseWithImage(event.prompt, event.bitmap)
                    } else {
                        getResponse(event.prompt)
                    }
                }
            }

            is ChatUiEvent.UpdatePrompt -> {
                _chatState.update {
                    it.copy(prompt = event.newPrompt)
                }
            }

            is ChatUiEvent.ShowIndicator -> {
                showIndicator()
            }
        }
    }
    
    /**
     * Add prompt
     *
     * @param prompt: String
     * @param bitmap: Bitmap?
     */
    private fun addPrompt(
        prompt: String,
        bitmap: Bitmap?,
    ) {
        // Add prompt to chat list
        _chatState.update {
            it.copy(
                chatList = it.chatList.toMutableList().apply {
                    add(0, Chat(prompt, bitmap, true))
                },
                prompt = "",
                bitmap = null,
            )
        }
    }

    /**
     * Get response
     *
     * @param prompt: String
     */
    private fun getResponse(prompt: String) {
        viewModelScope.launch {
            val chat = ChatData.getResponse(prompt)
            _chatState.update {
                it.copy(
                    chatList = it.chatList.toMutableList().apply {
                        add(0, chat)
                    },
                    // Hide loading indicator after response
                    showIndicator = false,
                )
            }
        }
    }

    /**
     * Get response with image
     *
     * @param prompt: String
     * @param bitmap: Bitmap
     */
    private fun getResponseWithImage(
        prompt: String,
        bitmap: Bitmap,
    ) {
        viewModelScope.launch {
            val chat = ChatData.getResponseWithImage(prompt, bitmap)
            _chatState.update {
                it.copy(
                    chatList = it.chatList.toMutableList().apply {
                        add(0, chat)
                    },
                    // Hide loading indicator after response
                    showIndicator = false,
                )
            }
        }
    }

    /**
     * Show indicator
     */
    private fun showIndicator() {
        // Show loading indicator
        _chatState.update {
            it.copy(showIndicator = true)
        }
    }
}
```
<h1>Chat Screen UI作成</h1>
Jetpack Composeを使用してビューを作成しました。以下のGitHub Gistをご覧ください。

https://gist.github.com/ihridoydas/8051134ccc31af790a0c423f3ceba644

#### <ins>結果:</ins> [GitHub サンプル](https://github.com/ihridoydas/ChatGemini)
##### かなりシンプルです…:blush:
以上、最後までお読みいただきありがとうございました。

<img width="400" alt="gemini2.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619500/b6105b8f-9f68-a393-2569-5042cc882663.gif">