Build AI-Powered Chatbot Apps for Android Using Firebase

Jaewoong E.
Jaewoong E.
Published December 4, 2024

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:

  1. 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.
  2. 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:

kt
1
2
3
dependencies { 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:

kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
internal 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:

kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
internal 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:

kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@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:

kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Box( 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:

Ready to integrate? Our team is standing by to help you. Contact us today and launch tomorrow!
kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@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:

kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
internal 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:

kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@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:

kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class 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:

kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class 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, or gemini-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:

kt
1
2
3
4
5
6
7
8
9
10
11
@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:

kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class 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:

kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@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:

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

Ready to Increase App Engagement?
Integrate Stream’s real-time communication components today and watch your engagement rate grow overnight!
Contact Us Today!