diff --git a/README.md b/README.md index f59dee2..4d28b45 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,19 @@ # Dogga -Simple Android Native Application consuming the https://dog.ceo/dog-api/ + +![Dogga App icon](app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp) + +Simple Android Native Application consuming the [Dog API](https://dog.ceo/dog-api/) + +This app is made just for portfolio and study purposes and it is in constant evolution. + + +It is made using: + +- MVVM +- Jetpack Compose +- Retrofit +- JUnit4 +- Mockk +- Kotlin DSL (kts script) +- Koin + diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100644 index 28dfeed..0000000 --- a/app/build.gradle +++ /dev/null @@ -1,66 +0,0 @@ -plugins { - id 'com.android.application' - id 'org.jetbrains.kotlin.android' -} - -android { - namespace 'dev.lhalegria.dogga' - compileSdk 33 - - defaultConfig { - applicationId "dev.lhalegria.dogga" - minSdk 30 - targetSdk 33 - versionCode 1 - versionName "1.0" - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - vectorDrawables { - useSupportLibrary true - } - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - kotlinOptions { - jvmTarget = '1.8' - } - buildFeatures { - compose true - } - composeOptions { - kotlinCompilerExtensionVersion '1.3.2' - } - packagingOptions { - resources { - excludes += '/META-INF/{AL2.0,LGPL2.1}' - } - } -} - -dependencies { - implementation 'androidx.core:core-ktx:1.10.1' - implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.0') - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' - implementation 'androidx.activity:activity-compose:1.7.2' - implementation platform('androidx.compose:compose-bom:2022.10.00') - implementation 'androidx.compose.ui:ui' - implementation 'androidx.compose.ui:ui-graphics' - implementation 'androidx.compose.ui:ui-tooling-preview' - implementation 'androidx.compose.material3:material3' - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' - androidTestImplementation platform('androidx.compose:compose-bom:2022.10.00') - androidTestImplementation 'androidx.compose.ui:ui-test-junit4' - debugImplementation 'androidx.compose.ui:ui-tooling' - debugImplementation 'androidx.compose.ui:ui-test-manifest' -} diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..a07ddc4 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,105 @@ +import BuildSetup.APPLICATION_ID +import BuildSetup.BUILD_TOOLS_VERSION +import BuildSetup.COMPILE_SDK_VERSION +import BuildSetup.COMPOSE_KOTLIN_COMPILER +import BuildSetup.MIN_SDK_VERSION +import BuildSetup.NAMESPACE +import BuildSetup.TARGET_SDK_VERSION +import BuildSetup.TEST_INSTRUMENTATION_RUNNER +import BuildSetup.VERSION_CODE +import BuildSetup.VERSION_NAME + +plugins { + id(Plugins.ANDROID_APPLICATION) + kotlin(Plugins.KOTLIN_ANDROID) +} + +@Suppress("UnstableApiUsage") +android { + namespace = NAMESPACE + compileSdk = COMPILE_SDK_VERSION + buildToolsVersion = BUILD_TOOLS_VERSION + + defaultConfig { + applicationId = APPLICATION_ID + + minSdk = MIN_SDK_VERSION + targetSdk = TARGET_SDK_VERSION + versionCode = VERSION_CODE + versionName = VERSION_NAME + + vectorDrawables.useSupportLibrary = true + testInstrumentationRunner = TEST_INSTRUMENTATION_RUNNER + } + + buildTypes { + debug { + applicationIdSuffix = BuildTypeDebug.applicationIdSuffix + versionNameSuffix = BuildTypeDebug.versionNameSuffix + } + + release { + isMinifyEnabled = BuildTypeRelease.isMinifyEnabled + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + + java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + + buildFeatures { + compose = true + } + + composeOptions { + kotlinCompilerExtensionVersion = COMPOSE_KOTLIN_COMPILER + } + + packaging { + resources.excludes.addAll( + listOf("/META-INF/{AL2.0,LGPL2.1}") + ) + } +} + +dependencies { + implementation(AndroidX.CORE) + implementation(AndroidX.LIFECYCLE) + implementation(AndroidX.Compose.ACTIVITY) + implementation(platform(AndroidX.Compose.BOM)) + implementation(AndroidX.Compose.LIFECYCLE) + implementation(AndroidX.Compose.UI) + implementation(AndroidX.Compose.UI_GRAPHICS) + implementation(AndroidX.Compose.UI_TOOLING) + implementation(AndroidX.Compose.MATERIAL_3) + implementation(AndroidX.Compose.NAVIGATION) + + implementation(platform(Other.KOTLIN_BOM)) + implementation(Other.COIL) + + implementation(Koin.KOIN) + implementation(Koin.KOIN_COMPOSE) + + implementation(Square.RETROFIT) + implementation(Square.OKHTTP) + implementation(Square.OKHTTP_LOG_INTERCEPTOR) + implementation(Square.GSON_CONVERTER) + + testImplementation(Test.JUNIT) + testImplementation(Test.MOCKK) + testImplementation(Test.CORE_TESTING) + testImplementation(Test.COROUTINES) + androidTestImplementation(Test.ANDROIDX_JUNIT) + androidTestImplementation(Test.ESPRESSO) + androidTestImplementation(platform(AndroidX.Compose.BOM)) + androidTestImplementation(Test.COMPOSE_UI_JUNIT4) + + debugImplementation(AndroidX.Compose.UI_TOOLING) + debugImplementation(Test.COMPOSE_TEST_MANIFEST) +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 481bb43..2f9dc5a 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,6 +1,6 @@ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. +# proguardFiles setting in build.gradle.kts. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html @@ -18,4 +18,4 @@ # If you keep the line number information, uncomment this to # hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +#-renamesourcefileattribute SourceFile diff --git a/app/src/androidTest/java/dev/lhalegria/dogga/ExampleInstrumentedTest.kt b/app/src/androidTest/kotlin/dev/lhalegria/dogga/ExampleInstrumentedTest.kt similarity index 100% rename from app/src/androidTest/java/dev/lhalegria/dogga/ExampleInstrumentedTest.kt rename to app/src/androidTest/kotlin/dev/lhalegria/dogga/ExampleInstrumentedTest.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 48f8250..9e0184d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,7 +2,10 @@ + + + - - diff --git a/app/src/main/ic_launcher-playstore.webp b/app/src/main/ic_launcher-playstore.webp new file mode 100644 index 0000000..3b43a8e Binary files /dev/null and b/app/src/main/ic_launcher-playstore.webp differ diff --git a/app/src/main/java/dev/lhalegria/dogga/MainActivity.kt b/app/src/main/java/dev/lhalegria/dogga/MainActivity.kt deleted file mode 100644 index 6e9ad86..0000000 --- a/app/src/main/java/dev/lhalegria/dogga/MainActivity.kt +++ /dev/null @@ -1,46 +0,0 @@ -package dev.lhalegria.dogga - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import dev.lhalegria.dogga.ui.theme.DoggaTheme - -class MainActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContent { - DoggaTheme { - // A surface container using the 'background' color from the theme - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - Greeting("Android") - } - } - } - } -} - -@Composable -fun Greeting(name: String, modifier: Modifier = Modifier) { - Text( - text = "Hello $name!", - modifier = modifier - ) -} - -@Preview(showBackground = true) -@Composable -fun GreetingPreview() { - DoggaTheme { - Greeting("Android") - } -} diff --git a/app/src/main/java/dev/lhalegria/dogga/ui/theme/Color.kt b/app/src/main/java/dev/lhalegria/dogga/ui/theme/Color.kt deleted file mode 100644 index 38b262c..0000000 --- a/app/src/main/java/dev/lhalegria/dogga/ui/theme/Color.kt +++ /dev/null @@ -1,11 +0,0 @@ -package dev.lhalegria.dogga.ui.theme - -import androidx.compose.ui.graphics.Color - -val Purple80 = Color(0xFFD0BCFF) -val PurpleGrey80 = Color(0xFFCCC2DC) -val Pink80 = Color(0xFFEFB8C8) - -val Purple40 = Color(0xFF6650a4) -val PurpleGrey40 = Color(0xFF625b71) -val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/app/src/main/java/dev/lhalegria/dogga/ui/theme/Type.kt b/app/src/main/java/dev/lhalegria/dogga/ui/theme/Type.kt deleted file mode 100644 index 435ccb9..0000000 --- a/app/src/main/java/dev/lhalegria/dogga/ui/theme/Type.kt +++ /dev/null @@ -1,34 +0,0 @@ -package dev.lhalegria.dogga.ui.theme - -import androidx.compose.material3.Typography -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.sp - -// Set of Material typography styles to start with -val Typography = Typography( - bodyLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp - ) - /* Other default text styles to override - titleLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 22.sp, - lineHeight = 28.sp, - letterSpacing = 0.sp - ), - labelSmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ) - */ -) \ No newline at end of file diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/DoggaApp.kt b/app/src/main/kotlin/dev/lhalegria/dogga/DoggaApp.kt new file mode 100644 index 0000000..350f766 --- /dev/null +++ b/app/src/main/kotlin/dev/lhalegria/dogga/DoggaApp.kt @@ -0,0 +1,25 @@ +package dev.lhalegria.dogga + +import android.app.Application +import android.content.res.Resources +import dev.lhalegria.dogga.datasource.di.dataSourceModule +import dev.lhalegria.dogga.di.mainModule +import org.koin.android.ext.koin.androidContext +import org.koin.core.context.startKoin + +class DoggaApp : Application() { + + override fun onCreate() { + super.onCreate() + appResources = this.resources + startKoin { + androidContext(this@DoggaApp) + modules(listOf(dataSourceModule, mainModule)) + } + } + + companion object { + lateinit var appResources: Resources + private set + } +} diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/datasource/di/DataSourceModule.kt b/app/src/main/kotlin/dev/lhalegria/dogga/datasource/di/DataSourceModule.kt new file mode 100644 index 0000000..474770d --- /dev/null +++ b/app/src/main/kotlin/dev/lhalegria/dogga/datasource/di/DataSourceModule.kt @@ -0,0 +1,11 @@ +package dev.lhalegria.dogga.datasource.di + +import dev.lhalegria.dogga.datasource.network.retrofitApi +import dev.lhalegria.dogga.datasource.service.BreedService +import org.koin.dsl.module + +val dataSourceModule = module { + + factory { retrofitApi } + factory { retrofitApi.create(BreedService::class.java) } +} diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/datasource/network/DoggaNetworkApi.kt b/app/src/main/kotlin/dev/lhalegria/dogga/datasource/network/DoggaNetworkApi.kt new file mode 100644 index 0000000..9825598 --- /dev/null +++ b/app/src/main/kotlin/dev/lhalegria/dogga/datasource/network/DoggaNetworkApi.kt @@ -0,0 +1,28 @@ +package dev.lhalegria.dogga.datasource.network + +import com.google.gson.FieldNamingPolicy +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.gson.LongSerializationPolicy +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +private const val BASE_URL = "https://dog.ceo/api/" + +private val httpLoggingInterceptor = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY +} + +private val gson: Gson = GsonBuilder() + .setLongSerializationPolicy(LongSerializationPolicy.DEFAULT) + .setFieldNamingStrategy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) + .create() + +val retrofitApi: Retrofit + get() = Retrofit.Builder() + .baseUrl(BASE_URL) + .client(OkHttpClient.Builder().addInterceptor(httpLoggingInterceptor).build()) + .addConverterFactory(GsonConverterFactory.create(gson)) + .build() diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/datasource/response/BreedImageResponse.kt b/app/src/main/kotlin/dev/lhalegria/dogga/datasource/response/BreedImageResponse.kt new file mode 100644 index 0000000..b68e1f0 --- /dev/null +++ b/app/src/main/kotlin/dev/lhalegria/dogga/datasource/response/BreedImageResponse.kt @@ -0,0 +1,8 @@ +package dev.lhalegria.dogga.datasource.response + +import com.google.gson.annotations.SerializedName + +data class BreedImageResponse( + @SerializedName("message") val imageUrl: String, + @SerializedName("status") val status: String +) diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/datasource/response/BreedResponse.kt b/app/src/main/kotlin/dev/lhalegria/dogga/datasource/response/BreedResponse.kt new file mode 100644 index 0000000..587aed2 --- /dev/null +++ b/app/src/main/kotlin/dev/lhalegria/dogga/datasource/response/BreedResponse.kt @@ -0,0 +1,8 @@ +package dev.lhalegria.dogga.datasource.response + +import com.google.gson.annotations.SerializedName + +data class BreedResponse( + @SerializedName("message") val breedsList: List, + @SerializedName("status") val status: String +) diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/datasource/service/BreedService.kt b/app/src/main/kotlin/dev/lhalegria/dogga/datasource/service/BreedService.kt new file mode 100644 index 0000000..fa8169c --- /dev/null +++ b/app/src/main/kotlin/dev/lhalegria/dogga/datasource/service/BreedService.kt @@ -0,0 +1,23 @@ +package dev.lhalegria.dogga.datasource.service + +import dev.lhalegria.dogga.datasource.response.BreedImageResponse +import dev.lhalegria.dogga.datasource.response.BreedResponse +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.Path + +interface BreedService { + + @GET("breeds/list") + suspend fun getBreeds(): Response + + @GET("breed/{breed}/list") + suspend fun getSubBreedsFromBreed( + @Path("breed") breed: String + ): Response + + @GET("breed/{breed}/images/random") + suspend fun getBreedImage( + @Path("breed") breed: String + ): Response +} diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/di/MainModule.kt b/app/src/main/kotlin/dev/lhalegria/dogga/di/MainModule.kt new file mode 100644 index 0000000..e3a0c92 --- /dev/null +++ b/app/src/main/kotlin/dev/lhalegria/dogga/di/MainModule.kt @@ -0,0 +1,24 @@ +package dev.lhalegria.dogga.di + +import dev.lhalegria.dogga.model.mapper.BreedMapper +import dev.lhalegria.dogga.model.mapper.IBreedMapper +import dev.lhalegria.dogga.repository.BreedRepository +import dev.lhalegria.dogga.repository.IBreedRepository +import dev.lhalegria.dogga.viewmodel.BreedViewModel +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.dsl.module + +val mainModule = module { + + single { BreedMapper() } + + factory { + BreedRepository(service = get(), mapper = get()) + } + + viewModel { + BreedViewModel( + repository = get() + ) + } +} diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/exception/ResponseErrorException.kt b/app/src/main/kotlin/dev/lhalegria/dogga/exception/ResponseErrorException.kt new file mode 100644 index 0000000..f0cff45 --- /dev/null +++ b/app/src/main/kotlin/dev/lhalegria/dogga/exception/ResponseErrorException.kt @@ -0,0 +1,12 @@ +package dev.lhalegria.dogga.exception + +import dev.lhalegria.dogga.DoggaApp +import dev.lhalegria.dogga.R + +class ResponseErrorException( + private val code: Int +): Exception() { + + override val message: String + get() = String.format(DoggaApp.appResources.getString(R.string.response_error), code) +} diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/exception/ResponseNoBodyException.kt b/app/src/main/kotlin/dev/lhalegria/dogga/exception/ResponseNoBodyException.kt new file mode 100644 index 0000000..661f7d6 --- /dev/null +++ b/app/src/main/kotlin/dev/lhalegria/dogga/exception/ResponseNoBodyException.kt @@ -0,0 +1,10 @@ +package dev.lhalegria.dogga.exception + +import dev.lhalegria.dogga.DoggaApp +import dev.lhalegria.dogga.R + +class ResponseNoBodyException : Exception() { + + override val message: String + get() = DoggaApp.appResources.getString(R.string.no_body_exception) +} diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/model/BreedModel.kt b/app/src/main/kotlin/dev/lhalegria/dogga/model/BreedModel.kt new file mode 100644 index 0000000..8774154 --- /dev/null +++ b/app/src/main/kotlin/dev/lhalegria/dogga/model/BreedModel.kt @@ -0,0 +1,6 @@ +package dev.lhalegria.dogga.model + +data class BreedModel( + val name: String, + val photo: String = "" +) diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/model/mapper/BreedMapper.kt b/app/src/main/kotlin/dev/lhalegria/dogga/model/mapper/BreedMapper.kt new file mode 100644 index 0000000..2e0fe4a --- /dev/null +++ b/app/src/main/kotlin/dev/lhalegria/dogga/model/mapper/BreedMapper.kt @@ -0,0 +1,10 @@ +package dev.lhalegria.dogga.model.mapper + +import dev.lhalegria.dogga.datasource.response.BreedResponse +import dev.lhalegria.dogga.model.BreedModel + +class BreedMapper : IBreedMapper { + + override fun breedsResponseToModel(response: BreedResponse) = + response.breedsList.map { BreedModel(it) } +} diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/model/mapper/IBreedMapper.kt b/app/src/main/kotlin/dev/lhalegria/dogga/model/mapper/IBreedMapper.kt new file mode 100644 index 0000000..9fcaac1 --- /dev/null +++ b/app/src/main/kotlin/dev/lhalegria/dogga/model/mapper/IBreedMapper.kt @@ -0,0 +1,9 @@ +package dev.lhalegria.dogga.model.mapper + +import dev.lhalegria.dogga.datasource.response.BreedResponse +import dev.lhalegria.dogga.model.BreedModel + +interface IBreedMapper { + + fun breedsResponseToModel(response: BreedResponse): List +} diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/repository/BreedRepository.kt b/app/src/main/kotlin/dev/lhalegria/dogga/repository/BreedRepository.kt new file mode 100644 index 0000000..35100cf --- /dev/null +++ b/app/src/main/kotlin/dev/lhalegria/dogga/repository/BreedRepository.kt @@ -0,0 +1,39 @@ +package dev.lhalegria.dogga.repository + +import dev.lhalegria.dogga.datasource.service.BreedService +import dev.lhalegria.dogga.exception.ResponseErrorException +import dev.lhalegria.dogga.exception.ResponseNoBodyException +import dev.lhalegria.dogga.model.BreedModel +import dev.lhalegria.dogga.model.mapper.IBreedMapper +import retrofit2.Response + +class BreedRepository( + private val service: BreedService, + private val mapper: IBreedMapper +) : IBreedRepository { + + override suspend fun getBreeds(): Result> = + getResultFromResponse(service.getBreeds()) + .map { mapper.breedsResponseToModel(it) } + + override suspend fun getSubBreed(masterBreed: String): Result> = + getResultFromResponse(service.getSubBreedsFromBreed(masterBreed)) + .map { mapper.breedsResponseToModel(it) } + + override suspend fun getBreedImage(breed: String): Result = + getResultFromResponse(service.getBreedImage(breed)) + .map { it.imageUrl } + + private fun getResultFromResponse(response: Response): Result = + try { + if (response.isSuccessful) { + response.body()?.let { + Result.success(it) + } ?: throw ResponseNoBodyException() + } else { + Result.failure(ResponseErrorException(response.code())) + } + } catch (ex: Exception) { + Result.failure(ex) + } +} diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/repository/IBreedRepository.kt b/app/src/main/kotlin/dev/lhalegria/dogga/repository/IBreedRepository.kt new file mode 100644 index 0000000..95e3990 --- /dev/null +++ b/app/src/main/kotlin/dev/lhalegria/dogga/repository/IBreedRepository.kt @@ -0,0 +1,12 @@ +package dev.lhalegria.dogga.repository + +import dev.lhalegria.dogga.model.BreedModel + +interface IBreedRepository { + + suspend fun getBreeds(): Result> + + suspend fun getSubBreed(masterBreed: String): Result> + + suspend fun getBreedImage(breed: String): Result +} diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/view/MainActivity.kt b/app/src/main/kotlin/dev/lhalegria/dogga/view/MainActivity.kt new file mode 100644 index 0000000..b10d6f8 --- /dev/null +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/MainActivity.kt @@ -0,0 +1,64 @@ +package dev.lhalegria.dogga.view + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import dev.lhalegria.dogga.model.BreedModel +import dev.lhalegria.dogga.view.composable.AppNavigation +import dev.lhalegria.dogga.view.composable.EmptyDataBox +import dev.lhalegria.dogga.view.composable.ErrorBox +import dev.lhalegria.dogga.view.composable.LoadingBox +import dev.lhalegria.dogga.view.ui.theme.DoggaTheme +import dev.lhalegria.dogga.viewmodel.BreedViewModel +import dev.lhalegria.dogga.viewmodel.RequestState +import org.koin.androidx.viewmodel.ext.android.viewModel + +class MainActivity : ComponentActivity() { + + private val viewModel: BreedViewModel by viewModel() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + viewModel.getBreeds() + + setContent { + DoggaTheme { + var retry by remember { mutableStateOf(false) } + val retryAction = { retry = true } + + if (retry) { + viewModel.getBreeds() + } + + val breedsState by viewModel.breedStateFlow.collectAsStateWithLifecycle() + when (breedsState) { + RequestState.Loading -> LoadingBox() + is RequestState.Error -> ErrorContainer(state = breedsState) { retry = true } + is RequestState.Success -> SuccessContainer(breedsState, retryAction) + } + } + } + } + + @Composable + fun ErrorContainer(state: RequestState>?, actionOnError: () -> Unit) { + val errorMessage = (state as? RequestState.Error)?.t?.message.orEmpty() + ErrorBox(errorCause = errorMessage, action = actionOnError) + } + + @Composable + fun SuccessContainer(state: RequestState>?, actionOnEmpty: () -> Unit) { + val breeds = (state as? RequestState.Success>)?.data + if (breeds?.isNotEmpty() == false) { + AppNavigation(breeds = breeds) + } else { + EmptyDataBox(actionOnEmpty) + } + } +} diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedDetail.kt b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedDetail.kt new file mode 100644 index 0000000..0ca031a --- /dev/null +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedDetail.kt @@ -0,0 +1,104 @@ +package dev.lhalegria.dogga.view.composable + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.navigation.NavHostController +import coil.compose.AsyncImage +import coil.request.ImageRequest +import dev.lhalegria.dogga.R +import dev.lhalegria.dogga.viewmodel.BreedViewModel +import dev.lhalegria.dogga.viewmodel.RequestState +import org.koin.androidx.compose.koinViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BreedDetail( + breed: String, + navController: NavHostController?, + viewModel: BreedViewModel = koinViewModel() +) { + viewModel.getBreedImage(breed) + + val imageState by viewModel.breedImageStateFlow.collectAsStateWithLifecycle() + + Scaffold(topBar = { + Toolbar( + title = stringResource(id = R.string.breed_detail), + icon = Icons.Default.ArrowBack + ) { + navController?.navigateUp() + } + }) { padding -> + Surface( + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top + ) { + when (imageState) { + RequestState.Loading -> BreedImage(name = breed) + is RequestState.Error -> BreedImage(name = breed) + is RequestState.Success -> + BreedImage(name = breed, url = (imageState as? RequestState.Success)?.data.orEmpty()) + } + + Text( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(top = 10.dp), + text = breed.replaceFirstChar { it.uppercaseChar() }, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.headlineMedium + ) + } + } + } +} + +@Composable +fun BreedImage(name: String, url: String = "") { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(url) + .crossfade(true) + .build(), + placeholder = painterResource(id = R.drawable.ic_placeholder), + error = painterResource(id = R.drawable.ic_placeholder), + contentDescription = String.format(stringResource(id = R.string.breed_image_content_description), name), + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .size(300.dp) + .padding(0.dp) + .background(color = MaterialTheme.colorScheme.onPrimary) + ) +} diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedList.kt b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedList.kt new file mode 100644 index 0000000..c9ae246 --- /dev/null +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedList.kt @@ -0,0 +1,49 @@ +package dev.lhalegria.dogga.view.composable + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Home +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.navigation.NavHostController +import dev.lhalegria.dogga.R +import dev.lhalegria.dogga.model.BreedModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BreedList( + dogs: List, + navController: NavHostController? +) { + Scaffold(topBar = { + Toolbar( + title = stringResource(id = R.string.breed_list), + icon = ImageVector.vectorResource(id = R.drawable.ic_toolbar_icon) + ) {} + }) { padding -> + Surface( + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + LazyColumn { + items(dogs) { + BreedListItem(dog = it) { + navController?.navigate("$BREED_DETAIL_ROUTE/${it.name}") + } + } + } + } + } +} diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedListItem.kt b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedListItem.kt new file mode 100644 index 0000000..5d051a9 --- /dev/null +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedListItem.kt @@ -0,0 +1,36 @@ +package dev.lhalegria.dogga.view.composable + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import dev.lhalegria.dogga.model.BreedModel + +@Composable +fun BreedListItem(dog: BreedModel, clickAction: () -> Unit) { + Card( + modifier = Modifier + .padding(top = 8.dp, bottom = 4.dp, start = 16.dp, end = 16.dp) + .fillMaxWidth() + .wrapContentHeight(align = Alignment.Top) + .clickable { clickAction.invoke() }, + elevation = CardDefaults.cardElevation(defaultElevation = 5.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start + ) { + BreedListItemIcon() + BreedListItemText(dog.name, Alignment.Start) + } + } +} diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedListItemContent.kt b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedListItemContent.kt new file mode 100644 index 0000000..907f193 --- /dev/null +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedListItemContent.kt @@ -0,0 +1,42 @@ +package dev.lhalegria.dogga.view.composable + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import dev.lhalegria.dogga.R + +@Composable +fun BreedListItemText(breedName: String, alignment: Alignment.Horizontal) { + Column( + modifier = Modifier.padding(8.dp), + horizontalAlignment = alignment + ) { + Text( + text = breedName.replaceFirstChar { it.uppercaseChar() }, + color = MaterialTheme.colorScheme.onPrimary, + style = MaterialTheme.typography.bodyLarge + ) + } +} + +@Composable +fun BreedListItemIcon() { + Image( + painterResource(R.drawable.ic_dog_face), + contentDescription = stringResource(id = R.string.dog_icon_content_description), + contentScale = ContentScale.Crop, + modifier = Modifier + .size(50.dp) + .padding(2.dp) + ) +} diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/EmptyDataBox.kt b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/EmptyDataBox.kt new file mode 100644 index 0000000..1beb1d4 --- /dev/null +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/EmptyDataBox.kt @@ -0,0 +1,38 @@ +package dev.lhalegria.dogga.view.composable + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.TextUnitType +import androidx.compose.ui.unit.dp +import dev.lhalegria.dogga.R +import dev.lhalegria.dogga.view.ui.theme.CaramelStrong + +@Composable +fun EmptyDataBox(action: () -> Unit) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + modifier = Modifier.padding(32.dp), + fontSize = TextUnit(15f, TextUnitType.Sp), + textAlign = TextAlign.Center, + color = CaramelStrong, + text = stringResource(id = R.string.empty_data_message) + ) + + TryAgainButton(modifier = Modifier) { + action.invoke() + } + } +} diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/ErrorBox.kt b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/ErrorBox.kt new file mode 100644 index 0000000..ae3c2cf --- /dev/null +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/ErrorBox.kt @@ -0,0 +1,48 @@ +package dev.lhalegria.dogga.view.composable + +import android.widget.Toast +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.TextUnitType +import androidx.compose.ui.unit.dp +import dev.lhalegria.dogga.R +import dev.lhalegria.dogga.view.ui.theme.CaramelStrong + +@Composable +fun ErrorBox(errorCause: String, action: () -> Unit) { + if (errorCause.isNotEmpty()) { + Toast.makeText( + LocalContext.current, + String.format(stringResource(R.string.error_cause), errorCause), + Toast.LENGTH_SHORT + ).show() + } + + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + modifier = Modifier.padding(32.dp), + fontSize = TextUnit(15f, TextUnitType.Sp), + textAlign = TextAlign.Center, + color = CaramelStrong, + text = stringResource(id = R.string.error_message) + ) + + TryAgainButton(modifier = Modifier) { + action.invoke() + } + } +} diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/Loading.kt b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/Loading.kt new file mode 100644 index 0000000..ed1da4c --- /dev/null +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/Loading.kt @@ -0,0 +1,27 @@ +package dev.lhalegria.dogga.view.composable + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun LoadingBox() { + Box( + Modifier.fillMaxSize() + .background(color = MaterialTheme.colorScheme.surface) + ) { + CircularProgressIndicator( + modifier = Modifier + .align(Alignment.Center) + .padding(50.dp), + progress = 1f + ) + } +} diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/Navigation.kt b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/Navigation.kt new file mode 100644 index 0000000..99e2b29 --- /dev/null +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/Navigation.kt @@ -0,0 +1,31 @@ +package dev.lhalegria.dogga.view.composable + +import androidx.compose.runtime.Composable +import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument +import dev.lhalegria.dogga.model.BreedModel + +private const val BREED_LIST_ROUTE = "breeds_list" +private const val BREED_NAME_ARG = "_arg_breed_name" +const val BREED_DETAIL_ROUTE = "breed_details" + +@Composable +fun AppNavigation(breeds: List = listOf()) { + val navController = rememberNavController() + NavHost(navController = navController, startDestination = BREED_LIST_ROUTE) { + composable(BREED_LIST_ROUTE) { + BreedList(breeds, navController) + } + composable( + route = "$BREED_DETAIL_ROUTE/{$BREED_NAME_ARG}", + arguments = listOf(navArgument(BREED_NAME_ARG) { + type = NavType.StringType + }) + ) { + BreedDetail(it.arguments?.getString(BREED_NAME_ARG).orEmpty(), navController) + } + } +} diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/Toolbar.kt b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/Toolbar.kt new file mode 100644 index 0000000..52f1245 --- /dev/null +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/Toolbar.kt @@ -0,0 +1,39 @@ +package dev.lhalegria.dogga.view.composable + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import dev.lhalegria.dogga.R +import dev.lhalegria.dogga.view.ui.theme.Caramel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun Toolbar(title: String, icon: ImageVector, iconClickAction: () -> Unit) { + TopAppBar( + navigationIcon = { + Icon( + icon, + String.format(stringResource(id = R.string.app_toolbar_content_description), title), + Modifier + .padding(12.dp) + .clickable { iconClickAction.invoke() } + ) + }, + title = { Text(title) }, + colors = TopAppBarDefaults.smallTopAppBarColors( + containerColor = Caramel, + titleContentColor = Color.White, + navigationIconContentColor = Color.White + ) + ) +} diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/TryAgainButton.kt b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/TryAgainButton.kt new file mode 100644 index 0000000..fc21b77 --- /dev/null +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/TryAgainButton.kt @@ -0,0 +1,30 @@ +package dev.lhalegria.dogga.view.composable + +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import dev.lhalegria.dogga.R +import dev.lhalegria.dogga.view.ui.theme.Caramel + +@Composable +fun TryAgainButton( + modifier: Modifier, + onClick: () -> Unit +) { + Button( + onClick = { onClick() }, + modifier = modifier, + colors = ButtonDefaults.buttonColors( + contentColor = Color.White, + containerColor = Caramel, + disabledContentColor = Color.White, + disabledContainerColor = Color.Gray + ) + ) { + Text(text = stringResource(id = R.string.try_again)) + } +} diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/view/preview/Previews.kt b/app/src/main/kotlin/dev/lhalegria/dogga/view/preview/Previews.kt new file mode 100644 index 0000000..97a6627 --- /dev/null +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/preview/Previews.kt @@ -0,0 +1,60 @@ +package dev.lhalegria.dogga.view.preview + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import dev.lhalegria.dogga.model.BreedModel +import dev.lhalegria.dogga.view.composable.BreedDetail +import dev.lhalegria.dogga.view.composable.BreedList +import dev.lhalegria.dogga.view.composable.EmptyDataBox +import dev.lhalegria.dogga.view.composable.ErrorBox +import dev.lhalegria.dogga.view.composable.LoadingBox +import dev.lhalegria.dogga.view.ui.theme.DoggaTheme + +val dogs = listOf( + BreedModel("vira-lata", ""), + BreedModel("salsicha", ""), + BreedModel("pit-bull", ""), + BreedModel("pastor alemão", ""), + BreedModel("lulu da pomerania", ""), + BreedModel("pintcher", ""), +) + +@Preview(showBackground = true) +@Composable +fun BreedLoadingPreview() { + DoggaTheme { + LoadingBox() + } +} + +@Preview(showBackground = true) +@Composable +fun BreedEmptyDataPreview() { + DoggaTheme { + EmptyDataBox {} + } +} + +@Preview(showBackground = true) +@Composable +fun BreedErrorDataPreview() { + DoggaTheme { + ErrorBox {} + } +} + +@Preview(showBackground = true) +@Composable +fun BreedListPreview() { + DoggaTheme { + BreedList(dogs = dogs, null) + } +} + +@Preview(showBackground = true) +@Composable +fun BreedDetailsPreview() { + DoggaTheme { + BreedDetail("", null) + } +} diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/view/ui/theme/Color.kt b/app/src/main/kotlin/dev/lhalegria/dogga/view/ui/theme/Color.kt new file mode 100644 index 0000000..46c1cfd --- /dev/null +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/ui/theme/Color.kt @@ -0,0 +1,10 @@ +package dev.lhalegria.dogga.view.ui.theme + +import androidx.compose.ui.graphics.Color + +val Caramel = Color(0xFFA58D11) +val CaramelStrong = Color(0xFF756304) +val CaramelSuperStrong = Color(0xFF3C3200) + +val SurfaceVariantLight = Color(0xFFD1CAA4) +val SurfaceVariantDark = Color(0xFF332B00) diff --git a/app/src/main/java/dev/lhalegria/dogga/ui/theme/Theme.kt b/app/src/main/kotlin/dev/lhalegria/dogga/view/ui/theme/Theme.kt similarity index 56% rename from app/src/main/java/dev/lhalegria/dogga/ui/theme/Theme.kt rename to app/src/main/kotlin/dev/lhalegria/dogga/view/ui/theme/Theme.kt index 5351f76..49a185a 100644 --- a/app/src/main/java/dev/lhalegria/dogga/ui/theme/Theme.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/ui/theme/Theme.kt @@ -1,58 +1,45 @@ -package dev.lhalegria.dogga.ui.theme +package dev.lhalegria.dogga.view.ui.theme import android.app.Activity -import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.core.view.WindowCompat private val DarkColorScheme = darkColorScheme( - primary = Purple80, - secondary = PurpleGrey80, - tertiary = Pink80 + primary = Caramel, + onPrimary = Color.White, + secondary = CaramelStrong, + tertiary = CaramelSuperStrong, + surface = Color.Black, + surfaceVariant = SurfaceVariantDark ) private val LightColorScheme = lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40 - - /* Other default colors to override - background = Color(0xFFFFFBFE), - surface = Color(0xFFFFFBFE), - onPrimary = Color.White, - onSecondary = Color.White, - onTertiary = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F), - */ + primary = Caramel, + onPrimary = Color.DarkGray, + secondary = CaramelStrong, + tertiary = CaramelSuperStrong, + surface = Color.White, + surfaceVariant = SurfaceVariantLight ) @Composable fun DoggaTheme( darkTheme: Boolean = isSystemInDarkTheme(), - // Dynamic color is available on Android 12+ - dynamicColor: Boolean = true, content: @Composable () -> Unit ) { val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } - darkTheme -> DarkColorScheme else -> LightColorScheme } + val view = LocalView.current if (!view.isInEditMode) { SideEffect { diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/view/ui/theme/Type.kt b/app/src/main/kotlin/dev/lhalegria/dogga/view/ui/theme/Type.kt new file mode 100644 index 0000000..ed8c83a --- /dev/null +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/ui/theme/Type.kt @@ -0,0 +1,17 @@ +package dev.lhalegria.dogga.view.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) +) diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/viewmodel/BreedViewModel.kt b/app/src/main/kotlin/dev/lhalegria/dogga/viewmodel/BreedViewModel.kt new file mode 100644 index 0000000..100dd55 --- /dev/null +++ b/app/src/main/kotlin/dev/lhalegria/dogga/viewmodel/BreedViewModel.kt @@ -0,0 +1,57 @@ +package dev.lhalegria.dogga.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dev.lhalegria.dogga.model.BreedModel +import dev.lhalegria.dogga.repository.IBreedRepository +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + +class BreedViewModel( + private val repository: IBreedRepository +): ViewModel() { + + private val _breedStateFlow = MutableStateFlow>>(RequestState.Loading) + val breedStateFlow: StateFlow>> + get() = _breedStateFlow + + private val _subBreedStateFlow = MutableStateFlow>>(RequestState.Loading) + val subBreedStateFlow: StateFlow>> + get() = _subBreedStateFlow + + private val _breedImageStateFlow = MutableStateFlow>(RequestState.Loading) + val breedImageStateFlow: StateFlow> + get() = _breedImageStateFlow + + fun getBreeds() = viewModelScope.launch(IO) { + repository.getBreeds() + .onSuccess { + _breedStateFlow.value = RequestState.Success(it) + } + .onFailure { + _breedStateFlow.value = RequestState.Error(it) + } + } + + fun getSubBreed(breed: String) = viewModelScope.launch(IO) { + repository.getSubBreed(breed) + .onSuccess { + _subBreedStateFlow.value = RequestState.Success(it) + } + .onFailure { + _subBreedStateFlow.value = RequestState.Error(it) + } + } + + fun getBreedImage(breed: String) = viewModelScope.launch(IO) { + repository.getBreedImage(breed) + .onSuccess { + _breedImageStateFlow.emit(RequestState.Success(it)) + } + .onFailure { + _breedImageStateFlow.emit(RequestState.Error(it)) + } + } +} diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/viewmodel/RequestState.kt b/app/src/main/kotlin/dev/lhalegria/dogga/viewmodel/RequestState.kt new file mode 100644 index 0000000..766c56e --- /dev/null +++ b/app/src/main/kotlin/dev/lhalegria/dogga/viewmodel/RequestState.kt @@ -0,0 +1,13 @@ +package dev.lhalegria.dogga.viewmodel + +sealed class RequestState { + + object Loading : RequestState() + + data class Success(val data: T) : RequestState() + + data class Error( + val t: Throwable, + var consumed: Boolean = false + ) : RequestState() +} diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml index 7706ab9..61bb4c2 100644 --- a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -1,30 +1,11 @@ - - - - - - - - + android:width="100dp" + android:height="87dp" + android:viewportWidth="512" + android:viewportHeight="449.32"> + + android:fillType="evenOdd" /> diff --git a/app/src/main/res/drawable/ic_dog_face.xml b/app/src/main/res/drawable/ic_dog_face.xml new file mode 100644 index 0000000..80f9249 --- /dev/null +++ b/app/src/main/res/drawable/ic_dog_face.xml @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml index 07d5da9..d1163de 100644 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -5,166 +5,6 @@ android:viewportWidth="108" android:viewportHeight="108"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_placeholder.xml b/app/src/main/res/drawable/ic_placeholder.xml new file mode 100644 index 0000000..c6f398d --- /dev/null +++ b/app/src/main/res/drawable/ic_placeholder.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_toolbar_icon.xml b/app/src/main/res/drawable/ic_toolbar_icon.xml new file mode 100644 index 0000000..cc7bdb2 --- /dev/null +++ b/app/src/main/res/drawable/ic_toolbar_icon.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index b3e26b4..036d09b 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,6 +1,5 @@ - - - - + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index b3e26b4..036d09b 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,6 +1,5 @@ - - - - + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp index c209e78..46ab92c 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..d9490e8 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp index b2dfe3d..200cb85 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp index 4f0f1d6..8e7bad0 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..7a7dfab Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp index 62b611d..cd3c939 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp index 948a307..cc835aa 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..31155f1 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp index 1b9a695..3cc4073 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp index 28d4b77..8daf1f7 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..fc1023e Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp index 9287f50..e41f0ae 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp index aa7d642..c95828b 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000..4f24503 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp index 9126ae3..58f1ecb 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index ca1931b..2224d23 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -7,4 +7,7 @@ #FF018786 #FF000000 #FFFFFFFF + + #FFA58D11 + #FF756304 diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..26ef28d --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #A58D11 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b2c6085..eab49c8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,14 @@ Dogga - \ No newline at end of file + There are no data to show + There was an error while trying to get the data + Try again + The Response has no body + The response has failure with code: %d + Dog breeds list + Dog breed details + Picture of a dog from the %s breed. + Dogga Application Toolbar with title %s + An icon representing a dog + The error cause was: %s + diff --git a/app/src/test/java/dev/lhalegria/dogga/ExampleUnitTest.kt b/app/src/test/java/dev/lhalegria/dogga/ExampleUnitTest.kt deleted file mode 100644 index f6db59b..0000000 --- a/app/src/test/java/dev/lhalegria/dogga/ExampleUnitTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package dev.lhalegria.dogga - -import org.junit.Test - -import org.junit.Assert.* - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} diff --git a/app/src/test/kotlin/dev/lhalegria/dogga/repository/BreedRepositoryTest.kt b/app/src/test/kotlin/dev/lhalegria/dogga/repository/BreedRepositoryTest.kt new file mode 100644 index 0000000..3aed0a9 --- /dev/null +++ b/app/src/test/kotlin/dev/lhalegria/dogga/repository/BreedRepositoryTest.kt @@ -0,0 +1,192 @@ +package dev.lhalegria.dogga.repository + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import dev.lhalegria.dogga.DoggaApp +import dev.lhalegria.dogga.datasource.response.BreedImageResponse +import dev.lhalegria.dogga.datasource.response.BreedResponse +import dev.lhalegria.dogga.datasource.service.BreedService +import dev.lhalegria.dogga.exception.ResponseErrorException +import dev.lhalegria.dogga.exception.ResponseNoBodyException +import dev.lhalegria.dogga.model.BreedModel +import dev.lhalegria.dogga.model.mapper.BreedMapper +import dev.lhalegria.dogga.model.mapper.IBreedMapper +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import okhttp3.ResponseBody.Companion.toResponseBody +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import retrofit2.Response + +@ExperimentalCoroutinesApi +class BreedRepositoryTest { + + @get:Rule + val rule = InstantTaskExecutorRule() + + private val service = mockk() + + private val mapper: IBreedMapper by lazy { + BreedMapper() + } + + private val repository: IBreedRepository by lazy { + BreedRepository(service, mapper) + } + + @Before + fun setup() { + mockkObject(DoggaApp) + every { DoggaApp.appResources.getString(any()) } returns MOCK_STRING_RES + } + + @Test + fun `GIVEN service getBreeds returns success WHEN calls getBreeds THEN returns success`() = runTest { + // GIVEN - Configuration + val mockResponse = BreedResponse( + breedsList = listOf("Spitz", "Pit-Bull"), + status = "success" + ) + + val expectedResponse = Result.success(mapper.breedsResponseToModel(mockResponse)) + coEvery { service.getBreeds() } returns Response.success(mockResponse) + + // WHEN - Action/Execution + val response = repository.getBreeds() + + // THEN - Assertion + coVerify { service.getBreeds() } + assertEquals(expectedResponse, response) + } + + @Test + fun `GIVEN service getBreeds returns success AND body is null WHEN calls getBreeds THEN returns no body exception`() = runTest { + // GIVEN - Configuration + val expectedResponse = Result.failure>(ResponseNoBodyException()) + coEvery { service.getBreeds() } returns Response.success(null) + + // WHEN - Action/Execution + val response = repository.getBreeds() + + // THEN - Assertion + coVerify { service.getBreeds() } + assertEquals(expectedResponse.toString(), response.toString()) + } + + @Test + fun `GIVEN service getBreeds returns failure WHEN calls getBreeds THEN returns failure`() = runTest { + // GIVEN - Configuration + val expectedResponse = Result.failure>(ResponseErrorException(MOCK_ERROR_CODE)) + coEvery { service.getBreeds() } returns Response.error(MOCK_ERROR_CODE, "".toResponseBody()) + + // WHEN - Action/Execution + val response = repository.getBreeds() + + // THEN - Assertion + coVerify { service.getBreeds() } + assertEquals(expectedResponse.toString(), response.toString()) + } + + @Test + fun `GIVEN service getSubBreedsFromBreed returns success WHEN calls getSubBreed THEN returns success`() = runTest { + // GIVEN - Configuration + val mockResponse = BreedResponse( + breedsList = listOf("Spitz", "Pit-Bull"), + status = "success" + ) + + val expectedResponse = Result.success(mapper.breedsResponseToModel(mockResponse)) + coEvery { service.getSubBreedsFromBreed(any()) } returns Response.success(mockResponse) + + // WHEN - Action/Execution + val response = repository.getSubBreed(MOCK_BREED) + + // THEN - Assertion + coVerify { service.getSubBreedsFromBreed(any()) } + assertEquals(expectedResponse, response) + } + + @Test + fun `GIVEN service getSubBreedsFromBreed returns success AND body is null WHEN calls getSubBreed THEN returns no body exception`() = runTest { + // GIVEN - Configuration + val expectedResponse = Result.failure>(ResponseNoBodyException()) + coEvery { service.getSubBreedsFromBreed(any()) } returns Response.success(null) + + // WHEN - Action/Execution + val response = repository.getSubBreed(MOCK_BREED) + + // THEN - Assertion + coVerify { service.getSubBreedsFromBreed(any()) } + assertEquals(expectedResponse.toString(), response.toString()) + } + + @Test + fun `GIVEN service getSubBreedsFromBreed returns failure WHEN calls getSubBreed THEN returns failure`() = runTest { + // GIVEN - Configuration + val expectedResponse = Result.failure>(ResponseErrorException(MOCK_ERROR_CODE)) + coEvery { service.getSubBreedsFromBreed(any()) } returns Response.error(MOCK_ERROR_CODE, "".toResponseBody()) + + // WHEN - Action/Execution + val response = repository.getSubBreed(MOCK_BREED) + + // THEN - Assertion + coVerify { service.getSubBreedsFromBreed(any()) } + assertEquals(expectedResponse.toString(), response.toString()) + } + + @Test + fun `GIVEN service getBreedImage returns success WHEN calls getBreedImage THEN returns success`() = runTest { + // GIVEN - Configuration + val mockResponse = BreedImageResponse("http://anyUrl", "success") + + val expectedResponse = Result.success(mockResponse.imageUrl) + coEvery { service.getBreedImage(any()) } returns Response.success(mockResponse) + + // WHEN - Action/Execution + val response = repository.getBreedImage(MOCK_BREED) + + // THEN - Assertion + coVerify { service.getBreedImage(any()) } + assertEquals(expectedResponse, response) + } + + @Test + fun `GIVEN service getBreedImage returns success AND body is null WHEN calls getBreedImage THEN returns no body exception`() = runTest { + // GIVEN - Configuration + val expectedResponse = Result.failure>(ResponseNoBodyException()) + coEvery { service.getBreedImage(any()) } returns Response.success(null) + + // WHEN - Action/Execution + val response = repository.getBreedImage(MOCK_BREED) + + // THEN - Assertion + coVerify { service.getBreedImage(any()) } + assertEquals(expectedResponse.toString(), response.toString()) + } + + @Test + fun `GIVEN service getBreedImage returns failure WHEN calls getBreedImage THEN returns failure`() = runTest { + // GIVEN - Configuration + val expectedResponse = Result.failure>(ResponseErrorException(MOCK_ERROR_CODE)) + coEvery { service.getBreedImage(any()) } returns Response.error(MOCK_ERROR_CODE, "".toResponseBody()) + + // WHEN - Action/Execution + val response = repository.getBreedImage(MOCK_BREED) + + // THEN - Assertion + coVerify { service.getBreedImage(any()) } + assertEquals(expectedResponse.toString(), response.toString()) + } + + private companion object { + const val MOCK_ERROR_CODE = 400 + const val MOCK_BREED = "cattledog" + const val MOCK_STRING_RES = "any string" + } +} diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 0afbd4d..0000000 --- a/build.gradle +++ /dev/null @@ -1,6 +0,0 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. -plugins { - id 'com.android.application' version '8.0.2' apply false - id 'com.android.library' version '8.0.2' apply false - id 'org.jetbrains.kotlin.android' version '1.7.20' apply false -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..f1273bf --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,21 @@ +import extensions.applyDefault + +allprojects { + repositories.applyDefault() +} + +tasks.register("clean", Delete::class) { + delete(rootProject.buildDir) +} + +buildscript { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } + + dependencies { + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.KOTLIN}") + } +} diff --git a/buildSrc/.gitignore b/buildSrc/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/buildSrc/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 0000000..0038480 --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,22 @@ +import Build_gradle.PluginsVersions.GRADLE_ANDROID +import Build_gradle.PluginsVersions.KOTLIN + +repositories { + google() + mavenCentral() + gradlePluginPortal() +} + +plugins { + `kotlin-dsl` +} + +object PluginsVersions { + const val GRADLE_ANDROID = "8.0.2" + const val KOTLIN = "1.8.21" +} + +dependencies { + implementation("com.android.tools.build:gradle:$GRADLE_ANDROID") + implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:$KOTLIN") +} diff --git a/buildSrc/src/main/kotlin/BuildSetup.kt b/buildSrc/src/main/kotlin/BuildSetup.kt new file mode 100644 index 0000000..3541f4f --- /dev/null +++ b/buildSrc/src/main/kotlin/BuildSetup.kt @@ -0,0 +1,16 @@ +object BuildSetup { + const val APPLICATION_ID = "dev.lhalegria.dogga" + const val NAMESPACE = "dev.lhalegria.dogga" + + const val BUILD_TOOLS_VERSION = "34.0.0" + const val COMPILE_SDK_VERSION = 33 + const val MIN_SDK_VERSION = 30 + const val TARGET_SDK_VERSION = 33 + + const val VERSION_CODE = 1 + const val VERSION_NAME = "1.0.0" + + const val COMPOSE_KOTLIN_COMPILER = "1.4.7" + + const val TEST_INSTRUMENTATION_RUNNER = "androidx.test.runner.AndroidJUnitRunner" +} diff --git a/buildSrc/src/main/kotlin/BuildType.kt b/buildSrc/src/main/kotlin/BuildType.kt new file mode 100644 index 0000000..7d5f024 --- /dev/null +++ b/buildSrc/src/main/kotlin/BuildType.kt @@ -0,0 +1,21 @@ + +interface BuildType { + + val isMinifyEnabled: Boolean + + companion object { + const val DEBUG = "debug" + const val RELEASE = "release" + } +} + +object BuildTypeDebug : BuildType { + override val isMinifyEnabled = false + + const val applicationIdSuffix = ".debug" + const val versionNameSuffix = "-DEV" +} + +object BuildTypeRelease : BuildType { + override val isMinifyEnabled = true +} diff --git a/buildSrc/src/main/kotlin/Modules.kt b/buildSrc/src/main/kotlin/Modules.kt new file mode 100644 index 0000000..30494db --- /dev/null +++ b/buildSrc/src/main/kotlin/Modules.kt @@ -0,0 +1,3 @@ +object Modules { + +} diff --git a/buildSrc/src/main/kotlin/Plugins.kt b/buildSrc/src/main/kotlin/Plugins.kt new file mode 100644 index 0000000..75a951e --- /dev/null +++ b/buildSrc/src/main/kotlin/Plugins.kt @@ -0,0 +1,4 @@ +object Plugins { + const val ANDROID_APPLICATION = "com.android.application" + const val KOTLIN_ANDROID = "android" +} diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt new file mode 100644 index 0000000..bc7f48e --- /dev/null +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -0,0 +1,20 @@ +object Versions { + const val ANDROIDX_CORE = "1.10.1" + const val ANDROIDX_LIFECYCLE = "2.6.1" + const val ANDROIDX_TEST = "1.1.5" + const val COIL = "2.4.0" + const val COMPOSE_ACTIVITY = "1.7.2" + const val COMPOSE_BOM = "2022.10.00" + const val COMPOSE_LIFECYCLE = "2.6.1" + const val COMPOSE_NAVIGATION = "2.6.0" + const val CORE_TESTING = "2.2.0" + const val COROUTINES_TEST = "1.7.0" + const val ESPRESSO = "3.5.1" + const val JUNIT = "4.13.2" + const val KOIN = "3.4.2" + const val KOIN_COMPOSE = "3.4.5" + const val KOTLIN = "1.8.21" + const val MOCKK = "1.13.5" + const val OKHTTP = "4.11.0" + const val RETROFIT = "2.9.0" +} diff --git a/buildSrc/src/main/kotlin/dependencies/AndroidX.kt b/buildSrc/src/main/kotlin/dependencies/AndroidX.kt new file mode 100644 index 0000000..a248b34 --- /dev/null +++ b/buildSrc/src/main/kotlin/dependencies/AndroidX.kt @@ -0,0 +1,16 @@ +object AndroidX { + const val CORE = "androidx.core:core-ktx:${Versions.ANDROIDX_CORE}" + const val LIFECYCLE = "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.ANDROIDX_LIFECYCLE}" + + object Compose { + const val BOM = "androidx.compose:compose-bom:${Versions.COMPOSE_BOM}" + const val ACTIVITY = "androidx.activity:activity-compose:${Versions.COMPOSE_ACTIVITY}" + const val LIFECYCLE = "androidx.lifecycle:lifecycle-runtime-compose:${Versions.COMPOSE_LIFECYCLE}" + const val UI = "androidx.compose.ui:ui" + const val UI_GRAPHICS = "androidx.compose.ui:ui-graphics" + const val UI_TOOLING = "androidx.compose.ui:ui-tooling" + const val PREVIEW = "androidx.compose.ui:ui-tooling-preview" + const val MATERIAL_3 = "androidx.compose.material3:material3" + const val NAVIGATION = "androidx.navigation:navigation-compose:${Versions.COMPOSE_NAVIGATION}" + } +} diff --git a/buildSrc/src/main/kotlin/dependencies/Koin.kt b/buildSrc/src/main/kotlin/dependencies/Koin.kt new file mode 100644 index 0000000..d0e2a49 --- /dev/null +++ b/buildSrc/src/main/kotlin/dependencies/Koin.kt @@ -0,0 +1,4 @@ +object Koin { + const val KOIN = "io.insert-koin:koin-android:${Versions.KOIN}" + const val KOIN_COMPOSE = "io.insert-koin:koin-androidx-compose:${Versions.KOIN_COMPOSE}" +} diff --git a/buildSrc/src/main/kotlin/dependencies/Other.kt b/buildSrc/src/main/kotlin/dependencies/Other.kt new file mode 100644 index 0000000..296086e --- /dev/null +++ b/buildSrc/src/main/kotlin/dependencies/Other.kt @@ -0,0 +1,4 @@ +object Other { + const val COIL = "io.coil-kt:coil-compose:${Versions.COIL}" + const val KOTLIN_BOM = "org.jetbrains.kotlin:kotlin-bom:${Versions.KOTLIN}" +} diff --git a/buildSrc/src/main/kotlin/dependencies/Square.kt b/buildSrc/src/main/kotlin/dependencies/Square.kt new file mode 100644 index 0000000..9df5c89 --- /dev/null +++ b/buildSrc/src/main/kotlin/dependencies/Square.kt @@ -0,0 +1,6 @@ +object Square { + const val RETROFIT = "com.squareup.retrofit2:retrofit:${Versions.RETROFIT}" + const val OKHTTP = "com.squareup.okhttp3:okhttp:${Versions.OKHTTP}" + const val OKHTTP_LOG_INTERCEPTOR = "com.squareup.okhttp3:logging-interceptor:${Versions.OKHTTP}" + const val GSON_CONVERTER = "com.squareup.retrofit2:converter-gson:${Versions.RETROFIT}" +} diff --git a/buildSrc/src/main/kotlin/dependencies/Test.kt b/buildSrc/src/main/kotlin/dependencies/Test.kt new file mode 100644 index 0000000..96fa506 --- /dev/null +++ b/buildSrc/src/main/kotlin/dependencies/Test.kt @@ -0,0 +1,10 @@ +object Test { + const val JUNIT = "junit:junit:${Versions.JUNIT}" + const val ANDROIDX_JUNIT = "androidx.test.ext:junit:${Versions.ANDROIDX_TEST}" + const val ESPRESSO = "androidx.test.espresso:espresso-core:${Versions.ESPRESSO}" + const val CORE_TESTING = "androidx.arch.core:core-testing:${Versions.CORE_TESTING}" + const val COROUTINES = "org.jetbrains.kotlinx:kotlinx-coroutines-test:${Versions.COROUTINES_TEST}" + const val COMPOSE_UI_JUNIT4 = "androidx.compose.ui:ui-test-junit4" + const val COMPOSE_TEST_MANIFEST = "androidx.compose.ui:ui-test-manifest" + const val MOCKK = "io.mockk:mockk:${Versions.MOCKK}" +} diff --git a/buildSrc/src/main/kotlin/extensions/RepositoryHandlerExtension.kt b/buildSrc/src/main/kotlin/extensions/RepositoryHandlerExtension.kt new file mode 100644 index 0000000..f43dca1 --- /dev/null +++ b/buildSrc/src/main/kotlin/extensions/RepositoryHandlerExtension.kt @@ -0,0 +1,10 @@ +package extensions + +import org.gradle.api.artifacts.dsl.RepositoryHandler +import org.gradle.kotlin.dsl.maven + +fun RepositoryHandler.applyDefault() { + google() + maven("https://maven.google.com/") + maven("https://plugins.gradle.org/m2/") +} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a9ba4cb..6533669 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Thu Jun 22 17:33:00 BRT 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index effc6b3..0000000 --- a/settings.gradle +++ /dev/null @@ -1,16 +0,0 @@ -pluginManagement { - repositories { - google() - mavenCentral() - gradlePluginPortal() - } -} -dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) - repositories { - google() - mavenCentral() - } -} -rootProject.name = "Dogga" -include ':app' diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..d81fa9a --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,3 @@ +rootProject.name = "Dogga" +rootProject.buildFileName = "build.gradle.kts" +include(":app")