Best Premium Templates For App and Software Downloading Site. Made By HIVE-Store

Android App Development

Stay ahead with the latest tools, trends, and best practices in Android development

Build Your Own Jetpack Compose AI Chatbot App in Android

Build Your Own Jetpack Compose AI Chatbot App in Android - Coding Bihar
Build Your Own Jetpack Compose AI Chatbot App in Android

Build Your Own AI Chatbot App in Android Using Jetpack Compose + Open AI API

Have you ever wanted to build your own AI chatbot app? One that talks like ChatGPT, looks modern, and runs natively on Android? Well, you're in the right place.

In this post, I’ll walk you through the why and how of creating a fully functional AI chatbot app using Jetpack Compose and the OpenAI GPT API.

🚀 Why Build an AI Chatbot App?

The popularity of AI chatbots has skyrocketed, and for good reason. They can answer questions, provide suggestions, write content, translate text, and even act as personal assistants. OpenAI’s GPT models have made it easier than ever to add this kind of intelligence to apps.

But instead of using someone else’s app, why not build your own? Here's why it’s worth your time:
  • You’ll learn a lot: Combine modern Android tools like Jetpack Compose, Retrofit, and ViewModel with real-world APIs.
  • It’s customizable: You can tailor the experience—UI, behavior, or personality—to your own needs or even build it for clients.
  • Build a feature-rich chatbot that adds serious value to your dev portfolio.
  • It's just cool. Seriously, building your own AI assistant is just plain awesome.

🧱 What We'll Use

To build this app, we’re using:
  • Jetpack Compose – for modern, declarative UI.
  • OpenAI API – for generating responses from GPT.
  • Retrofit – to call the OpenAI API.
  • MVVM architecture – to structure the app cleanly.
  • Kotlin – the official language of Android development.
  • Simple ViewModel + Repository pattern

🛠️ How to Build the App

Let’s break the process into clear steps. 

📦 Step-by-Step Implementation

🔹 1. 🔑 Get Your API Key

Go to: https://openrouter.ai/keys

Click “Generate Key”

Copy the key — it looks like:
ai-or-v1-e93f9914ddb99a8c0fed5372d68810813bcd895bad3eb414597c26af8a53831f

🔹 2. ✅ Add Dependencies (build.gradle)

plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.kotlin.compose)
    id("com.google.devtools.ksp")
}

implementation("androidx.room:room-runtime:2.7.2")
    implementation("androidx.room:room-ktx:2.7.2")
    ksp("androidx.room:room-compiler:2.7.2")

    implementation ("androidx.lifecycle:lifecycle-viewmodel-compose:2.9.2")
    implementation ("androidx.navigation:navigation-compose:2.9.3")
    implementation ("com.squareup.retrofit2:retrofit:3.0.0")
    implementation ("com.squareup.retrofit2:converter-gson:3.0.0")
Enable KSP in build.gradle:
plugins {
    alias(libs.plugins.android.application) apply false
    alias(libs.plugins.kotlin.android) apply false
    alias(libs.plugins.kotlin.compose) apply false
    id("com.google.devtools.ksp") version "2.0.21-1.0.27" apply false
}

Enable Internet permission in AndroidManifest.xml:

   <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

🔹 3. ✅ Setup Retrofit API Interface

OpenRouterApiService.kt

package com.example.mytalkapp

import retrofit2.http.Body
import retrofit2.http.Header
import retrofit2.http.POST


interface OpenRouterApiService {
    @POST("chat/completions")
    suspend fun sendMessage(
        @Body request: OpenRouterRequest,
        @Header("Authorization") auth: String,
        @Header("Content-Type") contentType: String = "application/json"
    ): OpenRouterResponse
}

🔹 4. ✅  Model and Database Setup

a. ChatMessage.kt
package com.example.mytalkapp

// ✅ File: ChatMessage.kt
data class ChatMessage(
    val text: String,
    val isUser: Boolean,
    val groupId: Long
)

