feat: add network, dto, mapping and repository initial setup

This commit is contained in:
Leonardo Murça 2025-07-17 21:15:36 -03:00
parent 7eb061fc9d
commit 3fd7e78090
24 changed files with 492 additions and 3 deletions

View file

@ -1,11 +1,13 @@
import com.android.sdklib.AndroidVersion.ApiBaseExtension.BAKLAVA
import com.android.sdklib.AndroidVersion.ApiBaseExtension.VANILLA_ICE_CREAM
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import java.util.Properties
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.hilt.android)
alias(libs.plugins.ksp)
}
@ -22,6 +24,8 @@ android {
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField("String", "API_BASE_URL", "\"${getSecret("API_BASE_URL")}\"")
buildConfigField("String", "ACCESS_TOKEN", "\"${getSecret("ACCESS_TOKEN")}\"")
}
buildTypes {
@ -45,6 +49,7 @@ android {
}
buildFeatures {
compose = true
buildConfig = true
}
}
@ -61,6 +66,10 @@ dependencies {
implementation(libs.hilt.android)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.core.splashscreen)
implementation(libs.retrofit2)
implementation(libs.retrofit.kotlinx.serialization.converter)
implementation(libs.kotlinx.serialization.json)
implementation(libs.okhttp)
ksp(libs.hilt.android.compiler)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
@ -70,3 +79,22 @@ dependencies {
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
}
fun getSecret(key: String): String {
// 1. Check Gradle -P property (CI)
project.findProperty(key)?.let { return it.toString() }
// 2. Check environment variable (CI)
System.getenv(key)?.let { return it }
// 3. Fallback to local.properties (local dev)
val localPropsFile = rootProject.file("local.properties")
if (localPropsFile.exists()) {
val props = Properties().apply {
load(localPropsFile.inputStream())
}
return props.getProperty(key) ?: ""
}
return ""
}

View file

@ -2,7 +2,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".CSGOApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
@ -11,7 +14,6 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.CSGOMatches"
android:name=".CSGOApplication"
tools:targetApi="31">
<activity
android:name=".MainActivity"

View file

@ -0,0 +1,64 @@
package xyz.leomurca.csgomatches.data.local
import xyz.leomurca.csgomatches.data.model.LeagueDto
import xyz.leomurca.csgomatches.data.model.MatchDto
import xyz.leomurca.csgomatches.data.model.OpponentDto
import xyz.leomurca.csgomatches.data.model.OpponentRecord
import xyz.leomurca.csgomatches.data.model.SerieDto
import xyz.leomurca.csgomatches.data.source.MatchDataSource
import xyz.leomurca.csgomatches.domain.model.Resource
class MatchLocalDataSource : MatchDataSource {
override suspend fun upcomingMatches(): Resource<List<MatchDto>> {
return Resource.Success(
data = listOf(
MatchDto(
beginAt = "2025-07-27T10:30:00Z",
opponents = emptyList(),
league = LeagueDto(
id = 5078,
name = "United21",
imageUrl = "https://cdn.pandascore.co/images/league/image/5078/800px-united21_allmode-png"
),
serie = SerieDto(
id = 9519,
fullName = "Season 35 2025"
),
status = "not_started"
),
MatchDto(
beginAt = "2025-07-21T10:30:00Z",
opponents = listOf(
OpponentDto(
type = "team",
opponent = OpponentRecord(
id = 128519,
name = "GenOne",
imageUrl = "https://cdn.pandascore.co/images/team/image/128519/genone_csgo.png"
)
),
OpponentDto(
type = "team",
opponent = OpponentRecord(
id = 134996,
name = "VOLT",
imageUrl = "https://cdn.pandascore.co/images/team/image/134996/127px_volt_2024_allmode.png"
)
)
),
league = LeagueDto(
id = 5078,
name = "United21",
imageUrl = "https://cdn.pandascore.co/images/league/image/5078/800px-united21_allmode-png"
),
serie = SerieDto(
id = 9519,
fullName = "Season 35 2025"
),
status = "not_started"
),
)
)
}
}

