From f4b479c062584e9082acbb1e81a96a2942465d2b Mon Sep 17 00:00:00 2001 From: Luiz Alegria Date: Thu, 22 Jun 2023 19:41:12 -0300 Subject: [PATCH 01/18] Feature: Add datasource layer to the project --- app/build.gradle | 8 ++++++ app/src/main/AndroidManifest.xml | 4 +-- .../dogga/datasource/di/DataSourceModule.kt | 11 ++++++++ .../datasource/network/DoggaNetworkApi.kt | 28 +++++++++++++++++++ .../datasource/response/BreedImageResponse.kt | 8 ++++++ .../datasource/response/BreedsResponse.kt | 8 ++++++ .../dogga/datasource/service/DogsService.kt | 23 +++++++++++++++ 7 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/dev/lhalegria/dogga/datasource/di/DataSourceModule.kt create mode 100644 app/src/main/java/dev/lhalegria/dogga/datasource/network/DoggaNetworkApi.kt create mode 100644 app/src/main/java/dev/lhalegria/dogga/datasource/response/BreedImageResponse.kt create mode 100644 app/src/main/java/dev/lhalegria/dogga/datasource/response/BreedsResponse.kt create mode 100644 app/src/main/java/dev/lhalegria/dogga/datasource/service/DogsService.kt diff --git a/app/build.gradle b/app/build.gradle index 28dfeed..6a1916d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,6 +56,14 @@ dependencies { implementation 'androidx.compose.ui:ui-graphics' implementation 'androidx.compose.ui:ui-tooling-preview' implementation 'androidx.compose.material3:material3' + + implementation "io.insert-koin:koin-android:3.4.2" + + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.okhttp3:okhttp:4.11.0' + implementation 'com.squareup.okhttp3:logging-interceptor:4.11.0' + implementation 'com.squareup.retrofit2:converter-gson:2.9.0' + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 48f8250..16ee2ba 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,17 +12,15 @@ android:supportsRtl="true" android:theme="@style/Theme.Dogga" tools:targetApi="31"> + - - diff --git a/app/src/main/java/dev/lhalegria/dogga/datasource/di/DataSourceModule.kt b/app/src/main/java/dev/lhalegria/dogga/datasource/di/DataSourceModule.kt new file mode 100644 index 0000000..4101471 --- /dev/null +++ b/app/src/main/java/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.DogsService +import org.koin.dsl.module + +val dataSourceModule = module { + + factory { retrofitApi } + factory { retrofitApi.create(DogsService::class.java) } +} diff --git a/app/src/main/java/dev/lhalegria/dogga/datasource/network/DoggaNetworkApi.kt b/app/src/main/java/dev/lhalegria/dogga/datasource/network/DoggaNetworkApi.kt new file mode 100644 index 0000000..9825598 --- /dev/null +++ b/app/src/main/java/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/java/dev/lhalegria/dogga/datasource/response/BreedImageResponse.kt b/app/src/main/java/dev/lhalegria/dogga/datasource/response/BreedImageResponse.kt new file mode 100644 index 0000000..b68e1f0 --- /dev/null +++ b/app/src/main/java/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/java/dev/lhalegria/dogga/datasource/response/BreedsResponse.kt b/app/src/main/java/dev/lhalegria/dogga/datasource/response/BreedsResponse.kt new file mode 100644 index 0000000..e92bb31 --- /dev/null +++ b/app/src/main/java/dev/lhalegria/dogga/datasource/response/BreedsResponse.kt @@ -0,0 +1,8 @@ +package dev.lhalegria.dogga.datasource.response + +import com.google.gson.annotations.SerializedName + +data class BreedsResponse( + @SerializedName("message") val breedsList: List, + @SerializedName("status") val status: String +) diff --git a/app/src/main/java/dev/lhalegria/dogga/datasource/service/DogsService.kt b/app/src/main/java/dev/lhalegria/dogga/datasource/service/DogsService.kt new file mode 100644 index 0000000..6624d02 --- /dev/null +++ b/app/src/main/java/dev/lhalegria/dogga/datasource/service/DogsService.kt @@ -0,0 +1,23 @@ +package dev.lhalegria.dogga.datasource.service + +import dev.lhalegria.dogga.datasource.response.BreedImageResponse +import dev.lhalegria.dogga.datasource.response.BreedsResponse +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.Path + +interface DogsService { + + @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 +} From ea6ddc4d5e54d00b95fae0f6a49e4556cc6910d2 Mon Sep 17 00:00:00 2001 From: Luiz Alegria Date: Thu, 22 Jun 2023 20:31:23 -0300 Subject: [PATCH 02/18] Feature: Add model layer (#2) --- .../main/java/dev/lhalegria/dogga/model/BreedModel.kt | 6 ++++++ .../dev/lhalegria/dogga/model/mapper/BreedsMapper.kt | 10 ++++++++++ .../dev/lhalegria/dogga/model/mapper/IBreedMapper.kt | 9 +++++++++ 3 files changed, 25 insertions(+) create mode 100644 app/src/main/java/dev/lhalegria/dogga/model/BreedModel.kt create mode 100644 app/src/main/java/dev/lhalegria/dogga/model/mapper/BreedsMapper.kt create mode 100644 app/src/main/java/dev/lhalegria/dogga/model/mapper/IBreedMapper.kt diff --git a/app/src/main/java/dev/lhalegria/dogga/model/BreedModel.kt b/app/src/main/java/dev/lhalegria/dogga/model/BreedModel.kt new file mode 100644 index 0000000..8774154 --- /dev/null +++ b/app/src/main/java/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/java/dev/lhalegria/dogga/model/mapper/BreedsMapper.kt b/app/src/main/java/dev/lhalegria/dogga/model/mapper/BreedsMapper.kt new file mode 100644 index 0000000..a0ae3ce --- /dev/null +++ b/app/src/main/java/dev/lhalegria/dogga/model/mapper/BreedsMapper.kt @@ -0,0 +1,10 @@ +package dev.lhalegria.dogga.model.mapper + +import dev.lhalegria.dogga.datasource.response.BreedsResponse +import dev.lhalegria.dogga.model.BreedModel + +class BreedsMapper : IBreedMapper { + + override fun breedsResponseToModel(response: BreedsResponse) = + response.breedsList.map { BreedModel(it) } +} diff --git a/app/src/main/java/dev/lhalegria/dogga/model/mapper/IBreedMapper.kt b/app/src/main/java/dev/lhalegria/dogga/model/mapper/IBreedMapper.kt new file mode 100644 index 0000000..b63f7ff --- /dev/null +++ b/app/src/main/java/dev/lhalegria/dogga/model/mapper/IBreedMapper.kt @@ -0,0 +1,9 @@ +package dev.lhalegria.dogga.model.mapper + +import dev.lhalegria.dogga.datasource.response.BreedsResponse +import dev.lhalegria.dogga.model.BreedModel + +interface IBreedMapper { + + fun breedsResponseToModel(response: BreedsResponse): List +} From 3b5693b03071851bbeaa616da953e92ffea2fa45 Mon Sep 17 00:00:00 2001 From: Luiz Alegria Date: Thu, 22 Jun 2023 20:37:17 -0300 Subject: [PATCH 03/18] Feature: Add view layer (#3) --- app/src/main/AndroidManifest.xml | 2 +- .../main/java/dev/lhalegria/dogga/{ => view}/MainActivity.kt | 4 ++-- .../java/dev/lhalegria/dogga/{ => view}/ui/theme/Color.kt | 4 ++-- .../java/dev/lhalegria/dogga/{ => view}/ui/theme/Theme.kt | 2 +- .../main/java/dev/lhalegria/dogga/{ => view}/ui/theme/Type.kt | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) rename app/src/main/java/dev/lhalegria/dogga/{ => view}/MainActivity.kt (93%) rename app/src/main/java/dev/lhalegria/dogga/{ => view}/ui/theme/Color.kt (74%) rename app/src/main/java/dev/lhalegria/dogga/{ => view}/ui/theme/Theme.kt (98%) rename app/src/main/java/dev/lhalegria/dogga/{ => view}/ui/theme/Type.kt (95%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 16ee2ba..d16a8c6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,7 +14,7 @@ tools:targetApi="31"> diff --git a/app/src/main/java/dev/lhalegria/dogga/MainActivity.kt b/app/src/main/java/dev/lhalegria/dogga/view/MainActivity.kt similarity index 93% rename from app/src/main/java/dev/lhalegria/dogga/MainActivity.kt rename to app/src/main/java/dev/lhalegria/dogga/view/MainActivity.kt index 6e9ad86..2d74ac0 100644 --- a/app/src/main/java/dev/lhalegria/dogga/MainActivity.kt +++ b/app/src/main/java/dev/lhalegria/dogga/view/MainActivity.kt @@ -1,4 +1,4 @@ -package dev.lhalegria.dogga +package dev.lhalegria.dogga.view import android.os.Bundle import androidx.activity.ComponentActivity @@ -10,7 +10,7 @@ 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 +import dev.lhalegria.dogga.view.ui.theme.DoggaTheme class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { diff --git a/app/src/main/java/dev/lhalegria/dogga/ui/theme/Color.kt b/app/src/main/java/dev/lhalegria/dogga/view/ui/theme/Color.kt similarity index 74% rename from app/src/main/java/dev/lhalegria/dogga/ui/theme/Color.kt rename to app/src/main/java/dev/lhalegria/dogga/view/ui/theme/Color.kt index 38b262c..e645b71 100644 --- a/app/src/main/java/dev/lhalegria/dogga/ui/theme/Color.kt +++ b/app/src/main/java/dev/lhalegria/dogga/view/ui/theme/Color.kt @@ -1,4 +1,4 @@ -package dev.lhalegria.dogga.ui.theme +package dev.lhalegria.dogga.view.ui.theme import androidx.compose.ui.graphics.Color @@ -8,4 +8,4 @@ val Pink80 = Color(0xFFEFB8C8) val Purple40 = Color(0xFF6650a4) val PurpleGrey40 = Color(0xFF625b71) -val Pink40 = Color(0xFF7D5260) \ No newline at end of file +val Pink40 = Color(0xFF7D5260) diff --git a/app/src/main/java/dev/lhalegria/dogga/ui/theme/Theme.kt b/app/src/main/java/dev/lhalegria/dogga/view/ui/theme/Theme.kt similarity index 98% rename from app/src/main/java/dev/lhalegria/dogga/ui/theme/Theme.kt rename to app/src/main/java/dev/lhalegria/dogga/view/ui/theme/Theme.kt index 5351f76..656cb4d 100644 --- a/app/src/main/java/dev/lhalegria/dogga/ui/theme/Theme.kt +++ b/app/src/main/java/dev/lhalegria/dogga/view/ui/theme/Theme.kt @@ -1,4 +1,4 @@ -package dev.lhalegria.dogga.ui.theme +package dev.lhalegria.dogga.view.ui.theme import android.app.Activity import android.os.Build diff --git a/app/src/main/java/dev/lhalegria/dogga/ui/theme/Type.kt b/app/src/main/java/dev/lhalegria/dogga/view/ui/theme/Type.kt similarity index 95% rename from app/src/main/java/dev/lhalegria/dogga/ui/theme/Type.kt rename to app/src/main/java/dev/lhalegria/dogga/view/ui/theme/Type.kt index 435ccb9..83d04a0 100644 --- a/app/src/main/java/dev/lhalegria/dogga/ui/theme/Type.kt +++ b/app/src/main/java/dev/lhalegria/dogga/view/ui/theme/Type.kt @@ -1,4 +1,4 @@ -package dev.lhalegria.dogga.ui.theme +package dev.lhalegria.dogga.view.ui.theme import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle @@ -31,4 +31,4 @@ val Typography = Typography( letterSpacing = 0.5.sp ) */ -) \ No newline at end of file +) From 6848115468565a9c02f43edf003eb897b118e6fa Mon Sep 17 00:00:00 2001 From: Luiz Alegria Date: Thu, 22 Jun 2023 20:39:26 -0300 Subject: [PATCH 04/18] Feature: Add repository layer (#4) --- .../dogga/datasource/di/DataSourceModule.kt | 4 +- .../{DogsService.kt => BreedsService.kt} | 2 +- .../java/dev/lhalegria/dogga/di/MainModule.kt | 16 ++++++++ .../dogga/repository/BreedRepository.kt | 38 +++++++++++++++++++ .../dogga/repository/IBreedRepository.kt | 12 ++++++ 5 files changed, 69 insertions(+), 3 deletions(-) rename app/src/main/java/dev/lhalegria/dogga/datasource/service/{DogsService.kt => BreedsService.kt} (95%) create mode 100644 app/src/main/java/dev/lhalegria/dogga/di/MainModule.kt create mode 100644 app/src/main/java/dev/lhalegria/dogga/repository/BreedRepository.kt create mode 100644 app/src/main/java/dev/lhalegria/dogga/repository/IBreedRepository.kt diff --git a/app/src/main/java/dev/lhalegria/dogga/datasource/di/DataSourceModule.kt b/app/src/main/java/dev/lhalegria/dogga/datasource/di/DataSourceModule.kt index 4101471..0f2fb14 100644 --- a/app/src/main/java/dev/lhalegria/dogga/datasource/di/DataSourceModule.kt +++ b/app/src/main/java/dev/lhalegria/dogga/datasource/di/DataSourceModule.kt @@ -1,11 +1,11 @@ package dev.lhalegria.dogga.datasource.di import dev.lhalegria.dogga.datasource.network.retrofitApi -import dev.lhalegria.dogga.datasource.service.DogsService +import dev.lhalegria.dogga.datasource.service.BreedsService import org.koin.dsl.module val dataSourceModule = module { factory { retrofitApi } - factory { retrofitApi.create(DogsService::class.java) } + factory { retrofitApi.create(BreedsService::class.java) } } diff --git a/app/src/main/java/dev/lhalegria/dogga/datasource/service/DogsService.kt b/app/src/main/java/dev/lhalegria/dogga/datasource/service/BreedsService.kt similarity index 95% rename from app/src/main/java/dev/lhalegria/dogga/datasource/service/DogsService.kt rename to app/src/main/java/dev/lhalegria/dogga/datasource/service/BreedsService.kt index 6624d02..75d444c 100644 --- a/app/src/main/java/dev/lhalegria/dogga/datasource/service/DogsService.kt +++ b/app/src/main/java/dev/lhalegria/dogga/datasource/service/BreedsService.kt @@ -6,7 +6,7 @@ import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Path -interface DogsService { +interface BreedsService { @GET("breeds/list") suspend fun getBreeds(): Response diff --git a/app/src/main/java/dev/lhalegria/dogga/di/MainModule.kt b/app/src/main/java/dev/lhalegria/dogga/di/MainModule.kt new file mode 100644 index 0000000..8531c2f --- /dev/null +++ b/app/src/main/java/dev/lhalegria/dogga/di/MainModule.kt @@ -0,0 +1,16 @@ +package dev.lhalegria.dogga.di + +import dev.lhalegria.dogga.model.mapper.BreedsMapper +import dev.lhalegria.dogga.model.mapper.IBreedMapper +import dev.lhalegria.dogga.repository.BreedRepository +import dev.lhalegria.dogga.repository.IBreedRepository +import org.koin.dsl.module + +val mainModule = module { + + single { BreedsMapper() } + + factory { + BreedRepository(service = get(), mapper = get()) + } +} diff --git a/app/src/main/java/dev/lhalegria/dogga/repository/BreedRepository.kt b/app/src/main/java/dev/lhalegria/dogga/repository/BreedRepository.kt new file mode 100644 index 0000000..c6da20a --- /dev/null +++ b/app/src/main/java/dev/lhalegria/dogga/repository/BreedRepository.kt @@ -0,0 +1,38 @@ +package dev.lhalegria.dogga.repository + +import dev.lhalegria.dogga.datasource.service.BreedsService +import dev.lhalegria.dogga.model.BreedModel +import dev.lhalegria.dogga.model.mapper.IBreedMapper +import retrofit2.Response +import java.io.IOException + +class BreedRepository( + private val service: BreedsService, + 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 IOException("Response has no body") + } else { + Result.failure(Exception("Response failure with code:${response.code()}")) + } + } catch (ex: Exception) { + Result.failure(ex) + } +} diff --git a/app/src/main/java/dev/lhalegria/dogga/repository/IBreedRepository.kt b/app/src/main/java/dev/lhalegria/dogga/repository/IBreedRepository.kt new file mode 100644 index 0000000..95e3990 --- /dev/null +++ b/app/src/main/java/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 +} From 4744eb95a82a8aafa3a488bf8359b9ef9c4e7bb7 Mon Sep 17 00:00:00 2001 From: Luiz Alegria Date: Fri, 23 Jun 2023 10:47:58 -0300 Subject: [PATCH 05/18] Feature: Add viewmodel layer (#5) * Feature: Add viewmodel layer * Feature: Add viewmodel layer --- .../java/dev/lhalegria/dogga/di/MainModule.kt | 8 +++ .../dogga/viewmodel/BreedViewModel.kt | 57 +++++++++++++++++++ .../lhalegria/dogga/viewmodel/RequestState.kt | 13 +++++ 3 files changed, 78 insertions(+) create mode 100644 app/src/main/java/dev/lhalegria/dogga/viewmodel/BreedViewModel.kt create mode 100644 app/src/main/java/dev/lhalegria/dogga/viewmodel/RequestState.kt diff --git a/app/src/main/java/dev/lhalegria/dogga/di/MainModule.kt b/app/src/main/java/dev/lhalegria/dogga/di/MainModule.kt index 8531c2f..bcd0c31 100644 --- a/app/src/main/java/dev/lhalegria/dogga/di/MainModule.kt +++ b/app/src/main/java/dev/lhalegria/dogga/di/MainModule.kt @@ -4,6 +4,8 @@ import dev.lhalegria.dogga.model.mapper.BreedsMapper 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 { @@ -13,4 +15,10 @@ val mainModule = module { factory { BreedRepository(service = get(), mapper = get()) } + + viewModel { + BreedViewModel( + repository = get() + ) + } } diff --git a/app/src/main/java/dev/lhalegria/dogga/viewmodel/BreedViewModel.kt b/app/src/main/java/dev/lhalegria/dogga/viewmodel/BreedViewModel.kt new file mode 100644 index 0000000..84a9e1e --- /dev/null +++ b/app/src/main/java/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 breedImage: 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.value = RequestState.Success(it) + } + .onFailure { + _breedImageStateFlow.value = RequestState.Error(it) + } + } +} diff --git a/app/src/main/java/dev/lhalegria/dogga/viewmodel/RequestState.kt b/app/src/main/java/dev/lhalegria/dogga/viewmodel/RequestState.kt new file mode 100644 index 0000000..766c56e --- /dev/null +++ b/app/src/main/java/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() +} From 2867a510c794595965a68bbbe711f3393ebf317b Mon Sep 17 00:00:00 2001 From: Luiz Alegria Date: Fri, 23 Jun 2023 14:20:47 -0300 Subject: [PATCH 06/18] Feature: Implement UI (#6) --- app/build.gradle | 3 + app/src/main/AndroidManifest.xml | 3 + .../main/java/dev/lhalegria/dogga/DoggaApp.kt | 18 ++ .../dev/lhalegria/dogga/view/BreedDetails.kt | 45 ++++ .../dev/lhalegria/dogga/view/MainActivity.kt | 216 ++++++++++++++++-- .../dogga/view/preview/ComposablePreviews.kt | 55 +++++ .../lhalegria/dogga/view/ui/theme/Color.kt | 2 + .../lhalegria/dogga/view/ui/theme/Theme.kt | 11 - .../dev/lhalegria/dogga/view/ui/theme/Type.kt | 17 -- .../main/res/drawable/ic_dog_placeholder.xml | 14 ++ 10 files changed, 341 insertions(+), 43 deletions(-) create mode 100644 app/src/main/java/dev/lhalegria/dogga/DoggaApp.kt create mode 100644 app/src/main/java/dev/lhalegria/dogga/view/BreedDetails.kt create mode 100644 app/src/main/java/dev/lhalegria/dogga/view/preview/ComposablePreviews.kt create mode 100644 app/src/main/res/drawable/ic_dog_placeholder.xml diff --git a/app/build.gradle b/app/build.gradle index 6a1916d..3850846 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,6 +56,9 @@ dependencies { implementation 'androidx.compose.ui:ui-graphics' implementation 'androidx.compose.ui:ui-tooling-preview' implementation 'androidx.compose.material3:material3' + implementation 'androidx.navigation:navigation-compose:2.6.0' + + implementation 'io.coil-kt:coil-compose:2.4.0' implementation "io.insert-koin:koin-android:3.4.2" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d16a8c6..9e0184d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,7 +2,10 @@ + + + Surface( + modifier = Modifier.fillMaxSize() + .padding(padding) + ) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top + ) { + // BreedPicture(breed.pictureUrl, 240.dp) + // BreedContent(breed.name, Alignment.CenterHorizontally) + } + } + } +} diff --git a/app/src/main/java/dev/lhalegria/dogga/view/MainActivity.kt b/app/src/main/java/dev/lhalegria/dogga/view/MainActivity.kt index 2d74ac0..3b6fe18 100644 --- a/app/src/main/java/dev/lhalegria/dogga/view/MainActivity.kt +++ b/app/src/main/java/dev/lhalegria/dogga/view/MainActivity.kt @@ -3,44 +3,230 @@ package dev.lhalegria.dogga.view import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row 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.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Home +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument +import coil.compose.AsyncImage +import coil.request.ImageRequest +import dev.lhalegria.dogga.R +import dev.lhalegria.dogga.model.BreedModel import dev.lhalegria.dogga.view.ui.theme.DoggaTheme +import dev.lhalegria.dogga.view.ui.theme.caramel +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 { - // A surface container using the 'background' color from the theme - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - Greeting("Android") + val breedsState by viewModel.breedStateFlow.collectAsState() + when (breedsState) { + is RequestState.Loading -> { + LoadingScreen() + } + is RequestState.Success -> { + val breeds = (breedsState as? RequestState.Success>)?.data + if (breeds?.isNotEmpty() == true) { + DogsApp(breeds = breeds) + } else { + // TODO: empty list state + } + } + is RequestState.Error -> { + // TODO: error state + } + } + } + } + } +} + +@Composable +fun DogsApp(breeds: List = listOf()) { + val navController = rememberNavController() + NavHost(navController = navController, startDestination = "breeds_list") { + composable("breeds_list") { + BreedListScreen(breeds, navController) + } + composable( + route = "breed_details/{breed}", + arguments = listOf(navArgument("breed") { + type = NavType.StringType + }) + ) { + BreedDetailsScreen(it.arguments?.getString("breed").orEmpty(), navController) + } + } +} + +@Composable +fun LoadingScreen() { + Box( + Modifier.fillMaxSize() + ) { + CircularProgressIndicator( + modifier = Modifier + .align(Alignment.Center) + .padding(50.dp), + progress = 1f, + color = caramel + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BreedListScreen(dogs: List, navController: NavHostController?) { + Scaffold(topBar = { + Toolbar( + title = "Breeds list", + icon = Icons.Default.Home + ) {} + }) { padding -> + Surface( + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + LazyColumn { + items(dogs) { + BreedCard(dog = it) { + navController?.navigate("dog_details/${it.name}") + } } } } } } +@OptIn(ExperimentalMaterial3Api::class) @Composable -fun Greeting(name: String, modifier: Modifier = Modifier) { - Text( - text = "Hello $name!", - modifier = modifier +fun Toolbar(title: String, icon: ImageVector, iconClickAction: () -> Unit) { + TopAppBar( + navigationIcon = { + Icon( + icon, + "Top app bar description", + Modifier + .padding(12.dp) + .clickable { iconClickAction.invoke() } + ) + }, + title = { Text(title) }, + colors = TopAppBarDefaults.smallTopAppBarColors( + containerColor = caramel, + titleContentColor = Color.White, + navigationIconContentColor = Color.White + ) ) } -@Preview(showBackground = true) @Composable -fun GreetingPreview() { - DoggaTheme { - Greeting("Android") +fun BreedCard(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 + ) { + BreedPicture(dog.photo, 72.dp) + BreedContent(dog.name, Alignment.Start) + } + } +} + +@Composable +fun BreedPicture(pictureUrl: String, imageSize: Dp) { + Card( + shape = CircleShape, + border = BorderStroke( + width = 2.dp, + color = MaterialTheme.colorScheme.background + ), + modifier = Modifier.padding(16.dp) + ) { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(pictureUrl) + .crossfade(true) + .build(), + placeholder = painterResource(id = R.drawable.ic_dog_placeholder), + contentDescription = "Breed picture description", + contentScale = ContentScale.Crop, + modifier = Modifier + .clip(CircleShape) + .size(imageSize) + ) + } +} + +@Composable +fun BreedContent(breedName: String, alignment: Alignment.Horizontal) { + Column( + modifier = Modifier.padding(8.dp), + horizontalAlignment = alignment + ) { + Text( + text = breedName, + style = MaterialTheme.typography.headlineSmall + ) } } diff --git a/app/src/main/java/dev/lhalegria/dogga/view/preview/ComposablePreviews.kt b/app/src/main/java/dev/lhalegria/dogga/view/preview/ComposablePreviews.kt new file mode 100644 index 0000000..2f41c41 --- /dev/null +++ b/app/src/main/java/dev/lhalegria/dogga/view/preview/ComposablePreviews.kt @@ -0,0 +1,55 @@ +package dev.lhalegria.dogga.view.preview + +import androidx.compose.foundation.layout.Box +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.material3.CircularProgressIndicator +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import dev.lhalegria.dogga.model.BreedModel +import dev.lhalegria.dogga.view.BreedDetailsScreen +import dev.lhalegria.dogga.view.ui.theme.DoggaTheme +import dev.lhalegria.dogga.view.ui.theme.caramel + +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 DogsListPreview() { + DoggaTheme { + Box( + Modifier.fillMaxSize() + ) { + CircularProgressIndicator( + modifier = Modifier + .align(Alignment.Center) + .padding(50.dp), + progress = 1f, + color = caramel + ) + } + + // BreedListScreen(dogs = dogs, null) + } +} + +@Preview(showBackground = true) +@Composable +fun UserProfileDetailsPreview() { + DoggaTheme { + BreedDetailsScreen("", null) + } +} diff --git a/app/src/main/java/dev/lhalegria/dogga/view/ui/theme/Color.kt b/app/src/main/java/dev/lhalegria/dogga/view/ui/theme/Color.kt index e645b71..0214ed4 100644 --- a/app/src/main/java/dev/lhalegria/dogga/view/ui/theme/Color.kt +++ b/app/src/main/java/dev/lhalegria/dogga/view/ui/theme/Color.kt @@ -9,3 +9,5 @@ val Pink80 = Color(0xFFEFB8C8) val Purple40 = Color(0xFF6650a4) val PurpleGrey40 = Color(0xFF625b71) val Pink40 = Color(0xFF7D5260) + +val caramel = Color(0xFFA58D11) diff --git a/app/src/main/java/dev/lhalegria/dogga/view/ui/theme/Theme.kt b/app/src/main/java/dev/lhalegria/dogga/view/ui/theme/Theme.kt index 656cb4d..c2010d9 100644 --- a/app/src/main/java/dev/lhalegria/dogga/view/ui/theme/Theme.kt +++ b/app/src/main/java/dev/lhalegria/dogga/view/ui/theme/Theme.kt @@ -25,22 +25,11 @@ 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), - */ ) @Composable fun DoggaTheme( darkTheme: Boolean = isSystemInDarkTheme(), - // Dynamic color is available on Android 12+ dynamicColor: Boolean = true, content: @Composable () -> Unit ) { diff --git a/app/src/main/java/dev/lhalegria/dogga/view/ui/theme/Type.kt b/app/src/main/java/dev/lhalegria/dogga/view/ui/theme/Type.kt index 83d04a0..ed8c83a 100644 --- a/app/src/main/java/dev/lhalegria/dogga/view/ui/theme/Type.kt +++ b/app/src/main/java/dev/lhalegria/dogga/view/ui/theme/Type.kt @@ -6,7 +6,6 @@ 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, @@ -15,20 +14,4 @@ val Typography = Typography( 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 - ) - */ ) diff --git a/app/src/main/res/drawable/ic_dog_placeholder.xml b/app/src/main/res/drawable/ic_dog_placeholder.xml new file mode 100644 index 0000000..e58425b --- /dev/null +++ b/app/src/main/res/drawable/ic_dog_placeholder.xml @@ -0,0 +1,14 @@ + + + + + + From f43385ddda1315062c7cb04d3e4f84920c98fd9b Mon Sep 17 00:00:00 2001 From: Luiz Alegria Date: Sun, 25 Jun 2023 14:57:04 -0300 Subject: [PATCH 07/18] Adjust: Organizing compose files (#7) --- .../dev/lhalegria/dogga/view/MainActivity.kt | 191 ++---------------- .../BreedDetail.kt} | 5 +- .../dogga/view/composable/BreedList.kt | 40 ++++ .../dogga/view/composable/BreedListItem.kt | 36 ++++ .../view/composable/BreedListItemContent.kt | 60 ++++++ .../dogga/view/composable/EmptyDataBox.kt | 35 ++++ .../dogga/view/composable/ErrorBox.kt | 42 ++++ .../dogga/view/composable/Loading.kt | 24 +++ .../dogga/view/composable/Toolbar.kt | 37 ++++ .../dogga/view/composable/TryAgainButton.kt | 30 +++ .../dogga/view/preview/ComposablePreviews.kt | 55 ----- .../lhalegria/dogga/view/preview/Previews.kt | 60 ++++++ .../lhalegria/dogga/view/ui/theme/Color.kt | 1 + app/src/main/res/values/strings.xml | 5 +- 14 files changed, 387 insertions(+), 234 deletions(-) rename app/src/main/java/dev/lhalegria/dogga/view/{BreedDetails.kt => composable/BreedDetail.kt} (90%) create mode 100644 app/src/main/java/dev/lhalegria/dogga/view/composable/BreedList.kt create mode 100644 app/src/main/java/dev/lhalegria/dogga/view/composable/BreedListItem.kt create mode 100644 app/src/main/java/dev/lhalegria/dogga/view/composable/BreedListItemContent.kt create mode 100644 app/src/main/java/dev/lhalegria/dogga/view/composable/EmptyDataBox.kt create mode 100644 app/src/main/java/dev/lhalegria/dogga/view/composable/ErrorBox.kt create mode 100644 app/src/main/java/dev/lhalegria/dogga/view/composable/Loading.kt create mode 100644 app/src/main/java/dev/lhalegria/dogga/view/composable/Toolbar.kt create mode 100644 app/src/main/java/dev/lhalegria/dogga/view/composable/TryAgainButton.kt delete mode 100644 app/src/main/java/dev/lhalegria/dogga/view/preview/ComposablePreviews.kt create mode 100644 app/src/main/java/dev/lhalegria/dogga/view/preview/Previews.kt diff --git a/app/src/main/java/dev/lhalegria/dogga/view/MainActivity.kt b/app/src/main/java/dev/lhalegria/dogga/view/MainActivity.kt index 3b6fe18..012a698 100644 --- a/app/src/main/java/dev/lhalegria/dogga/view/MainActivity.kt +++ b/app/src/main/java/dev/lhalegria/dogga/view/MainActivity.kt @@ -3,58 +3,21 @@ package dev.lhalegria.dogga.view import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -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.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Home -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.navigation.NavHostController import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument -import coil.compose.AsyncImage -import coil.request.ImageRequest -import dev.lhalegria.dogga.R 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 -import dev.lhalegria.dogga.view.ui.theme.caramel import dev.lhalegria.dogga.viewmodel.BreedViewModel import dev.lhalegria.dogga.viewmodel.RequestState import org.koin.androidx.viewmodel.ext.android.viewModel @@ -72,20 +35,9 @@ class MainActivity : ComponentActivity() { DoggaTheme { val breedsState by viewModel.breedStateFlow.collectAsState() when (breedsState) { - is RequestState.Loading -> { - LoadingScreen() - } - is RequestState.Success -> { - val breeds = (breedsState as? RequestState.Success>)?.data - if (breeds?.isNotEmpty() == true) { - DogsApp(breeds = breeds) - } else { - // TODO: empty list state - } - } - is RequestState.Error -> { - // TODO: error state - } + RequestState.Loading -> LoadingBox() + is RequestState.Error -> ErrorBox() + is RequestState.Success -> SuccessContainer(breedsState) } } } @@ -97,7 +49,7 @@ fun DogsApp(breeds: List = listOf()) { val navController = rememberNavController() NavHost(navController = navController, startDestination = "breeds_list") { composable("breeds_list") { - BreedListScreen(breeds, navController) + BreedList(breeds, navController) } composable( route = "breed_details/{breed}", @@ -105,128 +57,17 @@ fun DogsApp(breeds: List = listOf()) { type = NavType.StringType }) ) { - BreedDetailsScreen(it.arguments?.getString("breed").orEmpty(), navController) + BreedDetail(it.arguments?.getString("breed").orEmpty(), navController) } } } @Composable -fun LoadingScreen() { - Box( - Modifier.fillMaxSize() - ) { - CircularProgressIndicator( - modifier = Modifier - .align(Alignment.Center) - .padding(50.dp), - progress = 1f, - color = caramel - ) - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun BreedListScreen(dogs: List, navController: NavHostController?) { - Scaffold(topBar = { - Toolbar( - title = "Breeds list", - icon = Icons.Default.Home - ) {} - }) { padding -> - Surface( - modifier = Modifier - .fillMaxSize() - .padding(padding) - ) { - LazyColumn { - items(dogs) { - BreedCard(dog = it) { - navController?.navigate("dog_details/${it.name}") - } - } - } - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun Toolbar(title: String, icon: ImageVector, iconClickAction: () -> Unit) { - TopAppBar( - navigationIcon = { - Icon( - icon, - "Top app bar description", - Modifier - .padding(12.dp) - .clickable { iconClickAction.invoke() } - ) - }, - title = { Text(title) }, - colors = TopAppBarDefaults.smallTopAppBarColors( - containerColor = caramel, - titleContentColor = Color.White, - navigationIconContentColor = Color.White - ) - ) -} - -@Composable -fun BreedCard(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 - ) { - BreedPicture(dog.photo, 72.dp) - BreedContent(dog.name, Alignment.Start) - } - } -} - -@Composable -fun BreedPicture(pictureUrl: String, imageSize: Dp) { - Card( - shape = CircleShape, - border = BorderStroke( - width = 2.dp, - color = MaterialTheme.colorScheme.background - ), - modifier = Modifier.padding(16.dp) - ) { - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(pictureUrl) - .crossfade(true) - .build(), - placeholder = painterResource(id = R.drawable.ic_dog_placeholder), - contentDescription = "Breed picture description", - contentScale = ContentScale.Crop, - modifier = Modifier - .clip(CircleShape) - .size(imageSize) - ) - } -} - -@Composable -fun BreedContent(breedName: String, alignment: Alignment.Horizontal) { - Column( - modifier = Modifier.padding(8.dp), - horizontalAlignment = alignment - ) { - Text( - text = breedName, - style = MaterialTheme.typography.headlineSmall - ) +fun SuccessContainer(state: RequestState>?) { + val breeds = (state as? RequestState.Success>)?.data + if (breeds?.isNotEmpty() == true) { + DogsApp(breeds = breeds) + } else { + EmptyDataBox() } } diff --git a/app/src/main/java/dev/lhalegria/dogga/view/BreedDetails.kt b/app/src/main/java/dev/lhalegria/dogga/view/composable/BreedDetail.kt similarity index 90% rename from app/src/main/java/dev/lhalegria/dogga/view/BreedDetails.kt rename to app/src/main/java/dev/lhalegria/dogga/view/composable/BreedDetail.kt index ee174ca..add6ba0 100644 --- a/app/src/main/java/dev/lhalegria/dogga/view/BreedDetails.kt +++ b/app/src/main/java/dev/lhalegria/dogga/view/composable/BreedDetail.kt @@ -1,4 +1,4 @@ -package dev.lhalegria.dogga.view +package dev.lhalegria.dogga.view.composable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -13,12 +13,11 @@ import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController @OptIn(ExperimentalMaterial3Api::class) @Composable -fun BreedDetailsScreen(breed: String, navController: NavHostController?) { +fun BreedDetail(breed: String, navController: NavHostController?) { Scaffold(topBar = { Toolbar( diff --git a/app/src/main/java/dev/lhalegria/dogga/view/composable/BreedList.kt b/app/src/main/java/dev/lhalegria/dogga/view/composable/BreedList.kt new file mode 100644 index 0000000..d160fae --- /dev/null +++ b/app/src/main/java/dev/lhalegria/dogga/view/composable/BreedList.kt @@ -0,0 +1,40 @@ +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.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.NavHostController +import dev.lhalegria.dogga.model.BreedModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BreedList(dogs: List, navController: NavHostController?) { + Scaffold(topBar = { + Toolbar( + title = "Breeds list", + icon = Icons.Default.Home + ) {} + }) { padding -> + Surface( + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + LazyColumn { + items(dogs) { + BreedListItem(dog = it) { + navController?.navigate("dog_details/${it.name}") + } + } + } + } + } +} diff --git a/app/src/main/java/dev/lhalegria/dogga/view/composable/BreedListItem.kt b/app/src/main/java/dev/lhalegria/dogga/view/composable/BreedListItem.kt new file mode 100644 index 0000000..6fd3713 --- /dev/null +++ b/app/src/main/java/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 + ) { + BreedListItemPicture(dog.photo, 72.dp) + BreedListItemText(dog.name, Alignment.Start) + } + } +} diff --git a/app/src/main/java/dev/lhalegria/dogga/view/composable/BreedListItemContent.kt b/app/src/main/java/dev/lhalegria/dogga/view/composable/BreedListItemContent.kt new file mode 100644 index 0000000..e7b71b6 --- /dev/null +++ b/app/src/main/java/dev/lhalegria/dogga/view/composable/BreedListItemContent.kt @@ -0,0 +1,60 @@ +package dev.lhalegria.dogga.view.composable + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Card +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.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import dev.lhalegria.dogga.R + +@Composable +fun BreedListItemText(breedName: String, alignment: Alignment.Horizontal) { + Column( + modifier = Modifier.padding(8.dp), + horizontalAlignment = alignment + ) { + Text( + text = breedName, + style = MaterialTheme.typography.headlineSmall + ) + } +} + +@Composable +fun BreedListItemPicture(pictureUrl: String, imageSize: Dp) { + Card( + shape = CircleShape, + border = BorderStroke( + width = 2.dp, + color = MaterialTheme.colorScheme.background + ), + modifier = Modifier.padding(16.dp) + ) { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(pictureUrl) + .crossfade(true) + .build(), + placeholder = painterResource(id = R.drawable.ic_dog_placeholder), + contentDescription = "Breed picture description", + contentScale = ContentScale.Crop, + modifier = Modifier + .clip(CircleShape) + .size(imageSize) + ) + } +} diff --git a/app/src/main/java/dev/lhalegria/dogga/view/composable/EmptyDataBox.kt b/app/src/main/java/dev/lhalegria/dogga/view/composable/EmptyDataBox.kt new file mode 100644 index 0000000..2b17f3c --- /dev/null +++ b/app/src/main/java/dev/lhalegria/dogga/view/composable/EmptyDataBox.kt @@ -0,0 +1,35 @@ +package dev.lhalegria.dogga.view.composable + +import androidx.compose.foundation.layout.Box +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.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() { + Box(Modifier.fillMaxSize()) { + Box(Modifier.align(Alignment.Center)) { + Text( + modifier = Modifier + .align(Alignment.Center) + .padding(16.dp), + fontSize = TextUnit(20f, TextUnitType.Sp), + color = caramelStrong, + text = stringResource(id = R.string.empty_data_message) + ) + + TryAgainButton(modifier = Modifier.align(Alignment.BottomCenter)) { + + } + } + } +} diff --git a/app/src/main/java/dev/lhalegria/dogga/view/composable/ErrorBox.kt b/app/src/main/java/dev/lhalegria/dogga/view/composable/ErrorBox.kt new file mode 100644 index 0000000..78a31e1 --- /dev/null +++ b/app/src/main/java/dev/lhalegria/dogga/view/composable/ErrorBox.kt @@ -0,0 +1,42 @@ +package dev.lhalegria.dogga.view.composable + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +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 ErrorBox() { + Box(Modifier.fillMaxSize()) { + Box( + Modifier + .align(Alignment.Center) + .wrapContentSize() + ) { + Text( + modifier = Modifier + .align(Alignment.TopCenter) + .padding(16.dp), + fontSize = TextUnit(15f, TextUnitType.Sp), + textAlign = TextAlign.Center, + color = caramelStrong, + text = stringResource(id = R.string.error_message) + ) + + TryAgainButton(modifier = Modifier.align(Alignment.BottomCenter)) { + + } + } + } +} diff --git a/app/src/main/java/dev/lhalegria/dogga/view/composable/Loading.kt b/app/src/main/java/dev/lhalegria/dogga/view/composable/Loading.kt new file mode 100644 index 0000000..25cfce9 --- /dev/null +++ b/app/src/main/java/dev/lhalegria/dogga/view/composable/Loading.kt @@ -0,0 +1,24 @@ +package dev.lhalegria.dogga.view.composable + +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.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import dev.lhalegria.dogga.view.ui.theme.caramel + +@Composable +fun LoadingBox() { + Box(Modifier.fillMaxSize()) { + CircularProgressIndicator( + modifier = Modifier + .align(Alignment.Center) + .padding(50.dp), + progress = 1f, + color = caramel + ) + } +} diff --git a/app/src/main/java/dev/lhalegria/dogga/view/composable/Toolbar.kt b/app/src/main/java/dev/lhalegria/dogga/view/composable/Toolbar.kt new file mode 100644 index 0000000..7752477 --- /dev/null +++ b/app/src/main/java/dev/lhalegria/dogga/view/composable/Toolbar.kt @@ -0,0 +1,37 @@ +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.unit.dp +import dev.lhalegria.dogga.view.ui.theme.caramel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun Toolbar(title: String, icon: ImageVector, iconClickAction: () -> Unit) { + TopAppBar( + navigationIcon = { + Icon( + icon, + "Top app bar description", + 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/java/dev/lhalegria/dogga/view/composable/TryAgainButton.kt b/app/src/main/java/dev/lhalegria/dogga/view/composable/TryAgainButton.kt new file mode 100644 index 0000000..6625dd8 --- /dev/null +++ b/app/src/main/java/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/java/dev/lhalegria/dogga/view/preview/ComposablePreviews.kt b/app/src/main/java/dev/lhalegria/dogga/view/preview/ComposablePreviews.kt deleted file mode 100644 index 2f41c41..0000000 --- a/app/src/main/java/dev/lhalegria/dogga/view/preview/ComposablePreviews.kt +++ /dev/null @@ -1,55 +0,0 @@ -package dev.lhalegria.dogga.view.preview - -import androidx.compose.foundation.layout.Box -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.material3.CircularProgressIndicator -import androidx.compose.material3.Surface -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import dev.lhalegria.dogga.model.BreedModel -import dev.lhalegria.dogga.view.BreedDetailsScreen -import dev.lhalegria.dogga.view.ui.theme.DoggaTheme -import dev.lhalegria.dogga.view.ui.theme.caramel - -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 DogsListPreview() { - DoggaTheme { - Box( - Modifier.fillMaxSize() - ) { - CircularProgressIndicator( - modifier = Modifier - .align(Alignment.Center) - .padding(50.dp), - progress = 1f, - color = caramel - ) - } - - // BreedListScreen(dogs = dogs, null) - } -} - -@Preview(showBackground = true) -@Composable -fun UserProfileDetailsPreview() { - DoggaTheme { - BreedDetailsScreen("", null) - } -} diff --git a/app/src/main/java/dev/lhalegria/dogga/view/preview/Previews.kt b/app/src/main/java/dev/lhalegria/dogga/view/preview/Previews.kt new file mode 100644 index 0000000..bd5dcc5 --- /dev/null +++ b/app/src/main/java/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/java/dev/lhalegria/dogga/view/ui/theme/Color.kt b/app/src/main/java/dev/lhalegria/dogga/view/ui/theme/Color.kt index 0214ed4..8a5cabd 100644 --- a/app/src/main/java/dev/lhalegria/dogga/view/ui/theme/Color.kt +++ b/app/src/main/java/dev/lhalegria/dogga/view/ui/theme/Color.kt @@ -11,3 +11,4 @@ val PurpleGrey40 = Color(0xFF625b71) val Pink40 = Color(0xFF7D5260) val caramel = Color(0xFFA58D11) +val caramelStrong = Color(0xFF756304) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b2c6085..3a2ae75 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,6 @@ 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 + From 8532684fa98a377ec9164f2a392ba18f4df07021 Mon Sep 17 00:00:00 2001 From: Luiz Alegria Date: Sun, 25 Jun 2023 18:23:57 -0300 Subject: [PATCH 08/18] Refactor: Change gradle to use KTS (#8) --- app/build.gradle | 77 -------------- app/build.gradle.kts | 100 ++++++++++++++++++ app/proguard-rules.pro | 4 +- .../dogga/ExampleInstrumentedTest.kt | 0 .../dev/lhalegria/dogga/DoggaApp.kt | 0 .../dogga/datasource/di/DataSourceModule.kt | 0 .../datasource/network/DoggaNetworkApi.kt | 0 .../datasource/response/BreedImageResponse.kt | 0 .../datasource/response/BreedsResponse.kt | 0 .../dogga/datasource/service/BreedsService.kt | 0 .../dev/lhalegria/dogga/di/MainModule.kt | 0 .../dev/lhalegria/dogga/model/BreedModel.kt | 0 .../dogga/model/mapper/BreedsMapper.kt | 0 .../dogga/model/mapper/IBreedMapper.kt | 0 .../dogga/repository/BreedRepository.kt | 0 .../dogga/repository/IBreedRepository.kt | 0 .../dev/lhalegria/dogga/view/MainActivity.kt | 0 .../dogga/view/composable/BreedDetail.kt | 0 .../dogga/view/composable/BreedList.kt | 0 .../dogga/view/composable/BreedListItem.kt | 0 .../view/composable/BreedListItemContent.kt | 0 .../dogga/view/composable/EmptyDataBox.kt | 0 .../dogga/view/composable/ErrorBox.kt | 0 .../dogga/view/composable/Loading.kt | 0 .../dogga/view/composable/Toolbar.kt | 0 .../dogga/view/composable/TryAgainButton.kt | 0 .../lhalegria/dogga/view/preview/Previews.kt | 0 .../lhalegria/dogga/view/ui/theme/Color.kt | 0 .../lhalegria/dogga/view/ui/theme/Theme.kt | 0 .../dev/lhalegria/dogga/view/ui/theme/Type.kt | 0 .../dogga/viewmodel/BreedViewModel.kt | 0 .../lhalegria/dogga/viewmodel/RequestState.kt | 0 .../dev/lhalegria/dogga/ExampleUnitTest.kt | 0 build.gradle | 6 -- build.gradle.kts | 21 ++++ buildSrc/.gitignore | 1 + buildSrc/build.gradle.kts | 22 ++++ buildSrc/src/main/kotlin/BuildSetup.kt | 16 +++ buildSrc/src/main/kotlin/BuildType.kt | 21 ++++ buildSrc/src/main/kotlin/Modules.kt | 3 + buildSrc/src/main/kotlin/Plugins.kt | 4 + buildSrc/src/main/kotlin/Versions.kt | 15 +++ .../src/main/kotlin/dependencies/AndroidX.kt | 15 +++ buildSrc/src/main/kotlin/dependencies/Koin.kt | 3 + .../src/main/kotlin/dependencies/Other.kt | 4 + .../src/main/kotlin/dependencies/Square.kt | 6 ++ buildSrc/src/main/kotlin/dependencies/Test.kt | 7 ++ .../extensions/RepositoryHandlerExtension.kt | 10 ++ gradle/wrapper/gradle-wrapper.properties | 2 +- settings.gradle | 16 --- settings.gradle.kts | 3 + 51 files changed, 254 insertions(+), 102 deletions(-) delete mode 100644 app/build.gradle create mode 100644 app/build.gradle.kts rename app/src/androidTest/{java => kotlin}/dev/lhalegria/dogga/ExampleInstrumentedTest.kt (100%) rename app/src/main/{java => kotlin}/dev/lhalegria/dogga/DoggaApp.kt (100%) rename app/src/main/{java => kotlin}/dev/lhalegria/dogga/datasource/di/DataSourceModule.kt (100%) rename app/src/main/{java => kotlin}/dev/lhalegria/dogga/datasource/network/DoggaNetworkApi.kt (100%) rename app/src/main/{java => kotlin}/dev/lhalegria/dogga/datasource/response/BreedImageResponse.kt (100%) rename app/src/main/{java => kotlin}/dev/lhalegria/dogga/datasource/response/BreedsResponse.kt (100%) rename app/src/main/{java => kotlin}/dev/lhalegria/dogga/datasource/service/BreedsService.kt (100%) rename app/src/main/{java => kotlin}/dev/lhalegria/dogga/di/MainModule.kt (100%) rename app/src/main/{java => kotlin}/dev/lhalegria/dogga/model/BreedModel.kt (100%) rename app/src/main/{java => kotlin}/dev/lhalegria/dogga/model/mapper/BreedsMapper.kt (100%) rename app/src/main/{java => kotlin}/dev/lhalegria/dogga/model/mapper/IBreedMapper.kt (100%) rename app/src/main/{java => kotlin}/dev/lhalegria/dogga/repository/BreedRepository.kt (100%) rename app/src/main/{java => kotlin}/dev/lhalegria/dogga/repository/IBreedRepository.kt (100%) rename app/src/main/{java => kotlin}/dev/lhalegria/dogga/view/MainActivity.kt (100%) rename app/src/main/{java => kotlin}/dev/lhalegria/dogga/view/composable/BreedDetail.kt (100%) rename app/src/main/{java => kotlin}/dev/lhalegria/dogga/view/composable/BreedList.kt (100%) rename app/src/main/{java => kotlin}/dev/lhalegria/dogga/view/composable/BreedListItem.kt (100%) rename app/src/main/{java => kotlin}/dev/lhalegria/dogga/view/composable/BreedListItemContent.kt (100%) rename app/src/main/{java => kotlin}/dev/lhalegria/dogga/view/composable/EmptyDataBox.kt (100%) rename app/src/main/{java => kotlin}/dev/lhalegria/dogga/view/composable/ErrorBox.kt (100%) rename app/src/main/{java => kotlin}/dev/lhalegria/dogga/view/composable/Loading.kt (100%) rename app/src/main/{java => kotlin}/dev/lhalegria/dogga/view/composable/Toolbar.kt (100%) rename app/src/main/{java => kotlin}/dev/lhalegria/dogga/view/composable/TryAgainButton.kt (100%) rename app/src/main/{java => kotlin}/dev/lhalegria/dogga/view/preview/Previews.kt (100%) rename app/src/main/{java => kotlin}/dev/lhalegria/dogga/view/ui/theme/Color.kt (100%) rename app/src/main/{java => kotlin}/dev/lhalegria/dogga/view/ui/theme/Theme.kt (100%) rename app/src/main/{java => kotlin}/dev/lhalegria/dogga/view/ui/theme/Type.kt (100%) rename app/src/main/{java => kotlin}/dev/lhalegria/dogga/viewmodel/BreedViewModel.kt (100%) rename app/src/main/{java => kotlin}/dev/lhalegria/dogga/viewmodel/RequestState.kt (100%) rename app/src/test/{java => kotlin}/dev/lhalegria/dogga/ExampleUnitTest.kt (100%) delete mode 100644 build.gradle create mode 100644 build.gradle.kts create mode 100644 buildSrc/.gitignore create mode 100644 buildSrc/build.gradle.kts create mode 100644 buildSrc/src/main/kotlin/BuildSetup.kt create mode 100644 buildSrc/src/main/kotlin/BuildType.kt create mode 100644 buildSrc/src/main/kotlin/Modules.kt create mode 100644 buildSrc/src/main/kotlin/Plugins.kt create mode 100644 buildSrc/src/main/kotlin/Versions.kt create mode 100644 buildSrc/src/main/kotlin/dependencies/AndroidX.kt create mode 100644 buildSrc/src/main/kotlin/dependencies/Koin.kt create mode 100644 buildSrc/src/main/kotlin/dependencies/Other.kt create mode 100644 buildSrc/src/main/kotlin/dependencies/Square.kt create mode 100644 buildSrc/src/main/kotlin/dependencies/Test.kt create mode 100644 buildSrc/src/main/kotlin/extensions/RepositoryHandlerExtension.kt delete mode 100644 settings.gradle create mode 100644 settings.gradle.kts diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100644 index 3850846..0000000 --- a/app/build.gradle +++ /dev/null @@ -1,77 +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' - implementation 'androidx.navigation:navigation-compose:2.6.0' - - implementation 'io.coil-kt:coil-compose:2.4.0' - - implementation "io.insert-koin:koin-android:3.4.2" - - implementation 'com.squareup.retrofit2:retrofit:2.9.0' - implementation 'com.squareup.okhttp3:okhttp:4.11.0' - implementation 'com.squareup.okhttp3:logging-interceptor:4.11.0' - implementation 'com.squareup.retrofit2:converter-gson:2.9.0' - - 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..6d7418f --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,100 @@ +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.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(Square.RETROFIT) + implementation(Square.OKHTTP) + implementation(Square.OKHTTP_LOG_INTERCEPTOR) + implementation(Square.GSON_CONVERTER) + + testImplementation(Test.JUNIT) + 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/java/dev/lhalegria/dogga/DoggaApp.kt b/app/src/main/kotlin/dev/lhalegria/dogga/DoggaApp.kt similarity index 100% rename from app/src/main/java/dev/lhalegria/dogga/DoggaApp.kt rename to app/src/main/kotlin/dev/lhalegria/dogga/DoggaApp.kt diff --git a/app/src/main/java/dev/lhalegria/dogga/datasource/di/DataSourceModule.kt b/app/src/main/kotlin/dev/lhalegria/dogga/datasource/di/DataSourceModule.kt similarity index 100% rename from app/src/main/java/dev/lhalegria/dogga/datasource/di/DataSourceModule.kt rename to app/src/main/kotlin/dev/lhalegria/dogga/datasource/di/DataSourceModule.kt diff --git a/app/src/main/java/dev/lhalegria/dogga/datasource/network/DoggaNetworkApi.kt b/app/src/main/kotlin/dev/lhalegria/dogga/datasource/network/DoggaNetworkApi.kt similarity index 100% rename from app/src/main/java/dev/lhalegria/dogga/datasource/network/DoggaNetworkApi.kt rename to app/src/main/kotlin/dev/lhalegria/dogga/datasource/network/DoggaNetworkApi.kt diff --git a/app/src/main/java/dev/lhalegria/dogga/datasource/response/BreedImageResponse.kt b/app/src/main/kotlin/dev/lhalegria/dogga/datasource/response/BreedImageResponse.kt similarity index 100% rename from app/src/main/java/dev/lhalegria/dogga/datasource/response/BreedImageResponse.kt rename to app/src/main/kotlin/dev/lhalegria/dogga/datasource/response/BreedImageResponse.kt diff --git a/app/src/main/java/dev/lhalegria/dogga/datasource/response/BreedsResponse.kt b/app/src/main/kotlin/dev/lhalegria/dogga/datasource/response/BreedsResponse.kt similarity index 100% rename from app/src/main/java/dev/lhalegria/dogga/datasource/response/BreedsResponse.kt rename to app/src/main/kotlin/dev/lhalegria/dogga/datasource/response/BreedsResponse.kt diff --git a/app/src/main/java/dev/lhalegria/dogga/datasource/service/BreedsService.kt b/app/src/main/kotlin/dev/lhalegria/dogga/datasource/service/BreedsService.kt similarity index 100% rename from app/src/main/java/dev/lhalegria/dogga/datasource/service/BreedsService.kt rename to app/src/main/kotlin/dev/lhalegria/dogga/datasource/service/BreedsService.kt diff --git a/app/src/main/java/dev/lhalegria/dogga/di/MainModule.kt b/app/src/main/kotlin/dev/lhalegria/dogga/di/MainModule.kt similarity index 100% rename from app/src/main/java/dev/lhalegria/dogga/di/MainModule.kt rename to app/src/main/kotlin/dev/lhalegria/dogga/di/MainModule.kt diff --git a/app/src/main/java/dev/lhalegria/dogga/model/BreedModel.kt b/app/src/main/kotlin/dev/lhalegria/dogga/model/BreedModel.kt similarity index 100% rename from app/src/main/java/dev/lhalegria/dogga/model/BreedModel.kt rename to app/src/main/kotlin/dev/lhalegria/dogga/model/BreedModel.kt diff --git a/app/src/main/java/dev/lhalegria/dogga/model/mapper/BreedsMapper.kt b/app/src/main/kotlin/dev/lhalegria/dogga/model/mapper/BreedsMapper.kt similarity index 100% rename from app/src/main/java/dev/lhalegria/dogga/model/mapper/BreedsMapper.kt rename to app/src/main/kotlin/dev/lhalegria/dogga/model/mapper/BreedsMapper.kt diff --git a/app/src/main/java/dev/lhalegria/dogga/model/mapper/IBreedMapper.kt b/app/src/main/kotlin/dev/lhalegria/dogga/model/mapper/IBreedMapper.kt similarity index 100% rename from app/src/main/java/dev/lhalegria/dogga/model/mapper/IBreedMapper.kt rename to app/src/main/kotlin/dev/lhalegria/dogga/model/mapper/IBreedMapper.kt diff --git a/app/src/main/java/dev/lhalegria/dogga/repository/BreedRepository.kt b/app/src/main/kotlin/dev/lhalegria/dogga/repository/BreedRepository.kt similarity index 100% rename from app/src/main/java/dev/lhalegria/dogga/repository/BreedRepository.kt rename to app/src/main/kotlin/dev/lhalegria/dogga/repository/BreedRepository.kt diff --git a/app/src/main/java/dev/lhalegria/dogga/repository/IBreedRepository.kt b/app/src/main/kotlin/dev/lhalegria/dogga/repository/IBreedRepository.kt similarity index 100% rename from app/src/main/java/dev/lhalegria/dogga/repository/IBreedRepository.kt rename to app/src/main/kotlin/dev/lhalegria/dogga/repository/IBreedRepository.kt diff --git a/app/src/main/java/dev/lhalegria/dogga/view/MainActivity.kt b/app/src/main/kotlin/dev/lhalegria/dogga/view/MainActivity.kt similarity index 100% rename from app/src/main/java/dev/lhalegria/dogga/view/MainActivity.kt rename to app/src/main/kotlin/dev/lhalegria/dogga/view/MainActivity.kt diff --git a/app/src/main/java/dev/lhalegria/dogga/view/composable/BreedDetail.kt b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedDetail.kt similarity index 100% rename from app/src/main/java/dev/lhalegria/dogga/view/composable/BreedDetail.kt rename to app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedDetail.kt diff --git a/app/src/main/java/dev/lhalegria/dogga/view/composable/BreedList.kt b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedList.kt similarity index 100% rename from app/src/main/java/dev/lhalegria/dogga/view/composable/BreedList.kt rename to app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedList.kt diff --git a/app/src/main/java/dev/lhalegria/dogga/view/composable/BreedListItem.kt b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedListItem.kt similarity index 100% rename from app/src/main/java/dev/lhalegria/dogga/view/composable/BreedListItem.kt rename to app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedListItem.kt diff --git a/app/src/main/java/dev/lhalegria/dogga/view/composable/BreedListItemContent.kt b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedListItemContent.kt similarity index 100% rename from app/src/main/java/dev/lhalegria/dogga/view/composable/BreedListItemContent.kt rename to app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedListItemContent.kt diff --git a/app/src/main/java/dev/lhalegria/dogga/view/composable/EmptyDataBox.kt b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/EmptyDataBox.kt similarity index 100% rename from app/src/main/java/dev/lhalegria/dogga/view/composable/EmptyDataBox.kt rename to app/src/main/kotlin/dev/lhalegria/dogga/view/composable/EmptyDataBox.kt diff --git a/app/src/main/java/dev/lhalegria/dogga/view/composable/ErrorBox.kt b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/ErrorBox.kt similarity index 100% rename from app/src/main/java/dev/lhalegria/dogga/view/composable/ErrorBox.kt rename to app/src/main/kotlin/dev/lhalegria/dogga/view/composable/ErrorBox.kt diff --git a/app/src/main/java/dev/lhalegria/dogga/view/composable/Loading.kt b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/Loading.kt similarity index 100% rename from app/src/main/java/dev/lhalegria/dogga/view/composable/Loading.kt rename to app/src/main/kotlin/dev/lhalegria/dogga/view/composable/Loading.kt diff --git a/app/src/main/java/dev/lhalegria/dogga/view/composable/Toolbar.kt b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/Toolbar.kt similarity index 100% rename from app/src/main/java/dev/lhalegria/dogga/view/composable/Toolbar.kt rename to app/src/main/kotlin/dev/lhalegria/dogga/view/composable/Toolbar.kt diff --git a/app/src/main/java/dev/lhalegria/dogga/view/composable/TryAgainButton.kt b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/TryAgainButton.kt similarity index 100% rename from app/src/main/java/dev/lhalegria/dogga/view/composable/TryAgainButton.kt rename to app/src/main/kotlin/dev/lhalegria/dogga/view/composable/TryAgainButton.kt diff --git a/app/src/main/java/dev/lhalegria/dogga/view/preview/Previews.kt b/app/src/main/kotlin/dev/lhalegria/dogga/view/preview/Previews.kt similarity index 100% rename from app/src/main/java/dev/lhalegria/dogga/view/preview/Previews.kt rename to app/src/main/kotlin/dev/lhalegria/dogga/view/preview/Previews.kt diff --git a/app/src/main/java/dev/lhalegria/dogga/view/ui/theme/Color.kt b/app/src/main/kotlin/dev/lhalegria/dogga/view/ui/theme/Color.kt similarity index 100% rename from app/src/main/java/dev/lhalegria/dogga/view/ui/theme/Color.kt rename to app/src/main/kotlin/dev/lhalegria/dogga/view/ui/theme/Color.kt diff --git a/app/src/main/java/dev/lhalegria/dogga/view/ui/theme/Theme.kt b/app/src/main/kotlin/dev/lhalegria/dogga/view/ui/theme/Theme.kt similarity index 100% rename from app/src/main/java/dev/lhalegria/dogga/view/ui/theme/Theme.kt rename to app/src/main/kotlin/dev/lhalegria/dogga/view/ui/theme/Theme.kt diff --git a/app/src/main/java/dev/lhalegria/dogga/view/ui/theme/Type.kt b/app/src/main/kotlin/dev/lhalegria/dogga/view/ui/theme/Type.kt similarity index 100% rename from app/src/main/java/dev/lhalegria/dogga/view/ui/theme/Type.kt rename to app/src/main/kotlin/dev/lhalegria/dogga/view/ui/theme/Type.kt diff --git a/app/src/main/java/dev/lhalegria/dogga/viewmodel/BreedViewModel.kt b/app/src/main/kotlin/dev/lhalegria/dogga/viewmodel/BreedViewModel.kt similarity index 100% rename from app/src/main/java/dev/lhalegria/dogga/viewmodel/BreedViewModel.kt rename to app/src/main/kotlin/dev/lhalegria/dogga/viewmodel/BreedViewModel.kt diff --git a/app/src/main/java/dev/lhalegria/dogga/viewmodel/RequestState.kt b/app/src/main/kotlin/dev/lhalegria/dogga/viewmodel/RequestState.kt similarity index 100% rename from app/src/main/java/dev/lhalegria/dogga/viewmodel/RequestState.kt rename to app/src/main/kotlin/dev/lhalegria/dogga/viewmodel/RequestState.kt diff --git a/app/src/test/java/dev/lhalegria/dogga/ExampleUnitTest.kt b/app/src/test/kotlin/dev/lhalegria/dogga/ExampleUnitTest.kt similarity index 100% rename from app/src/test/java/dev/lhalegria/dogga/ExampleUnitTest.kt rename to app/src/test/kotlin/dev/lhalegria/dogga/ExampleUnitTest.kt 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..08e9409 --- /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.20" +} + +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..5764bef --- /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.3.2" + + 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..30f341a --- /dev/null +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -0,0 +1,15 @@ +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_NAVIGATION = "2.6.0" + const val ESPRESSO = "3.5.1" + const val JUNIT = "4.13.2" + const val KOIN = "3.4.2" + const val KOTLIN = "1.8.22" + 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..e5b7558 --- /dev/null +++ b/buildSrc/src/main/kotlin/dependencies/AndroidX.kt @@ -0,0 +1,15 @@ +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 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..370aba0 --- /dev/null +++ b/buildSrc/src/main/kotlin/dependencies/Koin.kt @@ -0,0 +1,3 @@ +object Koin { + const val KOIN = "io.insert-koin:koin-android:${Versions.KOIN}" +} 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..429b9ed --- /dev/null +++ b/buildSrc/src/main/kotlin/dependencies/Test.kt @@ -0,0 +1,7 @@ +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 COMPOSE_UI_JUNIT4 = "androidx.compose.ui:ui-test-junit4" + const val COMPOSE_TEST_MANIFEST = "androidx.compose.ui:ui-test-manifest" +} 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") From 5e12dd01480b9768fd5a5b166a3e115c1ab0aaeb Mon Sep 17 00:00:00 2001 From: Luiz Alegria Date: Sun, 25 Jun 2023 19:09:19 -0300 Subject: [PATCH 09/18] Adjust: Align error box views (#9) --- .../dogga/view/composable/EmptyDataBox.kt | 31 ++++++++-------- .../dogga/view/composable/ErrorBox.kt | 36 ++++++++----------- buildSrc/build.gradle.kts | 2 +- buildSrc/src/main/kotlin/BuildSetup.kt | 2 +- buildSrc/src/main/kotlin/Versions.kt | 2 +- 5 files changed, 34 insertions(+), 39 deletions(-) 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 index 2b17f3c..b0e4178 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/EmptyDataBox.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/EmptyDataBox.kt @@ -1,6 +1,7 @@ package dev.lhalegria.dogga.view.composable -import androidx.compose.foundation.layout.Box +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 @@ -8,6 +9,7 @@ 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 @@ -16,20 +18,19 @@ import dev.lhalegria.dogga.view.ui.theme.caramelStrong @Composable fun EmptyDataBox() { - Box(Modifier.fillMaxSize()) { - Box(Modifier.align(Alignment.Center)) { - Text( - modifier = Modifier - .align(Alignment.Center) - .padding(16.dp), - fontSize = TextUnit(20f, TextUnitType.Sp), - color = caramelStrong, - text = stringResource(id = R.string.empty_data_message) - ) + 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.align(Alignment.BottomCenter)) { - - } - } + TryAgainButton(modifier = Modifier) { } } } 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 index 78a31e1..bca2595 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/ErrorBox.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/ErrorBox.kt @@ -1,9 +1,9 @@ package dev.lhalegria.dogga.view.composable -import androidx.compose.foundation.layout.Box +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.foundation.layout.wrapContentSize import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -18,25 +18,19 @@ import dev.lhalegria.dogga.view.ui.theme.caramelStrong @Composable fun ErrorBox() { - Box(Modifier.fillMaxSize()) { - Box( - Modifier - .align(Alignment.Center) - .wrapContentSize() - ) { - Text( - modifier = Modifier - .align(Alignment.TopCenter) - .padding(16.dp), - fontSize = TextUnit(15f, TextUnitType.Sp), - textAlign = TextAlign.Center, - color = caramelStrong, - text = stringResource(id = R.string.error_message) - ) + 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.align(Alignment.BottomCenter)) { - - } - } + TryAgainButton(modifier = Modifier) { } } } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 08e9409..0038480 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -13,7 +13,7 @@ plugins { object PluginsVersions { const val GRADLE_ANDROID = "8.0.2" - const val KOTLIN = "1.8.20" + const val KOTLIN = "1.8.21" } dependencies { diff --git a/buildSrc/src/main/kotlin/BuildSetup.kt b/buildSrc/src/main/kotlin/BuildSetup.kt index 5764bef..3541f4f 100644 --- a/buildSrc/src/main/kotlin/BuildSetup.kt +++ b/buildSrc/src/main/kotlin/BuildSetup.kt @@ -10,7 +10,7 @@ object BuildSetup { const val VERSION_CODE = 1 const val VERSION_NAME = "1.0.0" - const val COMPOSE_KOTLIN_COMPILER = "1.3.2" + const val COMPOSE_KOTLIN_COMPILER = "1.4.7" const val TEST_INSTRUMENTATION_RUNNER = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 30f341a..a091b13 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -9,7 +9,7 @@ object Versions { const val ESPRESSO = "3.5.1" const val JUNIT = "4.13.2" const val KOIN = "3.4.2" - const val KOTLIN = "1.8.22" + const val KOTLIN = "1.8.21" const val OKHTTP = "4.11.0" const val RETROFIT = "2.9.0" } From dc0cefe8d99eb2eebf3f09f2024372b840ac6705 Mon Sep 17 00:00:00 2001 From: Luiz Alegria Date: Sun, 25 Jun 2023 22:54:27 -0300 Subject: [PATCH 10/18] Feature: Implement BreedDetail screen (#10) --- app/build.gradle.kts | 2 + .../dev/lhalegria/dogga/view/MainActivity.kt | 4 +- .../dogga/view/composable/BreedDetail.kt | 65 +++++++++++++++++-- .../dogga/view/composable/BreedList.kt | 7 +- .../dogga/view/composable/BreedListItem.kt | 2 +- .../view/composable/BreedListItemContent.kt | 44 ++++--------- .../dogga/view/composable/EmptyDataBox.kt | 4 +- .../dogga/view/composable/ErrorBox.kt | 4 +- .../dogga/view/composable/Loading.kt | 4 +- .../dogga/view/composable/Toolbar.kt | 4 +- .../dogga/view/composable/TryAgainButton.kt | 4 +- .../lhalegria/dogga/view/ui/theme/Color.kt | 8 +-- .../lhalegria/dogga/view/ui/theme/Theme.kt | 4 +- .../dogga/viewmodel/BreedViewModel.kt | 6 +- app/src/main/res/drawable/ic_dog_face.xml | 21 ++++++ .../main/res/drawable/ic_dog_placeholder.xml | 4 +- app/src/main/res/values/colors.xml | 3 + buildSrc/src/main/kotlin/Versions.kt | 2 + .../src/main/kotlin/dependencies/AndroidX.kt | 1 + buildSrc/src/main/kotlin/dependencies/Koin.kt | 1 + 20 files changed, 131 insertions(+), 63 deletions(-) create mode 100644 app/src/main/res/drawable/ic_dog_face.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6d7418f..a46eea6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -73,6 +73,7 @@ dependencies { 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) @@ -83,6 +84,7 @@ dependencies { implementation(Other.COIL) implementation(Koin.KOIN) + implementation(Koin.KOIN_COMPOSE) implementation(Square.RETROFIT) implementation(Square.OKHTTP) diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/view/MainActivity.kt b/app/src/main/kotlin/dev/lhalegria/dogga/view/MainActivity.kt index 012a698..2489382 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/view/MainActivity.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/MainActivity.kt @@ -4,8 +4,8 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -33,7 +33,7 @@ class MainActivity : ComponentActivity() { setContent { DoggaTheme { - val breedsState by viewModel.breedStateFlow.collectAsState() + val breedsState by viewModel.breedStateFlow.collectAsStateWithLifecycle() when (breedsState) { RequestState.Loading -> LoadingBox() is RequestState.Error -> ErrorBox() 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 index add6ba0..b0ff64e 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedDetail.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedDetail.kt @@ -5,19 +5,43 @@ 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.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?) { +fun BreedDetail( + breed: String, + navController: NavHostController?, + viewModel: BreedViewModel = koinViewModel() +) { + viewModel.getBreedImage(breed) + + val imageState by viewModel.breedImageStateFlow.collectAsStateWithLifecycle() Scaffold(topBar = { Toolbar( @@ -28,7 +52,8 @@ fun BreedDetail(breed: String, navController: NavHostController?) { } }) { padding -> Surface( - modifier = Modifier.fillMaxSize() + modifier = Modifier + .fillMaxSize() .padding(padding) ) { Column( @@ -36,9 +61,41 @@ fun BreedDetail(breed: String, navController: NavHostController?) { horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top ) { - // BreedPicture(breed.pictureUrl, 240.dp) - // BreedContent(breed.name, Alignment.CenterHorizontally) + 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_dog_placeholder), + error = painterResource(id = R.drawable.ic_dog_placeholder), + contentDescription = "Picture of a dog from the $name breed.", + contentScale = ContentScale.Fit, + modifier = Modifier + .fillMaxWidth() + .size(250.dp) + .padding(0.dp) + ) +} 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 index d160fae..af62730 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedList.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedList.kt @@ -16,7 +16,10 @@ import dev.lhalegria.dogga.model.BreedModel @OptIn(ExperimentalMaterial3Api::class) @Composable -fun BreedList(dogs: List, navController: NavHostController?) { +fun BreedList( + dogs: List, + navController: NavHostController? +) { Scaffold(topBar = { Toolbar( title = "Breeds list", @@ -31,7 +34,7 @@ fun BreedList(dogs: List, navController: NavHostController?) { LazyColumn { items(dogs) { BreedListItem(dog = it) { - navController?.navigate("dog_details/${it.name}") + navController?.navigate("breed_details/${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 index 6fd3713..5d051a9 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedListItem.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedListItem.kt @@ -29,7 +29,7 @@ fun BreedListItem(dog: BreedModel, clickAction: () -> Unit) { verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { - BreedListItemPicture(dog.photo, 72.dp) + 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 index e7b71b6..5111547 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedListItemContent.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedListItemContent.kt @@ -1,24 +1,17 @@ package dev.lhalegria.dogga.view.composable -import androidx.compose.foundation.BorderStroke +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.foundation.shape.CircleShape -import androidx.compose.material3.Card 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.draw.clip import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage -import coil.request.ImageRequest import dev.lhalegria.dogga.R @Composable @@ -28,33 +21,20 @@ fun BreedListItemText(breedName: String, alignment: Alignment.Horizontal) { horizontalAlignment = alignment ) { Text( - text = breedName, - style = MaterialTheme.typography.headlineSmall + text = breedName.replaceFirstChar { it.uppercaseChar() }, + style = MaterialTheme.typography.bodyLarge ) } } @Composable -fun BreedListItemPicture(pictureUrl: String, imageSize: Dp) { - Card( - shape = CircleShape, - border = BorderStroke( - width = 2.dp, - color = MaterialTheme.colorScheme.background - ), - modifier = Modifier.padding(16.dp) - ) { - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(pictureUrl) - .crossfade(true) - .build(), - placeholder = painterResource(id = R.drawable.ic_dog_placeholder), - contentDescription = "Breed picture description", - contentScale = ContentScale.Crop, - modifier = Modifier - .clip(CircleShape) - .size(imageSize) - ) - } +fun BreedListItemIcon() { + Image( + painterResource(R.drawable.ic_dog_face), + contentDescription = "Dog icon", + 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 index b0e4178..2bc5830 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/EmptyDataBox.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/EmptyDataBox.kt @@ -14,7 +14,7 @@ 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 +import dev.lhalegria.dogga.view.ui.theme.CaramelStrong @Composable fun EmptyDataBox() { @@ -27,7 +27,7 @@ fun EmptyDataBox() { modifier = Modifier.padding(32.dp), fontSize = TextUnit(15f, TextUnitType.Sp), textAlign = TextAlign.Center, - color = caramelStrong, + color = CaramelStrong, text = stringResource(id = R.string.empty_data_message) ) 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 index bca2595..416567f 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/ErrorBox.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/ErrorBox.kt @@ -14,7 +14,7 @@ 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 +import dev.lhalegria.dogga.view.ui.theme.CaramelStrong @Composable fun ErrorBox() { @@ -27,7 +27,7 @@ fun ErrorBox() { modifier = Modifier.padding(32.dp), fontSize = TextUnit(15f, TextUnitType.Sp), textAlign = TextAlign.Center, - color = caramelStrong, + color = CaramelStrong, text = stringResource(id = R.string.error_message) ) 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 index 25cfce9..2d3434a 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/Loading.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/Loading.kt @@ -8,7 +8,7 @@ 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.view.ui.theme.caramel +import dev.lhalegria.dogga.view.ui.theme.Caramel @Composable fun LoadingBox() { @@ -18,7 +18,7 @@ fun LoadingBox() { .align(Alignment.Center) .padding(50.dp), progress = 1f, - color = caramel + color = Caramel ) } } 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 index 7752477..8854e84 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/Toolbar.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/Toolbar.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp -import dev.lhalegria.dogga.view.ui.theme.caramel +import dev.lhalegria.dogga.view.ui.theme.Caramel @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -29,7 +29,7 @@ fun Toolbar(title: String, icon: ImageVector, iconClickAction: () -> Unit) { }, title = { Text(title) }, colors = TopAppBarDefaults.smallTopAppBarColors( - containerColor = caramel, + 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 index 6625dd8..fc21b77 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/TryAgainButton.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/TryAgainButton.kt @@ -8,7 +8,7 @@ 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 +import dev.lhalegria.dogga.view.ui.theme.Caramel @Composable fun TryAgainButton( @@ -20,7 +20,7 @@ fun TryAgainButton( modifier = modifier, colors = ButtonDefaults.buttonColors( contentColor = Color.White, - containerColor = caramel, + containerColor = Caramel, disabledContentColor = Color.White, disabledContainerColor = Color.Gray ) 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 index 8a5cabd..126ae7c 100644 --- 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 @@ -2,13 +2,11 @@ package dev.lhalegria.dogga.view.ui.theme import androidx.compose.ui.graphics.Color -val Purple80 = Color(0xFFD0BCFF) -val PurpleGrey80 = Color(0xFFCCC2DC) +val PurpleGrey80 = Color(0xFFF3EED1) val Pink80 = Color(0xFFEFB8C8) -val Purple40 = Color(0xFF6650a4) val PurpleGrey40 = Color(0xFF625b71) val Pink40 = Color(0xFF7D5260) -val caramel = Color(0xFFA58D11) -val caramelStrong = Color(0xFF756304) +val Caramel = Color(0xFFA58D11) +val CaramelStrong = Color(0xFF756304) diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/view/ui/theme/Theme.kt b/app/src/main/kotlin/dev/lhalegria/dogga/view/ui/theme/Theme.kt index c2010d9..b75a529 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/view/ui/theme/Theme.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/ui/theme/Theme.kt @@ -16,13 +16,13 @@ import androidx.compose.ui.platform.LocalView import androidx.core.view.WindowCompat private val DarkColorScheme = darkColorScheme( - primary = Purple80, + primary = CaramelStrong, secondary = PurpleGrey80, tertiary = Pink80 ) private val LightColorScheme = lightColorScheme( - primary = Purple40, + primary = Caramel, secondary = PurpleGrey40, tertiary = Pink40 ) diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/viewmodel/BreedViewModel.kt b/app/src/main/kotlin/dev/lhalegria/dogga/viewmodel/BreedViewModel.kt index 84a9e1e..100dd55 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/viewmodel/BreedViewModel.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/viewmodel/BreedViewModel.kt @@ -22,7 +22,7 @@ class BreedViewModel( get() = _subBreedStateFlow private val _breedImageStateFlow = MutableStateFlow>(RequestState.Loading) - val breedImage: StateFlow> + val breedImageStateFlow: StateFlow> get() = _breedImageStateFlow fun getBreeds() = viewModelScope.launch(IO) { @@ -48,10 +48,10 @@ class BreedViewModel( fun getBreedImage(breed: String) = viewModelScope.launch(IO) { repository.getBreedImage(breed) .onSuccess { - _breedImageStateFlow.value = RequestState.Success(it) + _breedImageStateFlow.emit(RequestState.Success(it)) } .onFailure { - _breedImageStateFlow.value = RequestState.Error(it) + _breedImageStateFlow.emit(RequestState.Error(it)) } } } 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_dog_placeholder.xml b/app/src/main/res/drawable/ic_dog_placeholder.xml index e58425b..825095e 100644 --- a/app/src/main/res/drawable/ic_dog_placeholder.xml +++ b/app/src/main/res/drawable/ic_dog_placeholder.xml @@ -5,10 +5,10 @@ android:viewportWidth="512"> 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/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index a091b13..0047d10 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -5,10 +5,12 @@ object Versions { 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 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 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 index e5b7558..a248b34 100644 --- a/buildSrc/src/main/kotlin/dependencies/AndroidX.kt +++ b/buildSrc/src/main/kotlin/dependencies/AndroidX.kt @@ -5,6 +5,7 @@ object AndroidX { 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" diff --git a/buildSrc/src/main/kotlin/dependencies/Koin.kt b/buildSrc/src/main/kotlin/dependencies/Koin.kt index 370aba0..d0e2a49 100644 --- a/buildSrc/src/main/kotlin/dependencies/Koin.kt +++ b/buildSrc/src/main/kotlin/dependencies/Koin.kt @@ -1,3 +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}" } From aec5d352e67b297cdbcdded6e44cfd0f400fc2b2 Mon Sep 17 00:00:00 2001 From: Luiz Alegria Date: Tue, 27 Jun 2023 01:06:57 -0300 Subject: [PATCH 11/18] Feature: Add repository unit tests (#11) --- app/build.gradle.kts | 3 + .../kotlin/dev/lhalegria/dogga/DoggaApp.kt | 7 + .../dogga/datasource/di/DataSourceModule.kt | 4 +- .../{BreedsResponse.kt => BreedResponse.kt} | 2 +- .../{BreedsService.kt => BreedService.kt} | 8 +- .../dev/lhalegria/dogga/di/MainModule.kt | 4 +- .../dogga/exception/ResponseErrorException.kt | 12 ++ .../exception/ResponseNoBodyException.kt | 10 + .../dogga/model/mapper/BreedMapper.kt | 10 + .../dogga/model/mapper/BreedsMapper.kt | 10 - .../dogga/model/mapper/IBreedMapper.kt | 4 +- .../dogga/repository/BreedRepository.kt | 11 +- app/src/main/res/values/strings.xml | 2 + .../dev/lhalegria/dogga/ExampleUnitTest.kt | 17 -- .../dogga/repository/BreedRepositoryTest.kt | 192 ++++++++++++++++++ buildSrc/src/main/kotlin/Versions.kt | 3 + buildSrc/src/main/kotlin/dependencies/Test.kt | 3 + 17 files changed, 259 insertions(+), 43 deletions(-) rename app/src/main/kotlin/dev/lhalegria/dogga/datasource/response/{BreedsResponse.kt => BreedResponse.kt} (88%) rename app/src/main/kotlin/dev/lhalegria/dogga/datasource/service/{BreedsService.kt => BreedService.kt} (73%) create mode 100644 app/src/main/kotlin/dev/lhalegria/dogga/exception/ResponseErrorException.kt create mode 100644 app/src/main/kotlin/dev/lhalegria/dogga/exception/ResponseNoBodyException.kt create mode 100644 app/src/main/kotlin/dev/lhalegria/dogga/model/mapper/BreedMapper.kt delete mode 100644 app/src/main/kotlin/dev/lhalegria/dogga/model/mapper/BreedsMapper.kt delete mode 100644 app/src/test/kotlin/dev/lhalegria/dogga/ExampleUnitTest.kt create mode 100644 app/src/test/kotlin/dev/lhalegria/dogga/repository/BreedRepositoryTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a46eea6..a07ddc4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -92,6 +92,9 @@ dependencies { 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)) diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/DoggaApp.kt b/app/src/main/kotlin/dev/lhalegria/dogga/DoggaApp.kt index 5b53a37..350f766 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/DoggaApp.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/DoggaApp.kt @@ -1,6 +1,7 @@ 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 @@ -10,9 +11,15 @@ 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 index 0f2fb14..474770d 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/datasource/di/DataSourceModule.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/datasource/di/DataSourceModule.kt @@ -1,11 +1,11 @@ package dev.lhalegria.dogga.datasource.di import dev.lhalegria.dogga.datasource.network.retrofitApi -import dev.lhalegria.dogga.datasource.service.BreedsService +import dev.lhalegria.dogga.datasource.service.BreedService import org.koin.dsl.module val dataSourceModule = module { factory { retrofitApi } - factory { retrofitApi.create(BreedsService::class.java) } + factory { retrofitApi.create(BreedService::class.java) } } diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/datasource/response/BreedsResponse.kt b/app/src/main/kotlin/dev/lhalegria/dogga/datasource/response/BreedResponse.kt similarity index 88% rename from app/src/main/kotlin/dev/lhalegria/dogga/datasource/response/BreedsResponse.kt rename to app/src/main/kotlin/dev/lhalegria/dogga/datasource/response/BreedResponse.kt index e92bb31..587aed2 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/datasource/response/BreedsResponse.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/datasource/response/BreedResponse.kt @@ -2,7 +2,7 @@ package dev.lhalegria.dogga.datasource.response import com.google.gson.annotations.SerializedName -data class BreedsResponse( +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/BreedsService.kt b/app/src/main/kotlin/dev/lhalegria/dogga/datasource/service/BreedService.kt similarity index 73% rename from app/src/main/kotlin/dev/lhalegria/dogga/datasource/service/BreedsService.kt rename to app/src/main/kotlin/dev/lhalegria/dogga/datasource/service/BreedService.kt index 75d444c..fa8169c 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/datasource/service/BreedsService.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/datasource/service/BreedService.kt @@ -1,20 +1,20 @@ package dev.lhalegria.dogga.datasource.service import dev.lhalegria.dogga.datasource.response.BreedImageResponse -import dev.lhalegria.dogga.datasource.response.BreedsResponse +import dev.lhalegria.dogga.datasource.response.BreedResponse import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Path -interface BreedsService { +interface BreedService { @GET("breeds/list") - suspend fun getBreeds(): Response + suspend fun getBreeds(): Response @GET("breed/{breed}/list") suspend fun getSubBreedsFromBreed( @Path("breed") breed: String - ): Response + ): Response @GET("breed/{breed}/images/random") suspend fun getBreedImage( diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/di/MainModule.kt b/app/src/main/kotlin/dev/lhalegria/dogga/di/MainModule.kt index bcd0c31..e3a0c92 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/di/MainModule.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/di/MainModule.kt @@ -1,6 +1,6 @@ package dev.lhalegria.dogga.di -import dev.lhalegria.dogga.model.mapper.BreedsMapper +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 @@ -10,7 +10,7 @@ import org.koin.dsl.module val mainModule = module { - single { BreedsMapper() } + single { BreedMapper() } factory { BreedRepository(service = get(), mapper = 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/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/BreedsMapper.kt b/app/src/main/kotlin/dev/lhalegria/dogga/model/mapper/BreedsMapper.kt deleted file mode 100644 index a0ae3ce..0000000 --- a/app/src/main/kotlin/dev/lhalegria/dogga/model/mapper/BreedsMapper.kt +++ /dev/null @@ -1,10 +0,0 @@ -package dev.lhalegria.dogga.model.mapper - -import dev.lhalegria.dogga.datasource.response.BreedsResponse -import dev.lhalegria.dogga.model.BreedModel - -class BreedsMapper : IBreedMapper { - - override fun breedsResponseToModel(response: BreedsResponse) = - 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 index b63f7ff..9fcaac1 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/model/mapper/IBreedMapper.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/model/mapper/IBreedMapper.kt @@ -1,9 +1,9 @@ package dev.lhalegria.dogga.model.mapper -import dev.lhalegria.dogga.datasource.response.BreedsResponse +import dev.lhalegria.dogga.datasource.response.BreedResponse import dev.lhalegria.dogga.model.BreedModel interface IBreedMapper { - fun breedsResponseToModel(response: BreedsResponse): List + 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 index c6da20a..35100cf 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/repository/BreedRepository.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/repository/BreedRepository.kt @@ -1,13 +1,14 @@ package dev.lhalegria.dogga.repository -import dev.lhalegria.dogga.datasource.service.BreedsService +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 -import java.io.IOException class BreedRepository( - private val service: BreedsService, + private val service: BreedService, private val mapper: IBreedMapper ) : IBreedRepository { @@ -28,9 +29,9 @@ class BreedRepository( if (response.isSuccessful) { response.body()?.let { Result.success(it) - } ?: throw IOException("Response has no body") + } ?: throw ResponseNoBodyException() } else { - Result.failure(Exception("Response failure with code:${response.code()}")) + Result.failure(ResponseErrorException(response.code())) } } catch (ex: Exception) { Result.failure(ex) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3a2ae75..c983e58 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3,4 +3,6 @@ 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 diff --git a/app/src/test/kotlin/dev/lhalegria/dogga/ExampleUnitTest.kt b/app/src/test/kotlin/dev/lhalegria/dogga/ExampleUnitTest.kt deleted file mode 100644 index f6db59b..0000000 --- a/app/src/test/kotlin/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/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 0047d10..bc7f48e 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -7,11 +7,14 @@ object Versions { 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/Test.kt b/buildSrc/src/main/kotlin/dependencies/Test.kt index 429b9ed..96fa506 100644 --- a/buildSrc/src/main/kotlin/dependencies/Test.kt +++ b/buildSrc/src/main/kotlin/dependencies/Test.kt @@ -2,6 +2,9 @@ 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}" } From f563bd58389325c14fc9994db309b0ba9f84cb33 Mon Sep 17 00:00:00 2001 From: Luiz Alegria Date: Tue, 27 Jun 2023 13:50:43 -0300 Subject: [PATCH 12/18] Adjust: Adjust theme colors (#12) --- .../dev/lhalegria/dogga/view/MainActivity.kt | 28 ++----------------- .../view/composable/BreedListItemContent.kt | 2 ++ .../dogga/view/composable/Loading.kt | 8 +++--- .../dogga/view/composable/Navigation.kt | 27 ++++++++++++++++++ .../lhalegria/dogga/view/ui/theme/Color.kt | 10 +++---- .../lhalegria/dogga/view/ui/theme/Theme.kt | 28 +++++++++---------- 6 files changed, 52 insertions(+), 51 deletions(-) create mode 100644 app/src/main/kotlin/dev/lhalegria/dogga/view/composable/Navigation.kt diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/view/MainActivity.kt b/app/src/main/kotlin/dev/lhalegria/dogga/view/MainActivity.kt index 2489382..e9d4c8e 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/view/MainActivity.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/MainActivity.kt @@ -6,14 +6,8 @@ import androidx.activity.compose.setContent import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.lifecycle.compose.collectAsStateWithLifecycle -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 -import dev.lhalegria.dogga.view.composable.BreedDetail -import dev.lhalegria.dogga.view.composable.BreedList +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 @@ -44,29 +38,11 @@ class MainActivity : ComponentActivity() { } } -@Composable -fun DogsApp(breeds: List = listOf()) { - val navController = rememberNavController() - NavHost(navController = navController, startDestination = "breeds_list") { - composable("breeds_list") { - BreedList(breeds, navController) - } - composable( - route = "breed_details/{breed}", - arguments = listOf(navArgument("breed") { - type = NavType.StringType - }) - ) { - BreedDetail(it.arguments?.getString("breed").orEmpty(), navController) - } - } -} - @Composable fun SuccessContainer(state: RequestState>?) { val breeds = (state as? RequestState.Success>)?.data if (breeds?.isNotEmpty() == true) { - DogsApp(breeds = breeds) + AppNavigation(breeds = breeds) } else { EmptyDataBox() } 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 index 5111547..b36dbdc 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedListItemContent.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedListItemContent.kt @@ -9,6 +9,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp @@ -22,6 +23,7 @@ fun BreedListItemText(breedName: String, alignment: Alignment.Horizontal) { ) { Text( text = breedName.replaceFirstChar { it.uppercaseChar() }, + color = MaterialTheme.colorScheme.onPrimary, style = MaterialTheme.typography.bodyLarge ) } 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 index 2d3434a..68bddbd 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/Loading.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/Loading.kt @@ -8,17 +8,17 @@ 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.view.ui.theme.Caramel @Composable fun LoadingBox() { - Box(Modifier.fillMaxSize()) { + Box( + Modifier.fillMaxSize() + ) { CircularProgressIndicator( modifier = Modifier .align(Alignment.Center) .padding(50.dp), - progress = 1f, - color = Caramel + 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..61ba545 --- /dev/null +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/Navigation.kt @@ -0,0 +1,27 @@ +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 + +@Composable +fun AppNavigation(breeds: List = listOf()) { + val navController = rememberNavController() + NavHost(navController = navController, startDestination = "breeds_list") { + composable("breeds_list") { + BreedList(breeds, navController) + } + composable( + route = "breed_details/{breed}", + arguments = listOf(navArgument("breed") { + type = NavType.StringType + }) + ) { + BreedDetail(it.arguments?.getString("breed").orEmpty(), navController) + } + } +} 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 index 126ae7c..46c1cfd 100644 --- 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 @@ -2,11 +2,9 @@ package dev.lhalegria.dogga.view.ui.theme import androidx.compose.ui.graphics.Color -val PurpleGrey80 = Color(0xFFF3EED1) -val Pink80 = Color(0xFFEFB8C8) - -val PurpleGrey40 = Color(0xFF625b71) -val Pink40 = Color(0xFF7D5260) - 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/kotlin/dev/lhalegria/dogga/view/ui/theme/Theme.kt b/app/src/main/kotlin/dev/lhalegria/dogga/view/ui/theme/Theme.kt index b75a529..49a185a 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/view/ui/theme/Theme.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/ui/theme/Theme.kt @@ -1,47 +1,45 @@ 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 = CaramelStrong, - secondary = PurpleGrey80, - tertiary = Pink80 + primary = Caramel, + onPrimary = Color.White, + secondary = CaramelStrong, + tertiary = CaramelSuperStrong, + surface = Color.Black, + surfaceVariant = SurfaceVariantDark ) private val LightColorScheme = lightColorScheme( primary = Caramel, - secondary = PurpleGrey40, - tertiary = Pink40 + onPrimary = Color.DarkGray, + secondary = CaramelStrong, + tertiary = CaramelSuperStrong, + surface = Color.White, + surfaceVariant = SurfaceVariantLight ) @Composable fun DoggaTheme( darkTheme: Boolean = isSystemInDarkTheme(), - 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 { From 7ec6e567c955a5b11a7da1d3dc04ef3999a8e4c2 Mon Sep 17 00:00:00 2001 From: Luiz Alegria Date: Tue, 27 Jun 2023 14:13:11 -0300 Subject: [PATCH 13/18] Adjust: Add string resources replacing hardcoded texts (#13) --- .../lhalegria/dogga/view/composable/BreedDetail.kt | 5 +++-- .../lhalegria/dogga/view/composable/BreedList.kt | 6 ++++-- .../dogga/view/composable/BreedListItemContent.kt | 4 ++-- .../lhalegria/dogga/view/composable/Navigation.kt | 14 +++++++++----- .../dev/lhalegria/dogga/view/composable/Toolbar.kt | 4 +++- app/src/main/res/values/strings.xml | 5 +++++ 6 files changed, 26 insertions(+), 12 deletions(-) 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 index b0ff64e..6928d21 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedDetail.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedDetail.kt @@ -21,6 +21,7 @@ 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 @@ -45,7 +46,7 @@ fun BreedDetail( Scaffold(topBar = { Toolbar( - title = "Breed details", + title = stringResource(id = R.string.breed_detail), icon = Icons.Default.ArrowBack ) { navController?.navigateUp() @@ -91,7 +92,7 @@ fun BreedImage(name: String, url: String = "") { .build(), placeholder = painterResource(id = R.drawable.ic_dog_placeholder), error = painterResource(id = R.drawable.ic_dog_placeholder), - contentDescription = "Picture of a dog from the $name breed.", + contentDescription = String.format(stringResource(id = R.string.breed_image_content_description), name), contentScale = ContentScale.Fit, modifier = Modifier .fillMaxWidth() 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 index af62730..fa5022c 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedList.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedList.kt @@ -11,7 +11,9 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.navigation.NavHostController +import dev.lhalegria.dogga.R import dev.lhalegria.dogga.model.BreedModel @OptIn(ExperimentalMaterial3Api::class) @@ -22,7 +24,7 @@ fun BreedList( ) { Scaffold(topBar = { Toolbar( - title = "Breeds list", + title = stringResource(id = R.string.breed_list), icon = Icons.Default.Home ) {} }) { padding -> @@ -34,7 +36,7 @@ fun BreedList( LazyColumn { items(dogs) { BreedListItem(dog = it) { - navController?.navigate("breed_details/${it.name}") + navController?.navigate("$BREED_DETAIL_ROUTE/${it.name}") } } } 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 index b36dbdc..907f193 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedListItemContent.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedListItemContent.kt @@ -9,9 +9,9 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color 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 @@ -33,7 +33,7 @@ fun BreedListItemText(breedName: String, alignment: Alignment.Horizontal) { fun BreedListItemIcon() { Image( painterResource(R.drawable.ic_dog_face), - contentDescription = "Dog icon", + contentDescription = stringResource(id = R.string.dog_icon_content_description), contentScale = ContentScale.Crop, modifier = Modifier .size(50.dp) 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 index 61ba545..99e2b29 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/Navigation.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/Navigation.kt @@ -8,20 +8,24 @@ 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 = "breeds_list") { - composable("breeds_list") { + NavHost(navController = navController, startDestination = BREED_LIST_ROUTE) { + composable(BREED_LIST_ROUTE) { BreedList(breeds, navController) } composable( - route = "breed_details/{breed}", - arguments = listOf(navArgument("breed") { + route = "$BREED_DETAIL_ROUTE/{$BREED_NAME_ARG}", + arguments = listOf(navArgument(BREED_NAME_ARG) { type = NavType.StringType }) ) { - BreedDetail(it.arguments?.getString("breed").orEmpty(), navController) + 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 index 8854e84..52f1245 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/Toolbar.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/Toolbar.kt @@ -11,7 +11,9 @@ 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) @@ -21,7 +23,7 @@ fun Toolbar(title: String, icon: ImageVector, iconClickAction: () -> Unit) { navigationIcon = { Icon( icon, - "Top app bar description", + String.format(stringResource(id = R.string.app_toolbar_content_description), title), Modifier .padding(12.dp) .clickable { iconClickAction.invoke() } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c983e58..0d7546e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -5,4 +5,9 @@ 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 From 93284b6f4df78f1f58bc23226ab2ba7d33040c6e Mon Sep 17 00:00:00 2001 From: Luiz Alegria Date: Tue, 27 Jun 2023 18:56:12 -0300 Subject: [PATCH 14/18] Feature: Add application icon (#14) --- app/src/main/ic_launcher-playstore.webp | Bin 0 -> 27616 bytes .../drawable-v24/ic_launcher_foreground.xml | 33 +--- .../res/drawable/ic_launcher_background.xml | 162 +----------------- .../res/mipmap-anydpi-v26/ic_launcher.xml | 7 +- .../mipmap-anydpi-v26/ic_launcher_round.xml | 7 +- app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 1404 -> 2216 bytes .../mipmap-hdpi/ic_launcher_foreground.webp | Bin 0 -> 2002 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 2898 -> 4222 bytes app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 982 -> 1402 bytes .../mipmap-mdpi/ic_launcher_foreground.webp | Bin 0 -> 1244 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 1772 -> 2536 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 1900 -> 1616 bytes .../mipmap-xhdpi/ic_launcher_foreground.webp | Bin 0 -> 3052 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 3918 -> 6014 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 2884 -> 2416 bytes .../mipmap-xxhdpi/ic_launcher_foreground.webp | Bin 0 -> 5194 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 5914 -> 5300 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 3844 -> 3366 bytes .../ic_launcher_foreground.webp | Bin 0 -> 7294 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 7778 -> 7138 bytes .../res/values/ic_launcher_background.xml | 4 + 21 files changed, 18 insertions(+), 195 deletions(-) create mode 100644 app/src/main/ic_launcher-playstore.webp create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp create mode 100644 app/src/main/res/values/ic_launcher_background.xml diff --git a/app/src/main/ic_launcher-playstore.webp b/app/src/main/ic_launcher-playstore.webp new file mode 100644 index 0000000000000000000000000000000000000000..3b43a8e6824ab8d76afec995837b3402c0d3691d GIT binary patch literal 27616 zcmV*YKv%y~Nk&H4YXAUOMM6+kP&iD?YXAT*|G|F%RdId)f4bb-(*F;RYt=l@<#J6f zkLB|0a=AQ|Yp`7&s>)Ti%jGJ&=6SB=9Hrv=KF|6+&w8GfdvAA0q%)Yi$Bjx`vUIFF zpTQu~oo{+HFbmoS#f;L!>dDN_wseM)&Yen;wp1jjn1NZCfqM+PQ-Q5bw;qrl1I)q- zm>IbTOIr$yjttVnMq4tlbc#iaWFGevZV<^RJs9{7Y%51{4?*tHz%0yQ^%xX*wgOfU z8*S+hC3T2pR0WJ;WKunuouR;%f_iUhfk+nyxyJ>}%nYhWqY#7W&Xa-39V4B=QWr~) z=9ab;wpt2E&%>4sES<1OTQV^UGpIsl2JX>dDcURwD6-L=3z(T1RF6R>C2c9}3=2d$ z>>lQxiWtSHfQ`%y3?c>fWRNqkD3RPliDBDFij%)^%y$bovpLG zyK8nkySqh1U;kmzwpFFt)Ua#iRYV@=#R2>JjQ{@&{ox}2agqPH$bVeqKQ8hg7x|Bi z{KrN9<0Aj@0|cSg|3Aw?MJK8nMZ*}{%GOn$fe4MS*!0TGkD~5T*FQfEx4!9x~2Zn)jjHU zrdM{~4n5&+nyG5kLY9L$w2N9lplFb~(sYz-^hB0N-E2GBkEnCaxzu9MQgtg$&(!rw z1MfKUjWhqS4CIYJF9yVM-_&zY^&X|vrRF>LbcY_bnf_+WR_|!<(-DrUQRRP#j@}X! zj@A)&?I)>sG)stE2Fm0GL>UJBZ@}i=cB5Z+zp#HP<`zSkq;U1 zni`Q{Kf{n2`*RA<)IFNLt9q5XZy1lt-@r*!C%`Lj^`J2SYTF*LJerA0MwBtM2Du+l za*~!ZO|QJgbbiP$%rnxY5{`qE4WF`Dd>rXsN-d|;a;tQOw>yhOWinHfrQmR-icwSz zDNt9My3sY1UZ9~2O=W5;OGntc!qpR=zVHp7(C7(kKk>;KddlB4QA?PcHIzR2{5sDY z%1%)>Qfi63L#4I+e^S-0SWeizsqRs=8t(bQyp4rN85`axqB<=Oo&mh-R)1R#r`eHf zDND&oaz994<<0KePjt6qP4!R17<@=jhHAgCnBQ``kLd|V*~v8|NbmALq~v5(L!QzR zj=l(tuVmgWds|Ph?FicSaK|3A?}~XshwjF^rEvKv5vNze5M%%Ex&XXF5P)9kM%1~44QUxQ_kRqa$jl9YH zDBkB%ihew=MjF%8z2nfarJFA6$kB2R(9?EG^pxLXx!iLjgm#JWlAsRKe4j9W`7VXJHxuv?7a_`*im;QX>IDnU-w2z`&9HQ}vCBl0!e5AN? zem11Yf9TTs;O`v|*!0tf zSNro~PM4Nn++gdBpX8v1(v^**;1KI4h#2&1aw;b_+&br)^$YX7GGZb+ZKrFgH(8dM z3J#0thBJhzx$nV#?_D;ZIMv?a{29GrEs`Les^S}vi-8;!SKrFM(9va%TZ zfSdzrMjPhTvXjyz#0F^$-Y>Tup(kdsegdjf!IY+IDf5YD%hsi4w}$c|O+tx1qFVn~F=t09U^!T+VT{@$dC@gj2Zee5 zdEY+?nL`>U z!z%L*SurhaQ^iR2x7IUtVi8Tp8@E+#cqhU;5%2`S%VP09Jue!s(R7c&M;ttI%c`@* zRUD-jX(GHr_|T>1EUxMg(h-iH@U@k_`FuicO)<`6%u#UYjZMSbjnKaQc*LqC2ZkmjlBl@=a&koA2n)DUmQ%Er0gGG zMZY>DkXN7fOdYqr9cN_=sD*v|XY)%r*dEY0` zH%xuQB#<}#J=H&rV&N{8 zkU7M|b5NLdsrgpZHSCVbsVpbRYHIyo47A-^KfrQO!Qldh5}NP*~&% zIf*s)hgZiU8bPOkupSUco~hHbRHixBw0%#mvg;w6>2E!Kt)|Crgr;5RRvMnE^XW%o z@K!CxnNkOTpHa3WsBZKc%HM{8@EuxuI=)sO8}Qsk9;tokUFKJ5@B|u4S2i+On-ome ze>}4*Z@i4Hhx->_HzxOXPVPdnwzEh}AM3yK+`mmi!Nul*l_IOU+Vn;^rBa zN7G`u99#NXe_q7FL&61t4ey=%T*|NQ2sGM;;YLrWEzHR`XUR$gte;RKyLqy{3L0#k zT@UrhC7(Fikj`PzRTdA}Bd)pDsl|Rxqir01hq}@}3&>5F9AzVGDD8~-Q8e9l`xft= zTcd6h;#wh%rn_Uy8a_U?(;yhRpJ0CEeN*=~9MAOgzE9%!3Z`vly`#P3EZ&N>phQ(E zI?8?iQm@#L1C$M)DlumnNc!hb52IrcZ~eoLpO!jVU-*_+>(Ej`w5}GWC=x`$cA?!;Mr;9Po-54k2z#|Whh#a zS;Kpj%C2~L;`q2U79Td#uVNJHe*Khl&Boyq$FqE!&G6N}!W>k)t?JvTj5r*HZ@x97DW-jtIr^O9yPJ5~3pRg{FP{g7jHg^O>E!W)hqk4LOaD`*Rg zp=f9r-KCarl*GCH)1bw4Xetw>T!|o6_i-8=-KkOJ$QQ(Mh`y(9>aG2jtM}GFI@6~x zd_rB}oSm$uh>B5^jjUwEU#DOMmV>3GrE(7NYijtVrSeAjb%m-%RaZn)8Bggd_k2E7 zo%db$YOI#2<#e?l(c}G6e^e|ymXqc4i=0~a-76Iws`Y;{=%{5SXPZvThpVw4xjkxS zA|*eV$!@tG4;a={ngFjYi*17 z$tEpjj;>U4lDtE5KUi8L5(tWm861pehA9>#1(B#!R-$OIijg;;)e(-Nh~}<29>}Zd zBN9JcOms8l2kJjQ>jzOsw7f&w%6joz69AK9#a-fu9aUEv1rs1dI>L6%RrZHVv6zVd zVb!t6XUNdEqZJ%(c=Y??0h1Dklt{X4@kp)CEzx)z^@OWp6h$Y>J1FOXK#+q_xPW1D z1cDltgR(+Zqv{Ixp{KvvJB|W*9g7bl^Pu#2czWjUaD^X6PN$KbS@ksufMK??{>y{4 zevop>4OjNeu753}KMB)klBvWAAd^A z>FidEy9g`(27jN0?xksTg~*eBv4TVT!td1Mr?Han2Dj4C6;2J6g5{vWZ_hi@t5jP|=arhuR5r4_Lvjv~r?yrs=Rk>) z!38?PHoq#H>37e~NAl9ngMUO^1JyyiZno^oPRT)?-}jDkTHN9C3q_&L?$+wtcC;uyU9hsB(q-F!LgJn2~_ zBj%7YHVd_sWqPE|m*YqD&aHuA7K!KE33w40)@_DYU1`XpJxAF{!@HLbGvUdvloj}T%Z7SU0C)8FwQ zG3Rs@ZZ2{VZ(rSv{7PUH95Q^u4m}n^TP%?@=bF<|E=AIyb1LjcB%CjBaUl%g^)uSa zL7!z7D><>z*87DyzPzQXEdQQg+~DX8MOdMx)R|YBKDp)jxp?elm)f_abIW%t1()*M zwH6#(=6s>S7cOf)?Tweg;;N0`x4!Vtsu(#{&H;w(7nQt2Z>lS@a_W?Z+c36W51#5? zialz@wbYMp8|scrmo^NaVCWyH@vTsp+G1)Jl7{qFk(D_*UhG_K!pCOuC!Lu zJq8c8F&;3#uym=#7RzP)zSWdUNtG`K&M*~>prMQ>#;y;3}vTKW5HYVroQm@ z{=iT^UW?-qjaP@B)-)!A{-J8r*8{>DgLLPfo2FMYHZaap zVCl1<;&a}mqnr|klEtkzwOp=qm`sz~_E}tY z2?0dUSvJELPA72hg#+Sfn1U0j3$h(9wG;=a1VSu4R%j|yM1X?!3D;b^K=1=DYLA*< zrIHb;TKZO?Y@~bFUV=q9&+cB!GYF_U3pA9zu^fEF;j>4n7(cO$vaU3BqqUvEFgn7; zY_C$il7MHa{^OlGEy2L)m+M?f37@(H%LxV|?A+6#bij(ne7hbhARr6fudCdna7jF& z-3S*jRJFx=M>`;n!8~KRe^{O%;F(jJ!He zk3rF3A~LWOV}u|P`&%1q9m@$6mCbJY_p-s*8D+g~L=Xv<-u>F(?}H&H7=@Shy27D) zEf>M?lpO zhJqmPP`$17N_{qbKskxG5)!-WM(@<(v2e%OS_EC}diW^;9jINZ7};j{Bj|)tmJST_ zhTA|$NqVPK&BWk69RM3XAE|$IG76%#opG+Y46udWKMo9^Fo%eX>%hP;6=TTTIK?)O(=Nmi$5m!@AQNP#&3-#$;Ye8T5BI=3UuUrI?+Qp%KJU~Rehzy_b zWw_NUt-fb6pDu!iGTcfNL;oV2j*FKPa$VmN%sXfU=^?zcnE_R$P|ni)Dz}QEXnFEy z%l|MTDYwn^y$6Fx=Nl%bS0=f_5=xpcM+Bb1D%Ifc zGpEjz5|X7Bz8*|lXoKlDHrhrRrDUutU1})~QIJx6YBwPinv4Cz62t07pVUz{GR@fC zVmYuD7DmdjZ%Jh%60+|qO3IAJs}!PN^ep9r423|!33~&0DTXLcf3krH_(VX>XzO#~ z5BbE2$&>NvCe(Q{pD>0+X_lECIWdZ429NI@hirI5ZwK=Fu2c$vu5x>p3Y2K1VRU?e zkW97sy9T65Hb}Ex*lK!MKY@0T`n~y5P!RtM-z0{J*?c)eX`c>>;klN&p71E7TGe8? zc7pn4$2SvGZ|&3*k#vF8tk!BOgNDp*R=AW>E@-=?e;k-!1u>&1-AGmHy-J>RCDKNW=t!W_9dei|v9fhAaFwVi8b)j7kDLPEd;^+BI$e;RJ0 z3jFKsM(|-UM_sX-PXrX5v?c;iFmGnQl9Ol&C7pWwNw8_|nL0)y<`eUk>+_&}kEH8W zr4)gpzNyRpa419d2_uW6VhDkbusw4Z=B!ULze+wm1p^U|0UK7qv}rbN#FQG0`B8Ys z0rV9%;9jY3_=MDe%KJf4(}&xQP!2KTss|xjn{&V`Hn5B>|{P&h03eK z=?q109H3olUd};+^m^)&F@f=ujE7hURm%*YfS}Av|M5T?&*1N~&5;eCqeGb4q_4NT)j}D7+r8t^95cKy;)u4@%u>yMQYt?F-AU$I7&Q`-skPa!Mp^Pr=_w%9V>ricFbGcl7lVuL( zEmR1uZnhkJf@&_ej1oglsh4)}=`H9A$2Hd>@xfR`yVat)(ug|QcL(tnl<~Nm&($dS zP_7gxG2C;*Kmf2?f;bMB7Vrr|o8esu$(uaVa=KD!50y64A4W&0 z(%=^sgo#(E8e%(w5Svyw=?*T&Vn|US%_|M+9?fSFC9>M1lyd z{l{B-8$^2Q+>(UV88)}?k!ZX@v?t#<(NZSYe){5vp$4OGm{=Z-h$wt;D7jP-Q;W$KdN(9`grk^4#2HVa+BweHceB*RG zp9Udlyo~|8g0c+%uv|dc-rt#-2slLVyH43iT#A#RAYCwTZX(PUg0hjHtA?(iUZtun zOgilkJYrBq>xyq6jDxkZM=1dpy|mh9ct49fRSn&99ikU4T>J)~uy;V?Y&SOC z1~~_45v5k!92hoGaosodh?5=O-FkW>@d9x$JaTIm!;Pr(zQg&v95`_AII4R!gvr3m z?t*Gz!fOnlkfBBFH@=bw#Q@bXkXN513|Zz^wN(tsAFboY4>NW>1T}qgooh}*rhZHMJF~}wpXAlw$n%|{4jCnz411_0yWqkQ8qI7<=BF$La4y- zOr6?KQcN5f-Y+bn>eI6Ar8J3Bi>vzda^MY*+*VgAaVOqTM7?txJoE3U;XWaHrNwKxH|&Hv?++9M=&R zpEx+xOc}(4X*IpyK$}o>VvFetqZ1@J*0Dzj+yoM`ZGCx~xw^Z>&AbT^p8j;3TL+IUtVAj-0^0 zFl3>0Tn@xspVU3Ur_WI5N#_7pZF;_pz?1gd&q<&!n_jtqySChNc|bkEOiiUT1k&L1 zfKR45wrl~7Vxm*av*90*-Nw+vKm-J4{%1S27--F`_3{;RmH4!pQLlp#N;~UmA@c77 z)sPsz&G56FK%*$M?S4>h%xW2l-2IP&JyZ59)dFt%v(^tByadA51*HU{Lyv8MWDf4; z(1h%Eh$p2#cyf$F+DxQX;o((o8Q^ZP(V2Zx^4FeZ`l|16YoE;G9d zKAi@OtMUsANcNyl><~#ZJi9R-UjY7%Cd||A_0Lgd_sR-qyTWPxe8iE z#R|}X;C)0={|p}AH;h4RJwG>xYry&m4`o1DK`67Z{?UceK@41ezqpcCp=_kZfU4#4 z{02u)sNR=DOM%8r>z^O)>5QP#p`{`FDQx&0C?OIy(;o*Yu&ulY3TYJzPPljyRB^K% zK_cJCvOHR-;wE-(1+FngEz%WUzcfe(@YMs<&hRh;Y8*Zx zL*2$tLS)g+a$u@Y7(;*FsxN%*klQUe2GyCWj%x{1KqSnMGMXMz;F@u2S#4qQ=@p|} zPj5c-LC{h&5uMU@sd*^x4(6@p(a5+%bM}2{klb{q9-qY!$-Q_C`sBj)J)&+jnnj`2 zwv2)(50A?IAfW;PtJ0u9FH<%$chF282fI=A<9YLGS5qp17me&IAibq(w`W> zo`)!VTLem69EIB4gW_T|LsSc(!e;mbc!`k)M$~yy?%uen8T|djKn&hdDxeZeEs$V628G4ll|+fnKs6@e zgGGG0#Vp$qfUJi~R|*JHU~ROGNj|Ayz&j4Pqbnj6BijsL4BRoDYU-0ZrdLJ`E+C zk*t^X6$ymw>weM1Qf}j%|EQy2XZq~#)xDPWh0hQmdl5_<2)}9`L%*1H6@(im65yNJ zSN*_5z(Lo9kLoIy8-@1ugzJ%-3GkFcHpi`&34O*gYk&@fmIUJQ0Y(s-L# zKLNe$$H#?`<{l7 zyII_9LF@H$NOHt_TM_3Sg0sIHLIj>467gv`s2TE$A?$hw;#ymY)iwu&71Ut5M zbvpqKlzh7pMlAJQWB3H1Ka~T+%;d^y{as6J4^vaXfy=w0` zg4+8l-pk#U=ho8$^%@e)+q+WHDlgg$KeR2eT@Q|?WeBEv#{q=(8b(K2@}}+$;x**W zvd!>)#t(3JDLU zHD3i#Y_{CTS`J7^2@8VEPeP36;a&`|o%hI(!Y$D_%UBPBc> zgp}43;q{=3Q@P#epe{8(0zlu?MM`wl? za&tWz9b*{np-p`oQiDmugc5*sHtj5*p7HA}j&c`-X_Y8pwN22rBJg+<*JDaB?*Qbt z9v-Ak=Vr?eZmVODsw+a%kaOUqAI~9dL)vS5i|L#IIWM!F>rq?2cLCU7*TZOg--VFZ zdU`8q8q67RgdSUv_S?HA79IdXXlE=^u7|d={lYRG@Tav?PcQ^ouHD&WTa=8*r|B^B z8i@9KdK1d@+P+6}f$C}?Z*+vs^>|}ooC9Eg?^9VQ?e6qRT?j$EvAh}?U8ACF$qj}# zBzj{YLWG3ptV3l0{KIN9*CWd}POb%_ZW>DEBXvbU%f)_SaYD`kx<=*GWsn7*c`PR& zBs-rH16acPA?`0cKP?1I(7M!O0ZOIW46xstI~ck~VY6w^g*ea6rwt+JdDb)lLf_B-_0ZiGGubgM<^ zRDYV!{dlkpz~d7U^4y0BA!kE6x1_q#h!EOUa9CSd#!s?tSi@szyiNB`sdFU3{>I!G7`Bm0GKOb$p4Rg(}YU)8{BN1`dG%}GkslZ&Lg+a-53zoNBqNbk7w#UXE!2N z+gzk-RMt;mxN-&+3iXBG@EHH1A2@zM9QubvAg=;>?H?B2aXfzhsz%3T@C3Wj4-r>a zxQ9WTG1|gJ*wpKpD*zG!e~ALnqm<$S1K^$-2+iIoJ2?{?%jJf8TbqRzGBu-_Ud5X3 zrR2}cSVR-yHTZiAr4or$oLv?VSQzbl=XQr4o%=V-JH&9E-4G3>|2*3f{PNZD@Js|e zO?<)O3kQFnM8M+_TYa=$4{fhNeF0272Qps`pFo5zwYugiVDuvau(wbEj>q8>K=0I? zfl%i98Y$#HtI-oPXvgwmd?hwB&@cYsn7V{Scnu1(jh0)jZgd%UDGe$+#pFuuM)+JD zV%Ud&+nx?RVs@1UXxmVuLCJ#<+OEq&hBL0Y4q(G|6o6?!*;(b0+E=2>vo)2u2NXKr zg!FuCDE(sBQV*x&wfF`l|LGSDL{P#_`W6$LE&KS>5?o|J|2Dn9z6` z6dXopvE&+{(w^&5iWC5+mH{MBQ?}iQF8S!IARw$D^eh#W6jzpmD;JN!J>*DX1f7nX zUb&(ZxkY-NDJkmEqq}gAhBz&RcPmaRI8=;qQ!S+Iolx-XU8HM4^Q~)v*G>Vy>D>GgHd#%$&-Bu zw)j;QkFO00@DhStG`C#0II5+^=!9<@Y#mhnPK4JAHA4_~S_WXlDGI^Y>mp{-LuTv)itRp)(f;=rLsVEzu^2mQL=Zf;4+)k5XwmZS#s|YAf6LN#?emzS+}L zfJ@gick3Rt{10)%zdQ*Ik3?J+#M_TI+J>K?D?Cl1>p!09l{HzGX+gXOCeoz())UCS zP`UsdY=K8^trVfciN2`|r19yQ6oB?4@+1JS)PDk9eY;yN2m!q9Q>ac7v-yRc$+j=v z;2j2csVPm)!6B~FTK`v;p@@EVaM%H4*aUd>Dz%d})mrRif+jxbHs;qg(E<9DXkn2VL+U$%R&W{D}{P3kHLGV9-{z!&q4tG z)Qvt3wB6TW|D>XV>6P_MeE`vT`^6Lh+Yvks zoClvUu6xzUgbwd}Cxk9FhsxwavC$KGF(Tes49_)4!4+gh(MgJV&f=_9u* z-Yf?jZYmhe*>LQY zno^xx9w0@R_0CNL5in%NS?8LIMHHyx_Q|~>JT=0t*~u-*z2z*T64>~C|5ONn%nT7C zG&s6JVJ-z|_E%E?nlHx$$a55{Wk!Tzb)|g^6xrvDuOtVNzrOb*&snN)IVh0RzK2gk zQcMkxLCyhEJKN+H`_klygNI8gsv8YQn?%!?uB8Dqf&9Yq9jLyz4}x!;8b1lD7;Cf* zuK~EE<#bU19u21>fL%?t1y$WmUwEL<;jF#pKDu@c>n}y5}OI4hM36OD@1_?_P=mJecAW$G27H(NFMbkW!g}O3>nEUcLJ;yHTY537O<6B%Fm0p&v&wpzDF7dZ#zC;C z#dM+=b|X9)5t>cgC};#@<0t@@TOG_>$a+?*=|)9$ZrgoO zR7b=0Kq_(p4K{o{b7v(Uv|R2mI!GZuXj)7MiD7<~AiKQ0Q;U`0VM!cMgG|ZQJt{<0 z>=U5q(R^)XA;JStP9I8#dRt2Y0DB=0r&H_cMudqya~Z-7wXdiY4HiJXbf%2?c{uRq z{or(cMA7vs5~I;Jg5CVZ;q!4VW#Ewr@<=T9nF6WBF?k28HY7Dh?kNOCd;jg|7M^c^HqgFsM_G-tLVaSUIKu^yS;r*>Y zfx&K;%-tdc5J6{~;Ya1XZTC`!5J^{ytExehR9}R$s$kx&qW~B^5kx}_r}G*S6397V zeiWx6IM+NXnNzE627ME+cFpw}4Cb~PFXP)l0fczKy3_)Rd*5o;LwTUsLj<1N^Rbe0(?n){TkBY;5O=sz9>Ku0)`3q|?1Jy3$?oi44w{%}ax9OPXvmOsiy64VjkY051qKk6P<~jGq3g`=s7XIMzzdud-k6C*Vs0qH8T6 zBf?DwQ1#_sg~nV#o9Vj&s@!g(0O$yN55PJr|HVi;4;R&b;;rcb{KHbs5QYdyK7T+Q zS#RrT26)%#iCzuIgHVD2Wg~J6q`5CoNrhO54UYc7TnQ0il$;FNkd)5;2_h88vK;|{ z)3e+!taOw^0nk#`IiSg(cL<67z|;m?2U^?cR({{)69Q@Vf8V(!M#?#mj1>6pZ8xMz zxCofDTk;W`;jc`#HEm(Fok5>C9w8^(wW1uuhuI zH*Q37`M1NpQ!}&qbbiw43hQ1?i|KG|nZBuaIBA+?h(S)HZGk73WeCYXKE(`4sfb7D=7d94%?1kJ`igI;ur-P zd?e&>mt?isk=Jk=1cgRV+;rJic%;^@SVVD84^8fk9=Yw%(#@v*q_R^4+=d*(Cm6LH z(mp?dg88)%xN;ZT^>7IVK;8*0mkUS}(m`QXfBzuhvo$)#a;Vn+&e6qOhFK<8+Gcpy z2Jkut?}NV&XY&LVgO5O7-@0bqZY~4Aa*hx{6y6>y&&C|1omocLi<1QUf6PRD+!ch$TC8CRj!|7G9e z&drK(*%z+@@NP+(I_i`+3BlroRvjG`J0(59GU7>tL3YSvE zH*mp9gKMjlnxgjJN{fsk1mLa9%Oya6a04)( z7KqUo!n;P>KnaGIERT8@(B=-IU8~{Zm~NKUbXg#td3qqPQ5KqegL#J%@rZS;1w5It zoC7nPEQ?#NN8kzfwsB4qk9%%dFRQk&a9z?bMA8K%GEk}nsycF53?Q5i&8K}S$EU8v zbO3@L;$8KQmQMvRJ93{e1_)4=bb@zGF)>IGT1R zzwm9SXS^{H@FgpXQt_}qLurQ+5p+SR5+dlE{;D9Zx>L<^?8Nyuw4UA&-UHOvfnjET z6qJD4Pt-T{fK!^^p+`7XohdcF@_V3n%BG-%k%+gTMBb&?r*&5SqrWBwZs;d4N*oLK zZU0N909)?p2ug&~{%(}fYBT)V(D8c5;S?FCQhRni1PJt&!FzoBdW<|YZ>yx@tgculR2T?*zDd4Ef6XErou5cI*Co#gg=5m2Ds%NRpqy+4C=xKnI zkN~d_Y~oOB@8``1XQ$xq8l(z8_C1+_4nmLI#^A+@w1j2f5|Mbp{+M|1_pY8E%7ksx>W^v+1%vvu2f3%{=QHXS*h3RI}kD#xZqe4lSjn>>3P~Z`YbfSbG`#y@w(t z36y|#I3xqY$a1SW)WoXjQOW?>Im5YTsli}>X}b{uI0;pKQins!gOgX~Lb-bX*hf6z zihAeApBGYOm~P*buor(d73;B+in#jWL2)rObC|Ajhe#Jp4Ku`v*A_l)E1*TUvM~n)~2gbSCm7|50GOhv5sr5-+4hJKqC;XR^ zM`ZfW*2BZ62Fx7;TnV#JFdPZYwVZCK*)9y>9qK0Y8=ZQh92M9f-aEGpp8z`aVs%3@ zW8sGJl(ONS@0J@)r&aH9OYmp^D{pem^$0wPrC8*nOt?o_O=bR;Sa={KUG*&0vuMF` zuv3eHq^C%_T+I>lnHl+EpkDO0cT)t6MBFd^0g^|rH8^?^N3It3D3#lx(X?jEj{M-1oqW~d)2yj$`T-Til%*!Jc&_Z z={Vw$MF5e32)dQuICyZpMb}(6z7j&LUz%L$+n`)h>Jz75%;1Lc6cc37(HS4TF&K=)OYS)NaHZ8@it-PQrVf>%0gSx zi8PJXsmHV7-43U7lWi%4ilrx9b1m_EahUpFvbZW@`q5K0gYesBej9L*d^^o5HD)>PJ)(c{bG-(0Vc8>)Dd>)mOL%ygyQhWd=hFP$fo$j zu}}gMx$S#WM>(h^bmSaZ`7Qq`JlkJBacXhZGU&0ROU+kHIjH$20(P|algJoCQ1k8J z0BofOEDsB~~Ulw)8>5IzYLzN3B#t`yYo`&r;QYJe-NQ(5dABSI?$9whVQ6>mX~k8~{5x z5%BRiX(ozJd?uJT;MAlHh+|#h5NP;q^JA{1j&n8|)0Wu`zrOHp26MvJ!?REV;${8E zBO)!7ZaNO{`a+xGn+)z##kUR3#fLaR`=nm;<(Np^(23<7XtwMjyc0tk3F2+5=_$lX ze|IlUfW{NThZf8AS$xWP!|W<%11;_wt(FProC$3MrjfKq9I3NbUO=d^QLD!XgFWuUj1DU&^P9u*tR5ssRsDJGPjT5phw~ zR^@}UW_9^NV$*o((t}NxZT{*gx&m6B=Y8*3d>rnRd+kPW-R56c45o^MuDJ%PA}^ox z(L#nGjfw{QCm^hrgC&YvZWxNN20VP011d%~6j96RcFXlp+Q+~Rc#9367`!iX&yCj8 zGgZ|n#7+{f=%FP-aKg1$RpNn?!R>oEA8NZ%iL1_|KmIV(=-Py5X?fJXVVo_2*eZ4- zoJIV!{%H$qc+2VPQVZ_6b&pRN`|I5_w=c_YKp!N1cPm$j4ehZu7K#i&KfK#;l)Uy{)6tlSkK{=W(3nl`n%t z)kMI(;=ts|1*GWnQA?R;YCq9%%cE_ub>>%D=Y1Q9Ku6fKuxIC4(TR`2{&<<px(7 zF0DpKSk5i?F_4%%C@id&DHVH$sa1?pamMJ0&91!O(LQf}6t$oDjJ7aG|2h&FJQp47 zm-?WPL@c87^&SV$JRkVPN%9Oe{rpk7Qi-!JOr0n7%$;n&6yh38tMH7v(mJ$A7#%S3 zQ?fXj#METTlIETnPa2lhev-#QvaxjQr;;^th@o^CK0)8qlY&(N_tdC!r8)QxU%?@H zhkl)$1I1*qYx)d03TN>p=bIs7upnzSJ)wL^9uL`l)1T>;QA%&!>WZ8KNn`Vgoiw&> zqiJkAjs5NS4}8C1)?PDf?tNY7c^r4pX!{VmwSUgxyg{&7H(7UP#GVpMGn9JXBE-*4 zyAF-|2w(7~A7;0^8~@Ivi*QIgj8Kn=P2xo|T9nf8t3H-(2w{fQD7rvP^|G}LRXanj zMJDx$@r(jt3nN8LQ~J2DW0tfZ}+us3(l z4`^$@zzg>uQaF^8S+^*|Xkr;(o;~f{Qve31g^Db$V*0k)Skb9#)m^t=PB)RtpLHFb zN`kN6{T2p9+FAe7gqzf*&)7*4HH29DneF^v+#l>84JzX;sBn~IGN~v?9F;qK!5Eo_ zb|mE(C8HE)iF(NKyAXvH04A`WqsJYny|O6X<-4?^uFjhG{MOH`^$-t-gRdTQXlFZ@GAAGHhO>gEWF444Kw)L25j&XzIG-a%5s%-C^XT{hWHQfpsty z8S`Eu-}(~pVdA$1xT;)1OQo~^Utj6&IL3HrqlAIU8NwB-4c2NXP#iU2FYW`#VqJzf zz}zN^2?lB5&USzIV*QozsxLr; zoDMRCFu^ZMi;WtF)%lvf)Iwy0#yXI;4*38ITZ#O~7yQ@R z`X-hHp6}!Pze0b1&O?W4a;M}{-fpy0H>Hdx;8e=3Fy&FvLp zWi=b11yBa(MmM^x{3u?*{ zB*bCYiAmetX=_Ne)^-y2fn-}5q|kCdw_$zvk+9Y$03yy}f1--_VGemm4H+SNRDKLT zEYAzhn|7A6dDBhEIP#i^#J{)3MGA{M4MNG%KnP(rhTIzJpZA~4&4^qmP=Vu9GRHKb zu<5KiKKPSTahY!eHZtGqz!%s?bE_Al9AdTx|fZzE8`u7LPjRffN zfrMH4I5Sh`FiDm=jULnnY0VH&eNEILOiuB>9>$qH)i%MZGnULzn2Gz&*F!~#PkjqS zpEkNqe_udY>u$kgS9~n0{y}4JAY_jBZo&J;ezu`uK{ZLw>+Y9}Rr&TY{t(v^9@A!t zUYvfp!Y-HkeRZhYsl=668)=g($gr%GjxY1ow+RzFOEA-X7AEx1;JjD#unbdS{Y_-Q z59b!iV^l~Pb4}%5V>s28N0pRKeQ*@H*$+pTgQC9&4jJMyJ)dIt`P$z@d2?DM^}WH|DcLvm(p)iDr)uXO-PI|&z4Jn z=nz>Qt0$!Z)#X!S8I5FGY3aJ3&vl*pN}kcDoIVo>_`?FaK*0evyaMWsCB$80O4Y%; zI9;Mlv16{FbZm_YtPoEDKEhi? zF(Koy#b{R4KsMHYQmX9~|HKq~`bk@UJ48koTWkV5fH5YURq&U3ODp|Irc?+5($hZGt zXS;`l4(Y+fsHhay6Jwcr2FYHuIc7yAj$hC}(9_{^ z8Y*b|rX;Ccr-u`gV#=5&L`7jYF1KU?hKQkYe9*@Xpe1+|`>qcR*CACRHwTZ5n4lE9tzBB>DI<3zv$%2B^>!sVuE3*xf6S2LA46lfF|*(8b7%P<6G# zJ=SOz0R)cTM6k6S&@jBp(La-?L|pGuxe}+EhU0H0U`fL~LD2OS+bFoOmrgEXS$FH8 zIAsdAnBKDe$$2sqq=RBZH+}Leo@=EeTTi)l6HcJzzCuHpYq6bLs~JD{XwtNK`r#@* zG13}-E>pLqt+JsvP94;RQgFE|+c7d=J{Z0cv$*SyR3ZU#h8*x# z01%ia^LTDU?nX*uKK?4aVXqf`WoYMw5q`h0@93KYUq{;5wk&zw4ItBt~wADv` zc04km-F=(}6~@tMXm^V=__IoF8ZDU?z+NAPPSegOgS`X{BUN&r#q$WF9G9U_X@y z!&~iZVvFm=@DQTBQ}wvr;{;l8!{(ec z*!mM3!lZ1X}7L+Y!bPQpX|HG3dMb(2=1KrZpCI?bI@3lL8} zexy5e5!No~ZZ(NEs_|^93Kf?OeTFhaV|3<~3bj{g)ZLk( z!@1F6)1g5AbL44$gUr`LDGi#)(Hm=XWFk_N{plCxT$U-BGvKj^!K|LAjXRDmIdL{l ze7=9D<>gCD^47;hTh0%I+y)70iJw4!djYSz-QamhDn(1!3J!uJfjW=cZw`|)p~A)G zZJD~vha3C&Pqu-9my^fV==(4DU)z%$9=Tt_%5b?=qUgozIrfjr;03>@fMB(t7k6Gy<;gK*%lj!hhcgwryjWGbWTre~Ll|MR zkX$Eoz4YErbl{V$kBaK0Kt9ZBkEkGO@UZ$EWej;Y8z-bC8dBuwzyV;mhG;Cq8xmN~)88+OuyQpLsye-O3q~19r@c7}f zV}Lx=D!F-QG5_~2BYSg&U_Whb|E9i>2h61Q#Gz|+2WYj(64Zhqo zP}HewSBZ9rL#J$4Qx=IhQb@eNh>rW=8j1+4r+L&oR)_XkG<7II>HS3`fVJJ7o)p!< z9f%FZPImtuIyX{Z1)Km z@u$~2WacT;C+V*yuc$z$Uag*ps_=EgeVyV2YT1x`;cyC(JsXu80!7`E|2{HHEs-^f zE7euVt79^o6_@tTiR>I?hQS_c4Milm!_%b0k`TopSW+@&cCVek3xvx;Y@whH5tV6u z^3pmW@DMsR8TeTa#f3%e)P!sjW_*#jM#RQ?5I`6Bw^;Oge(nc}lt_O+!IPr%);n>{u+kXd- zQ%>;oXwjY?QlLAj7$YvxE$tt*>RjI9OCC1;nE5!__vrk@eyvloLoQDQh}oX6&|aX>KW9qlr)t@o7b`7SrG6RvPB2InlCn-?rgI~?x)LMEjyx|^0ZdRv#y`}1-5>3-x7B)2?cR%D+HKQ)!M)n|? zs{Xq0+GJvkW>LT!*d-1gg?0s{`}bg-G2W|(jzr3zk!~Vmz>{UO{6z z!*~>h{hp>?xgw-dRd8-M)^X!E51>}&Qoy*M!dA{shV~bPd_WsTN+!1#%2{E68NYsH zW^2#*0lS&~umT4+iE11>2I&FdTqw-YTUq7tTt*{A!VxDM$y7m`aAj_SAA9+P1obyA z4yQ^;g_Fw0&o_Q8=+3~GiH{uLVK7Yhw{N5(z%HaH`HP&>EE+k^KL&42`b1b6Sie|V7GJdVO7Sf>OlY$zM1iMWC8J?q z**=+mvls&Z?e*M3Hkxm#m4V4hu|#RWF=fVa%o4b(A_9{b8KI~3lb&)Q?YA(Ly-Ou7 zx@TmKE;G`r0?UB}$hg8?P1t1bstDy3Q=l;da?Z+*lfXJoG1(H<3dwhSnmSGdgAZ)Y zC4q9>%aT?=-SUHde7Z84fEx)WWA}RBcli=`ja-Y3EbMm|x^YXxY*E1o<}Ch>+$x;9 zAA{iDuAQ`AjibhSi|1&HOdYh*FD$f+pgJZt1-m#84Co^-7ZuJe=esU~^8L)-2Q&EK zc{L$Pi6ckMuqD2R3Lms7ZW&QmE)^wqkt2jt0-UXUyNzeW(-`FQZZc|Kwmn4boIGP( zkvwy|hd^9Ij8qnr?AhztX3lCyux()?{`b)PVcc{Em)Mg4M# z{T~W>;bkkyl?y-mJNvDVhwaNe5l7$}12m+@k2k_=L>5iHF2L<~20;Ap5BvjGyeq@q@qrv%M|0{Y zyn-DQnR(>YDD*tb0wQBn;OSPucXdLQFYaRkL6~mq0tVla&2PjZ_r?y!@r1i!scA2f z3%Sb0RIw*u-*OLj>Km+CX!{f(+PO`BU_)3Wy!FQ9mdnsvqdi)!CjFgXt;Zy~iz|}s zD;Ti56ho0MIFhO7rX&_@5HtN$BhZhCOLQ0UH6IujF#e;;??hp?Xh{gYh4T8AniK3E zrEe714=cvHdP8zon~xQ#_QNfL1X_UB*bjFSd;{4uneh$mqB$Rwf|A~y4*UGUc5#xU zUl;AE8Q+v^#8tF^`HD@j(-Vq(ErbYVBZJ^)$higgGZSabzR9l({!H-n&~k1)-ku)Q z^Eh&JlA6rywq6_%6q0~3!IE|gf*ZL=T{NdHhB68^X97H{>QdPDmtOqIN;2D(hXiB1{L!Rd zY6unam8M_TS2{$N^m#mRXVRT#LgLR++Y9ciOIZCqB=!LK>%i@s-*~38c;dsn(%Yb+ z0(FOSONnkI(md=|bWNmTvYm9e)7?*`6>>bonsIX`PNO-vjbo5|FV@JOGcWq(P?fes{U46y8vJ|+mW+AFn}aQLO*wV&86K2i<% znWnzMwKW%%O0C@qvmBux-UHd(MSOb|>}Xa%2W;2YW%1SUub0K-g|nTSNB z!`sW>4RP5lRi4)-w>oR)DZ(2~vmb|lnXv|9v;Jpa12B11bUedD{(D7U;@!ri-1(BD z`!LqPc`y7QVz?Jct`Na18%Huf1w)^J0|tVmZ}JUUN*0My=f)Z-hQo&K6ilm!?}-G- zID9eq=ekp`vBzso0NtZn4n(}daFb_t3pLYMX&9|Q{y7n3(8e}y`26i_akkv7xgZi?QX6r5z8~#3uU<_Q zThfP)Xw?+^2Vb2Q!E3Hb!BwQ|c-JCWkg~WfAO&T(;nqFBP=y!P_jV1t=`&qfz!Snl z+C1#;)IWAR^Y(ct@Mj7iJ?}3Zm$b@(x_{9L%}Duc-ufqU@PQ-$HYmvL!oR*XM3q}6 zQ9|J|3Nocg$!c`RZ;}BL9j)Ta{N_2gJsAc$0N;l2uMNZ99}vHlc^hiN^%6U#t>RMP zoNm*#7J{04s#ywH=am)@e}F8f@R5|N+l7(B(Bt^11Ib@TZ3(aRk)YAJc%nRUhgmL# zzuFQb2}!5}f(qJgC|sFfct>8+{H+HxSw87)4n7v~>H{6epL>JE`c$h&y*#9D9cAY6 z=$lndqB4A*5C0P_YRkF&5ZuYl_GwNL15wi*L)*Mh9%@s+wHx|2_|T`zu|>52ubC9% z-_f?HQnEIjTQ|mr&Cp`-yv4`mD1`G)4KUeULc;ZYc;LN7o|meOH4LeTTQ~?2QS(4R zjnaF7dh5V*-!c?x-)#44g?$PdC8?6WdA#BIHu$5P3h!Ea-(y%qPQ~a@&0UJbs+ue& zt~x?SM&}<2djks>>}k0J_3$)K$|>S@eTj_bp!=A+E0VD9;Jpt`lGy8C*`c|*iAumE zz%;I~2R*|aKYlA8`gHb#(kpFtFmxg0kr_QsW(w1zLxw)Ur6PE6Qyv`SyyF;&?}W(N zzg}Y!dCykRgud@b;q6f>46#qwOlm?1-21OvRpL?^mXfF?toGAj+xEe7#kUdL=u=gW z(5GjZwAsFBiPMB&IcOsD=Bca+7y4c_n4w$v2BQ3i#F%Z*H3TD2wJ(td>?q5AM1^Ai zPPky`V7$fkaorj25VE3lElADX%b-^Me6CPLTe4Cv18$c%Wmpn0$v*Kq=vW1dkU->G zVR9HuIKMybIOzQ3qgwQ*!Ud-r?V;u+Opyg$>nY=I>4RpYRr8ww1DWJM-b5Jp+3C!0 z%e8yy--lNIAx4j^|31Ld+kmlU2#fCyC;bZ(=6H9SUt`s2b4`VEK!~~H;a>TUdMi6? zvDEPsau4l`ng}s|f9VhN(-aHWZ2!s0r$$-YJmwZO=J||*ka}#b7v)u6j`&(1h*(+^ zab7~$G)BKgsq5e@N?kY}8xQdEL@W8h3g-z%)Z%E zE+_bQ41>^*Lg=wPzWW932TQQMLA!T0dM#sYpZKE%4Lw9ZG(n%&Ke$!vY`g zXn7Ja8@Ta{$rRyK&Tj1`>L2n#g>iRz@L)fb9T|Mb_p+ht@A9yzP@(>?f$-`g`;wp( zczljsF`htyvq#7(KU&c5u^M|a?QRgR_`aw}a)iAgmfwuOMYg47nmdoYY5$@;AvTOU zRIou5G21+<{vv2fw~Vu_Umq~crCBwq9(%mmaI{D=l>$5oKssk!Jw*2MxGS?|t(NAN} z%vph8HeZO@rI9OsVata+@_Y>G(_!A_;oU~<8k+I6y9Dy%%^ue%bIt9gbunVp{y5=^ zSU=rwKEt0~1i~u#DX|V5A4Q;Sk4X;pib1~7`5gU!zOnREsD4U!hpO%rd99%)C4MU5 z^XPXQQ4*(fZtaIe`3S*xD`;z_Cem_rm+Mcm3-6=183p$f4?j;c4y*%=07AiT=jfMW zjk{`wd6Luu<>}s11FZeCpmn(}Kq(cSFpN-%d>VEPR22jh-D1kD2AZcwILV!p4R`P)*SZa{SAQ0z@AL=sxP-B-S~)-H3V*OW>n5V+SrphOwkq=^bTE zAdZqhy8ZT6?6Ngp1no>dLV?$b>QPGMG7aDv*ct*Sp)InV)}b7J3_`7=%f5P)-&6wA ze)zmtF!4lnx(7Rt2vjQIpOCXAQnxay=#88047ZvT29H-YCUgFs-kjt^$~Ph1vUVh@ zC+7-B(CtTo8f~?R6DI>7cat7lZpY2IVxuBVc<>&Du4)n5Jq90|@rG2yh`=oto=J`^g~|;$(qJ zYBQwWaO!FVIinx)CUceiCr6pv?wT~tGD+&3w%pv%%c z*1w@Kv;$N^{`g}+N8qP0FzXFC)OiSf$No(8844)3ujJkc&twM=QKg?b`ITb_vj7@u zNiMdaGQvSXA>AwfN`M;b-oJT3_%=N1Z%Lh7hyQttzj6xLdL69*kxgoB6X_p4WJ}Jx zmTLItq@9s=dL~#eT~Ku??!?6xQ0jN_iaf~dR7zLcw1J=!IV7xdsGbyIBYCe>1F9 z9+YG2{%G%E3Pk4{>MOm9l(zDMX0=4EYgQqntXR8*+zSUBKv5yj-Zu0`Sbu7MR#$<+ zYOstc1=XqK=R+4qWq9$Pe&hXifWt@41w$N?HY+Kf)kB=t4EPofX6LftO*k)vijTF& z*)Jp_ThRNHBt2xMWsXzEA1^`zS?WGs>y*5_f7c-3(doJloi<%2P~C3-{qpIQ%T z!jM}vVxwt(wOQnj)?~pfleY$M4qpG$`%1%Z^R7ST+t&oVSN=8byb>2PuGD6_L))Bd<4yv>Vz zD8bT`7m>mFUtC1+0LlgO>W}arcWqG)1xoRvfkqF4{>b_YLi zE}*=IcR654fn+%+5gjr949d$gy?$5d`3((pt-9*qWn6cDh>u^UqS9;fclD-!)ih{@ zHio&TWBNWPO*!YLx1}_cBO^D1pD$&8RT8Kzw1?=+?=Q*4B_oH#xO?;VXk+kL`)VD`bX4M=v3_r2P^3&2^@Y+bBwgmkJk<$mT z+nypC_ZN5`i%q^;Ev60MiO&Ri23~!ld|XfXCPsxRzkyuMu8N4=61;4xi&nXSREI4CyrzrEV306&sEo~n&{!1OW! z=?xfw3*@ZYc_UEzv4#b7r^jtLPQjpiRw{15LbDBKtMk znD2>xfsy}Ue-aSOF)EnL2oHd2Y16lbF7N_ z1WRA3by#y+QL8KEOoW6t21O1)Yc;h)X9;1UBW6hZPnyv%b`Dn-^d_*x1jVQ+bL$1q zJUuSzU%tnsH13AT#m*oXTsAn40%z)?Wgc)z0+$5ued34qQ<`e8coaie0>5rgfd|^~l&X}$C5)?$&F`UrFL(--#E#kxCq;`nWyebPDfmO@Nj6rkl5C6_w< z>wq}xxkJY3(DcY{av$tcjbUwI@9tG>3w)9BNij#K4bfFDRr$Y zm}W;#GYhrP{t8WCe1?T5P+s2zecE~vI{h>~bu;BA6t+BU%+c=wb{oCw3YnGi(hq|X zSmP-7T0}VAT&K;`(6dx>uQv6bV>EQENdyTH%4~Q;c zF{=WUZt{3_mw6rp)!&%!M3zdd1RmOej^@n@Yr~rKxusKw19Ms9gG(#E|2n!2k2taS zQqM54-b96E=dy3Sy4$Q$?aEh{$(Gb07fF4wv8b&)KkXZ-c&EgOO45*RM;&?;v|V_H z@nMtPAG&vWtTLoeVOu$F=myMQ?T1k_^w?5;ECU+J9r|EpboaO+1uUb&8!Zy0C%mrF zJ#$H-LW%c#rNz+y&T5J`ZyJWxDtI3n`;NmRm8=|%{`yN=%gk3JR9w}$zx>?pEw1w& zBkf_JI5gjV@uvqG(537RssS-&II`ck>~a{}U$p=ug-lQ-h<|ga3SGlTZs+2ROE z3x{iK1UqCP2HCm6?D$n=a0lKh^&jg2RB4msrj0f?|42>q4(?H+R|O4W11<{EqOy?M zQ#ogBb5qLb2Q8Hwy^=yEsYA=f*N@&MRJd~k41Bz(p=1?2Qs?^rXjCd|IKjWfzX7f& zfNzW4egf}AKTBooF(ruACWo&R9HfnlZ_T>;$v;6)^oZ7OGg8q?{h?|e$DDbQ2VOX4-X3Rhc50~s9D9tmtA=x9edna07 z@$U!gEIt+vQ2(Q%)Q|bsXsHsuusaqAI@3HO_IaPrj7`^EkMVFe#vEJFv8D1ryT);ZhFTN>b=2gbp z`%nt|zFJjOq4&)So6?@aJb4$h=53#lB;=u`g`nn#P{!#u4#RWjC%w-=A1CC?^iSYU zfm~|vE_o8C4S?=@3-3sKeGj;n+1UQ^oUJ0W?^x*>5KbsGpV@eW3icv?pskBCCeYO6 z)>=t^CcUVBg#R&`ZAxX9%*fU07F|TJY;v9y;VonDs%fRo28?B!1&sgw!fs5-NvhnL QgBD@l`v0$v+6Y4ZKfEN6Z~y=R literal 0 HcmV?d00001 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..477f079 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:height="87dp" + android:viewportHeight="449.32" + android:viewportWidth="512" + android:width="100dp"> + + android:fillType="evenOdd" /> 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/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 c209e78ecd372343283f4157dcfd918ec5165bb3..46ab92cb1be2c09152b6fbccfe94145c5063130e 100644 GIT binary patch literal 2216 zcmV;Z2v_$~Nk&GX2mkOGB^~92Iv5VL*|@w&gs72GVZ=`Nb(T&Q&jRLS>kdm;P~f2|3MX$@X8>z zfMejO0QK>X8cA~GaQ@c2wlzcJ0G#7UwpHc)%-!AH{eLV1iUe4*awiL?)wV6m@9u_H zl5b#3c@l|S zL}Di8B*mPKII#RT7v)G~buHqEy@ZQ{|F8^aB;N>7a&426g%&Dd9I`Zn`^>e79*?S$ zvLZmvNNI6docw1&0(vqYz1yXyOCEEH7n?_K_^Y%X0cKqKgv-TgXU6M{i;2>ve7Zc^ zxEU{f#;41ptsf%844++zcI)@++T+rVRh@`=ys_;rm%Nfqo zM2@ZPyz#`%drBolLzGa=6vTd9`S8c*AWWp@@{DlEy3JLiYiqM^y~1_-Wryt4EDD3- zFkD_3dYP#nQ;YTbwcU#AK3)Hrox>sb>!IiNiz^>9LNW8WL>N3PCpJSNrOleaAYeql z#@|foH!l4qTz>2ZW6FcpO|D0i6;&0~M0nr)_&jMduqAS1i)-{v-{e9dDX%1O%ln~_ z{&R%{XtJD9xO!3VL{DhS^qYEBA(ul671#g5eu@em_#c|y6VQ;~LM90nlWrDrR)Y*#cpHuRnW z0NSi(rdky`qLEMeuAhV3o{l4EJg%BJOt{fa~E z_G>mRl@Fth?b*L08_LS1iASzy0|%w# zlGP5={tAQaSn-iSd~MinX??aF1Xg4Xizx*!+N6a-`T?+5Yn*kZJ$*w{cE*N5rusMn zz$cZzinIfO=(6EXD@QkNlBQNW=r&hRApksREiILV1^^tq%UE{!U`U<{KuPJW)I1u1 z@pZ+O52v{hh;OyVxzN$Fgoe}{r$PAUH0NXM#}WV*$N#6Q@BmH?hNQq@cF2ml;|jn% zS*iXnKsoTm2S0oo>o8|GXo^ynhR7MIb~EqO?)~wZas*hL73(I~4WR!D!VbLKgLi)b z@OgasQdAmf^=f|X{gqUH3&UV0goYesegk)@v%o`#^@7m5u3o>|XM)I3KvAjq^65eA z04`bsmOi*0xMf29el2hq5E{~xMk5&%5D<?f-SizQplF_x`-R}E0`X=@JEl&d|2fXzPWu=DI4uSFL1DEj6 zfsuT$NB;-D9KjYbx=z=w0r(nIigHFUL0Or^@>6@9Qx2G(n>~C8m_?4Y3xjmmu141( zY=Oub-pcSV-ax}60R;B}iLkxz`-e9|; z`0A}{|6$Lb3-q7y6((-jlYul(PnD)9nGE?21Rv6aH4hmm)?QxO&b z`2NQqeU*QEQBV>R1v$k(om&)!4f-7eCML{P+*dk6jIx z7@Bb7K93wNGz1a{fkU<{wnq>q<&A2xFQzWHU6GDp0O@5VVeH!;HASHufL9a2WiEBz z$m6;9`!)Ke3FU_}AmdQE7#oV2xcc=>YA*L|*zZFjwIw}QH*C3m-=2p)vCjfhK5+Ev zw!7Ev=icqU`hQ}xqY6g1|Ka}!KIl=2c2}#3h;o9Or`;ZI-Hgf_ za=BQLfL6k-iAT zkFIT`f`ukvl&q(Gmq)LuR?|pin>+P-tDW{?E?u<8B@=QCt!rbNEOE!UPfo qk_1wL|GXye5f5yr2h%@8TWh?)bSK`O z^Z@d={gn7J{iyxL_y_%J|L>ep{dUxUP8a{byupH&!UNR*OutO~0{*T4q5R6@ApLF! z5{w?Z150gC7#>(VHFJZ-^6O@PYp{t!jH(_Z*nzTK4 zkc{fLE4Q3|mA2`CWQ3{8;gxGizgM!zccbdQoOLZc8hThi-IhN90RFT|zlxh3Ty&VG z?Fe{#9RrRnxzsu|Lg2ddugg7k%>0JeD+{XZ7>Z~{=|M+sh1MF7~ zz>To~`~LVQe1nNoR-gEzkpe{Ak^7{{ZBk2i_<+`Bq<^GB!RYG+z)h;Y3+<{zlMUYd zrd*W4w&jZ0%kBuDZ1EW&KLpyR7r2=}fF2%0VwHM4pUs}ZI2egi#DRMYZPek*^H9YK zay4Iy3WXFG(F14xYsoDA|KXgGc5%2DhmQ1gFCkrgHBm!lXG8I5h*uf{rn48Z!_@ z4Bk6TJAB2CKYqPjiX&mWoW>OPFGd$wqroa($ne7EUK;#3VYkXaew%Kh^3OrMhtjYN?XEoY`tRPQsAkH-DSL^QqyN0>^ zmC>{#F14jz4GeW{pJoRpLFa_*GI{?T93^rX7SPQgT@LbLqpNA}<@2wH;q493)G=1Y z#-sCiRNX~qf3KgiFzB3I>4Z%AfS(3$`-aMIBU+6?gbgDb!)L~A)je+;fR0jWLL-Fu z4)P{c7{B4Hp91&%??2$v9iRSFnuckHUm}or9seH6 z>%NbT+5*@L5(I9j@06@(!{ZI?U0=pKn8uwIg&L{JV14+8s2hnvbRrU|hZCd}IJu7*;;ECgO%8_*W Kmw_-CKmY()leWbG 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 0000000000000000000000000000000000000000..d9490e83d207122cfc93f658118bc90921227764 GIT binary patch literal 2002 zcmV;@2QBzgNk&G>2LJ$9MM6+kP&iD!2LJ#sp+G1Qx2K?OBggzn-~FBd=70VL1SOJ0 zG588Yk6{zT6k}Lm01OKZn;3oowQbwhw#_5kVJebE{MxUt?HZk=>hyFTYTt`ZozGb81@A|< zuUfft_em4{Up~KRXz!8h-YX^O@~>{4)W7Gpv*UgncUI~!* z!K$W)Xd6uz?murrRUu$L5oIUR=u3FcQi~o$5)dx{JEbKX^Mx*tRR-WVA}USMj7nb?|YAXxfmR!XikD964nc`dT7XM{2iR=8FJ!v_E3}%Uh)5Ok4}6J z&p1$888?fj`vNaZn7_dR9l4pZH0N1CRU@627PG@`9@X`A8db5vd4A+Q(VdRjDBK!Z zOxBV2_|b8iTg=e9i?gn9b6wz0tr=J?oMr>1Nm}poo`n5de4v$7$_A(Tig!i(EDwg? zZJaZ7r22{1{tu6GLNy4|ohZR%g2Hu;do?_r2i6T;odI2rYTbWWT~c}1>gQ!p-FkE3NFl6O@!u(X;j2IQA{bb6@8$aVwV5Z_+GE@ zM_eePf8_1n8ON-JigqwgF`LpDCyFX!kzx@QiFUEq7%38ciH)IHNnWf-KME4<;=F89 zETSmNiQKuYQas5Ku^$X=MC5nA3dM&U6OqOiW9{S*_9#}7>vkf!$Ob#(Z=O|5hvz(! z;pa#w{=l3n?#E%F;t38Lb{n`Tj(nKg>>B!+tC$I#`>h&|+NBJdHEU3^-Jf9oei~S$ zn1w&x?lulP)0aGHw*p^%#SD}-{ok$tD1W=Z!hWHm{&kkyJ;|5Ol+9~)U-FG8hpWC9 ziF()%l#~GehoeM3Q%z9F?|fzV8rjZtC*Rt%H(9PqEK=Meo>$=Xvh=8^^kt{v6_0FD zl%}b+<8Np4Eq6N8T&}qTo}?sMC9CaDr^wx~H$iL@_QG9~aLjIts2f-EWPtk`bDgO@ zW-}3Kf1QU#(^yjk*4IrX(`3I}lFINhmaQaN0I!emZxwR8B% z6O894a%u-R0A8zx@=4;{O_nWbF2C_YaJ#AOC_#6uWOs14kn3u(WLXn&`a8&r31+m# zEfT_8uto=C9AuwF#Ga55MC=I;M~5Q%Fvnwq=x8!EG8hMVJ&2nyhe%-J{Jno&y7bq+ z1xW!i#N?(R9w09!GRQ`*x7KRrWsd~T%te_qBS7}xZK{=8#G?`+Qwn%eE1Tj50n`0x z`X>i8C?;EL8@`s2LTdBm z?6GnE$dM?UmIeGB8%dZNz;=~-6=*E|^Fsm5fQi;S}&OkoCI(lLZ&vc7^9Utc*Z-hkVx^Gy(`XEBM z9^lIs5~+>pd2l)Lze&6dr7K(#!Dj&v{x=VTHB%zG@d#9p`7g#)!-EpxT{%$HGju(R z;J7=|$#I}$nnue<9%-pDtps>hBK{lXWK*-t8;qFI#)-o}awHsSK$}8vUNS kOpw;EOJb#_>F%)Tp?7{elUrDL`S7QYEof%Q4gcQ=CN9nJr~m)} literal 0 HcmV?d00001 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 b2dfe3d1ba5cf3ee31b3ecc1ced89044a1f3b7a9..200cb85279c3caa56492510576580c7f03c7c4e4 100644 GIT binary patch literal 4222 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 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp index 4f0f1d64e58ba64d180ce43ee13bf9a17835fbca..8e7bad0b30a9b43cafbc080a2854461b62938173 100644 GIT binary patch literal 1402 zcmV-=1%>)jNk&F;1pok7MM6+kP&iCx1pojqFTe{B^-#LD?T{qpZ)szvU(##a*38Px zjC5w(wr$(CZF`maBF^dkcg{Vwtu`Y^QDduZV^|v7gc#Xw+*TH3^CrPV0Qix#kpcoq z0KhU*Ub%nS+bBsnwr$(C&9!as1#%z&1e@5lZA_7VvtGG@F_rc7O@Lfi)}p=eCI6g= z>i-9K32{P()SzJmVMyub{j%e5dde^CsvPyTltutUmLzUUalxSi-h<@_zN-F|+0o%C zc?oesfy|DMEe>h&O3-H}0zrTOOAB)=*ExE^?C{G1_-6+CQ~9UoxDwQJzNT%;|9u{i zg-tUx1HBW2>M>je>LdN@TgeCf&DsNjn1TAYRi*>K@vJrZ`eol{#=q|2ovG zcQu`1rA)>Fk{zTHSEj0%HCh2pXGDYTS4E6|a9oldlwRsRN{zn(3*WW(l@`;u+ zshU{SJuYMOv035Xu(C9yE{w^C2Gp@mT+yBjI**!xZR+}=Woc+#niU>QNGHN?x10&F z^|y~MsQPO$HXSKxa?or>OiCB(FOPDnR>st9PQup)LK#yjuNw8Y*W`F%J$!iXCNVCs zMwGQGVgw~jYDTzGR#gc8{b}Kf=Ze5b>s??8X}p4Zw=SUFEa)^UERKY5Z>Le#n$v(s z?h&C6DRUnIqw`LA)gjahsx`7~0U!OP3#cGk!^@f-1Ylac;s7nAyQju0!0r)wEkZz9 zRU4O%RZtypf-yN=usJCtePTKh2@hZYr~q1XO1qH-5$mmvg`_w(q|Q}T1NdnVB5J;N zeOeFH+ityjj{=BSDyZHa0q)rP3io#JO~1o?UkR!9n)GiY(2jFjaT1Cn-A2Qn0d1_5 z$@wX1GJoyqHkw|i#f>gPB7+tAFU-|Zmg`+ed93oEc0@s{f{EDnA2osbeHF}$qUL25OVdh{ls`HY$oZuRX0 ztvKN(=cRG_rj|3IPwSH{I1cF5JD#+I^!tB8^d7zCqlKS$jaC3~uhp^d#^xg>t`zWD z_`B2h0`<1X!q2;pzSo5iS?9PkTT#7xAAlXCEwT8M z0R!sD-IYYIZ|fUf4)tvVNAG^@?p^N@u)R0yU^6X5kuQv%j3G z!PW`s)bi)gsTb}gVszemdrUr3>vcdKs*2h8d}3y}Q_2(o2WT~48z&gR4?E7BM=gq9 zi7DwyXRc8k1MUT0?=q@(-GO?v=A@;cZ=JZx1(pqj>aSJz(ej`kxnueBtI-NV3l&vw zd^+~Q-bv})mL@A~%dWlu{9NKnX+m+N6{qa4w>{jKr8t7_0Huc5ym1ejzzVGTYtgR_ zjmd{bmaXA=Y5nzs>rBnp?iY=K)d}7!Rz{T~%9>h9ld*$D&>?INNoWj5(vHA7P%gDB zUeidrUZrmWY9lm`O`*K I|52_h0Y>P#`~Uy| literal 982 zcmV;{11bDcNk&G_0{{S5MM6+kP&il$0000G0000l001ul06|PpNU8t;00Dqo+t#w^ z^1csucXz7-Qrhzl9HuHB%l>&>1tG2^vb*E&k^T3$FG1eQZ51g$uv4V+kI`0<^1Z@N zk?Jjh$olyC%l>)Xq;7!>{iBj&BjJ`P&$fsCfpve_epJOBkTF?nu-B7D!hO=2ZR}

C%4 zc_9eOXvPbC4kzU8YowIA8cW~Uv|eB&yYwAObSwL2vY~UYI7NXPvf3b+c^?wcs~_t{ ze_m66-0)^{JdOMKPwjpQ@Sna!*?$wTZ~su*tNv7o!gXT!GRgivP}ec?5>l1!7<(rT zds|8x(qGc673zrvYIz;J23FG{9nHMnAuP}NpAED^laz3mAN1sy+NXK)!6v1FxQ;lh zOBLA>$~P3r4b*NcqR;y6pwyhZ3_PiDb|%n1gGjl3ZU}ujInlP{eks-#oA6>rh&g+!f`hv#_%JrgYPu z(U^&XLW^QX7F9Z*SRPpQl{B%x)_AMp^}_v~?j7 zapvHMKxSf*Mtyx8I}-<*UGn3)oHd(nn=)BZ`d$lDBwq_GL($_TPaS{UeevT(AJ`p0 z9%+hQb6z)U9qjbuXjg|dExCLjpS8$VKQ55VsIC%@{N5t{NsW)=hNGI`J=x97_kbz@ E0Of=7!T<#diWpAZ$FZ`#zR} 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

FJ$wK}0tWNB{uH;AM~i diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp index 948a3070fe34c611c42c0d3ad3013a0dce358be0..cc835aa1b1e2ed968d3b6bf4bdc34de74d93532a 100644 GIT binary patch delta 1295 zcmV+q1@QXp4$uq+Qb|TeNCvS6Q~`hT1ONcg7XY0BDqsL$06vjAnMoz1BB3bs-XO3N z32Xqw(1bi)!XP)%#@e4SAFf`m{(=9yZ=U)9ex&uM;39u7!<5ROP*@4R+@OBR=6`Xo z6M2zz6a9WPHevgJt6W)Jm2Qr(Ln&|pCf`_@0&?Pqe8c*Icc6XBgc0H9LQw-}Eu&#a)JCtve_(I8f2;vzb zd08gw)GHoP#;1XYuLJ+zhb#a9{>s$7pfv z{SY^IM?O=Z`KieBOl3O)rUZZYDTovI>@ynYRa?T~Cn5O+RVU)zES8wbHF-$`fBN2g zt0t?xDi_WDb!M9A^v`Yh9dZX%$^eFZ_5P2W$`oeH{6L>+k5qG-ECMiDbWmr@?sUa% zagu7#^g$31X6W#GbMuccKsG0J1P*gI_v4vhR(8XT1^%D#RA!T381{d==OO5Vyi~`f zn#Wc_DX+x3{oprjA<3zeUS-N$$XKYink9K4moF1~*L;b=5;P8HC>R?-g5K!_A!~t< zJAB>Fc*a9YmUk}~j1wz@KJJq*I)fPHvqj+Oo-(IuFx3pZ%-rqi? zD`l)MbhyXe`Ob9lGTDEl=RX4AU$6WF5LSf)TL^*#QMSqTFt?$81TGkAd7rUWbRO!| z`8J4sd3r*{!eMp3R`_k^_{mdkm@Aek=#^$hjsjC{JE}9gI*bIrFp>x2CgKugTwWFt ze?Ml+KPNy>GWuD(0d`cO;O?aHgszxiEnIP0vm3Z|Wej}pK!<<7imE?WImScP+G1Ek zoZuFlATOyaNrZkN$@wkoh$h$C2c<@eQtXtZyxa zx$B4}v^n8&`^vZ{nff_ShG6%j+!`pq5kOG&|Cnyfk(M%3$);ef7}||rjxTbi@JlnE ztgk~~Ij1HlSv`N4{We%C;Qd-+CA}`ffQOz2eA;a%Fcn)0dsB9M$@Su`DXe~Ed!5v( zzVftaA<|J$3#`OV?JZ%hMSAQ0l0$zTxS;1xoM2g)@GZ-L2WIk@P*t8W#*iO^8_pkA z3O-Q+5H2ZK<3dPfK&E#I!t$$<9Ey2W))xx<$)oc`*x7$h^)iAdG3aE3=K=ut#JZwd zA!-}eG2HB!eg?Nwl_eVD{NQm{N&lNOmQ@&a&4HnC8@1d!iU3_sY z&rO@E9wdJ?UIIP)n=2Lf`r~l*X$m?@w+Ti!8X1X;;Lg*zG?mXl0@M}W*x^BES`XW~ zy>ICPm|>d0P?vv!lq@NY5G(9zDi0>b8zC-;Aw79L zS|k=c4D&Ne3~4Ee?=l0asU$!ox*uvhcKY1gUgsNyJjVWPWbY|=V+4l}PncI~XX~U+ zLQu7H0xT`CqzR_S;0fm&2s1e36LP$G*9$A|m89PaGMq1}PEe;lB6~G|?>9rhgH!T= F002IGVj2Jd delta 1582 zcmV+}2GRM@4D1dDQb|TeWCyVZQ~`eu1^@u?9sr#IDqsL$06sAiheDzu4ON2x5CMW( z8Q#`UR1woHjE3XzHMaZof8ux3N6ZK6FRHId2kNg`|MRa?4=t}{@7bT&?`ZG-dCVW& z^Ih;6zWZEPdCm|!h`lB8ON|a*S+Vt$^Z@;C4w=;vtUb13a!scD)3twVNfLkd!m6!m zkJ{~EcmV&faFI$9Lcv1e|8}!YA?z%koiXq*5yI3d4I~iCkkxil&mM4zLo$aL16bj2 zGSKMe9`wHLa&h#i5GQ0n8@aWjkL$B^b!H2^QYO@iFc0{(-~yXj2TWz?VG{v`Jgg}WyYnwGgn>{HFaG7E~p zt=)sOO}*yd(UU-D(E&x{xKEl6OcU?pl)K%#U$dn1mDF19Sw@l8G!K6h@`QO^0yXcD zl-d z=lHcwvdoL<)yj(u-HD@#U@8Q%*SWrPD} zW-kW_5A1mmT&x+Bll_A!QJ~YsO2QlJjUJzR1be7UChc#Nvq$w7jG3t zAkL>vXE(1%v|)d%`FwiN2rhDarAbA4?cqnPv<4KOlEhU>V zQ0z8KF_w}C?|XZMPTBvA*em)>g61E-@CMU^q7`V}f_@&Q%NQ`63$tK>Gca?$domIB zFyD2t#vd zZX-u3>-Be1u9gtTBiMB^xwWQ_rgvGpZ6(C@e23c!^K=>ai-RquhqT`ZQof;9Bu!AD z(i^PCbYV%yha9zuoKMp`U^z;3!+&d@Hud&_iy!O-$b5h9Hg`oF$kdrm?-bL{62YM> zcTh5zW#-utU@q|PhYZ~Jo+dM_6z0NPY zxGiTzc|#nK6EzvFa{{&Q~(vOmcy$`6i|-B3hLsmq50*GAr{Y)4>q1OH^^b zNJh%E>}vKG_fNzNrDKPOpa0+f2*LY==u{XW1Z00hCzo%k)ROh&aj&?Jc34Ii&lcNc zxJ8;7OA1IbEHD+NOD~o#MFwa$34-dYA?_~Qbaox3E4Rl8^qdVgc6GwhNO-3EuWh1~ z42V4REw~~*7v;5(qS;Q@N`hwWO&0B!s6rO0Bn)Xww`>U5?vA)U(a86I@c-Dj2cB)K zA0B^4q<7KOO96xsPR{hU&apj;5A)}6v`#`8fe>+`U)YaXv#sY(c--|XEB+TVyZ%Ie z4L$gi#Fc++`h6%vzsS$pjz9aLt+ZL(g;n$Dzy5=m=_TV(3H8^C{r0xdp#a%}ht55d zOq?yhwYPrtp-m1xXp;4X;)NhxxUpe~2`t_+$n_q(m2+DeVJ^V+6P-Op}|mTpm4q5wFhW`a%xsYhS;4G_@X@ g7eQx%?877l?T7XZ57oEqo$`B>NUOvE00o&NX8-^I 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 0000000000000000000000000000000000000000..31155f1e48d4448b0ce95626c9b19d3119e39fbc GIT binary patch literal 3052 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 literal 3918 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 UtN^qcCUzN6I`8!2kh+%u02@ogZ2$lO delta 2406 zcmV-s37Pir62uk;Qb|TeJPWY~`T>7H2><{PF94kZDv$t>06sAkheDzu40NJ$5CMW% zn^Lls?8^p^QGWURbKu3ZduZQZ((s2?zE`}<{RM^KxuveWFjpS4CtURDY zcc_o)UP~4_7uVy9fOPfhN)!YGSq^YMoJE)CymTvZn^xb>Zhu1b*fY&LYwN$s2$(du z>|Bh?c^aZxsjj^&o$-GI4cF`o&WHc>;x5l_;KIe)3zGeStyx}vT(*C1KbQi8SnvP< z{{LVY>;L|mywpqPUxQza2TVp6{I}NGo*ZAEfuY{330cGQ+5cnJf96e9>H33JNxx99 z=&*mxkTLpQl00kN^g=}p6bi!=V;6<8)=I@LTA`hnEj;4o!=#e9wYoK~DxxInomVrZ z-|znDO7$#CRx)Z&yp-}SrD{&|(MQtfUz~n3 z5@RLfUy=aqrhCX0M}J_r5QsK~NmRCR|Nm&L41UdsLjWxSUlM;zL#M)P!IYihiVt1Rt7xrI$E$T4c+w>a8WcROXtdiKT#Dg{V0!d z1tIg`cataY4YPlOMcD?v#{IW@k7%0OZ~jR_OV^HNTPOcqo5%r1zE+^{;l@J; z{wJ5E*C=d9>$eV;r=VL?%6Q((ctR-U>H~Kh`|6;6=ktHa_c^6DAvW{h*FL@0vNGI! zsH82^KO(l)M&+v%-Dx}(evLtGNhd-RSMnOfZDm^KT7<+ke|JPx7Cs;%vULjp*jm?RwjZe z!TGaj1Xq7kclODlb1Kz1_EKLy(NSLCN`eUl(rBCLT*jx@wNvze0|TSqgE(QArOZU) z_?qH(sj#TwzElLs9q)(0u!_P|R%Cy_0JFQxgGV>1z4?_uq<8_gM0`c*Hh|;UMz~vr zg1gQXp{ufg`hM_qU;U>+zmvc5blCLSq@PrEBSC-SP0g)VQ4WHE=q}BK*RQopDgFrm zj%6~5!dS8l?;nW8LDin6-D5)c+(-A4XCz1+%VBg=bwqx)f9&*L(rjOom|AddyT-<> zk1V?6DAO$OkABb`TXP@JT`(Rt_ML#-8_~pwwAcl>>Pdu7#YO^F=7mP0bmXqxbSRDC!96*~4j=`H)`6>(MP4KH4g6sE=wv(3u8sDb43DesE&cN&h~{aH~s#8Mp8 ziD_@+4dC(Ou?;-tsjsIW`>x1Afmn2CUkceOt^;yEKud{SF`c^?Lt6m8)SGoGi=BV4 zA6#EgGS-zY%Vi)N*Yb`U;6o}KR}dq{r9pT5wqZ@3NOE8-9-(}D|Nc5732CSYQbL)! zgPQ#RbD8BhK3dl{sUuPvei0tkvnJBxDEsa&rA5u(5mYno7SYzFk^0sMS310sP^|4%(MT z%g%lGhX{#rs<|Vv#Jji{3g4|Uas?ypf!je@JcDBtWWxROy8b?$x1CcC{m_4hk=D@t zU~qUM1G4<}HdwhyyMFzAFTVb6g*H!Sdi^tsE<01#DpVE^EPKt;V$%i!5PR3l1k}@7 zFhdTm?IDC8Cb3TA68%@Md8l~CxALe|B=-)Y85`chh@hGa$a3Rt^GVx^@`aVv+iop? zKmVBM6VX1nh#zOVy8ZV?$nbwf7djUsEPlyG@u(j#@eE(2P^D`YDwe+!j&$Ks6oQZ!`LLjn0}`RWR7129nQ<;!p-L1w$=F#fJVX>s{%guHQo5EUp0aS6qki zlLnWBW8d?(MZdrN?v_LvdJ-f{>)KIu|DlI}@@YP|py&PrQjZ!}g|vT?tfnDt>{U)* zguFML3P!OSlz;zGA#T+E@8@cgQ_mv1o7RSU=Zo_82F?&&2r;WE@wk}JHYEZ9nYUc( zVv~iTCa3u8e4q(yq<29VoNbKk0>Of2Lg?rLX`JAsJv&-=NoC^e$NGRvSNfOk YzBc_>8IX0nz_jyKzRf$?Z~y=R0K@Iq4FCWD 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 0000000000000000000000000000000000000000..fc1023e9a627592a78fb8f1769ee650cd8d557c7 GIT binary patch literal 5194 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 T`M7w_YLE$3z&s!T00000cE(Mf delta 3194 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 ST8^`Gfhcb5)9m$ literal 0 HcmV?d00001 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 9126ae37cbc3587421d6889eadd1d91fbf1994d4..58f1ecb351ef7ad3f400423d65a7ef64758ebc18 100644 GIT binary patch delta 4226 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 Y9nUBjQ<8)W675106sAmh(e+vAs0GboFD@P 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 t`f1Sx;C + + #A58D11 + \ No newline at end of file From f3535af6c6683f496ec2a028e5cbdadc0fb94acb Mon Sep 17 00:00:00 2001 From: Luiz Alegria Date: Tue, 27 Jun 2023 19:35:44 -0300 Subject: [PATCH 15/18] Adjust: Adjust icons and colors (#15) --- .../lhalegria/dogga/view/composable/BreedDetail.kt | 10 ++++++---- .../lhalegria/dogga/view/composable/BreedList.kt | 6 +++++- .../dev/lhalegria/dogga/view/composable/Loading.kt | 3 +++ .../res/drawable-v24/ic_launcher_foreground.xml | 4 ++-- app/src/main/res/drawable/ic_dog_placeholder.xml | 14 -------------- app/src/main/res/drawable/ic_placeholder.xml | 10 ++++++++++ app/src/main/res/drawable/ic_toolbar_icon.xml | 10 ++++++++++ 7 files changed, 36 insertions(+), 21 deletions(-) delete mode 100644 app/src/main/res/drawable/ic_dog_placeholder.xml create mode 100644 app/src/main/res/drawable/ic_placeholder.xml create mode 100644 app/src/main/res/drawable/ic_toolbar_icon.xml 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 index 6928d21..0ca031a 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedDetail.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedDetail.kt @@ -1,5 +1,6 @@ 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 @@ -90,13 +91,14 @@ fun BreedImage(name: String, url: String = "") { .data(url) .crossfade(true) .build(), - placeholder = painterResource(id = R.drawable.ic_dog_placeholder), - error = painterResource(id = R.drawable.ic_dog_placeholder), + 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.Fit, + contentScale = ContentScale.Crop, modifier = Modifier .fillMaxWidth() - .size(250.dp) + .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 index fa5022c..c9ae246 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedList.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/BreedList.kt @@ -7,11 +7,15 @@ 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 @@ -25,7 +29,7 @@ fun BreedList( Scaffold(topBar = { Toolbar( title = stringResource(id = R.string.breed_list), - icon = Icons.Default.Home + icon = ImageVector.vectorResource(id = R.drawable.ic_toolbar_icon) ) {} }) { padding -> Surface( 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 index 68bddbd..ed1da4c 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/Loading.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/Loading.kt @@ -1,9 +1,11 @@ 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 @@ -13,6 +15,7 @@ import androidx.compose.ui.unit.dp fun LoadingBox() { Box( Modifier.fillMaxSize() + .background(color = MaterialTheme.colorScheme.surface) ) { CircularProgressIndicator( modifier = Modifier 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 477f079..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,8 +1,8 @@ + android:viewportHeight="449.32"> - - - - - 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 @@ + + + From 67f4298995c2bdd285a89a67b683f80c955a90ea Mon Sep 17 00:00:00 2001 From: Luiz Alegria Date: Tue, 27 Jun 2023 21:02:59 -0300 Subject: [PATCH 16/18] Feature: Implement try-again button (#16) --- .../dev/lhalegria/dogga/view/MainActivity.kt | 37 +++++++++++++------ .../dogga/view/composable/EmptyDataBox.kt | 6 ++- .../dogga/view/composable/ErrorBox.kt | 16 +++++++- .../lhalegria/dogga/view/preview/Previews.kt | 4 +- app/src/main/res/values/strings.xml | 1 + 5 files changed, 47 insertions(+), 17 deletions(-) diff --git a/app/src/main/kotlin/dev/lhalegria/dogga/view/MainActivity.kt b/app/src/main/kotlin/dev/lhalegria/dogga/view/MainActivity.kt index e9d4c8e..b10d6f8 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/view/MainActivity.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/MainActivity.kt @@ -5,6 +5,9 @@ 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 @@ -22,28 +25,40 @@ class MainActivity : ComponentActivity() { 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 -> ErrorBox() - is RequestState.Success -> SuccessContainer(breedsState) + is RequestState.Error -> ErrorContainer(state = breedsState) { retry = true } + is RequestState.Success -> SuccessContainer(breedsState, retryAction) } } } } -} -@Composable -fun SuccessContainer(state: RequestState>?) { - val breeds = (state as? RequestState.Success>)?.data - if (breeds?.isNotEmpty() == true) { - AppNavigation(breeds = breeds) - } else { - EmptyDataBox() + @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/EmptyDataBox.kt b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/EmptyDataBox.kt index 2bc5830..1beb1d4 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/EmptyDataBox.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/EmptyDataBox.kt @@ -17,7 +17,7 @@ import dev.lhalegria.dogga.R import dev.lhalegria.dogga.view.ui.theme.CaramelStrong @Composable -fun EmptyDataBox() { +fun EmptyDataBox(action: () -> Unit) { Column( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, @@ -31,6 +31,8 @@ fun EmptyDataBox() { text = stringResource(id = R.string.empty_data_message) ) - TryAgainButton(modifier = Modifier) { } + 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 index 416567f..ae3c2cf 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/ErrorBox.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/composable/ErrorBox.kt @@ -1,5 +1,6 @@ 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 @@ -8,6 +9,7 @@ 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 @@ -17,7 +19,15 @@ import dev.lhalegria.dogga.R import dev.lhalegria.dogga.view.ui.theme.CaramelStrong @Composable -fun ErrorBox() { +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, @@ -31,6 +41,8 @@ fun ErrorBox() { text = stringResource(id = R.string.error_message) ) - TryAgainButton(modifier = Modifier) { } + TryAgainButton(modifier = Modifier) { + action.invoke() + } } } 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 index bd5dcc5..97a6627 100644 --- a/app/src/main/kotlin/dev/lhalegria/dogga/view/preview/Previews.kt +++ b/app/src/main/kotlin/dev/lhalegria/dogga/view/preview/Previews.kt @@ -31,7 +31,7 @@ fun BreedLoadingPreview() { @Composable fun BreedEmptyDataPreview() { DoggaTheme { - EmptyDataBox() + EmptyDataBox {} } } @@ -39,7 +39,7 @@ fun BreedEmptyDataPreview() { @Composable fun BreedErrorDataPreview() { DoggaTheme { - ErrorBox() + ErrorBox {} } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0d7546e..eab49c8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -10,4 +10,5 @@ Picture of a dog from the %s breed. Dogga Application Toolbar with title %s An icon representing a dog + The error cause was: %s From 29e4e3c472f75f32a1bf6b87ffb46a68c8f70bb9 Mon Sep 17 00:00:00 2001 From: Luiz Alegria Date: Tue, 27 Jun 2023 21:07:14 -0300 Subject: [PATCH 17/18] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index f59dee2..f5fce2a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ # Dogga Simple Android Native Application consuming the https://dog.ceo/dog-api/ + +This app is made just for portfolio and study purposes and it is in constanting evolution. From 063d0eedd0aeaad82dcc8b32b64de0d83b1b49a7 Mon Sep 17 00:00:00 2001 From: Luiz Alegria Date: Tue, 27 Jun 2023 22:49:58 -0300 Subject: [PATCH 18/18] Update README.md --- README.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f5fce2a..4d28b45 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,19 @@ # Dogga -Simple Android Native Application consuming the https://dog.ceo/dog-api/ -This app is made just for portfolio and study purposes and it is in constanting evolution. +![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 +