data class OpenRouterMessage(
    val role: String,
    val content: String
)

data class OpenRouterRequest(
    val model: String = "mistral-7b", // or llama3-8b, etc.
    val messages: List
)

data class OpenRouterResponse(
    val choices: List
)

data class Choice(
    val message: OpenRouterMessage
)
b. ChatEntity.kt
package com.example.mytalkapp

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "chat_messages")
data class ChatEntity(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    val text: String,
    val isUser: Boolean,
    val groupId: Long
)
b. ChatDao.kt
package com.example.mytalkapp

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query

@Dao
interface ChatDao {
    @Insert
    suspend fun insertMessage(message: ChatEntity)

    @Query("SELECT * FROM chat_messages ORDER BY id ASC")
    suspend fun getAllMessages(): List

    @Query("DELETE FROM chat_messages")
    suspend fun clearAll()

    @Query("DELETE FROM chat_messages WHERE isUser = 0")
    suspend fun deleteAllAiMessages()

    @Query("DELETE FROM chat_messages WHERE groupId = :groupId")
    suspend fun deleteGroup(groupId: Long)
}
🟢 5. ChatDatabase
package com.example.mytalkapp

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase

@Database(entities = [ChatEntity::class], version = 1)
abstract class ChatDatabase : RoomDatabase() {
    abstract fun chatDao(): ChatDao

    companion object {
        fun getInstance(context: Context): ChatDatabase {
            return Room.databaseBuilder(
                context.applicationContext,
                ChatDatabase::class.java,
                "chat_db"
            ).build()
        }
    }
}
🟢 6. ChatRepository.kt
package com.example.mytalkapp

class ChatRepository(private val dao: ChatDao) {
    suspend fun getAllMessages() = dao.getAllMessages()
    suspend fun insert(message: ChatEntity) = dao.insertMessage(message)
    suspend fun clearAll() = dao.clearAll()
    suspend fun clearOnlyAI() = dao.deleteAllAiMessages()
    suspend fun deleteGroup(groupId: Long) = dao.deleteGroup(groupId)
}
🟢 7. OpenRouterApiService.kt
package com.example.mytalkapp

import retrofit2.http.Body
import retrofit2.http.Header
import retrofit2.http.POST


interface OpenRouterApiService {
    @POST("chat/completions")
    suspend fun sendMessage(
        @Body request: OpenRouterRequest,
        @Header("Authorization") auth: String,
        @Header("Content-Type") contentType: String = "application/json"
    ): OpenRouterResponse
}
🟢 8. OpenRouterApiClient.kt
package com.example.mytalkapp

