feat: Implement initial project structure and network layer

This commit introduces the foundational structure of the Kahoot Quiz application and implements the core network layer.

Key changes include:
- Added new Gradle modules: `core:network`, `domain`, `model:data`, and `ui:quiz`.
- Configured Detekt for static code analysis in the new modules.
- Implemented Retrofit and Gson for network communication and JSON parsing.
- Defined DTOs for the Kahoot quiz API response, splitting them into logical files (QuizResponseDto, CommonDtos, CoverDtos, QuestionDtos, MetadataDtos, ContentTagsDto) for better organization.
- Created `QuizApi` interface with a GET request for fetching quiz data.
- Added `QuizService` interface and its initial implementation `QuizServiceImpl`.
- Set up Hilt for dependency injection in the network module, providing Retrofit and QuizApi instances.
- Included a `sample_quiz.json` file for testing and development.
- Added unit tests (`QuizResponseDtoParsingTest`) to verify the correct parsing of the sample JSON into DTOs.
- Updated `.gitignore` to exclude additional generated files and IDE specific folders.
- Modified `settings.gradle.kts` to include the new modules.
- Updated `app/build.gradle.kts` to include dependencies on the new `ui:quiz` and `model:data` modules and removed unused dependencies.
This commit is contained in:
GitHub Actions Bot
2025-09-02 22:14:50 +02:00
parent d4ec154b8c
commit f12ac0826e
27 changed files with 902 additions and 4 deletions

5
.gitignore vendored
View File

@@ -8,7 +8,7 @@
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
build/
/captures
.externalNativeBuild
.cxx
@@ -16,4 +16,5 @@ local.properties
# Project exclude paths
/build-logic/convention/build/
/build-logic/convention/build/classes/kotlin/main/
/build-logic/convention/build/classes/kotlin/main/
/.idea/

Binary file not shown.

View File

@@ -27,8 +27,10 @@ android {
}
dependencies {
implementation(projects.ui.quiz)
implementation(projects.model.data)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.app.update.ktx)
}

View File

@@ -0,0 +1,22 @@
plugins {
alias(libs.plugins.kahootquiz.android.library)
alias(libs.plugins.kahootquiz.android.library.hilt)
}
android {
namespace = "dev.adriankuta.kahootquiz.core.network"
defaultConfig {
buildConfigField("String", "KAHOOT_BASE_URL", "\"https://create.kahoot.it\"")
}
buildFeatures {
buildConfig = true
}
}
dependencies {
// Gson for JSON serialization/deserialization
implementation(libs.retrofit)
implementation(libs.retrofit.converter.gson)
}

View File

@@ -0,0 +1,10 @@
# Deviations from defaults
formatting:
TrailingCommaOnCallSite:
active: true
autoCorrect: true
useTrailingCommaOnCallSite: true
TrailingCommaOnDeclarationSite:
active: true
autoCorrect: true
useTrailingCommaOnDeclarationSite: true

View File

@@ -0,0 +1,4 @@
package dev.adriankuta.kahootquiz.core.network
interface QuizService {
}

View File

@@ -0,0 +1,4 @@
package dev.adriankuta.kahootquiz.core.network
internal class QuizServiceImpl: QuizService {
}

View File

@@ -0,0 +1,29 @@
package dev.adriankuta.kahootquiz.core.network.di
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dev.adriankuta.kahootquiz.core.network.BuildConfig
import dev.adriankuta.kahootquiz.core.network.retrofit.QuizApi
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun providesRetrofit(): Retrofit =
Retrofit.Builder()
.baseUrl(BuildConfig.KAHOOT_BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
@Provides
@Singleton
fun providesQuizApi(retrofit: Retrofit): QuizApi =
retrofit.create(QuizApi::class.java)
}

View File

@@ -0,0 +1,20 @@
package dev.adriankuta.kahootquiz.core.network.model
// Commonly reused DTOs
data class LanguageInfoDto(
val language: String?,
val lastUpdatedOn: Long?,
val readAloudSupported: Boolean?
)
// Minimal channel info
data class ChannelDto(val id: String?)
// Geometry helpers
data class PointDto(
val x: Int?,
val y: Int?
)

View File

@@ -0,0 +1,8 @@
package dev.adriankuta.kahootquiz.core.network.model
// Content tags DTOs
data class ContentTagsDto(
val curriculumCodes: List<String>?,
val generatedCurriculumCodes: List<String>?
)