View file

@ -0,0 +1,30 @@
package xyz.leomurca.csgomatches.data.mapper
import xyz.leomurca.csgomatches.data.model.MatchDto
import xyz.leomurca.csgomatches.domain.model.League
import xyz.leomurca.csgomatches.domain.model.Match
import xyz.leomurca.csgomatches.domain.model.Opponent
import xyz.leomurca.csgomatches.domain.model.Serie
fun MatchDto.toDomain(): Match {
return Match(
beginAt = beginAt ?: "",
opponents = opponents.map { op ->
Opponent(
id = op.opponent.id,
name = op.opponent.name ?: "",
imageUrl = op.opponent.imageUrl ?: ""
)
},
league = League(
id = league.id,
name = league.name ?: "",
imageUrl = league.imageUrl ?: ""
),
serie = Serie(
id = serie.id,
name = serie.fullName ?: ""
),
status = status ?: ""
)
}

View file

@ -0,0 +1,9 @@
package xyz.leomurca.csgomatches.data.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ErrorDto(
@SerialName("error") val message: String
)

View file

@ -0,0 +1,21 @@
package xyz.leomurca.csgomatches.data.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class LeagueDto(
val id: Long,
val name: String?,
@SerialName("image_url")
val imageUrl: String?
)
//"league": {
// "id": 5078,
// "name": "United21",
// "url": null,
// "slug": "cs-go-united21",
// "modified_at": "2023-12-22T16:36:10Z",
// "image_url": "https://cdn.pandascore.co/images/league/image/5078/800px-united21_allmode-png"
//},

View file

@ -0,0 +1,17 @@
package xyz.leomurca.csgomatches.data.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class MatchDto(
@SerialName("begin_at")
val beginAt: String?,
val opponents: List<OpponentDto>,
val league: LeagueDto,
val serie: SerieDto,
val status: String?
)

View file

@ -0,0 +1,31 @@
package xyz.leomurca.csgomatches.data.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class OpponentDto(
val type: String?,
val opponent: OpponentRecord
)
@Serializable
data class OpponentRecord(
val id: Long,
val name: String?,
@SerialName("image_url")
val imageUrl: String?
)
//{
// "type": "Team",
// "opponent": {
// "id": 126694,
// "name": "BIG Academy",
// "location": "DE",
// "slug": "big-academy",
// "modified_at": "2025-07-17T10:49:15Z",
// "acronym": "BIG.A",
// "image_url": "https://cdn.pandascore.co/images/team/image/126694/big.png"
// }
//}

View file

@ -0,0 +1,26 @@
package xyz.leomurca.csgomatches.data.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class SerieDto(
val id: Long,
@SerialName("full_name")
val fullName: String?
)
//"serie": {
// "id": 9519,
// "name": "",
// "year": 2025,
// "begin_at": "2025-07-18T08:00:00Z",
// "end_at": "2025-08-04T21:00:00Z",
// "winner_id": null,
// "winner_type": "Team",
// "slug": "cs-go-united21-35-2025",
// "modified_at": "2025-07-17T10:55:39Z",
// "league_id": 5078,
// "season": "35",
// "full_name": "Season 35 2025"
//},

View file

@ -0,0 +1,15 @@
package xyz.leomurca.csgomatches.data.remote
import okhttp3.Interceptor
import okhttp3.Response
class AuthorizationInterceptor(
private val token: String
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request().newBuilder()
.addHeader("Authorization", "Bearer $token")
.build()
return chain.proceed(request)
}
}

View file