import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object OpenRouterClient {
    fun create(): OpenRouterApiService {
        return Retrofit.Builder()
            .baseUrl("https://openrouter.ai/api/v1/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(OpenRouterApiService::class.java)
    }
}
🟢 9.ChatViewModel
package com.example.mytalkapp

import android.Manifest
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import androidx.annotation.RequiresPermission
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch

class ChatViewModel(
    private val api: OpenRouterApiService,
    private val apiKey: String,
    private val repository: ChatRepository
) : ViewModel() {

    var chatList by mutableStateOf>(emptyList())
        private set

    var isTyping by mutableStateOf(false)
        private set

    private val messageHistory = mutableListOf()

    init {
        viewModelScope.launch {
            val messages = repository.getAllMessages()
            chatList = messages.map { ChatMessage(it.text, it.isUser, it.groupId) }
            messageHistory.addAll(
                messages.map { OpenRouterMessage(if (it.isUser) "user" else "assistant", it.text) }
            )
        }
    }

    fun clearAllChats() {
        viewModelScope.launch {
            repository.clearAll()
            chatList = emptyList()
            messageHistory.clear()
        }
    }

    fun clearOnlyAI() {
        viewModelScope.launch {
            repository.clearOnlyAI()
            chatList = chatList.filter { it.isUser }
            messageHistory.removeAll { it.role == "assistant" }
        }
    }

    @RequiresPermission(Manifest.permission.ACCESS_NETWORK_STATE)
    fun sendMessage(userInput: String, context: Context) {
        if (userInput.isBlank()) return

        if (!isNetworkAvailable(context)) {
            chatList = chatList + ChatMessage("❌ No internet connection.", isUser = false, groupId = -1L)

            return
        }

        val groupId = System.currentTimeMillis()

        // Add user message to UI and history
        val userMessage = ChatMessage(userInput, isUser = true, groupId = groupId)
        chatList = chatList + userMessage
        messageHistory.add(OpenRouterMessage("user", userInput))

        isTyping = true

        viewModelScope.launch {
            try {
                // Save user message to Room
                repository.insert(ChatEntity(text = userInput, isUser = true, groupId = groupId))

                val response = api.sendMessage(
                    request = OpenRouterRequest(
                        model = "mistralai/mistral-7b-instruct",
                        messages = messageHistory
                    ),
                    auth = "Bearer $apiKey"
                )

                val reply = response.choices.firstOrNull()?.message?.content ?: "No response"

                val aiMessage = ChatMessage(reply, isUser = false, groupId = groupId)
                chatList = chatList + aiMessage
                messageHistory.add(OpenRouterMessage("assistant", reply))

                // Save AI reply to Room
                repository.insert(ChatEntity(text = reply, isUser = false, groupId = groupId))

            } catch (e: Exception) {
                chatList = chatList + ChatMessage("❌ No internet connection.", isUser = false, groupId = -1L)
            } finally {
                isTyping = false
            }
        }
    }

    fun deletePair(groupId: Long) {
        viewModelScope.launch {
            // 1. Delete from Room database
            repository.deleteGroup(groupId)

            // 2. Collect the text of messages that will be deleted
            val textsToRemove = chatList
                .filter { it.groupId == groupId }
                .map { it.text }

            // 3. Remove from in-memory chat list
            chatList = chatList.filterNot { it.groupId == groupId }

            // 4. Remove from message history
            messageHistory.removeAll { it.content in textsToRemove }
        }
    }
}

@RequiresPermission(Manifest.permission.ACCESS_NETWORK_STATE)
fun isNetworkAvailable(context: Context): Boolean {
    val connectivityManager =
        context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

    val network = connectivityManager.activeNetwork ?: return false
    val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false

    return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
}
🟢 10. ChatScreen UI
package com.example.mytalkapp

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay

@Composable
fun ChatScreen(viewModel: ChatViewModel) {
    val context = LocalContext.current // ✅ Add this at the top
    val chatList by remember { derivedStateOf { viewModel.chatList } }
    val isTyping by remember { derivedStateOf { viewModel.isTyping } }
    var userInput by remember { mutableStateOf("") }
    val listState = rememberLazyListState()

    LaunchedEffect(chatList.size) {
        listState.animateScrollToItem(0)
    }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(Color(0xFF101010))
            .systemBarsPadding()
            .padding(12.dp)
    ) {
        Text("Coding Bihar Chatbot", color = Color.Green, style = MaterialTheme.typography.titleLarge)
        LazyColumn(
            state = listState,
            reverseLayout = true,
            modifier = Modifier
                .weight(1f)
                .fillMaxSize()
        ) {
            items(chatList.reversed()) { msg ->
                ChatBubble(message = msg)
                Spacer(modifier = Modifier.height(6.dp))
            }

            // 🟡 Typing animation
            if (isTyping) {
                item {
                    Row(
                        modifier = Modifier.fillMaxWidth(),
                        horizontalArrangement = Arrangement.Start
                    ) {
                        Box(
                            modifier = Modifier
                                .background(Color(0xFF2E2E2E), RoundedCornerShape(18.dp))
                                .padding(horizontal = 16.dp, vertical = 10.dp)
                        ) {
                            TypingDots()
                        }
                    }
                }
            }
        }

        // 🟡 Message input bar
        Row(
            modifier = Modifier
                .padding(top = 8.dp)
                .background(Color(0xFF1E1E1E), shape = RoundedCornerShape(30.dp))
                .padding(horizontal = 12.dp, vertical = 6.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            TextField(
                value = userInput,
                onValueChange = { userInput = it },
                modifier = Modifier
                    .weight(1f)
                    .background(Color.Transparent),
                placeholder = { Text("Type something...", color = Color.Gray) },
                colors = TextFieldDefaults.colors(
                    focusedContainerColor = Color.Transparent,
                    unfocusedContainerColor = Color.Transparent,
                    focusedTextColor = Color.Red,
                    unfocusedTextColor = Color.White,
                    focusedIndicatorColor = Color.Transparent,
                    unfocusedIndicatorColor = Color.Transparent
                )
            )

            IconButton(
                onClick = {
                    if (userInput.isNotBlank()) {
                        viewModel.sendMessage(userInput, context )
                        userInput = ""
                    }
                }
            ) {
                Icon(Icons.Default.PlayArrow, contentDescription = "Send", tint = Color.White)
            }
        }

        Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
            Button(onClick = { viewModel.clearAllChats() }) {
                Text("Clear All")
            }
            Button(onClick = { viewModel.clearOnlyAI() }) {
                Text("Clear AI")
            }
            val lastGroupId = chatList.lastOrNull()?.groupId
            Button(
                onClick = {
                    lastGroupId?.let {
                        viewModel.deletePair(it)
                    }
                }
            ) {
                Text("Delete Pair")
            }

        }

    }

}