View File

@@ -0,0 +1,32 @@
package dev.adriankuta.kahootquiz.core.network.model
// Cover metadata and related DTOs
data class CoverMetadataDto(
val id: String?,
val altText: String?,
val contentType: String?,
val origin: String?,
val externalRef: String?,
val resources: String?,
val width: Int?,
val height: Int?,
val extractedColors: List<ExtractedColorDto>?,
val blurhash: String?,
val crop: CropDto?
)
// Color extracted from cover image
data class ExtractedColorDto(
val swatch: String?,
val rgbHex: String?
)
// Crop descriptor
data class CropDto(
val origin: PointDto?,
val target: PointDto?,
val circular: Boolean?
)

View File

@@ -0,0 +1,42 @@
package dev.adriankuta.kahootquiz.core.network.model
// Metadata section DTOs
data class MetadataDto(
val access: AccessDto?,
val duplicationProtection: Boolean?,
val featuredListMemberships: List<FeaturedListMembershipDto>?,
val lastEdit: LastEditDto?,
val versionMetadata: VersionMetadataDto?
)
// Access settings
data class AccessDto(
val groupRead: List<String>?,
val folderGroupIds: List<String>?,
val features: List<String>?
)
// Featured list membership
data class FeaturedListMembershipDto(
val list: String?,
val addedAt: Long?
)
// Last edit information
data class LastEditDto(
val editorUserId: String?,
val editorUsername: String?,
val editTimestamp: Long?
)
// Version metadata
data class VersionMetadataDto(
val version: Int?,
val created: Long?,
val creator: String?
)

View File

@@ -0,0 +1,81 @@
package dev.adriankuta.kahootquiz.core.network.model
// Question and choice related DTOs
data class QuestionDto(
val type: String?,
val question: String?,
val time: Int?,
val points: Boolean?,
val pointsMultiplier: Int?,
val choices: List<ChoiceDto>?,
val layout: String?,
val image: String?,
val imageMetadata: ImageMetadataDto?,
val resources: String?,
val video: VideoDto?,
val questionFormat: Int?,
val languageInfo: LanguageInfoDto?,
val media: List<MediaItemDto>?,
val choiceRange: ChoiceRangeDto?
)
// Choice option
data class ChoiceDto(
val answer: String?,
val correct: Boolean?,
val languageInfo: LanguageInfoDto?
)
// Optional video attachment
data class VideoDto(
val id: String? = null,
val startTime: Int?,
val endTime: Int?,
val service: String?,
val fullUrl: String?
)
// Image metadata appearing in multiple places
data class ImageMetadataDto(
val id: String?,
val altText: String? = null,
val contentType: String?,
val origin: String? = null,
val externalRef: String? = null,
val resources: String? = null,
val width: Int? = null,
val height: Int? = null,
val effects: List<String>? = null,
val crop: CropDto? = null
)
// Generic media item on question
data class MediaItemDto(
val type: String?,
val zIndex: Int?,
val isColorOnly: Boolean?,
val id: String?,
val altText: String? = null,
val contentType: String?,
val origin: String? = null,
val externalRef: String? = null,
val resources: String? = null,
val width: Int? = null,
val height: Int? = null,
val crop: CropDto? = null
)
// Slider range for "slider" question type
data class ChoiceRangeDto(
val start: Int?,
val end: Int?,
val step: Int?,
val correct: Int?,
val tolerance: Int?
)

View File

@@ -0,0 +1,10 @@
package dev.adriankuta.kahootquiz.core.network.model
// This file used to contain all DTOs in one place.
// The DTOs have been split into separate files for maintainability:
// - QuizResponseDto.kt
// - CommonDtos.kt
// - CoverDtos.kt
// - QuestionDtos.kt
// - MetadataDtos.kt
// Keeping this file as a placeholder to avoid breaking any imports by file path (package remains the same).

View File