@ -0,0 +1,28 @@
package xyz.leomurca.csgomatches.data.remote
import kotlinx.serialization.json.Json
import xyz.leomurca.csgomatches.data.model.ErrorDto
import xyz.leomurca.csgomatches.data.model.MatchDto
import xyz.leomurca.csgomatches.data.source.MatchDataSource
import xyz.leomurca.csgomatches.domain.model.Resource
class MatchRemoteDataSourceImpl(
private val matchesApiService: MatchesApiService,
private val json: Json
) : MatchDataSource {
override suspend fun upcomingMatches(): Resource<List<MatchDto>> {
return try {
val response = matchesApiService.upcomingMatches()
if (response.isSuccessful) {
Resource.Success(response.body() ?: throw Exception("Empty response body"))
} else {
val errorBody = response.errorBody()?.string() ?: ""
val networkError = json.decodeFromString<ErrorDto>(errorBody)
Resource.Error(networkError.message)
}
} catch (e: Exception) {
Resource.Error(e.message.toString())
}
}
}

View file

@ -0,0 +1,11 @@
package xyz.leomurca.csgomatches.data.remote
import retrofit2.Response
import retrofit2.http.GET
import xyz.leomurca.csgomatches.data.model.MatchDto
interface MatchesApiService {
@GET("matches")
suspend fun upcomingMatches(): Response<List<MatchDto>>
}

View file

@ -0,0 +1,25 @@
package xyz.leomurca.csgomatches.data.repository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import xyz.leomurca.csgomatches.data.mapper.toDomain
import xyz.leomurca.csgomatches.data.source.MatchDataSource
import xyz.leomurca.csgomatches.di.AppDispatchers
import xyz.leomurca.csgomatches.di.Dispatcher
import xyz.leomurca.csgomatches.domain.model.Match
import xyz.leomurca.csgomatches.domain.model.Resource
import xyz.leomurca.csgomatches.domain.repository.MatchRepository
class MatchRepositoryImpl(
@Dispatcher(AppDispatchers.IO) private val ioDispatcher: CoroutineDispatcher,
private val matchRemoteDataSource: MatchDataSource
) : MatchRepository {
override suspend fun upcomingMatches(): Resource<List<Match>> {
return withContext(ioDispatcher) {
when (val result = matchRemoteDataSource.upcomingMatches()) {
is Resource.Success -> Resource.Success(result.data.map { it.toDomain() })
is Resource.Error -> Resource.Error(result.message)
}
}
}
}

View file

@ -0,0 +1,8 @@
package xyz.leomurca.csgomatches.data.source
import xyz.leomurca.csgomatches.data.model.MatchDto
import xyz.leomurca.csgomatches.domain.model.Resource
interface MatchDataSource {
suspend fun upcomingMatches(): Resource<List<MatchDto>>
}

View file

@ -0,0 +1,30 @@
package xyz.leomurca.csgomatches.di
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import jakarta.inject.Qualifier
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class Dispatcher(val appDispatcher: AppDispatchers)
enum class AppDispatchers {
Default,
IO,
}
@Module
@InstallIn(SingletonComponent::class)
object DispatchersModule {
@Provides
@Dispatcher(AppDispatchers.IO)
fun providesIODispatcher(): CoroutineDispatcher = Dispatchers.IO
@Provides
@Dispatcher(AppDispatchers.Default)
fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
}

View file