@Composable
fun ChatBubble(message: ChatMessage) {
    val bubbleColor = if (message.isUser) Color(0xFF007AFF) else Color(0xFF2E2E2E)
    val textColor = if (message.isUser) Color.White else Color.LightGray
    val avatar = if (message.isUser) "👤" else "🤖"

    Row(
        modifier = Modifier.fillMaxWidth(),
        horizontalArrangement = if (message.isUser) Arrangement.End else Arrangement.Start,
        verticalAlignment = Alignment.Top
    ) {
        if (!message.isUser) {
            Text(avatar, modifier = Modifier.padding(end = 4.dp))
        }

        Box(
            modifier = Modifier
                .background(bubbleColor, RoundedCornerShape(18.dp))
                .padding(12.dp)
                .widthIn(max = 300.dp)
        ) {
            Text(message.text, color = textColor)
        }

        if (message.isUser) {
            Text(avatar, modifier = Modifier.padding(start = 4.dp))
        }
    }
}

@Composable
fun TypingDots() {
    val dotCount = remember { mutableStateOf(1) }
    LaunchedEffect(Unit) {
        while (true) {
            dotCount.value = (dotCount.value % 3) + 1
            delay(400)
        }
    }

    Text(text = ".".repeat(dotCount.value), color = Color.LightGray)
}

🟢 11. MainActivity
package com.example.mytalkapp

import android.app.Activity
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.mytalkapp.ui.theme.MyTalkAppTheme
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

class MainActivity : ComponentActivity() {
  //  private val apiKey = "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"
    @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        val db = ChatDatabase.getInstance(applicationContext)
        val repo = ChatRepository(db.chatDao())
        val apiKey = "your api key"
        val viewModel = ChatViewModel(
            api = OpenRouterClient.create(),
            apiKey = apiKey,
            repository = repo
        )

        setContent {
            MyTalkAppTheme {
                /*Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    Greeting(
                        name = "Android",
                        modifier = Modifier.padding(innerPadding)
                    )
                }*/
                ChatScreen(viewModel)
     
            }
        }
    }
}

Output:

My Talk App Ai chat bot using Jetpack Compose


Special Message

Welcome to Coding