@@ -0,0 +1,36 @@
package dev.adriankuta.kahootquiz.core.network.model
import com.google.gson.annotations.SerializedName
// Root response for a Kahoot quiz details (network layer DTO)
data class QuizResponseDto(
val uuid: String?,
val language: String?,
val creator: String?,
@SerializedName("creator_username") val creatorUsername: String?,
val compatibilityLevel: Int?,
@SerializedName("creator_primary_usage") val creatorPrimaryUsage: String?,
val folderId: String?,
val themeId: String?,
val visibility: Int?,
val audience: String?,
val title: String?,
val description: String?,
val quizType: String?,
val cover: String?,
val coverMetadata: CoverMetadataDto?,
val questions: List<QuestionDto>?,
val contentTags: ContentTagsDto?,
val metadata: MetadataDto?,
val resources: String?,
val slug: String?,
val languageInfo: LanguageInfoDto?,
val inventoryItemIds: List<String>?,
val channels: List<ChannelDto>?,
val isValid: Boolean?,
val playAsGuest: Boolean?,
val hasRestrictedContent: Boolean?,
val type: String?,
val created: Long?,
val modified: Long?
)

View File

@@ -0,0 +1,10 @@
package dev.adriankuta.kahootquiz.core.network.retrofit
import dev.adriankuta.kahootquiz.core.network.model.QuizResponseDto
import retrofit2.http.GET
interface QuizApi {
@GET("/rest/kahoots/fb4054fc-6a71-463e-88cd-243876715bc1")
suspend fun getQuiz(): QuizResponseDto
}

View File

@@ -0,0 +1,100 @@
package dev.adriankuta.kahootquiz.core.network
import com.google.common.truth.Truth.assertThat
import com.google.gson.Gson
import dev.adriankuta.kahootquiz.core.network.model.QuizResponseDto
import org.junit.Test
import java.io.InputStreamReader
class QuizResponseDtoParsingTest {
private fun loadSample(): QuizResponseDto {
val stream = checkNotNull(javaClass.classLoader?.getResourceAsStream("sample_quiz.json")) {
"sample_quiz.json not found on test classpath"
}
stream.use { input ->
return Gson().fromJson(InputStreamReader(input), QuizResponseDto::class.java)
}
}
@Test
fun parses_root_fields_correctly() {
val dto = loadSample()
assertThat(dto.uuid).isEqualTo("fb4054fc-6a71-463e-88cd-243876715bc1")
assertThat(dto.title).isEqualTo("Seven Wonders of the Ancient World")
assertThat(dto.creatorUsername).isEqualTo("KahootStudio")
assertThat(dto.creatorPrimaryUsage).isEqualTo("teacher")
assertThat(dto.quizType).isEqualTo("quiz")
assertThat(dto.isValid).isTrue()
assertThat(dto.playAsGuest).isTrue()
assertThat(dto.hasRestrictedContent).isFalse()
assertThat(dto.created).isGreaterThan(0)
assertThat(dto.modified).isGreaterThan(0)
}
@Test
fun parses_cover_metadata_and_colors() {
val dto = loadSample()
val cover = checkNotNull(dto.coverMetadata)
assertThat(cover.id).isEqualTo("0b64142f-0624-4014-9f50-b65e6be93d8f")
assertThat(cover.contentType).isEqualTo("image/jpeg")
assertThat(cover.extractedColors).isNotNull()
assertThat(cover.extractedColors).hasSize(4)
assertThat(cover.extractedColors?.first()?.swatch).isEqualTo("VIBRANT")
assertThat(cover.blurhash).isNotNull()
val crop = checkNotNull(cover.crop)
assertThat(crop.circular).isFalse()
assertThat(crop.origin?.x).isEqualTo(227)
assertThat(crop.target?.y).isEqualTo(1299)
}
@Test
fun parses_questions_and_choices() {
val dto = loadSample()
val questions = checkNotNull(dto.questions)
assertThat(questions).hasSize(12)
// First question true/false
val q1 = questions[0]
assertThat(q1.type).isEqualTo("quiz")
assertThat(q1.layout).isEqualTo("TRUE_FALSE")
assertThat(q1.choices).hasSize(2)
assertThat(q1.choices?.get(0)?.answer).isEqualTo("True")
assertThat(q1.choices?.get(0)?.correct).isTrue()
assertThat(q1.choices?.get(1)?.answer).isEqualTo("False")
assertThat(q1.choices?.get(1)?.correct).isFalse()
// Open ended question exists and has accepted answers
val openEnded = questions.first { it.type == "open_ended" }
assertThat(openEnded.choices).isNotNull()
val answers = openEnded.choices!!.map { it.answer }
assertThat(answers).containsAtLeast("Helios", "helios")
// Slider question has choiceRange
val slider = questions.first { it.type == "slider" }
val range = checkNotNull(slider.choiceRange)
assertThat(range.start).isEqualTo(0)
assertThat(range.end).isEqualTo(7)
assertThat(range.step).isEqualTo(1)
assertThat(range.correct).isEqualTo(1)
assertThat(range.tolerance).isEqualTo(0)
}
@Test
fun parses_metadata_and_channels() {
val dto = loadSample()
val metadata = checkNotNull(dto.metadata)
assertThat(metadata.duplicationProtection).isTrue()
assertThat(metadata.featuredListMemberships).isNotNull()
assertThat(metadata.featuredListMemberships).isNotEmpty()
assertThat(metadata.versionMetadata?.version).isEqualTo(4)
assertThat(metadata.lastEdit?.editorUsername).isEqualTo("KahootStudio")
val channels = checkNotNull(dto.channels)
assertThat(channels).hasSize(1)
assertThat(channels.first().id).isEqualTo("247c3eb4-af80-4c1f-b006-558682c7bd2f")
assertThat(dto.languageInfo?.readAloudSupported).isTrue()
}
}