@ -0,0 +1,89 @@
package xyz.leomurca.csgomatches.di
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.serialization.json.Json
import okhttp3.Interceptor
import retrofit2.Retrofit
import xyz.leomurca.csgomatches.data.remote.MatchesApiService
import javax.inject.Singleton
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import xyz.leomurca.csgomatches.BuildConfig
import xyz.leomurca.csgomatches.data.local.MatchLocalDataSource
import xyz.leomurca.csgomatches.data.remote.AuthorizationInterceptor
import xyz.leomurca.csgomatches.data.remote.MatchRemoteDataSourceImpl
import xyz.leomurca.csgomatches.data.repository.MatchRepositoryImpl
import xyz.leomurca.csgomatches.data.source.MatchDataSource
import xyz.leomurca.csgomatches.domain.repository.MatchRepository
@Module
@InstallIn(SingletonComponent::class)
internal object NetworkModule {
@Provides
@Singleton
fun providesNetworkJson(): Json = Json {
ignoreUnknownKeys = true
isLenient = true
explicitNulls = false
}
@Provides
@Singleton
fun provideAuthorizationInterceptor(): Interceptor {
return AuthorizationInterceptor(BuildConfig.ACCESS_TOKEN)
}
@Provides
@Singleton
fun provideOkHttpClient(authInterceptor: Interceptor): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(authInterceptor)
.build()
}
@Provides
@Singleton
fun provideRetrofit(json: Json, okHttpClient: OkHttpClient): Retrofit {
val contentType = "application/json".toMediaType()
return Retrofit.Builder()
.baseUrl(BuildConfig.API_BASE_URL)
.client(okHttpClient)
.addConverterFactory(json.asConverterFactory(contentType))
.build()
}
@Provides
@Singleton
fun provideMatchesApiService(retrofit: Retrofit): MatchesApiService {
return retrofit.create(MatchesApiService::class.java)
}
@Provides
@Singleton
fun provideMatchRemoteDataSource(
matchesApiService: MatchesApiService,
json: Json
): MatchDataSource {
val useRemote = false
return if (useRemote) {
MatchRemoteDataSourceImpl(matchesApiService, json)
} else {
MatchLocalDataSource()
}
}
@Provides
@Singleton
fun providesMatchRepository(
@Dispatcher(AppDispatchers.IO) ioDispatcher: CoroutineDispatcher,
matchRemoteDataSource: MatchDataSource
): MatchRepository {
return MatchRepositoryImpl(ioDispatcher, matchRemoteDataSource)
}
}

View file

@ -0,0 +1,7 @@
package xyz.leomurca.csgomatches.domain.model
data class League(
val id: Long,
val name: String,
val imageUrl: String
)

View file

@ -0,0 +1,10 @@
package xyz.leomurca.csgomatches.domain.model
data class Match(
val beginAt: String,
val opponents: List<Opponent>,
val league: League,
val serie: Serie,
val status: String
)

View file

@ -0,0 +1,7 @@
package xyz.leomurca.csgomatches.domain.model
data class Opponent(
val id: Long,
val name: String,
val imageUrl: String
)

View file

@ -0,0 +1,6 @@
package xyz.leomurca.csgomatches.domain.model
sealed class Resource<out T> {
data class Success<T>(val data: T) : Resource<T>()
data class Error(val message: String) : Resource<Nothing>()
}

View file

@ -0,0 +1,6 @@
package xyz.leomurca.csgomatches.domain.model
data class Serie(
val id: Long,
val name: String
)

View file

@ -0,0 +1,9 @@
package xyz.leomurca.csgomatches.domain.repository
import xyz.leomurca.csgomatches.domain.model.Match
import xyz.leomurca.csgomatches.domain.model.Resource
interface MatchRepository {
suspend fun upcomingMatches(): Resource<List<Match>>
}

View file

@ -3,6 +3,7 @@ plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.hilt.android) apply false
alias(libs.plugins.ksp) apply false
}

View file

@ -12,6 +12,10 @@ hilt = "2.56.2"
ksp = "2.2.0-2.0.2"
navigation = "2.9.2"
splashScreen = "1.0.1"
retrofit = "1.0.0"
retrofit2 = "2.11.0"
kotlinxSerializationJson = "1.8.1"
okhttp = "4.12.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@ -32,6 +36,10 @@ hilt-android = { group = "com.google.dagger", name="hilt-android", version.ref =
hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" }
androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "splashScreen" }
retrofit2 = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit2" }
retrofit-kotlinx-serialization-converter = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "retrofit" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
@ -39,5 +47,6 @@ kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }