Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions quiz-kmp/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.gradle
.idea
*.iml
build/
local.properties
*.keystore
*.jks
*.DS_Store
composeApp/build/
gradle-wrapper.jar
Empty file.
76 changes: 76 additions & 0 deletions quiz-kmp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# DynaQuiz

Aplicativo de quiz multiplataforma desenvolvido com **Kotlin Multiplatform** e **Compose Multiplatform**.

---

## Pré-requisitos

- **Android Studio** Ladybug (2024.2.1) ou superior
- **JDK 17** ou superior
- **Android SDK** API 24+

---

## Como rodar

1. Abra a pasta `quiz-kmp/` no Android Studio (**File > Open**)
2. Aguarde o Gradle sincronizar (clique em **Sync Now** se necessário)
3. Conecte um dispositivo Android ou inicie um emulador (API 24+)
4. Selecione a configuração `composeApp` e clique em **Run** (▶)

### Rodar testes

```bash
./gradlew :composeApp:allTests
```

---

## Arquitetura

**Clean Architecture + MVVM**

| Camada | Responsabilidade |
|---|---|
| `data/` | API (Ktor), banco de dados (SQLDelight), repositórios |
| `domain/` | Modelos, interfaces de repositório, casos de uso |
| `presentation/` | Telas, ViewModels, navegação, tema |
| `di/` | Módulos de injeção de dependência (Koin) |

---

## Tecnologias

| Biblioteca | Versão |
|---|---|
| Kotlin Multiplatform | 2.1.0 |
| Compose Multiplatform | 1.7.3 |
| Ktor | 3.0.3 |
| SQLDelight | 2.0.2 |
| Koin | 4.0.0 |
| Kotlinx Coroutines | 1.9.0 |

---

## API

**Base URL:** `https://quiz-api-bwi5hjqyaq-uc.a.run.app`

| Endpoint | Método | Descrição |
|---|---|---|
| `/question` | GET | Retorna uma pergunta aleatória |
| `/answer?questionId={id}` | POST | Verifica se a resposta está correta |

---

## Premissas

- Jogadores são identificados apenas pelo nome/apelido, sem autenticação.
- Cada sessão tem exatamente 10 perguntas.
- O placar é salvo localmente (SQLite) e exibe os 20 melhores resultados.
- Sem cache de perguntas — falhas de rede exibem tela de erro com botão de retry.

---

Desenvolvido como parte do **Desafio Kotlin Multiplatform**.
8 changes: 8 additions & 0 deletions quiz-kmp/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
plugins {
alias(libs.plugins.androidApplication) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
alias(libs.plugins.composeMultiplatform) apply false
alias(libs.plugins.composeCompiler) apply false
alias(libs.plugins.kotlinSerialization) apply false
alias(libs.plugins.sqldelight) apply false
}
108 changes: 108 additions & 0 deletions quiz-kmp/composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidApplication)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
alias(libs.plugins.kotlinSerialization)
alias(libs.plugins.sqldelight)
}

kotlin {
androidTarget {
@OptIn(ExperimentalKotlinGradlePluginApi::class)
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}

sourceSets {
androidMain.dependencies {
implementation(compose.preview)
implementation(libs.activity.compose)
implementation(libs.core.ktx)
implementation(libs.koin.android)
implementation(libs.ktor.client.okhttp)
implementation(libs.sqldelight.android.driver)
implementation(libs.kotlinx.coroutines.android)
}

commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.ui)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
implementation(compose.materialIconsExtended)

implementation(libs.lifecycle.viewmodel)
implementation(libs.lifecycle.viewmodel.compose)
implementation(libs.lifecycle.runtime.compose)

implementation(libs.navigation.compose)

implementation(libs.koin.core)
implementation(libs.koin.compose)
implementation(libs.koin.compose.viewmodel)

implementation(libs.ktor.client.core)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.ktor.client.logging)

implementation(libs.sqldelight.coroutines.extensions)

implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.serialization.json)

implementation(libs.kotlinx.datetime)
implementation(libs.kermit)
}

commonTest.dependencies {
implementation(libs.kotlin.test)
implementation(libs.kotlinx.coroutines.test)
}
}
}

android {
namespace = "com.dynamox.quiz"
compileSdk = libs.versions.android.compileSdk.get().toInt()

defaultConfig {
applicationId = "com.dynamox.quiz"
minSdk = libs.versions.android.minSdk.get().toInt()
targetSdk = libs.versions.android.targetSdk.get().toInt()
versionCode = 1
versionName = "1.0.0"
}

packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}

buildTypes {
getByName("release") {
isMinifyEnabled = false
}
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}

sqldelight {
databases {
create("QuizDatabase") {
packageName.set("com.dynamox.quiz.database")
}
}
}
28 changes: 28 additions & 0 deletions quiz-kmp/composeApp/src/androidMain/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.INTERNET" />

<application
android:name=".QuizApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="DynaQuiz"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.DynaQuiz">

<activity
android:name=".MainActivity"
android:exported="true"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.dynamox.quiz

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
App()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.dynamox.quiz

import android.app.Application
import com.dynamox.quiz.di.androidModule
import com.dynamox.quiz.di.appModules
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin

/**
* Classe Application personalizada do Android
* O Android instancia esta classe ANTES de qualquer Activity ou Service.
* Local para inicializar bibliotecas globais como o Koin, pois garante que as dependências estarão
* disponíveis quando qualquer tela for criada.
*/
class QuizApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@QuizApplication)
modules(appModules + androidModule)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.dynamox.quiz.data.local

import android.content.Context
import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.driver.android.AndroidSqliteDriver
import com.dynamox.quiz.database.QuizDatabase

class AndroidDatabaseDriverFactory(private val context: Context) : DatabaseDriverFactory {
override fun createDriver(): SqlDriver {
return AndroidSqliteDriver(QuizDatabase.Schema, context, "quiz_database.db")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.dynamox.quiz.di

import com.dynamox.quiz.data.local.AndroidDatabaseDriverFactory
import com.dynamox.quiz.data.local.DatabaseDriverFactory
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.bind
import org.koin.dsl.module

val androidModule = module {
single { AndroidDatabaseDriverFactory(androidContext()) } bind DatabaseDriverFactory::class
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:startColor="#0D1B2A"
android:endColor="#1A2744"
android:angle="135"
android:type="linear" />
</shape>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<!-- Outer glow circle -->
<path
android:fillColor="#334CC9F0"
android:pathData="M54,20 C35.2,20 20,35.2 20,54 C20,72.8 35.2,88 54,88 C72.8,88 88,72.8 88,54 C88,35.2 72.8,20 54,20 Z" />
<!-- Inner circle -->
<path
android:fillColor="#4CC9F0"
android:pathData="M54,28 C39.6,28 28,39.6 28,54 C28,68.4 39.6,80 54,80 C68.4,80 80,68.4 80,54 C80,39.6 68.4,28 54,28 Z" />
<!-- Question mark body -->
<path
android:fillColor="#0D1B2A"
android:pathData="M51,63 L57,63 L57,69 L51,69 Z" />
<path
android:fillColor="#0D1B2A"
android:pathData="M54,39 C48.5,39 44,43.5 44,49 L50,49 C50,46.8 51.8,45 54,45 C56.2,45 58,46.8 58,49 C58,51 56.8,52.4 55.1,53.7 C52.6,55.6 51,57.7 51,61 L57,61 C57,59.3 57.9,58.2 59.6,56.9 C62,55 64,52.5 64,49 C64,43.5 59.5,39 54,39 Z" />
</vector>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
4 changes: 4 additions & 0 deletions quiz-kmp/composeApp/src/androidMain/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">DynaQuiz</string>
</resources>
9 changes: 9 additions & 0 deletions quiz-kmp/composeApp/src/androidMain/res/values/themes.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.DynaQuiz" parent="android:Theme.Material.Light.NoActionBar">
<item name="android:windowBackground">@android:color/black</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">false</item>
</style>
</resources>
12 changes: 12 additions & 0 deletions quiz-kmp/composeApp/src/commonMain/kotlin/com/dynamox/quiz/App.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.dynamox.quiz

import androidx.compose.runtime.Composable
import com.dynamox.quiz.presentation.navigation.NavGraph
import com.dynamox.quiz.presentation.theme.DynaQuizTheme

@Composable
fun App() {
DynaQuizTheme {
NavGraph()
}
}
Loading