View File

@@ -0,0 +1,404 @@
{
"uuid": "fb4054fc-6a71-463e-88cd-243876715bc1",
"language": "English",
"creator": "4c1574ee-de54-40a2-be15-8d72b333afad",
"creator_username": "KahootStudio",
"compatibilityLevel": 27,
"creator_primary_usage": "teacher",
"folderId": "20915e7a-34d5-458b-91e9-2ed290484438",
"themeId": "ace311f1-2de4-450b-ba7d-d8a7a81705a5",
"visibility": 1,
"audience": "Social",
"title": "Seven Wonders of the Ancient World",
"description": "A geography quiz about the Seven Wonders of the Ancient World. See how much you know about these ancient buildings and monuments!\n#trivia #history #geography #sevenwonders",
"quizType": "quiz",
"cover": "https://media.kahoot.it/0b64142f-0624-4014-9f50-b65e6be93d8f",
"coverMetadata": {
"id": "0b64142f-0624-4014-9f50-b65e6be93d8f",
"altText": "The Pyramids, Giza, Egypt",
"contentType": "image/jpeg",
"origin": "Getty Images",
"externalRef": "1360795720",
"resources": "Nick Brundle Photography/Moment/Getty Images",
"width": 2309,
"height": 1299,
"extractedColors": [
{
"swatch": "VIBRANT",
"rgbHex": "#28a8d8"
},
{
"swatch": "LIGHT_VIBRANT",
"rgbHex": "#88c8e0"
},
{
"swatch": "DARK_VIBRANT",
"rgbHex": "#307890"
},
{
"swatch": "LIGHT_MUTED",
"rgbHex": "#d0d0c0"
}
],
"blurhash": "UuJ*#Qxtx]xaCAj[W=WqEma}M{R*M|WVn#j?",
"crop": {
"origin": {"x": 227, "y": 0},
"target": {"x": 1948, "y": 1299},
"circular": false
}
},
"questions": [
{
"type": "quiz",
"question": "\u003Cb\u003ETrue or false: \u003C/b\u003EThe list of seven wonders is based on ancient Greek guidebooks for tourists.",
"time": 20000,
"points": true,
"pointsMultiplier": 1,
"choices": [
{"answer": "True", "correct": true, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}},
{"answer": "False", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}
],
"layout": "TRUE_FALSE",
"image": "https://media.kahoot.it/b2709905-1c6e-45a0-9cc1-34c6580495e5",
"imageMetadata": {
"id": "b2709905-1c6e-45a0-9cc1-34c6580495e5",
"altText": "Old engraved illustration of bird's eye View of Alexandria, Egypt",
"contentType": "image/jpeg",
"origin": "Getty Images",
"externalRef": "1352146635",
"resources": "mikroman6/Moment/Getty Images",
"width": 2133,
"height": 1406
},
"resources": "mikroman6/Moment/Getty Images",
"video": {"startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""},
"questionFormat": 0,
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true},
"media": []
},
{
"type": "quiz",
"question": "The Great Pyramid of Giza is the oldest of the wonders. What was its purpose?",
"time": 30000,
"points": true,
"pointsMultiplier": 1,
"choices": [
{"answer": "A monument to the god Ra", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}},
{"answer": "A tomb", "correct": true, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}},
{"answer": "A momument to a great war victory", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}},
{"answer": "A temple", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}
],
"resources": "",
"video": {"startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""},
"questionFormat": 0,
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true},
"media": [
{
"type": "background_image",
"zIndex": -1,
"isColorOnly": false,
"id": "0b64142f-0624-4014-9f50-b65e6be93d8f",
"altText": "The Pyramids, Giza, Egypt",
"contentType": "image/jpeg",
"origin": "Getty Images",
"externalRef": "1360795720",
"resources": "Nick Brundle Photography/Moment/Getty Images",
"width": 2309,
"height": 1299,
"crop": {"origin": {"x": 227, "y": 0}, "target": {"x": 1948, "y": 1299}, "circular": false}
}
]
},
{
"type": "quiz",
"question": "Why were the Hanging Gardens of Babylon supposedly built?",
"time": 30000,
"points": true,
"pointsMultiplier": 1,
"choices": [
{"answer": "As a tourist destination", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}},
{"answer": "A monument to Ninurta, the god of farmers", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}},
{"answer": "An engagement gift from a king to his future queen", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}},
{"answer": "A gift for the king's wife", "correct": true, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}
],
"image": "https://media.kahoot.it/7bce7efb-3d94-495c-905f-9c14190b7910",
"imageMetadata": {
"id": "7bce7efb-3d94-495c-905f-9c14190b7910",
"contentType": "image/*",
"resources": "https://upload.wikimedia.org/wikipedia/commons/a/ae/Hanging_Gardens_of_Babylon.jpg CC0",
"effects": []
},
"resources": "https://upload.wikimedia.org/wikipedia/commons/a/ae/Hanging_Gardens_of_Babylon.jpg CC0",
"video": {"startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""},
"questionFormat": 0,
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true},
"media": []
},
{
"type": "quiz",
"question": "The Temple of Artemis was located in the city Ephesus. It's the territory of _____ today.",
"time": 30000,
"points": true,
"pointsMultiplier": 1,
"choices": [
{"answer": "Greece", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}},
{"answer": "Turkey", "correct": true, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}},
{"answer": "Syria", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}},
{"answer": "Iran", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}
],
"image": "https://media.kahoot.it/f999f2a2-5450-4821-a3c8-94288720bd46",
"imageMetadata": {
"id": "f999f2a2-5450-4821-a3c8-94288720bd46",
"contentType": "image/*",
"resources": "Zee Prime at cs.wikipedia [GFDL (http://www.gnu.org/copyleft/fdl.html), CC-BY-SA-3.0 (http://creativecommons.org/licenses/by-sa/3.0/) or CC BY-SA 2.5 (https://creativecommons.org/licenses/by-sa/2.5)], from Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/1/1d/Miniaturk_009.jpg",
"effects": []
},
"resources": "Zee Prime at cs.wikipedia [GFDL (http://www.gnu.org/copyleft/fdl.html), CC-BY-SA-3.0 (http://creativecommons.org/licenses/by-sa/3.0/) or CC BY-SA 2.5 (https://creativecommons.org/licenses/by-sa/2.5)], from Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/1/1d/Miniaturk_009.jpg",
"video": {"startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""},
"questionFormat": 0,
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true},
"media": []
},
{
"type": "quiz",
"question": "The second Temple of Artemis was burnt down by Herostratus. Why did he set the temple on fire?",
"time": 30000,
"points": true,
"pointsMultiplier": 1,
"choices": [
{"answer": "To become famous", "correct": true, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}},
{"answer": "It was an accident", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}},
{"answer": "He was angry at the gods", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}},
{"answer": "Because of a bet", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}
],
"image": "https://media.kahoot.it/fe2c5c06-6d2e-4a5a-9441-a9c77391130e_opt",
"imageMetadata": {
"id": "fe2c5c06-6d2e-4a5a-9441-a9c77391130e",
"contentType": "image/*",
"resources": " [Public domain], via Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/a/a9/Temple_of_Artemis.jpg",
"effects": []
},
"resources": " [Public domain], via Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/a/a9/Temple_of_Artemis.jpg",
"video": {"id": "", "startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""},
"questionFormat": 0,
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true},
"media": []
},
{
"type": "quiz",
"question": "The Statue of Zeus was built by the sculptor Phidias. Where was it located?",
"time": 30000,
"points": true,
"pointsMultiplier": 1,
"choices": [
{"answer": "Sparta", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}},
{"answer": "Athens", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}},
{"answer": "Olympia", "correct": true, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}},
{"answer": "Delphi", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}
],
"image": "https://media.kahoot.it/9074a275-1874-4cb9-9c9f-248173ceae9d",
"imageMetadata": {
"id": "9074a275-1874-4cb9-9c9f-248173ceae9d",
"contentType": "image/*",
"resources": " [Public domain or Public domain], via Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/6/66/Le_Jupiter_Olympien_ou_l%27art_de_la_sculpture_antique.jpg",
"effects": [],
"crop": {"origin": {"x": 53, "y": 0}, "target": {"x": 577, "y": 866}, "circular": false}
},
"resources": " [Public domain or Public domain], via Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/6/66/Le_Jupiter_Olympien_ou_l%27art_de_la_sculpture_antique.jpg",
"video": {"startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""},
"questionFormat": 0,
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true},
"media": []
},
{
"type": "quiz",
"question": "The Mausoleum at Halicarnassus was a tomb for a Persian governor. Who was buried there?",
"time": 20000,
"points": true,
"pointsMultiplier": 1,
"choices": [
{"answer": "Darius", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}},
{"answer": "Xerxes", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}},
{"answer": "Cyrus", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}},
{"answer": "Mausoleus", "correct": true, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}
],
"image": "https://media.kahoot.it/38f43ef3-4507-4f11-ae33-f3e833a47d19",
"imageMetadata": {
"id": "38f43ef3-4507-4f11-ae33-f3e833a47d19",
"altText": "Yellow Question Mark on Purple Background, Paper Craft",
"contentType": "image/jpeg",
"origin": "Getty Images",
"externalRef": "1501192535",
"resources": "MirageC/Moment/Getty Images",
"width": 2120,
"height": 1414
},
"resources": "MirageC/Moment/Getty Images",
"video": {"startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""},
"questionFormat": 0,
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true},
"media": []
},
{
"type": "open_ended",
"question": "The Colossus of Rhodes was 108 feet (33 metres) high. Which God was it based on?",
"time": 60000,
"pointsMultiplier": 2,
"choices": [
{"answer": "Helios", "correct": true, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}},
{"answer": "helios", "correct": true, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}
],
"image": "https://media.kahoot.it/d4ccbf4e-1026-46ad-ab35-84dc17c4d3a0_opt",
"imageMetadata": {
"id": "d4ccbf4e-1026-46ad-ab35-84dc17c4d3a0",
"contentType": "image/*",
"resources": "By gravure sur bois de Sidney Barclay numérisée Google [Public domain], via Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/5/5f/Colosse_de_Rhodes_%28Barclay%29.jpg",
"effects": [],
"crop": {"origin": {"x": 49, "y": 83}, "target": {"x": 531, "y": 796}, "circular": false}
},
"resources": "By gravure sur bois de Sidney Barclay numérisée Google [Public domain], via Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/5/5f/Colosse_de_Rhodes_%28Barclay%29.jpg",
"video": {"id": "", "startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""},
"questionFormat": 0,
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true},
"media": []
},
{
"type": "quiz",
"question": "The Lighthouse of Alexandria was destroyed in the 14th century. How?",
"time": 30000,
"points": true,
"pointsMultiplier": 1,
"choices": [
{"answer": "Fire", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}},
{"answer": "Earthquake", "correct": true, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}},
{"answer": "Tidal Wave", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}},
{"answer": "Storm", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}
],
"image": "https://media.kahoot.it/e2d22765-942b-4dbd-9fd6-d71142d775c3",
"imageMetadata": {
"id": "e2d22765-942b-4dbd-9fd6-d71142d775c3",
"contentType": "image/*",
"resources": "Emad Victor SHENOUDA [Attribution], from Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/3/33/PHAROS2013-3000x2250.jpg",
"effects": [],
"crop": {"origin": {"x": 0, "y": 10}, "target": {"x": 1024, "y": 683}, "circular": false}
},
"resources": "Emad Victor SHENOUDA [Attribution], from Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/3/33/PHAROS2013-3000x2250.jpg",
"video": {"startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""},
"questionFormat": 0,
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true},
"media": []
},
{
"type": "quiz",
"question": "Which of the seven wonders was the tallest?",
"time": 30000,
"points": true,
"pointsMultiplier": 1,
"choices": [
{"answer": "The Colossus of Rhodes", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}},
{"answer": "The Lighthouse of Alexandria", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}},
{"answer": "The Mausoleum at Halicarnassus", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}},
{"answer": "The Great Pyramid of Giza", "correct": true, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}
],
"image": "https://media.kahoot.it/19382163-196f-495d-9a84-c2d8c3fd716c",
"imageMetadata": {
"id": "19382163-196f-495d-9a84-c2d8c3fd716c",
"contentType": "image/*",
"resources": "By The original uploader was Mark22 at English Wikipedia (Transferred from en.wikipedia to Commons.) [Public domain], via Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/b/b7/SevenWondersOfTheWorld.png",
"effects": [],
"crop": {"origin": {"x": 19, "y": 0}, "target": {"x": 491, "y": 736}, "circular": false}
},
"resources": "By The original uploader was Mark22 at English Wikipedia (Transferred from en.wikipedia to Commons.) [Public domain], via Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/b/b7/SevenWondersOfTheWorld.png",
"video": {"id": "", "startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""},
"questionFormat": 0,
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true},
"media": []
},
{
"type": "slider",
"question": "How many of the Seven Wonders still exist?",
"time": 20000,
"pointsMultiplier": 2,
"choiceRange": {"start": 0, "end": 7, "step": 1, "correct": 1, "tolerance": 0},
"image": "https://media.kahoot.it/b431b3aa-4a46-49c9-b4ac-aa1dde40333f",
"imageMetadata": {
"id": "b431b3aa-4a46-49c9-b4ac-aa1dde40333f",
"contentType": "image/*",
"resources": "By Kandi [GFDL (http://www.gnu.org/copyleft/fdl.html) or CC BY-SA 3.0 (https://creativecommons.org/licenses/by-sa/3.0)], from Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/a/a4/Seven_Wonders_of_the_Ancient_World.png",
"effects": []
},
"resources": "By Kandi [GFDL (http://www.gnu.org/copyleft/fdl.html) or CC BY-SA 3.0 (https://creativecommons.org/licenses/by-sa/3.0)], from Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/a/a4/Seven_Wonders_of_the_Ancient_World.png",
"video": {"id": "", "startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""},
"questionFormat": 0,
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true},
"media": []
},
{
"type": "quiz",
"question": "Which of the Seven Wonders is the only one that still exists?",
"time": 30000,
"points": true,
"pointsMultiplier": 1,
"choices": [
{"answer": "The Great Pyramid of Giza", "correct": true, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}},
{"answer": "The Temple of Artemis", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}},
{"answer": "The Mausoleum at Halicarnassus", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}},
{"answer": "The Colossus of Rhodes", "correct": false, "languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true}}
],
"image": "https://media.kahoot.it/34b01038-031c-4d23-b8a0-55402916586f_opt",
"imageMetadata": {
"id": "34b01038-031c-4d23-b8a0-55402916586f",
"contentType": "image/*",
"resources": "By Varios [CC BY-SA 3.0 (https://creativecommons.org/licenses/by-sa/3.0)], via Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/d/d6/Siete_maravillas_antiguas.jpg",
"effects": [],
"crop": {"origin": {"x": 19, "y": 0}, "target": {"x": 491, "y": 736}, "circular": false}
},
"resources": "By Varios [CC BY-SA 3.0 (https://creativecommons.org/licenses/by-sa/3.0)], via Wikimedia Commons\nhttps://upload.wikimedia.org/wikipedia/commons/d/d6/Siete_maravillas_antiguas.jpg",
"video": {"id": "", "startTime": 0, "endTime": 0, "service": "youtube", "fullUrl": ""},
"questionFormat": 0,
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true},
"media": []
}
],
"contentTags": {"curriculumCodes": [], "generatedCurriculumCodes": []},
"metadata": {
"access": {
"groupRead": [
"b5c71d39-c229-4eeb-8648-cd8518ec068a",
"99159f29-7004-460b-a87a-cd4aab39ba4c",
"4597f49e-8b3b-40a5-985a-e6da01761947",
"ff2abab7-29f9-4669-8085-66e1089045a0",
"4caead57-3dd8-41c0-8f60-c5da69881b8e",
"36022fd9-43e1-4b36-9c98-a6a3b2b53038"
],
"folderGroupIds": [],
"features": ["PremiumEduContent"]
},
"duplicationProtection": true,
"featuredListMemberships": [
{"list": "youngfeatured", "addedAt": 1682336780289},
{"list": "featured", "addedAt": 1682336738189}
],
"lastEdit": {
"editorUserId": "4c1574ee-de54-40a2-be15-8d72b333afad",
"editorUsername": "KahootStudio",
"editTimestamp": 1727277651459
},
"versionMetadata": {
"version": 4,
"created": 1727277651160,
"creator": "4c1574ee-de54-40a2-be15-8d72b333afad"
}
},
"resources": "Nick Brundle Photography/Moment/Getty Images",
"slug": "seven-wonders-of-the-ancient-world",
"languageInfo": {"language": "en-US", "lastUpdatedOn": 1741920189202, "readAloudSupported": true},
"inventoryItemIds": [],
"channels": [{"id": "247c3eb4-af80-4c1f-b006-558682c7bd2f"}],
"isValid": true,
"playAsGuest": true,
"hasRestrictedContent": false,
"type": "quiz",
"created": 1527169083018,
"modified": 1754523196463
}

