Build an AI-Powered Quiz App with Compose and Gemini: Part 2 (ViewModel)

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

Hello there 😎,

This is the second part of this series of articles. We are building a quiz app powered by Gemini AI. If you haven't, read the first part here:

It's time to bring this game to life. Let’s setup the navigation and develop the first part of the UI layer. We’ll use compose navigation and use some helper code to make it type-safe.

Table of Contents

  • Setting up the navigation
  • The quiz game core: The ViewModel
    – MVI Architecture
    – ViewModel Properties
    – The ViewModel Initialization
  • Quiz Game Logic
  • The Process Event Function
  • Wrap up

Setting up the navigation

ℹ️ In Navigation 2.8.0, safe args was made available for Navigation Compose. Please consider checking the documentation and following that approach instead.

In the utils package, create the AppNavigation kotlin file. In the file, create a new value class to represent the navigation routes:

In the same file, we’ll add the AppNavigation sealed interface, where we will create a data object for each destination of our app:

  1. Every destination will need to provide a NavRoute
  2. We define the DifficultyLevelDestination
  3. We define the QuizGameDestination
  4. The getNavRoute helper function will help us build a nav route with an argument to be passed to this destination.

The Quiz Game Core: ViewModel

In the quizGame package, create the QuizGameViewModel class:

Our game will have 2 possible states in this screen: In Progress and Finished. We will represent those states with a sealed interface. Inside the QuizGameViewModel class, create the GameState sealed interface:

Each data class contain the information the quiz need in each state.

Inside the GameState, we will create a copy helper function that will help us later when updating the view state:

This is only needed in the InProgress state, as the Finished state is not intended to change.

MVI Architecture

In this project we are going to use the Model-View-Intent architecture in the UI layer. We’ll need to define the State, Effect and Event (intent) definitions. Let’s start with the state:

  1. By default the screen will be loading
  2. The initial game state will be in progress
  3. The viewEffect will be used for one-shot events

Now, let’s define the possible one-shot events with the ViewEffect sealed interface:

Finally, we need to define the events or intents. This are all the possible actions the composables would like to trigger in the viewmodel:

  1. Will be used when the user want to show the next question by skipping or because the countdown finished
  2. When the user chooses and answer, this event will be sent
  3. This event will help consume the event to avoid it being emitted more than once. Learn more about this in the docs.

ViewModel Properties

  1. We will expose the view state as a flow to be collected by the view.
  2. The questions will be stored here
  3. This map will be filled as the user advances in the quiz
  4. This property will hold the index of the currently selected question

The ViewModel Initialization

There are certain operations that we want to perform as soon as the viewModel is created. We place these in the init block. The first one is loading the quiz:

  1. First we get the difficulty level argument from the navigation arguments using the savedStateHandle object.
  2. Using this string, we get the proper enum value.
  3. We launch a new coroutine that calls the generateQuiz suspend function using the difficulty level
  4. If there was an issue parsing the enum value, we send an error view event
  5. If there was an issue getting the navigation argument, we send an error view event

Next step in the init block is to collect the quizSharedFlow from the QuizRepository:

  1. If the response is of type success, we add the new questions to the questions property
  2. Only if the view state is loading, we initialize the viewState with the first question as the selected question, and set the loading to false.
  3. If the response is of type error we log it and show an error snackbar.

Quiz Game Logic

The logic of the game is simple and it fits on a single function that we’ll name showNextQuestionOrFinish():

  1. We increase by one the selected question index
  2. We store in a variable whether the game finished, by checking the selected index and comparing it with the amount of questions
  3. If the game is not finished we update the state with the new selected question

Now, let’s focus in the game finished logic. Replace the TODO comment with this:

  1. We map the questions to include only the answer the user chose or null if there was no answer
  2. The total correct answer are saved in a variable
  3. We calculate the score percentage to be used in the score screen
  4. The state is updated with the calculated values

The Process Event Function

To complete the MVI cycle, we just need to provide a public function to process the events (intents). To do this create the processEvent() function:

  1. We make use of the function we just created when the NextQuestion event is sent
  2. When an answer is selected, we first update the state to set the selectedAnswerId
  3. Next, we save the selected answer in the answersMap property
  4. Lastly we wait for 5 seconds and then show the next question
  5. The ConsumeEffect will set the effect back to null so the view doesn’t get it again on the next state update.

Wrap Up

Now the logic is in place, it’s time to make it usable and beautiful. Follow up for the third article of the series to add the UI using Jetpack Compose. Stay tuned.

Part 3:

--

--

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