- É um app desenvolvido utilizando Jetpack Compose para composição da UI.
- As preferências do jogo são salvas com SharedPreferences.
- A arquitetura foi baseada em MVI. E você pode checar a branch
redux-arch
para conferir a arquitetura utilizando REDUX. - Na branch
Main
você vai encontrar o projeto sem REDUX.
- Funcionalidades
- Estrutura responsável pelo Estado do jogo
- Reducer
- Middleware
- Entendendo Redux no MaTriviaApp
- Funções Assíncronas - Thunks
- O papel do ViewModel
- Pré-visualizações
- O usuário pode selecionar a dificuldade do jogo: Fácil, Média ou Difícil.
- O usuário pode selecionar a categoria de perguntas que o mesmo deseja responder.
- O usuário pode selecionar o tipo de pergunta que o mesmo pode responder: se múltipla escolha, verdadeiro e falso ou ambos os tipos.
- O usuário pode responder somente 1 pergunta por vez.
- As perguntas são escolhidas de forma aleatória, de acordo com os critérios pré-selecionados anteriormente.
- Em casos de perguntas de múltipla escolha, as opções são apresentadas e o usuário pode dentre as opções.
- Em casos de perguntas de verdadeiro ou falso, as opções são apresentadas como (Verdadeiro ou falso) e o usuário pode escolher dentre as opções.
- Se o usuário errar a pergunta, uma mensagem é apresentada sobre o erro, a resposta certa é destacada e o jogo é encerrado.
- Se o usuário acertar a pergunta, uma mensagem de sucesso é apresentada, a resposta é destacada e uma nova pergunta é apresentada.
- O usuário pode responder quantas perguntas quiser até errar. O quantitativo de perguntas respondidas é mostrado.
- O usuário tem apenas 10 segundos para responder cada pergunta.
- O usuário pode desistir do jogo. O usuário pode confirmar se deseja desistir do jogo.
- No final, o quantitativo de acertos do usuário é mostrado.
- Um ranking com os 10 últimos jogos é apresentado (do maior para o menor).
- Verificação de conexão com a Internet.
data class GameState(
val questions: List<Question> = listOf(),
val gameStatus: GameStatus = GameStatus.SETUP,
val gameCriteriaUiModel: GameCriteriaUiModel = GameCriteriaUiModel(),
val correctAnswers: Int = 0,
val isCorrectOrIncorrect: Boolean? = null,
val currentQuestion: Question? = null,
val optionsAnswers: List<AnswerOptionUiModel> = listOf(),
val timeIsFinished: Boolean = false,
val confirmWithdrawal: Boolean = false,
val disableSelection: Boolean = false,
val timeState: Int? = null,
val ranking: List<RankingExternal> = listOf(),
val networkIsActive: Boolean? = null,
val networkWarning: Boolean? = null
) {
val numberQuestion = correctAnswers + 1
}
Função pura responsável por lidar com a gerenciamento do GameState
Ações que mudam o networkWarning
e networkWarning
- Exemplo
val reducer: Reducer<GameState> = { state, action ->
when (action) {
//======================
// NETWORK GAME ACTIONS
//======================
is NetworkActions.ChangeNetworkState -> {
state.copy(networkIsActive = action.network)
}
is NetworkActions.NetworkWarning -> {
state.copy(networkWarning = true)
}
else -> {
state.copy()
}
}
}
Responsável para disparar as funções assíncronas
Enquanto gameStatus
== GameStatus.STARTED - Exemplos
fun uiMiddleware(
timerThunk: TimerThunk,
rankingThunks: GetRankingThunk,
questionThunks: GetQuestionThunk,
categoryThunks: PrefsAndCriteriaThunk
) = middleware<GameState> { store, next, action ->
next(action)
val dispatch = store.dispatch
when (action) {
//======================
// PLAYING GAME ACTIONS
//======================
is PlayingGameActions.CheckAnswer -> {
store.dispatch(PlayingGameActions.DisableSelection(action.answerId))
when (action.answerId == CORRECT_ANSWER_ID) {
true -> {
store.dispatch(PlayingGameActions.HandleCorrectAnswer)
store.dispatch(timerThunk.stopTimerJob())
}
else -> {
store.dispatch(PlayingGameActions.HandleIncorrectAnswer)
store.dispatch(timerThunk.stopTimerJob())
}
}
}
is PlayingGameActions.ContinueGame -> {
when (action.isCorrectOrIncorrect) {
true -> {
dispatch(timerThunk.getTimerThunk())
}
else -> {
dispatch(rankingThunks.getRanking())
}
}
}
is PlayingGameActions.GetNewQuestion -> {
dispatch(questionThunks.getQuestionThunk())
}
is PlayingGameActions.GiveUpGameConfirm -> {
dispatch(timerThunk.stopTimerJob())
dispatch(rankingThunks.getRanking())
}
(...)
}
}
![Screenshot 2024-03-25 at 19 53 54](https://private-user-images.githubusercontent.com/72306040/316675064-c275ff32-f877-4d7d-a83b-921c30e0852a.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzk2MTcxMDksIm5iZiI6MTczOTYxNjgwOSwicGF0aCI6Ii83MjMwNjA0MC8zMTY2NzUwNjQtYzI3NWZmMzItZjg3Ny00ZDdkLWE4M2ItOTIxYzMwZTA4NTJhLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMTUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjE1VDEwNTMyOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTBkNTkyNDRhMTdkYmI5MDllOGJkNTAwYjc3ZTIwNzQ2NWU3M2JlYjdkZWJjOTM2YmJiNzRhYzFhYjllY2I3YmUmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.PTkq63hsgfFwti8_izo7qbMoxfVH9zR2BBVwzTIZZrc)
EndGameActions.kt
Responsável por todas as interações quando a variável gameStatus
== GameStatus.END
sealed interface EndGameActions {
data object PlayAgain : EndGameActions
data object BackToGameSetup : EndGameActions
}
MenuGameActions.kt
Responsável por todas as interações quando a variável gameStatus
== GameStatus.SETUP
sealed interface MenuGameActions {
data object ExpandMenuCategoryField : MenuGameActions
data object ExpandMenuTypeField : MenuGameActions
data object ExpandMenuDifficultyField : MenuGameActions
data class OnSelectCategoryField(val questionCategory: Category) :
MenuGameActions
data class OnSelectTypeField(val questionType: QuestionType) :
MenuGameActions
data class OnSelectDifficultyField(val questionDifficulty: QuestionDifficulty) :
MenuGameActions
data object PrepareGame : MenuGameActions
data object StartGame : MenuGameActions
data class UpdateCriteriaFieldsState(val gameCriteria: GameCriteriaUiModel) :
MenuGameActions
data object FetchCriteriaFields :
MenuGameActions
}
NetworkActions.kt
Responsável por todas as interações quando a variável networkWarning
!= null
sealed interface NetworkActions {
data object NetworkWarning : NetworkActions
data object TryAgain : NetworkActions
data class ChangeNetworkState(val network: Boolean?) : NetworkActions
}
PlayingGameActions.kt
Responsável por todas as interações quando a variável gameStatus
== GameStatus.STARTED
sealed interface PlayingGameActions {
data class UpdateQuestion(val triple: Triple<List<Question>, Question, List<AnswerOptionUiModel>>) :
PlayingGameActions
data class UpdateStatus(val gameStatus: GameStatus) : PlayingGameActions
data object GetNewQuestion
data class CheckAnswer(val answerId: Int) : PlayingGameActions
data object HandleIncorrectAnswer : PlayingGameActions
data object HandleCorrectAnswer : PlayingGameActions
data class ContinueGame(val isCorrectOrIncorrect: Boolean) : PlayingGameActions
data class EndOfTheGame(val ranking: List<RankingExternal>) : PlayingGameActions
data object OnTopBarGiveUp : PlayingGameActions
data class DisableSelection(val optionId: Int) : PlayingGameActions
data object GiveUpGameConfirm : PlayingGameActions
data object GiveUpGameGoBack : PlayingGameActions
}
TimerActions.kt
Responsável pelas interações que lidam com o estado do tempo
sealed interface TimerActions {
data object Update : TimerActions
data object Over : TimerActions
data object TimerThunkDispatcher : TimerActions
}
GetQuestionThunkImpl.kt
Thunk responsável pela função assíncrona que busca novas questões.
class GetQuestionThunkImpl(
@DefaultDispatcher dispatcher: CoroutineDispatcher,
private val triviaRepository: TriviaRepository
) : GetQuestionThunk {
private val scope = CoroutineScope(dispatcher + Job())
override fun getQuestionThunk(): Thunk<GameState> = { dispatch, getState, _ ->
scope.launch {
val gameState = getState()
delay(500L)
if(gameState.questions.isEmpty()) {
gameState.networkIsActive?.let {
var typePrefs = gameState.gameCriteriaUiModel.typeField.field?.selected?.id.toString()
var difficultyPrefs = gameState.gameCriteriaUiModel.difficultyField.field?.selected?.id.toString()
var categoryPrefs = gameState.gameCriteriaUiModel.categoryField.field?.selected?.id.toString()
typePrefs = if (typePrefs == ANY) {
DEFAULT
} else {
when (typePrefs) {
ID_MULTIPLE_TYPE -> MULTIPLE_TYPE
else -> {
BOOLEAN_TYPE
}
}
}
difficultyPrefs = if (difficultyPrefs == ANY) {
DEFAULT
} else {
when (difficultyPrefs) {
ID_EASY_DIFFICULT -> EASY_DIFFICULT
ID_MEDIUM_DIFFICULT -> MEDIUM_DIFFICULT
else -> {
HARD_DIFFICULT
}
}
}
if (categoryPrefs == ANY) {
categoryPrefs = DEFAULT
}
val newQuestions = async {
triviaRepository.getQuestions(
difficulty = difficultyPrefs,
type = typePrefs,
category = categoryPrefs
)
.map { triviaQuestion ->
val randomOptions = answerOptions(triviaQuestion)
val fromApi = triviaQuestion.question
val textFromHtmlFromApi = HtmlCompat.fromHtml(fromApi, HtmlCompat.FROM_HTML_MODE_LEGACY)
Question(
type = triviaQuestion.type,
difficulty = triviaQuestion.difficulty,
category = triviaQuestion.category,
question = textFromHtmlFromApi.toString(),
correctAnswer = triviaQuestion.correctAnswer,
incorrectAnswer = triviaQuestion.incorrectAnswer,
answerOptions = randomOptions
)
}
}.await()
while(newQuestions.isEmpty()) {
delay(1)
}
val questions = newQuestions.toMutableList()
val currentQuestion = questions.last()
questions.remove(currentQuestion)
val optionsAnswers = currentQuestion.answerOptions.map { answerOption ->
AnswerOptionUiModel(
id = answerOption.id,
option = answerOption.answer,
)
}
dispatch(
PlayingGameActions.UpdateQuestion(
Triple(
questions,
currentQuestion,
optionsAnswers
)
)
)
} ?: run { dispatch(NetworkActions.NetworkWarning) }
} else {
val triple = getQuestionsFromCache(gameState)
dispatch(
PlayingGameActions.UpdateQuestion(
Triple(
triple.first,
triple.second,
triple.third
)
)
)
}
}
}
private fun getQuestionsFromCache(
gameState: GameState,
): Triple<List<Question>, Question,List<AnswerOptionUiModel>> {
val questions = gameState.questions.toMutableList()
val currentQuestion = questions.last()
questions.remove(currentQuestion)
val optionsAnswers = currentQuestion.answerOptions.map { answerOption ->
AnswerOptionUiModel(
id = answerOption.id,
option = answerOption.answer,
)
}
return Triple(questions, currentQuestion, optionsAnswers)
}
private suspend fun answerOptions(triviaQuestion: TriviaQuestion): MutableList<AnswerOption> {
return coroutineScope {
async {
val answerOptions = mutableListOf<AnswerOption>()
var id = 0
triviaQuestion.incorrectAnswer.forEach { answer ->
if (id == 0) {
answerOptions.add(
AnswerOption(
id = id,
answer = HtmlCompat.fromHtml(triviaQuestion.correctAnswer, HtmlCompat.FROM_HTML_MODE_LEGACY).toString()
)
)
id++
answerOptions.add(AnswerOption(id = id, answer = HtmlCompat.fromHtml(answer, HtmlCompat.FROM_HTML_MODE_LEGACY).toString()))
} else {
answerOptions.add(AnswerOption(id = id, answer = HtmlCompat.fromHtml(answer, HtmlCompat.FROM_HTML_MODE_LEGACY).toString()))
}
id++
}
answerOptions.shuffle()
answerOptions
}.await()
}
}
companion object {
const val ANY = "0"
const val DEFAULT = ""
const val EASY_DIFFICULT = "easy"
const val MEDIUM_DIFFICULT = "medium"
const val HARD_DIFFICULT = "hard"
const val MULTIPLE_TYPE = "multiple"
const val BOOLEAN_TYPE = "boolean"
const val ID_MULTIPLE_TYPE = "1"
const val ID_EASY_DIFFICULT = "1"
const val ID_MEDIUM_DIFFICULT = "2"
}
}
GetRankingThunkImpl.kt
Thunk responsável pela função assíncrona que busca o top 10 dos últimos jogos e inserir o resultado do último jogo.
class GetRankingThunkImpl(@DefaultDispatcher dispatcher: CoroutineContext, private val rankingRepository: RankingRepository) :
GetRankingThunk {
private val scope = CoroutineScope(dispatcher)
override fun getRanking(): Thunk<GameState> = { dispatch, getState, _->
scope.launch {
val ranking = rankingRepository.getRanking().map { rankingLocal->
RankingExternal(
id = rankingLocal.id,
correctAnswers = rankingLocal.correctAnswers.toString(),
createdAt = DateUtils.getDateFormatted(rankingLocal.createdAt)
)
}
val rankingLocal = RankingLocal(
correctAnswers = getState().correctAnswers, createdAt = System.currentTimeMillis()
)
rankingRepository.insert(rankingLocal)
dispatch(PlayingGameActions.EndOfTheGame(ranking))
}
}
}
PrefsAndCriteriaThunkImpl.kt
Thunk responsável pela função assíncrona que busca o valor dos campos de critérios do jogo de acordo com as preferências salvas ou salvar novas preferências.
class PrefsAndCriteriaThunkImpl(
networkContext: CoroutineDispatcher,
private val preferences: Preferences,
private val repository: TriviaRepository
) : PrefsAndCriteriaThunk {
private val scope = CoroutineScope(networkContext)
private val categories = mutableListOf<Category>()
private val difficulties = repository.getQuestionDifficulties()
private val types = repository.getQuestionTypes()
override fun getCriteriaFields(): Thunk<GameState> = { dispatch, getState, _ ->
scope.launch {
val state = getState()
state.networkIsActive?.let {
categories.ifEmpty {
async {
repository.getCategories().forEach { category ->
categories.add(category)
}
if (categories.find { it.id == 0 } == null) {
categories.add(Category(id = 0, name = defaultValue))
}
categories
}.await()
}
val typeField = TypeFieldModel(
selected = getQuestionTypeFromIndex(preferences.getQuestionType()),
options = types
)
val difficulty = DifficultyFieldModel(
selected = getQuestionDifficultyFromIndex(preferences.getQuestionDifficulty()),
options = difficulties
)
val categories = CategoryFieldModel(
selected = getQuestionCategoryFromId(preferences.getQuestionCategory()),
options = categories
)
dispatch(
MenuGameActions.UpdateCriteriaFieldsState(
GameCriteriaUiModel(
typeField = DropDownMenu(field = typeField),
difficultyField = DropDownMenu(field = difficulty),
categoryField = DropDownMenu(field = categories)
)
)
)
} ?: run { dispatch(NetworkActions.NetworkWarning) }
}
}
override fun updatePreferences(type: Int, difficulty: Int, category: Int): Thunk<GameState> =
{ dispatch, _, _ ->
scope.launch {
preferences.updateGamePrefs(type, difficulty, category)
Log.d(
"PREFERENCES_LOGGER",
"PREFS_TYPE : ${preferences.getQuestionType()} " +
"\n PREFS_DIFFICULTY: ${preferences.getQuestionDifficulty()} " +
"\n PREFS_CATEGORY: ${preferences.getQuestionCategory()} "
)
dispatch(MenuGameActions.StartGame)
}
}
private fun getQuestionDifficultyFromIndex(index: Int): QuestionDifficulty {
return difficulties[index]
}
private fun getQuestionTypeFromIndex(index: Int): QuestionType {
return types[index]
}
private fun getQuestionCategoryFromId(id: Int): Category {
val category = categories.indexOfFirst { it.id == id }
return categories[category]
}
companion object {
private const val defaultValue = "Any Category"
}
}
TimerThunkImpl.kt
Thunk responsável pelas funções assíncronas que lidam com o início do tempo ou sua pausa.
class TimerThunkImpl(@DefaultDispatcher dispatcher: CoroutineContext) : TimerThunk, CoroutineScope {
override val coroutineContext: CoroutineContext = dispatcher + Job()
private var countDownTimerJob: Job? = null
override fun getTimerThunk(): Thunk<GameState> = { dispatch, getState, _ ->
dispatch(PlayingGameActions.GetNewQuestion)
getState().networkIsActive?.let {
countDownTimerJob = CoroutineScope(coroutineContext).launch {
var value = 10
while (value > 0) {
delay(1000)
dispatch(TimerActions.Update)
value--
}
if(value == 0) {
dispatch(TimerActions.Over)
countDownTimerJob?.cancel()
}
}
}
countDownTimerJob as Job
}
override fun stopTimerJob() {
countDownTimerJob?.cancel()
}
}
O nosso TriviaGameVm, viewmodel, é responsável por lidar com eventos inesperados relacionados ao ciclo de vida da Activity. Além disso, ele intermedia as ações da UI com o nosso Store.
@HiltViewModel
class TriviaGameVm @Inject constructor(
private val gameUseCases: GameThunks
) : ViewModel() {
val gameStore = createStore(reducer, GameState(), applyMiddleware(
createThunkMiddleware(), uiMiddleware(gameUseCases.timerThunk, gameUseCases.getRanking,gameUseCases.getQuestion,gameUseCases.getCategories)))
fun onMenuGameAction(menuGameAction: MenuGameActions) {
gameStore.dispatch(menuGameAction)
}
fun onGamePlayingAction(gamePlayingActions: PlayingGameActions) {
gameStore.dispatch(gamePlayingActions)
}
fun onEndGameActions(endGameAction: EndGameActions) {
gameStore.dispatch(endGameAction)
}
fun changeNetworkState(state: Boolean?) {
gameStore.dispatch(NetworkActions.ChangeNetworkState(state))
Log.d("networkState", state?.let { "available" } ?: run { "unavailable" })
}
fun onResume() {
if(gameStore.state.gameStatus == GameStatus.SETUP) {
gameStore.dispatch(MenuGameActions.FetchCriteriaFields)
}
}
fun tryNetworkConnection() {
gameStore.dispatch(NetworkActions.TryAgain)
}
}
- Configurações do jogo
![](https://private-user-images.githubusercontent.com/72306040/316669222-45793102-e696-4387-b580-1263ac50c8c7.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzk2MTcxMDksIm5iZiI6MTczOTYxNjgwOSwicGF0aCI6Ii83MjMwNjA0MC8zMTY2NjkyMjItNDU3OTMxMDItZTY5Ni00Mzg3LWI1ODAtMTI2M2FjNTBjOGM3LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMTUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjE1VDEwNTMyOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWFkYjJiZjA4MmJhZjZlMjkyOGE4MzhkMjE5YWRkYWFhYjU3M2RhNTgyZWZiZjBkYmMzYzdjZDA1M2ZkOGQ2M2MmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.ytZ-Odr0bqHKQYOLHTfGGh8qtTw3ly3PU0sOm35Myds)
- Quando o usuário marca a Resposta errada.
![](https://private-user-images.githubusercontent.com/72306040/316672014-f52e1fe0-dfbe-444c-9433-85947bc7fddb.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzk2MTcxMDksIm5iZiI6MTczOTYxNjgwOSwicGF0aCI6Ii83MjMwNjA0MC8zMTY2NzIwMTQtZjUyZTFmZTAtZGZiZS00NDRjLTk0MzMtODU5NDdiYzdmZGRiLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMTUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjE1VDEwNTMyOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTM3Yjk0OWU4MzBlNDhhNmU4MGQwMjdiOWJiNjNhMmE1MGQyODk1NDViOTg0OTM5YjczOGY4YTE3NTNmZjIzZmMmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0._c_FoDdzQilqrEBJmE8Du_oW6FozaM2N17mNFj-ma_Q)
- Quando o usuário marca a Resposta certa.
![](https://private-user-images.githubusercontent.com/72306040/316672095-d4acd373-bfc9-4137-be1e-c9da8333cb90.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzk2MTcxMDksIm5iZiI6MTczOTYxNjgwOSwicGF0aCI6Ii83MjMwNjA0MC8zMTY2NzIwOTUtZDRhY2QzNzMtYmZjOS00MTM3LWJlMWUtYzlkYTgzMzNjYjkwLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMTUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjE1VDEwNTMyOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWNkZjVlNzc0ZTQxYzk0NmVhYThkZGNlMWE5YzRlN2NiZWVhNDIxNDQyODlhNDE4MWZhZDI3YTNiZGQ0ZWQ4ODImWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.qjj3isXErpSQ9TyFWCRLJQlR41kIrXeFNDDv_VuCcPQ)
- Quando os 10 segundos acabam.
![](https://private-user-images.githubusercontent.com/72306040/316672299-76f044a0-5aa1-4356-8485-cd8a81495440.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzk2MTcxMDksIm5iZiI6MTczOTYxNjgwOSwicGF0aCI6Ii83MjMwNjA0MC8zMTY2NzIyOTktNzZmMDQ0YTAtNWFhMS00MzU2LTg0ODUtY2Q4YTgxNDk1NDQwLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMTUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjE1VDEwNTMyOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWY4OWZjYTEyNWRmNGNjYmFlN2JhMzU5OWY1OTkxMzBlM2VkOTg5MjNlMDk4NTBiODRlMjBhYmEzZGZjNzU4ODUmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.A3JdQbSbJE5jZ5fZhNSu8bAtIsMsH4o8omm9AtU2oCc)
- Quando o usuário clica em desistir do jogo no ícone inserido na top bar.
![](https://private-user-images.githubusercontent.com/72306040/316672669-43aa9b5f-26e1-4090-99e1-9807fcc75b55.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzk2MTcxMDksIm5iZiI6MTczOTYxNjgwOSwicGF0aCI6Ii83MjMwNjA0MC8zMTY2NzI2NjktNDNhYTliNWYtMjZlMS00MDkwLTk5ZTEtOTgwN2ZjYzc1YjU1LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMTUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjE1VDEwNTMyOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTQ1OWIyYzdhZmQ0OTMxN2I5ZTFmNTVkZjJmNzQ2ZGEwNjRlMmUzMTg5NDhiNDFhNjU5MTBiMDc2MzExNDQwYzQmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.Iiaq9qB16j8ICJNoEp8wVyT3aKGmd96ihQEaYIxNRVQ)
- Quando o jogo é encerrado depois do usuário confirmar a desistência do jogo ou após a mensagem sobre a resposta errada ou quando o tempo acaba.
![](https://private-user-images.githubusercontent.com/72306040/316672742-7ef9db9d-a2be-400f-a25a-182e3e97447e.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzk2MTcxMDksIm5iZiI6MTczOTYxNjgwOSwicGF0aCI6Ii83MjMwNjA0MC8zMTY2NzI3NDItN2VmOWRiOWQtYTJiZS00MDBmLWEyNWEtMTgyZTNlOTc0NDdlLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMTUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjE1VDEwNTMyOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTYxNzhjYWVmYzhjY2Y1Y2MxN2EyZDFjNzU2NWFkODYwYTQzZTYxOTFjZThmN2RmMGI5YTVkNzE4YzU3MmQyZmEmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.dqMOHZReLjcmDsSmFDDix7__YVmRvBb4yPmRH5EpG50)
- Quando o usuário está sem internet.
![](https://private-user-images.githubusercontent.com/72306040/316672821-cd9ed9cd-a090-4104-b2cc-7d602057f453.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzk2MTcxMDksIm5iZiI6MTczOTYxNjgwOSwicGF0aCI6Ii83MjMwNjA0MC8zMTY2NzI4MjEtY2Q5ZWQ5Y2QtYTA5MC00MTA0LWIyY2MtN2Q2MDIwNTdmNDUzLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMTUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjE1VDEwNTMyOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTI4YWM4MjgzMDhkMDk5OWZhNWVjZDBhYjkwNDY4MTVmNjgxM2MwYjAyMjY4NGMxZjRjMmZmZTcwNjNjYWU4MzAmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.ZGZdGCk2uRDkpJwCqJ6RnH7mhGmoy8XtNIdMkTIn0bY)
- Resumo do Fluxo do jogo
![](https://private-user-images.githubusercontent.com/72306040/316673346-8058b550-61b9-4d34-bfc8-5194393e28cb.gif?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzk2MTcxMDksIm5iZiI6MTczOTYxNjgwOSwicGF0aCI6Ii83MjMwNjA0MC8zMTY2NzMzNDYtODA1OGI1NTAtNjFiOS00ZDM0LWJmYzgtNTE5NDM5M2UyOGNiLmdpZj9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMTUlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjE1VDEwNTMyOVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWJlYzY5MTZmMTgyZTYwMTUxZGZkNzc1YTk0ODc4NzA5YzU4MDIzMGY2ZDRiN2Y3NTM4YmFhMWUzMGM3YmZkZTUmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.Z4HCtzNDdnHm13o1q2_ts-envsfCNJND-DsrGSHBFbE)