11
domain/build.gradle.kts Normal file
View File

@@ -0,0 +1,11 @@
plugins {
alias(libs.plugins.kahootquiz.android.library)
alias(libs.plugins.kahootquiz.android.library.hilt)
}
android {
namespace = "dev.adriankuta.kahootquiz.domain"
}
dependencies {
}

View File

@@ -0,0 +1,10 @@
# Deviations from defaults
formatting:
TrailingCommaOnCallSite:
active: true
autoCorrect: true
useTrailingCommaOnCallSite: true
TrailingCommaOnDeclarationSite:
active: true
autoCorrect: true
useTrailingCommaOnDeclarationSite: true

View File

@@ -1,4 +1,5 @@
[versions]
retrofit = "3.0.0"
targetSdk = "36"
compileSdk = "36"
minSdk = "23"
@@ -115,6 +116,8 @@ kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockk" }
play-services-ads = { module = "com.google.android.gms:play-services-ads", version.ref = "playServicesAds" }
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
review-ktx = { module = "com.google.android.play:review-ktx", version.ref = "reviewKtx" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }

View File

@@ -0,0 +1,13 @@
plugins {
alias(libs.plugins.kahootquiz.android.library)
alias(libs.plugins.kahootquiz.android.library.hilt)
}
android {
namespace = "dev.adriankuta.kahootquiz.model.data"
}
dependencies {
implementation(projects.core.network)
implementation(projects.domain)
}

