Build an AI-Powered Quiz App with Compose and Gemini: Part 1 (Data Layer)
Greetings, fellow AI enthusiasts! 🤖
Chances are, the title piqued your interest. In today’s world, we’re bombarded with AI content, but the fear of missing out is real.
In this post, I’ll guide you through building a simple quiz app for Android using Jetpack Compose and leveraging the power of Gemini AI.
Table of Contents
- Prerequisites
- Project Setup
– Dependencies
– Gemini API Configuration
– The Dispatcher Provider
– Dependency Injection: AppModule
– Providing through CompositionLocals - AI-Powered Data Layer: The Quiz Repository
- Wrapping up
Prerequisites
This article assumes that you have knowledge of the following:
- Android development with Jetpack Compose
- Navigation Compose
- Dependency Injection with Hilt
- Kotlin Coroutines & Flows
Project Setup
Using Android Studio, create a new project using Compose.
Dependencies
To use the Gemini API you'll need to add the following dependency in the libs.versions.toml file:
In this project, we'll also use other libraries like hilt, navigation compose, kotlinx-serialization, secrets gradle plugin, etc. Find the full libs.versions.toml file here.
After syncing gradle, open the project's build.gradle.kt file and add the plugins and classpath dependencies:
Now open the build.gradle.kt file inside the app folder and apply the following changes:
Apply the plugins
Enable the buildConfig build feature:
Add the dependencies
See the full file here.
Gemini API Configuration
To have access to the Gemini API, you need to get an API key. You can get one accessing the AI Studio portal.
Once you have your API key, edit your local.properties file and add these lines:
The Dispatcher Provider
According to the coroutines best practices, we need to inject the dispatchers where we need them. In order to comply with that, create a utils
package and create the DispatcherProvider
interface with its implementation:
Dependency Injection: AppModule
Create the di
package and add the AppModule.kt
file:
First, provide the dispatcher provider implementation:
Now, we are going to provide the GenerativeModel, which is the gemini configuration to use the api:
You can learn more about all these properties in the Gemini API docs. See the full AppModule.kt
file here.
And now we are officially ready for the fun! 🙂↕️
Providing through CompositionLocals
In our composables we'll need access to the navController
and the SnackbarHostState
. To avoid passing it through the arguments, we are going to implicitly provide those, using composition locals.
💡 Learn more about CompositionLocals in this article.
In the utils
package create the LocalNavController
and LocalSnackbarHostState
kotlin files and add this in each one respectively:
Now, in the MainActivity
, wrap the app composable with the CompositionLocalProvider
composable and provide both values:
AI-Powered Data Layer: The Quiz Repository
Now, let's build the data layer which is the core of this application. Start by creating a data package, inside create 2 sub-packages: dto
and common
:
Data Transfer Objects (DTOs)
Inside the DTO package, create 2 classes. These will model the data generated by Gemini.
First create AnswerDTO:
Now create the QuestionDTO class:
Domain Models
To follow the best practices, we are going to use a different class for the domain models. In the same level of data, create the models package. And add the Quiz file with these 3 data classes:
We'll later from the DTO to the domain types.
We will also add the DifficultyLevel enum class to hold the difficulty levels of the quiz:
Common Files
Going back to the data package, inside common
we'll create 2 files. Start by creating the NetworkResponse interface:
This interface will help us improve the communication of the data layer with the UI layer.
Now, create the Extensions file and place this code inside:
This will help us train the model with these sample responses.
The QuizRepository
Directly inside the data package, add the QuizRepository
class:
Given that the Gemini API is very slow, we will load the quiz questions in chunks.
The best for this scenario is to use flows, add a MutableStateFlow property with its immutable counterpart:
We will also need the kotlinx-serialization Json instance as a property. It will help us deserialize the json string that Gemini will provide us:
When using a Generative AI tool, we need a prompt. Add the buildPrompt
function which accepts the DifficultyLevel
and an integer to specify the amount of questions we want:
Next, we will use this prompt to get the response from Gemini using the SDK. Create the getGeminiResponse
function:
Inside of the function, let's add the code to generate the content based on our interaction with Gemini:
Now, replace the //TODO
comment with the actual implementation:
- We make use of the function we previously created to build a prompt and add it to the conversation with gemini using the
text()
function. - If
useModelTraining
is true, we add the sample prompt to the conversation as well as a follow up question to take advantage of the context of Gemini. - If we don't want to train the model, we just ask for the output of the prompt.
Next, let's make use of this function to get the response and convert it to a Quiz object. Add the getQuizResponse()
function:
- We get the response if it succeeded, otherwise get null
- If it succeeded, we get the result otherwise null
- We get the
text
property from the Gemini response. - Any occurrence of ```json in the string is removed
- Any occurrence of ``` in the string is removed
- We log the resulted json for debugging purposes, can be removed later
- The json string is decoded and parsed to a list of QuestionDTO
- We map the DTO objects to the domain models
- We build a Quiz object with the list of
Question
- If we reached this point we wrap the response in
NetworkResponse.Success
- In case of null in step 1 or 2, here we return a
NetworkResponse.Error()
For safety reasons we also catch some exceptions, so we gracefully handle any error.
Lastly, we just need to provide a public function to generate the quiz. Add the generateQuiz()
function:
- A coroutine is launched that loads 4 initial questions and emit the result
- Another coroutine is launched that loads 6 more questions and use model training to prevent question duplications.
Here we take advantage of the concurrency that coroutines offer to make both calls in parallel, making this operation more performant.
See the full QuizRepository
code here.
Wrapping Up
In this article we gave the first steps towards building the quiz app. In the part 2, we will cover the viewModel implementation. Stay tuned and subscribe so you don't miss it.
Have fun 🧑🏽💻👩🏻💻