AI-powered chatbots are widely used across industries like education, food delivery, and now, even software development. Since the release of large language models (LLMs) from Google and OpenAI, implementing AI-powered chatbots in projects has become much more accessible.
Google’s Generative AI offers substantial benefits by enabling content creation, personalization, decision support, and simulation, which improve productivity and user engagement across various domains. Generative AI can streamline workflows, automate repetitive tasks, and allow professionals to focus on higher-level objectives, making it an ideal choice if you're considering launching a chatbot service.
In this article, we’ll guide you through implementing an AI-powered chatbot application for Android using Firebase's Realtime Database, covering key components like chat channels and message screens.
To get started, clone the AI Chat Android GitHub repository to your local machine using the command below. Once cloned, you can open the project in Android Studio for setup and exploration.
https://github.com/GetStream/ai-chat-android
Next, let's configure the secret properties needed before building the project.
Configure Secret Properties With API Keys
The AI Chat Android project uses the secrets-gradle-plugin for secure API configuration, ensuring that sensitive information remains protected and is not exposed in public repositories. To start configuring API keys, create a file named secrets.properties
in the root directory and add the following properties:
GEMINI_API_KEY=
REALTIME_DATABASE_URL=
You'll need the following API keys to build AI Chat Android locally:
- Google Cloud API Key: To access Google’s generative AI SDK, obtain a Google Cloud API key from Google AI Studio. This can be easily generated with your Google account.
- Firebase Realtime Database URL: Follow the Firebase setup guidelines and Realtime Database configuration instructions to set up your Realtime Database. Retrieve the Firebase Realtime Database URL as shown in the example image below.
Build AI Chat Bot App
After completing the project build, you should see the following results:
Now it’s time to build your own AI-powered chatbot, inspired by the AI Chat Android project! This post will guide you through the key features and implementation details of AI Chat Android, giving you the knowledge and tools to develop your own application.
Firebase Realtime Database
Firebase Realtime Database is a cloud-hosted database that uses data synchronization instead of typical HTTP requests. This means every time data changes, connected devices receive updates within milliseconds, enabling collaborative and responsive experiences without complex networking code.
In this project, Firebase Realtime Database is utilized for both remote storage and real-time communication, creating a responsive chatbot system powered by Gemini. The project uses the firebase-android-ktx library, which enables you to observe changes in the Realtime Database as a Flow, offering flexible and customizable serialization options.
To get started with this library, add the following dependency to your app's module.gradle.kts
file:
123dependencies { implementation("com.github.skydoves:firebase-database-ktx:0.2.0") }
You can continuously observe changes in snapshot data as a Flow by applying the flow()
method with a custom serialization approach to your DatabaseReference
instance using kotlinx-serialization, as shown in the example below:
1234567891011121314151617181920212223internal class ChannelsRepositoryImpl @Inject constructor( private val databaseReference: DatabaseReference, private val json: Json, ) : ChannelsRepository { override fun fetchChannels(): Flow<Result<ChannelsSnapshot?>> { return databaseReference.flow( path = { snapshot -> snapshot }, decodeProvider = { jsonString -> json.decodeFromString(jsonString) }, ) } override fun fetchChannel(index: Int): Flow<Result<Channel?>> { return databaseReference.flow( path = { snapshot -> snapshot.child("channels/$index") }, decodeProvider = { jsonString -> json.decodeFromString(jsonString) }, ) } }
Implement a Channel List
Start by implementing the channel list screen, which displays a list of channels, allowing users to select an existing thread to continue their conversation with the AI bot. Additionally, users should have the option to create a new channel for a fresh chat.
Repository
To start, let's outline the key functions needed for the channel list screen. You should implement the following three primary methods:
fetchChannels
: This method retrieves all existing channels from the Firebase Realtime Database to display them in the channel list.fetchChannel
: Use this method to fetch details of a specific channel, particularly when navigating to a chatbot message screen.addChannel
: This method allows users to add a new channel to the Firebase Realtime Database.
You can implement the specifications outlined above in a class named ChannelsRepositoryImpl
, as shown in the example code below:
1234567891011121314151617181920212223242526272829303132internal class ChannelsRepositoryImpl @Inject constructor( private val databaseReference: DatabaseReference, private val json: Json, ) : ChannelsRepository { override fun fetchChannels(): Flow<Result<ChannelsSnapshot?>> { return databaseReference.flow( path = { snapshot -> snapshot }, decodeProvider = { jsonString -> json.decodeFromString(jsonString) }, ) } override fun fetchChannel(index: Int): Flow<Result<Channel?>> { return databaseReference.flow( path = { snapshot -> snapshot.child("channels/$index") }, decodeProvider = { jsonString -> json.decodeFromString(jsonString) }, ) } override fun addChannel(channels: List<Channel>) { val newChannels = channels + Channel( id = UUID.randomUUID().toString(), messages = listOf(Message.defaultMessage()), ) databaseReference.child("channels").setValue(newChannels) } }
In the addChannel
function, the entire existing list of channels is retrieved, and the new channel is added by setting the updated list. This approach addresses a limitation in Firebase Realtime Database, which doesn’t allow adding a single entry to a specific node directly. Due to its NoSQL structure, Firebase Realtime Database lacks native querying, so locating a specific node requires iterating through the list using a given index, as demonstrated in the fetchChannel
method.
ViewModel
Now, it’s time to fetch channel data from the repository in the ViewModel and hold it as a state to be observed by the UI. You can achieve this by using various Flow extensions, such as mapLatest
, filterNotNull
, and converting it to StateFlow with the stateIn
method, as shown in the example below:
1234567891011121314151617181920212223242526@HiltViewModel class ChannelsViewModel @Inject constructor( private val channelsRepository: ChannelsRepository, ) : ViewModel() { val channels: StateFlow<List<Channel>> = channelsRepository.fetchChannels() .mapLatest { result -> result.getOrNull() } .filterNotNull() .map { snapshot -> snapshot.channels } .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = emptyList(), ) fun handleEvents(channelsEvent: ChannelsEvent) { when (channelsEvent) { is ChannelsEvent.CreateChannel -> channelsRepository.addChannel(channels.value) } } } sealed interface ChannelsEvent { data object CreateChannel : ChannelsEvent }
Let's break down each operation one-by-one:
- mapLatest: Transforms elements from the original flow using the specified transform function. If a new value is emitted, any ongoing transformation for the previous value is canceled.
- filterNotNull: Filters the original flow to include only non-null values.
- stateIn: Converts a cold flow into a hot
StateFlow
that begins in the provided coroutine scope, sharing the most recent value emitted by the upstream flow with multiple subscribers.
Compose UI
In the Channels
composable function, you’ll find three main components: a top app bar, a body section, and a floating action button, as shown in the code example below:
1234567891011121314151617181920Box( modifier = Modifier .fillMaxSize() .background(AIChatTheme.colors.background), ) { Column(modifier = Modifier.fillMaxSize()) { ChannelAppBar() ChannelContentBody( channels = channels, onChannelClick = { index, channel -> navigateToMessages.invoke(index, channel) }, ) } ChannelFloatingButton( channelSize = channels.size, channelsViewModel = channelsViewModel, ) }
In the ChannelContentBody
composable function, a LazyColumn
is used to display a list of channels, with each item represented by the ChannelItem
composable function, as shown in the example below:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758@Composable private fun ChannelContentBody( channels: List<Channel>, onChannelClick: (Int, Channel) -> Unit, ) { LazyColumn(modifier = Modifier.fillMaxSize()) { itemsIndexed(items = channels, key = { _, item -> item.id }) { index, channel -> ChannelItem(index = index, channel = channel, onChannelClick = onChannelClick) } } } @Composable private fun ChannelItem( index: Int, channel: Channel, onChannelClick: (Int, Channel) -> Unit, ) { Box( modifier = Modifier .fillMaxWidth() .clickable { onChannelClick.invoke(index, channel) }, ) { ListItem( modifier = Modifier.fillMaxWidth(), colors = ListItemDefaults.colors(containerColor = AIChatTheme.colors.itemContent), leadingContent = { Image( modifier = Modifier .align(Alignment.CenterStart) .clip(CircleShape) .size(42.dp), painter = painterResource(io.getstream.ai.chat.core.designsystem.R.drawable.ic_gemini), contentDescription = null, ) }, headlineContent = { Text( text = "Channel${channel.id.take(3)}", fontSize = 18.sp, fontWeight = FontWeight.Bold, color = AIChatTheme.colors.textHighEmphasis, ) }, supportingContent = { Text( text = channel.messages.last().message, fontSize = 14.sp, color = AIChatTheme.colors.textLowEmphasis, overflow = TextOverflow.Ellipsis, maxLines = 2, ) }, ) HorizontalDivider() } }
As a result, it will display the list of channels that are stored and updated by Firebase Realtime Database that is created by users like the screenshot below:
Implement an AI Chat Bot
Now it’s time to implement the AI chatbot messaging screen, enabling users to type questions for the AI and receive responses in a streaming format.
Repository
First, let’s consider implementing the AI chat messaging screen. The two primary functions you'll need are: fetching messages from Firebase in real-time and sending messages to Firebase. You can implement these features as shown in the methods below:
fetchMessages
: Retrieves messages from Firebase and displays updates in real-time, creating a live chat experience.sendMessage
: Sends a message to Firebase, enabling message history storage so that past conversations are displayed when the user returns to the app.
However, you’ve already implemented a similar approach with the fetchChannel
method in the channels repository, where the response contains all message information. This illustrates the benefits of using the repository pattern, which promotes flexibility and reusability of domain logic.
Implement these specifications in a class named MessagesRepositoryImpl
will be simple, as shown in the example code below:
12345678910111213141516internal class MessagesRepositoryImpl @Inject constructor( private val databaseReference: DatabaseReference, ) : MessagesRepository { override fun sendMessage(index: Int, channel: Channel, message: String, sender: String) { val messages = channel.messages.toMutableList() messages.add( Message( sender = sender, message = message, ), ) val newChannel = channel.copy(messages = messages) databaseReference.child("channels/$index").setValue(newChannel) } }
ViewModel
Next, implement the messages ViewModel, which fetches data from the repository and stores it as observable states for the UI layers. Additionally, retrieve the channel information containing all message data. You can implement these specifications as shown in the example code below:
12345678910111213141516171819202122232425@HiltViewModel(assistedFactory = MessagesViewModel.Factory::class) class MessagesViewModel @AssistedInject constructor( channelsRepository: ChannelsRepository, private val messagesRepository: MessagesRepository, @Assisted private val index: Int, ) : ViewModel() { val channelState: StateFlow<Channel?> = channelsRepository.fetchChannel(index) .mapLatest { result -> result.getOrNull() } .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = null, ) val messages: StateFlow<List<Message>> = channelState .mapLatest { it?.messages }.filterNotNull() .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = emptyList(), ) }
Next, handle events passed from the UI layers and send messages to the Firebase Realtime Database, as shown in the example code below:
1234567891011121314151617181920212223242526272829303132333435363738394041424344class MessagesViewModel: ViewModel() { .. private val events: MutableStateFlow<MessagesEvent> = MutableStateFlow(MessagesEvent.Nothing) val latestResponse: StateFlow<String?> = events.flatMapLatest { event -> if (event is MessagesEvent.SendMessage) { generativeChat.value.sendMessageStream(event.message).map { it.text } } else { flowOf("") } }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = null, ) fun handleEvents(messagesEvent: MessagesEvent) { this.events.value = messagesEvent when (messagesEvent) { is MessagesEvent.SendMessage -> sendMessage( message = messagesEvent.message, sender = messagesEvent.sender, ) is MessagesEvent.CompleteGeneration -> { sendMessage( message = messagesEvent.message, sender = messagesEvent.sender, ) } is MessagesEvent.Nothing -> Unit } } private fun sendMessage(message: String, sender: String) { messagesRepository.sendMessage( index = index, channel = channelState.value!!, message = message, sender = sender, ) } }
In the code above, the latestResponse
property holds the generated text as a stream whenever the MessagesEvent.SendMessage
event comes in. This allows you to display responses in a real-time, streaming format, with each response from the AI platform appearing chunk by chunk.
Now, it’s time to set up the generative model and create a chat instance to request responses from Gemini using its Android SDK, as shown in the code below:
1234567891011121314151617181920212223242526272829class MessageViewModel: ViewModel() { .. private val model = GenerativeModel( modelName = "gemini-pro", apiKey = BuildConfig.GEMINI_API_KEY, generationConfig = generationConfig { temperature = 0.5f candidateCount = 1 maxOutputTokens = 1000 topK = 30 topP = 0.5f }, ) private val generativeChat: StateFlow<Chat> = messages.mapLatest { messageList -> model.startChat( history = messageList.map { singleMessage -> Content( parts = listOf(TextPart(singleMessage.message)), ) }, ) }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = model.startChat(), ) }
As you can see in the code above, you can create an instance of GenerativeModel
with the following details:
- Model name: Choose from
gemini-1.5-flash
,gemini-1.5-pro
, orgemini-1.0-pro
. - API key: Use the API key generated from Google AI Studio.
Optionally, you can define model parameters, setting values for properties like temperature, topK, topP, and the maximum number of output tokens.
Compose UI
Finally, implement the UI layer to display chat messages in real time. Start with the Messages
composable function, which consists of three main parts: the top app bar, the messages list body, and the input text field, as shown in the code below:
1234567891011@Composable fun Messages(..) { Column(modifier = Modifier.fillMaxSize()) { MessagesAppBar(channel = channel, onBackClick = onBackClick) MessageList(messages = messages, state = state, generatedMessage = generatedMessage) MessageInput(..) } }
In this section, we won’t go into the details of the top app bar or messages list, as they are similar to the implementation in the channels screen. Instead, the focus is on creating a responsive message item that reflects chunked messages directly from Gemini in real time.
To achieve this, you can use the latestResponse
property you implemented in the MessagesViewModel
, as shown in the code below:
12345678910111213141516171819202122232425262728293031class MessagesViewModel: ViewModel() { .. fun isCompleted(text: String?): Boolean { return generativeChat.value.history.any { it.parts.any { it.asTextOrNull() == text } } } } @Composable fun Messages(..) { val latestResponse by messagesViewModel.latestResponse.collectAsStateWithLifecycle() var generatedMessage by remember { mutableStateOf("") } LaunchedEffect(key1 = latestResponse) { latestResponse?.let { generatedMessage += it } } val isCompleted by remember { derivedStateOf { messagesViewModel.isCompleted(generatedMessage) } } LaunchedEffect(key1 = isCompleted) { if (isCompleted) { messagesViewModel.handleEvents( MessagesEvent.CompleteGeneration( message = generatedMessage, sender = "AI", ), ) generatedMessage = "" } } }
Finally, implement the responsive message box within the messages list, as demonstrated in the code below:
1234567891011121314151617181920212223242526272829303132333435363738@Composable private fun ColumnScope.MessageList( messages: List<Message>, state: LazyListState, generatedMessage: String, ) { LazyColumn( state = state, modifier = Modifier .fillMaxSize() .weight(1f) .padding(vertical = 16.dp, horizontal = 12.dp), ) { items(items = messages, key = { it.id }) { message -> if (message.isBot) { BotMessageItem(message = message) } else { UserMessageItem(message = message) } } if (generatedMessage.isNotBlank()) { item { Column(horizontalAlignment = Alignment.CenterHorizontally) { BotMessageItem( message = Message( id = UUID.randomUUID().toString(), message = generatedMessage, sender = "AI", ), ) CircularProgressIndicator(modifier = Modifier.size(34.dp)) } } } } }
After building the project, you’ll see the result below, where chat messages update in real-time as responses stream in from Gemini:
Stream Chat Android
There are multiple ways to implement a cloud-based, real-time chat messaging system. While Firebase Realtime Database is a popular option, you could also create your own backend server to achieve similar functionality. However, building a custom solution from scratch involves significant costs, including setting up socket protocols, UI components, databases, and more.
In this case, you could use the Stream Chat Android SDK , which enables seamless integration of real-time chat systems while offering fully customizable chat UI components, theming options, offline support, and additional features to enhance your app's chat experience.
A similar AI-powered chatbot project that leverages Gemini and a Stream Chat SDK, with additional features like photo reasoning and text summarization, is gemini-android, which you can explore on GitHub. For further insights, check out Build an AI Chat Android App With Google’s Generative AI.
Conclusion
In this article, you’ve explored how to implement an AI-powered chatbot application for Android using Gemini and Firebase Realtime Database. As AI technology becomes essential rather than optional, it's transforming user experiences across applications. For additional examples and insights, visit the GitHub repositories and an article linked below:
- AI Chat Android on GitHub
- Gemini Android on GitHub
- Build an AI Chat Android App With Google’s Generative AI
If you have any questions or feedback on this article, you can find the author on Twitter @github_skydoves or GitHub if you have any questions or feedback. If you’d like to stay up to date with Stream, follow us on Twitter @getstream_io for more great technical content.
As always, happy coding!
— Jaewoong