View File

@@ -0,0 +1,10 @@
# Deviations from defaults
formatting:
TrailingCommaOnCallSite:
active: true
autoCorrect: true
useTrailingCommaOnCallSite: true
TrailingCommaOnDeclarationSite:
active: true
autoCorrect: true
useTrailingCommaOnDeclarationSite: true

View File

@@ -25,3 +25,7 @@ rootProject.name = "KahootQuiz"
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
include(":app")
include(":core:network")
include(":domain")
include(":model:data")
include(":ui:quiz")

View File

@@ -0,0 +1,10 @@
# Deviations from defaults
formatting:
TrailingCommaOnCallSite:
active: true
autoCorrect: true
useTrailingCommaOnCallSite: true
TrailingCommaOnDeclarationSite:
active: true
autoCorrect: true
useTrailingCommaOnDeclarationSite: true

12
ui/quiz/build.gradle.kts Normal file
View File

@@ -0,0 +1,12 @@
plugins {
alias(libs.plugins.kahootquiz.android.library.compose)
alias(libs.plugins.kahootquiz.android.library.hilt)
}
android {
namespace = "dev.adriankuta.kahootquiz.ui.quiz"
}
dependencies {
implementation(projects.domain)
}

View File

@@ -0,0 +1,10 @@
# Deviations from defaults
formatting:
TrailingCommaOnCallSite:
active: true
autoCorrect: true
useTrailingCommaOnCallSite: true
TrailingCommaOnDeclarationSite:
active: true
autoCorrect: true
useTrailingCommaOnDeclarationSite: true