Build an AI-Powered Quiz App with Compose and Gemini: Part 1 (Data Layer)

Eury Pérez Beltré
ProAndroidDev
Published in
5 min readJul 9, 2024

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:

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

  1. We get the response if it succeeded, otherwise get null
  2. If it succeeded, we get the result otherwise null
  3. We get the text property from the Gemini response.
  4. Any occurrence of ```json in the string is removed
  5. Any occurrence of ``` in the string is removed
  6. We log the resulted json for debugging purposes, can be removed later
  7. The json string is decoded and parsed to a list of QuestionDTO
  8. We map the DTO objects to the domain models
  9. We build a Quiz object with the list of Question
  10. If we reached this point we wrap the response in NetworkResponse.Success
  11. 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:

  1. A coroutine is launched that loads 4 initial questions and emit the result
  2. 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 🧑🏽‍💻👩🏻‍💻

PART 2:

--

--

🔹 12+ years in Software Development | 9 years focused on Android Development | Google Developer Expert🔹