diff --git a/.github/workflows/AndroidBuild.yml b/.github/workflows/AndroidBuild.yml
new file mode 100644
index 000000000..5caefb0be
--- /dev/null
+++ b/.github/workflows/AndroidBuild.yml
@@ -0,0 +1,37 @@
+name: AndroidBuild
+on:
+ push:
+ branches: [ '**' ]
+ pull_request:
+ branches: [ '**' ]
+
+jobs :
+ build :
+ runs-on : ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4.2.2
+
+ - name: Setup Java JDK
+ uses: actions/setup-java@v4.5.0
+ with:
+ java-version: '17'
+ distribution: 'zulu'
+
+ - name: Run Lint
+ run: ./gradlew lint
+
+ - name: Run Detekt
+ run: ./gradlew detekt
+
+ - name: Build with Gradle
+ run: ./gradlew build
+
+ - name: Run Tests
+ run: ./gradlew test
+
+ - name: Upload a Build Artifact
+ uses: actions/upload-artifact@v4.4.3
+ with:
+ name: DesafioAndroid.apk
+ path: app/build/outputs/apk/debug/app-debug.apk
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 2b75303ac..0430edd5e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,13 +1,55 @@
+# Gradle files
+.gradle/
+build/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+replay_pid*
+
+# Android Studio generated files and folders
+captures/
+.externalNativeBuild/
+.cxx/
+*.apk
+output.json
+
+# IDE
*.iml
-.gradle
-/local.properties
-/.idea/caches
-/.idea/libraries
-/.idea/modules.xml
-/.idea/workspace.xml
-/.idea/navEditor.xml
-/.idea/assetWizardSettings.xml
-.DS_Store
-/build
-/captures
-.externalNativeBuild
+.idea/
+misc.xml
+deploymentTargetDropDown.xml
+render.experimental.xml
+
+# Keystore files
+*.jks
+*.keystore
+
+# Google Services (e.g. APIs or Firebase)
+google-services.json
+
+# Android Profiling
+*.hprof
\ No newline at end of file
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
deleted file mode 100644
index 45b565415..000000000
--- a/.idea/codeStyles/Project.xml
+++ /dev/null
@@ -1,125 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- xmlns:android
-
- ^$
-
-
-
-
-
-
-
-
- xmlns:.*
-
- ^$
-
-
- BY_NAME
-
-
-
-
-
-
- .*:id
-
- http://schemas.android.com/apk/res/android
-
-
-
-
-
-
-
-
- .*:name
-
- http://schemas.android.com/apk/res/android
-
-
-
-
-
-
-
-
- name
-
- ^$
-
-
-
-
-
-
-
-
- style
-
- ^$
-
-
-
-
-
-
-
-
- .*
-
- ^$
-
-
- BY_NAME
-
-
-
-
-
-
- .*
-
- http://schemas.android.com/apk/res/android
-
-
- ANDROID_ATTRIBUTE_ORDER
-
-
-
-
-
-
- .*
-
- .*
-
-
- BY_NAME
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
deleted file mode 100644
index 6e6eec114..000000000
--- a/.idea/codeStyles/codeStyleConfig.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/dictionaries/mdime.xml b/.idea/dictionaries/mdime.xml
deleted file mode 100644
index 8cfcdaab3..000000000
--- a/.idea/dictionaries/mdime.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
- moshi
-
-
-
\ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
deleted file mode 100644
index 15a15b218..000000000
--- a/.idea/encodings.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
deleted file mode 100644
index 7ac24c777..000000000
--- a/.idea/gradle.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index 703e5d4b8..000000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
deleted file mode 100644
index 7f68460d8..000000000
--- a/.idea/runConfigurations.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 94a25f7f4..000000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Makefile b/Makefile
new file mode 100644
index 000000000..c80d39ab6
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,53 @@
+# Define variables
+GRADLEW = gradlew.bat
+ADB = adb
+
+# Default target (runs `make build`)
+all: build
+
+# Build the debug APK
+build:
+ $(GRADLEW) assembleDebug
+
+# Run unit and instrumented tests
+all-tests:
+ $(GRADLEW) test
+ $(GRADLEW) uitest
+
+# Run unit tests
+test:
+ $(GRADLEW) test
+
+# Run instrumentation tests on a connected device/emulator
+uitest:
+ make start-emulator
+ $(GRADLEW) connectedAndroidTest
+
+# Run lint and detekt
+lint:
+ $(GRADLEW) lint
+ $(GRADLEW) detekt
+
+# Clean the project
+clean:
+ $(GRADLEW) clean
+
+# Install the debug APK on a connected device/emulator
+install:
+ $(ADB) install -r app/build/outputs/apk/debug/app-debug.apk
+
+# Uninstall the app from the device/emulator
+uninstall:
+ $(ADB) uninstall com.example.yourapp
+
+# Open the logcat to debug
+logcat:
+ $(ADB) logcat -s "YourAppTag"
+
+start-emulator:
+ @echo "Starting emulator..."
+ adb devices | findstr emulator || start cmd /c "emulator -avd Medium_Phone_API_34 -no-snapshot-load"
+ @echo "Waiting for device to be ready..."
+ adb wait-for-device
+
+.PHONY: all build release test uitest clean install uninstall logcat start-emulator
diff --git a/app/build.gradle b/app/build.gradle
index a7fbdc0e9..bbe464d5d 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,17 +1,20 @@
-apply plugin: 'com.android.application'
-
-apply plugin: 'kotlin-android'
-
-apply plugin: 'kotlin-android-extensions'
-
-apply plugin: 'kotlin-kapt'
+plugins {
+ id('com.android.application')
+ id('kotlin-android')
+ id('kotlin-kapt')
+ id('kotlin-parcelize')
+ id('org.jetbrains.kotlin.plugin.compose')
+ id('io.gitlab.arturbosch.detekt')
+}
android {
- compileSdkVersion 29
+ namespace = "com.picpay.desafio.android"
+
defaultConfig {
applicationId "com.picpay.desafio.android"
- minSdkVersion 21
- targetSdkVersion 29
+ compileSdk 34
+ minSdkVersion 24
+ targetSdkVersion 34
versionCode 1
versionName "1.0"
@@ -19,6 +22,7 @@ android {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
+
buildTypes {
debug {}
@@ -30,15 +34,55 @@ android {
}
compileOptions {
- sourceCompatibility 1.8
- targetCompatibility 1.8
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
- jvmTarget = JavaVersion.VERSION_1_8.toString()
+ jvmTarget = JavaVersion.VERSION_17.toString()
+ }
+
+ buildFeatures {
+ compose true
+ }
+
+ kapt {
+ correctErrorTypes true
+
+ arguments {
+ arg("room.schemaLocation", "$projectDir/schemas".toString())
+ }
+ }
+
+ lint {
+ abortOnError = false
+ warningsAsErrors = true
+ checkReleaseBuilds = false
+ disable.add("UnusedResources")
+ enable.add("ObsoleteSdkInt")
+ lintConfig = file("lint.xml")
+ }
+
+ packagingOptions {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ merges += "META-INF/LICENSE.md"
+ merges += "META-INF/LICENSE-notice.md"
+ }
}
}
+detekt {
+ config = files("$rootDir/config/detekt/detekt.yml")
+ buildUponDefaultConfig = true
+ allRules = false
+ autoCorrect = true
+}
+
+tasks.named("check") {
+ dependsOn("detekt")
+}
+
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
@@ -46,17 +90,15 @@ dependencies {
implementation "androidx.core:core-ktx:$core_ktx_version"
- implementation "androidx.appcompat:appcompat:$appcompat_version"
- implementation "androidx.constraintlayout:constraintlayout:$constraintlayout_version"
-
- implementation "com.google.android.material:material:$material_version"
-
- implementation "org.koin:koin-core:$koin_version"
- implementation "org.koin:koin-android:$koin_version"
- implementation "org.koin:koin-androidx-viewmodel:$koin_version"
-
- implementation "com.google.dagger:dagger:$dagger_version"
- kapt "com.google.dagger:dagger-compiler:$dagger_version"
+ implementation(project.dependencies.platform("io.insert-koin:koin-bom:$koin_version"))
+ implementation "io.insert-koin:koin-core"
+ implementation "io.insert-koin:koin-core-coroutines"
+ implementation "io.insert-koin:koin-android"
+ implementation "io.insert-koin:koin-androidx-compose"
+ implementation "io.insert-koin:koin-androidx-compose-navigation"
+ implementation "io.insert-koin:koin-androidx-workmanager"
+ implementation "io.insert-koin:koin-androidx-navigation"
+ implementation "io.insert-koin:koin-test"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
@@ -69,24 +111,38 @@ dependencies {
implementation "io.reactivex.rxjava2:rxjava:$rxjava_version"
implementation "io.reactivex.rxjava2:rxandroid:$rxandroid_version"
- implementation 'com.google.code.gson:gson:2.8.6'
+ implementation "com.google.code.gson:gson:$gson_version"
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
- implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
- implementation "com.squareup.okhttp3:mockwebserver:$okhttp_version"
- implementation "com.squareup.picasso:picasso:$picasso_version"
- implementation "de.hdodenhof:circleimageview:$circleimageview_version"
+ implementation(platform "com.squareup.okhttp3:okhttp-bom:$okhttp_version")
+ implementation "com.squareup.okhttp3:okhttp"
+ implementation"com.squareup.okhttp3:logging-interceptor"
+ testImplementation "com.squareup.okhttp3:mockwebserver"
+ implementation 'androidx.compose.ui:ui-test-junit4-android:1.7.8'
testImplementation "junit:junit:$junit_version"
- testImplementation "org.mockito:mockito-core:$mockito_version"
- testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:$mockito_kotlin_version"
testImplementation "androidx.arch.core:core-testing:$core_testing_version"
- implementation "org.koin:koin-test:$koin_version"
-
- androidTestImplementation "androidx.test:runner:$test_runner_version"
- androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version"
- androidTestImplementation "androidx.test:core-ktx:$core_ktx_test_version"
-}
+ testImplementation("io.mockk:mockk:$mockk_version")
+ androidTestImplementation("io.mockk:mockk-android:$mockk_version")
+
+ implementation(platform("androidx.compose:compose-bom:$compose_version"))
+ androidTestImplementation(platform("androidx.compose:compose-bom:$compose_version"))
+ implementation("androidx.compose.material3:material3")
+ implementation("androidx.compose.ui:ui-tooling-preview")
+ debugImplementation("androidx.compose.ui:ui-tooling")
+ androidTestImplementation("androidx.compose.ui:ui-test-junit4")
+ debugImplementation("androidx.compose.ui:ui-test-manifest")
+ implementation("androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_viewmodel_compose_version")
+ implementation("androidx.compose.runtime:runtime-livedata")
+ implementation("androidx.compose.runtime:runtime-rxjava2")
+
+ implementation("io.coil-kt.coil3:coil-compose:$coil_version")
+ implementation("io.coil-kt.coil3:coil-network-okhttp:$coil_version")
+
+ implementation("androidx.room:room-runtime:$room_version")
+ kapt("androidx.room:room-compiler:$room_version")
+ implementation("androidx.room:room-ktx:$room_version")
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/picpay/desafio/android/MainActivityTest.kt b/app/src/androidTest/java/com/picpay/desafio/android/MainActivityTest.kt
deleted file mode 100644
index e4a4978eb..000000000
--- a/app/src/androidTest/java/com/picpay/desafio/android/MainActivityTest.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-package com.picpay.desafio.android
-
-import androidx.lifecycle.Lifecycle
-import androidx.test.core.app.launchActivity
-import androidx.test.espresso.Espresso.onView
-import androidx.test.espresso.assertion.ViewAssertions.matches
-import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
-import androidx.test.espresso.matcher.ViewMatchers.withText
-import androidx.test.platform.app.InstrumentationRegistry
-import okhttp3.mockwebserver.Dispatcher
-import okhttp3.mockwebserver.MockResponse
-import okhttp3.mockwebserver.MockWebServer
-import okhttp3.mockwebserver.RecordedRequest
-import org.junit.Test
-
-
-class MainActivityTest {
-
- private val server = MockWebServer()
-
- private val context = InstrumentationRegistry.getInstrumentation().targetContext
-
- @Test
- fun shouldDisplayTitle() {
- launchActivity().apply {
- val expectedTitle = context.getString(R.string.title)
-
- moveToState(Lifecycle.State.RESUMED)
-
- onView(withText(expectedTitle)).check(matches(isDisplayed()))
- }
- }
-
- @Test
- fun shouldDisplayListItem() {
- server.dispatcher = object : Dispatcher() {
- override fun dispatch(request: RecordedRequest): MockResponse {
- return when (request.path) {
- "/users" -> successResponse
- else -> errorResponse
- }
- }
- }
-
- server.start(serverPort)
-
- launchActivity().apply {
- // TODO("validate if list displays items returned by server")
- }
-
- server.close()
- }
-
- companion object {
- private const val serverPort = 8080
-
- private val successResponse by lazy {
- val body =
- "[{\"id\":1001,\"name\":\"Eduardo Santos\",\"img\":\"https://randomuser.me/api/portraits/men/9.jpg\",\"username\":\"@eduardo.santos\"}]"
-
- MockResponse()
- .setResponseCode(200)
- .setBody(body)
- }
-
- private val errorResponse by lazy { MockResponse().setResponseCode(404) }
- }
-}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/picpay/desafio/android/RecyclerViewMatchers.kt b/app/src/androidTest/java/com/picpay/desafio/android/RecyclerViewMatchers.kt
deleted file mode 100644
index 62be92ebd..000000000
--- a/app/src/androidTest/java/com/picpay/desafio/android/RecyclerViewMatchers.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-package com.picpay.desafio.android
-
-import android.view.View
-import androidx.recyclerview.widget.RecyclerView
-import androidx.test.espresso.Espresso
-import androidx.test.espresso.assertion.ViewAssertions
-import androidx.test.espresso.matcher.BoundedMatcher
-import androidx.test.espresso.matcher.ViewMatchers
-import org.hamcrest.Description
-import org.hamcrest.Matcher
-
-object RecyclerViewMatchers {
-
- fun atPosition(
- position: Int,
- itemMatcher: Matcher
- ) = object : BoundedMatcher(RecyclerView::class.java) {
- override fun describeTo(description: Description?) {
- description?.appendText("has item at position $position: ")
- itemMatcher.describeTo(description)
- }
-
- override fun matchesSafely(item: RecyclerView?): Boolean {
- val viewHolder = item?.findViewHolderForAdapterPosition(position) ?: return false
- return itemMatcher.matches(viewHolder.itemView)
- }
- }
-
- fun checkRecyclerViewItem(resId: Int, position: Int, withMatcher: Matcher) {
- Espresso.onView(ViewMatchers.withId(resId)).check(
- ViewAssertions.matches(
- atPosition(
- position,
- ViewMatchers.hasDescendant(withMatcher)
- )
- )
- )
- }
-}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/picpay/desafio/android/data/features/contacts/local/ContactDatabaseTest.kt b/app/src/androidTest/java/com/picpay/desafio/android/data/features/contacts/local/ContactDatabaseTest.kt
new file mode 100644
index 000000000..fca237389
--- /dev/null
+++ b/app/src/androidTest/java/com/picpay/desafio/android/data/features/contacts/local/ContactDatabaseTest.kt
@@ -0,0 +1,103 @@
+package com.picpay.desafio.android.data.features.contacts.local
+
+import android.content.Context
+import androidx.room.Room
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.picpay.desafio.android.data.features.contacts.local.dao.ContactDao
+import com.picpay.desafio.android.data.features.contacts.local.database.ContactDatabase
+import com.picpay.desafio.android.data.features.contacts.local.entities.database.ContactDb
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.io.IOException
+
+@RunWith(AndroidJUnit4::class)
+class ContactDatabaseTest {
+ private lateinit var dbContacts: List
+ private lateinit var dao: ContactDao
+ private lateinit var contactDatabase: ContactDatabase
+
+ @Before
+ fun createInstances() {
+ val contact = ContactDb(
+ id = 1,
+ name = "Rodrigo M",
+ username = "rodrigom",
+ image = "https://example.com/rodrigom.jpg"
+ )
+
+ dbContacts = List(3) { index ->
+ contact.copy(
+ id = index + 1,
+ name = "Rodrigo M ${index + 1}",
+ username = "rodrigom${index + 1}",
+ image = "https://example.com/rodrigom${index + 1}.jpg"
+ )
+ }
+ }
+
+ @Before
+ fun createDb() {
+ val context = ApplicationProvider.getApplicationContext()
+ contactDatabase = Room.inMemoryDatabaseBuilder(
+ context,
+ ContactDatabase::class.java
+ ).build()
+
+ dao = contactDatabase.dao
+ }
+
+ @After
+ @Throws(IOException::class)
+ fun closeDb() {
+ contactDatabase.close()
+ }
+
+ @Test
+ fun should_write_in_database() {
+ lateinit var result: List
+
+ runBlocking {
+ result = dao.queryContacts().first()
+ }
+ assertTrue(result.isEmpty())
+
+ runBlocking {
+ dao.insertAll(dbContacts)
+ result = dao.queryContacts().first()
+ }
+ assertTrue(result.isNotEmpty())
+ }
+
+ @Test
+ fun should_read_from_database() {
+ lateinit var result: ContactDb
+
+ runBlocking {
+ dao.deleteAll()
+
+ dao.insertAll(dbContacts.subList(0, 1))
+ result = dao.queryContacts().first()[0]
+ }
+
+ assertEquals(result.id, dbContacts[0].id)
+ }
+
+ @Test
+ fun should_clear_database() {
+ lateinit var result: List
+
+ runBlocking {
+ dao.insertAll(dbContacts)
+ dao.deleteAll()
+ result = dao.queryContacts().first()
+ }
+ assertTrue(result.isEmpty())
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/picpay/desafio/android/presentation/ui/features/contacts/compose/ContactComposableTest.kt b/app/src/androidTest/java/com/picpay/desafio/android/presentation/ui/features/contacts/compose/ContactComposableTest.kt
new file mode 100644
index 000000000..157436550
--- /dev/null
+++ b/app/src/androidTest/java/com/picpay/desafio/android/presentation/ui/features/contacts/compose/ContactComposableTest.kt
@@ -0,0 +1,46 @@
+package com.picpay.desafio.android.presentation.ui.features.contacts.compose
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertTextEquals
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import com.picpay.desafio.android.domain.features.contacts.model.Contact
+import org.junit.Rule
+import org.junit.Test
+
+class ContactComposableTest {
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ @Test
+ fun assert_contact_properties_are_displayed_correctly() {
+ // Given
+ val contact = Contact(
+ id = 1,
+ name = "Rodrigo Murta",
+ username = "rodrigo.mmurta",
+ image = "https://randomuser.me/api/portraits/men/1.jpg"
+ )
+
+ // When
+ composeTestRule.setContent {
+ ContactComposable(
+ modifier = Modifier,
+ contact = contact
+ )
+ }
+
+ // Then
+ composeTestRule.onNodeWithTag(IMAGE_TAG)
+ .assertIsDisplayed()
+
+ composeTestRule.onNodeWithTag(USERNAME_TAG)
+ .assertIsDisplayed()
+ .assertTextEquals("rodrigo.mmurta")
+
+ composeTestRule.onNodeWithTag(NAME_TAG)
+ .assertIsDisplayed()
+ .assertTextEquals("Rodrigo Murta")
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/picpay/desafio/android/presentation/ui/utils/compose/CacheAlertComposableTest.kt b/app/src/androidTest/java/com/picpay/desafio/android/presentation/ui/utils/compose/CacheAlertComposableTest.kt
new file mode 100644
index 000000000..5b00ba12c
--- /dev/null
+++ b/app/src/androidTest/java/com/picpay/desafio/android/presentation/ui/utils/compose/CacheAlertComposableTest.kt
@@ -0,0 +1,33 @@
+package com.picpay.desafio.android.presentation.ui.utils.compose
+
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertTextEquals
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import com.picpay.desafio.android.domain.utils.ErrorInformation
+import org.junit.Rule
+import org.junit.Test
+
+class CacheAlertComposableTest {
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ @Test
+ fun assert_cache_alert_is_displayed_correctly() {
+ // Given
+ val errorMessage = "Ocorreu um erro. Exibindo dados em cache."
+
+ // When
+ composeTestRule.setContent {
+ CacheAlertComposable(
+ error = ErrorInformation(
+ message = errorMessage
+ )
+ )
+ }
+
+ // Then
+ composeTestRule.onNodeWithTag(TEXT_TAG).assertIsDisplayed()
+ composeTestRule.onNodeWithTag(TEXT_TAG).assertTextEquals(errorMessage)
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/picpay/desafio/android/presentation/ui/utils/compose/ErrorFeedbackComposableKtTest.kt b/app/src/androidTest/java/com/picpay/desafio/android/presentation/ui/utils/compose/ErrorFeedbackComposableKtTest.kt
new file mode 100644
index 000000000..7f91fc400
--- /dev/null
+++ b/app/src/androidTest/java/com/picpay/desafio/android/presentation/ui/utils/compose/ErrorFeedbackComposableKtTest.kt
@@ -0,0 +1,61 @@
+package com.picpay.desafio.android.presentation.ui.utils.compose
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertAny
+import androidx.compose.ui.test.assertHasClickAction
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.hasTestTag
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onChildren
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
+import com.picpay.desafio.android.presentation.ui.utils.FeedbackListener
+import io.mockk.mockk
+import io.mockk.verify
+import org.junit.Rule
+import org.junit.Test
+
+private const val ERROR_FEEDBACK_TAG = "errorFeedbackTag"
+
+class ErrorFeedbackComposableKtTest {
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ // Prepare
+ private val listener = mockk(relaxed = true)
+
+ private fun setContent() = composeTestRule.setContent {
+ ErrorFeedbackComposable(
+ modifier = Modifier.testTag(ERROR_FEEDBACK_TAG),
+ listener = listener,
+ )
+ }
+
+ @Test
+ fun error_should_be_displayed() {
+ // Given
+ setContent()
+
+ // Then
+ composeTestRule.onNodeWithTag(ERROR_FEEDBACK_TAG)
+ .assertIsDisplayed()
+ .onChildren()
+ .assertAny((hasTestTag(ERROR_TEXT_TAG)))
+ .assertAny((hasTestTag(ERROR_BUTTON_TAG)))
+ }
+
+ @Test
+ fun listener_should_be_called_when_button_clicked() {
+ // Given
+ setContent()
+
+ // When
+ composeTestRule.onNodeWithTag(ERROR_BUTTON_TAG)
+ .assertHasClickAction()
+ .performClick()
+
+ // Then
+ verify(exactly = 1) { listener.onButtonClicked() }
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/picpay/desafio/android/presentation/ui/utils/compose/ScreenComposableKtTest.kt b/app/src/androidTest/java/com/picpay/desafio/android/presentation/ui/utils/compose/ScreenComposableKtTest.kt
new file mode 100644
index 000000000..2bbd9d0e3
--- /dev/null
+++ b/app/src/androidTest/java/com/picpay/desafio/android/presentation/ui/utils/compose/ScreenComposableKtTest.kt
@@ -0,0 +1,115 @@
+package com.picpay.desafio.android.presentation.ui.utils.compose
+
+import androidx.compose.material3.Text
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import com.picpay.desafio.android.domain.utils.ErrorInformation
+import com.picpay.desafio.android.domain.utils.State
+import com.picpay.desafio.android.presentation.ui.features.contacts.compose.CACHE_TAG
+import com.picpay.desafio.android.presentation.ui.features.contacts.compose.ERROR_TAG
+import com.picpay.desafio.android.presentation.ui.features.contacts.compose.LOADING_IMAGE_TAG
+import com.picpay.desafio.android.presentation.ui.features.contacts.compose.LOADING_TAG
+import com.picpay.desafio.android.presentation.ui.utils.FeedbackListener
+import io.mockk.mockk
+import org.junit.Rule
+import org.junit.Test
+
+private const val SUCCESS_TAG = "successTag"
+
+class ScreenComposableTest {
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ // Prepare
+ private val listener = mockk(relaxed = true)
+
+ private fun setCompose(screenState: State) {
+ composeTestRule.setContent {
+ ScreenComposable(
+ screenState = screenState,
+ listener = listener,
+ toolbar = {},
+ successContent = {
+ Text(text = "Success Content", modifier = Modifier.testTag(SUCCESS_TAG))
+ }
+ )
+ }
+ }
+
+ @Test
+ fun screenComposable_should_display_loading_content_when_state_is_Loading() {
+ // Given
+ val state = State.Loading
+
+ // When
+ setCompose(state)
+
+ // Then
+ composeTestRule.onNodeWithTag(LOADING_TAG).assertIsDisplayed()
+ composeTestRule.onNodeWithTag(LOADING_IMAGE_TAG).assertIsDisplayed()
+
+ composeTestRule.onNodeWithTag(ERROR_TAG).assertDoesNotExist()
+ composeTestRule.onNodeWithTag(CACHE_TAG).assertDoesNotExist()
+ composeTestRule.onNodeWithTag(SUCCESS_TAG).assertDoesNotExist()
+ }
+
+ @Test
+ fun screenComposable_should_display_error_content_when_state_is_Error() {
+ // Given
+ val state = State.Error(
+ error = ErrorInformation(
+ message = "Ocorreu um erro, tente novamente."
+ )
+ )
+
+ // When
+ setCompose(state)
+
+ // Then
+ composeTestRule.onNodeWithTag(ERROR_TAG).assertIsDisplayed()
+
+ composeTestRule.onNodeWithTag(LOADING_TAG).assertDoesNotExist()
+ composeTestRule.onNodeWithTag(CACHE_TAG).assertDoesNotExist()
+ composeTestRule.onNodeWithTag(SUCCESS_TAG).assertDoesNotExist()
+ }
+
+ @Test
+ fun screenComposable_should_display_cache_alert_and_success_content_when_state_is_ErrorWithCache() {
+ // Given
+ val state = State.ErrorWithCache(
+ error = ErrorInformation(
+ message = "Não foi possível conectar ao servidor. Exibindo dados em cache."
+ )
+ )
+
+ // When
+ setCompose(state)
+
+ // Then
+ composeTestRule.onNodeWithTag(CACHE_TAG).assertIsDisplayed()
+ composeTestRule.onNodeWithTag(SUCCESS_TAG).assertIsDisplayed()
+
+ composeTestRule.onNodeWithTag(ERROR_TAG).assertDoesNotExist()
+ composeTestRule.onNodeWithTag(LOADING_TAG).assertDoesNotExist()
+ }
+
+ @Test
+ fun screenComposable_should_display_success_content_when_state_is_Success() {
+ // Given
+ val state = State.Success(Any())
+
+ // When
+ setCompose(state)
+
+ // Then
+ composeTestRule.onNodeWithTag(SUCCESS_TAG).assertIsDisplayed()
+
+ composeTestRule.onNodeWithTag(LOADING_TAG).assertDoesNotExist()
+ composeTestRule.onNodeWithTag(ERROR_TAG).assertDoesNotExist()
+ composeTestRule.onNodeWithTag(CACHE_TAG).assertDoesNotExist()
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7bdf2ce38..96b624331 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -6,6 +6,7 @@
-
+
diff --git a/app/src/main/java/com/picpay/desafio/android/App.kt b/app/src/main/java/com/picpay/desafio/android/App.kt
new file mode 100644
index 000000000..644e52c31
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/App.kt
@@ -0,0 +1,20 @@
+package com.picpay.desafio.android
+
+import android.app.Application
+import com.picpay.desafio.android.data.di.DataModule
+import com.picpay.desafio.android.domain.di.DomainModule
+import com.picpay.desafio.android.presentation.di.PresentationModule
+import org.koin.android.ext.koin.androidContext
+import org.koin.core.context.startKoin
+
+class App : Application() {
+ override fun onCreate() {
+ super.onCreate()
+
+ startKoin { androidContext(this@App) }
+
+ DomainModule.load()
+ DataModule.load()
+ PresentationModule.load()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/MainActivity.kt b/app/src/main/java/com/picpay/desafio/android/MainActivity.kt
deleted file mode 100644
index 2447de98d..000000000
--- a/app/src/main/java/com/picpay/desafio/android/MainActivity.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-package com.picpay.desafio.android
-
-import android.view.View
-import android.widget.ProgressBar
-import android.widget.Toast
-import androidx.appcompat.app.AppCompatActivity
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
-import com.google.gson.Gson
-import com.google.gson.GsonBuilder
-import okhttp3.OkHttpClient
-import retrofit2.Call
-import retrofit2.Callback
-import retrofit2.Response
-import retrofit2.Retrofit
-import retrofit2.converter.gson.GsonConverterFactory
-
-class MainActivity : AppCompatActivity(R.layout.activity_main) {
-
- private lateinit var recyclerView: RecyclerView
- private lateinit var progressBar: ProgressBar
- private lateinit var adapter: UserListAdapter
-
- private val url = "https://609a908e0f5a13001721b74e.mockapi.io/picpay/api/"
-
- private val gson: Gson by lazy { GsonBuilder().create() }
-
- private val okHttp: OkHttpClient by lazy {
- OkHttpClient.Builder()
- .build()
- }
-
- private val retrofit: Retrofit by lazy {
- Retrofit.Builder()
- .baseUrl(url)
- .client(okHttp)
- .addConverterFactory(GsonConverterFactory.create(gson))
- .build()
- }
-
- private val service: PicPayService by lazy {
- retrofit.create(PicPayService::class.java)
- }
-
- override fun onResume() {
- super.onResume()
-
- recyclerView = findViewById(R.id.recyclerView)
- progressBar = findViewById(R.id.user_list_progress_bar)
-
- adapter = UserListAdapter()
- recyclerView.adapter = adapter
- recyclerView.layoutManager = LinearLayoutManager(this)
-
- progressBar.visibility = View.VISIBLE
- service.getUsers()
- .enqueue(object : Callback> {
- override fun onFailure(call: Call>, t: Throwable) {
- val message = getString(R.string.error)
-
- progressBar.visibility = View.GONE
- recyclerView.visibility = View.GONE
-
- Toast.makeText(this@MainActivity, message, Toast.LENGTH_SHORT)
- .show()
- }
-
- override fun onResponse(call: Call>, response: Response>) {
- progressBar.visibility = View.GONE
-
- adapter.users = response.body()!!
- }
- })
- }
-}
diff --git a/app/src/main/java/com/picpay/desafio/android/PicPayService.kt b/app/src/main/java/com/picpay/desafio/android/PicPayService.kt
deleted file mode 100644
index c26edac1f..000000000
--- a/app/src/main/java/com/picpay/desafio/android/PicPayService.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.picpay.desafio.android
-
-import retrofit2.Call
-import retrofit2.http.GET
-
-
-interface PicPayService {
-
- @GET("users")
- fun getUsers(): Call>
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/User.kt b/app/src/main/java/com/picpay/desafio/android/User.kt
deleted file mode 100644
index aa28171c9..000000000
--- a/app/src/main/java/com/picpay/desafio/android/User.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.picpay.desafio.android
-
-import android.os.Parcelable
-import com.google.gson.annotations.SerializedName
-import kotlinx.android.parcel.Parcelize
-
-@Parcelize
-data class User(
- @SerializedName("img") val img: String,
- @SerializedName("name") val name: String,
- @SerializedName("id") val id: Int,
- @SerializedName("username") val username: String
-) : Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/UserListAdapter.kt b/app/src/main/java/com/picpay/desafio/android/UserListAdapter.kt
deleted file mode 100644
index 538c98a4a..000000000
--- a/app/src/main/java/com/picpay/desafio/android/UserListAdapter.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-package com.picpay.desafio.android
-
-import android.view.LayoutInflater
-import android.view.ViewGroup
-import androidx.recyclerview.widget.DiffUtil
-import androidx.recyclerview.widget.RecyclerView
-
-class UserListAdapter : RecyclerView.Adapter() {
-
- var users = emptyList()
- set(value) {
- val result = DiffUtil.calculateDiff(
- UserListDiffCallback(
- field,
- value
- )
- )
- result.dispatchUpdatesTo(this)
- field = value
- }
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserListItemViewHolder {
- val view = LayoutInflater.from(parent.context)
- .inflate(R.layout.list_item_user, parent, false)
-
- return UserListItemViewHolder(view)
- }
-
- override fun onBindViewHolder(holder: UserListItemViewHolder, position: Int) {
- holder.bind(users[position])
- }
-
- override fun getItemCount(): Int = users.size
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/UserListDiffCallback.kt b/app/src/main/java/com/picpay/desafio/android/UserListDiffCallback.kt
deleted file mode 100644
index 7c734d37b..000000000
--- a/app/src/main/java/com/picpay/desafio/android/UserListDiffCallback.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.picpay.desafio.android
-
-import androidx.recyclerview.widget.DiffUtil
-import com.picpay.desafio.android.User
-
-class UserListDiffCallback(
- private val oldList: List,
- private val newList: List
-) : DiffUtil.Callback() {
-
- override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
- return oldList[oldItemPosition].username.equals(newList[newItemPosition].username)
- }
-
- override fun getOldListSize(): Int {
- return oldList.size
- }
-
- override fun getNewListSize(): Int {
- return newList.size
- }
-
- override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
- return true
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/UserListItemViewHolder.kt b/app/src/main/java/com/picpay/desafio/android/UserListItemViewHolder.kt
deleted file mode 100644
index 1d8240eb3..000000000
--- a/app/src/main/java/com/picpay/desafio/android/UserListItemViewHolder.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.picpay.desafio.android
-
-import android.view.View
-import androidx.recyclerview.widget.RecyclerView
-import com.squareup.picasso.Callback
-import com.squareup.picasso.Picasso
-import kotlinx.android.synthetic.main.list_item_user.view.*
-
-class UserListItemViewHolder(
- itemView: View
-) : RecyclerView.ViewHolder(itemView) {
-
- fun bind(user: User) {
- itemView.name.text = user.name
- itemView.username.text = user.username
- itemView.progressBar.visibility = View.VISIBLE
- Picasso.get()
- .load(user.img)
- .error(R.drawable.ic_round_account_circle)
- .into(itemView.picture, object : Callback {
- override fun onSuccess() {
- itemView.progressBar.visibility = View.GONE
- }
-
- override fun onError(e: Exception?) {
- itemView.progressBar.visibility = View.GONE
- }
- })
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/data/di/DaoModule.kt b/app/src/main/java/com/picpay/desafio/android/data/di/DaoModule.kt
new file mode 100644
index 000000000..c253afb22
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/data/di/DaoModule.kt
@@ -0,0 +1,13 @@
+package com.picpay.desafio.android.data.di
+
+import com.picpay.desafio.android.data.features.contacts.local.database.ContactDatabase
+import org.koin.android.ext.koin.androidContext
+import org.koin.dsl.module
+
+object DaoModule {
+ fun getModule() = dao
+
+ private val dao = module {
+ single { ContactDatabase.getInstance(androidContext()).dao }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/data/di/DataModule.kt b/app/src/main/java/com/picpay/desafio/android/data/di/DataModule.kt
new file mode 100644
index 000000000..383f474c9
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/data/di/DataModule.kt
@@ -0,0 +1,37 @@
+package com.picpay.desafio.android.data.di
+
+import android.util.Log
+import com.google.gson.GsonBuilder
+import com.picpay.desafio.android.data.utils.Constants.OK_HTTP
+import okhttp3.OkHttpClient
+import okhttp3.logging.HttpLoggingInterceptor
+import org.koin.core.context.loadKoinModules
+import org.koin.dsl.module
+
+object DataModule {
+ fun load() = loadKoinModules(
+ listOf(
+ RepositoryModule.getModule(),
+ ServiceModule.getModule(),
+ DaoModule.getModule(),
+ networkModule,
+ )
+ )
+
+ private val networkModule = module {
+ single { createOkHttpClient() }
+
+ single { GsonBuilder().create() }
+ }
+
+ private fun createOkHttpClient(): OkHttpClient {
+ val interceptor = HttpLoggingInterceptor {
+ Log.i(OK_HTTP, it)
+ }
+ interceptor.level = HttpLoggingInterceptor.Level.BODY
+
+ return OkHttpClient.Builder()
+ .addInterceptor(interceptor)
+ .build()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/data/di/RepositoryModule.kt b/app/src/main/java/com/picpay/desafio/android/data/di/RepositoryModule.kt
new file mode 100644
index 000000000..291250661
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/data/di/RepositoryModule.kt
@@ -0,0 +1,18 @@
+package com.picpay.desafio.android.data.di
+
+import com.picpay.desafio.android.data.features.contacts.repository.ContactsRepositoryImpl
+import com.picpay.desafio.android.domain.features.contacts.repository.ContactsRepository
+import org.koin.dsl.module
+
+object RepositoryModule {
+ fun getModule() = repository
+
+ private val repository = module {
+ single {
+ ContactsRepositoryImpl(
+ service = get(),
+ dao = get()
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/com/picpay/desafio/android/data/di/ServiceModule.kt b/app/src/main/java/com/picpay/desafio/android/data/di/ServiceModule.kt
new file mode 100644
index 000000000..272747a47
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/data/di/ServiceModule.kt
@@ -0,0 +1,31 @@
+package com.picpay.desafio.android.data.di
+
+import com.google.gson.Gson
+import com.picpay.desafio.android.data.features.contacts.service.ContactsService
+import com.picpay.desafio.android.data.utils.Constants
+import okhttp3.OkHttpClient
+import org.koin.dsl.module
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+
+object ServiceModule {
+ fun getModule() = service
+
+ private inline fun createService(
+ client: OkHttpClient,
+ converterFactory: Gson,
+ ): T = Retrofit.Builder()
+ .baseUrl(Constants.BASE_URL)
+ .client(client)
+ .addConverterFactory(
+ GsonConverterFactory.create(
+ converterFactory
+ )
+ )
+ .build()
+ .create(T::class.java)
+
+ private val service = module {
+ single { createService(get(), get()) }
+ }
+}
diff --git a/app/src/main/java/com/picpay/desafio/android/data/features/contacts/local/dao/ContactDao.kt b/app/src/main/java/com/picpay/desafio/android/data/features/contacts/local/dao/ContactDao.kt
new file mode 100644
index 000000000..f0abc058b
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/data/features/contacts/local/dao/ContactDao.kt
@@ -0,0 +1,20 @@
+package com.picpay.desafio.android.data.features.contacts.local.dao
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import com.picpay.desafio.android.data.features.contacts.local.entities.database.ContactDb
+import kotlinx.coroutines.flow.Flow
+
+@Dao
+interface ContactDao {
+ @Insert(onConflict = OnConflictStrategy.IGNORE)
+ suspend fun insertAll(list: List)
+
+ @Query("SELECT * FROM contact")
+ fun queryContacts(): Flow>
+
+ @Query("DELETE FROM contact")
+ suspend fun deleteAll()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/data/features/contacts/local/database/ContactDatabase.kt b/app/src/main/java/com/picpay/desafio/android/data/features/contacts/local/database/ContactDatabase.kt
new file mode 100644
index 000000000..b485712d6
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/data/features/contacts/local/database/ContactDatabase.kt
@@ -0,0 +1,39 @@
+package com.picpay.desafio.android.data.features.contacts.local.database
+
+import android.content.Context
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import com.picpay.desafio.android.data.features.contacts.local.dao.ContactDao
+import com.picpay.desafio.android.data.features.contacts.local.entities.database.ContactDb
+
+@Database(
+ entities = [ContactDb::class],
+ version = 1,
+ exportSchema = false
+)
+
+abstract class ContactDatabase : RoomDatabase() {
+ abstract val dao: ContactDao
+
+ companion object {
+ @Volatile
+ private var INSTANCE: ContactDatabase? = null
+
+ fun getInstance(context: Context): ContactDatabase {
+ synchronized(this) {
+ var instance = INSTANCE
+ if (instance == null) {
+ instance = Room.databaseBuilder(
+ context.applicationContext,
+ ContactDatabase::class.java,
+ "contact_cache_db"
+ ).fallbackToDestructiveMigration()
+ .build()
+ INSTANCE = instance
+ }
+ return instance
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/data/features/contacts/local/entities/database/ContactDb.kt b/app/src/main/java/com/picpay/desafio/android/data/features/contacts/local/entities/database/ContactDb.kt
new file mode 100644
index 000000000..c3caf1177
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/data/features/contacts/local/entities/database/ContactDb.kt
@@ -0,0 +1,25 @@
+package com.picpay.desafio.android.data.features.contacts.local.entities.database
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import com.picpay.desafio.android.domain.features.contacts.model.Contact
+
+@Entity(tableName = "contact")
+data class ContactDb(
+ @PrimaryKey
+ val id: Int,
+ val name: String,
+ val username: String,
+ val image: String
+)
+
+fun ContactDb.toDomain() = Contact(
+ id = id,
+ name = name,
+ username = username,
+ image = image
+)
+
+fun List.toDomain() = this.map {
+ it.toDomain()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/data/features/contacts/model/ContactRemote.kt b/app/src/main/java/com/picpay/desafio/android/data/features/contacts/model/ContactRemote.kt
new file mode 100644
index 000000000..0f0bcd4c8
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/data/features/contacts/model/ContactRemote.kt
@@ -0,0 +1,37 @@
+package com.picpay.desafio.android.data.features.contacts.model
+
+import android.os.Parcelable
+import com.google.gson.annotations.SerializedName
+import com.picpay.desafio.android.data.features.contacts.local.entities.database.ContactDb
+import com.picpay.desafio.android.domain.features.contacts.model.Contact
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class ContactRemote(
+ @SerializedName("id") val id: Int?,
+ @SerializedName("name") val name: String?,
+ @SerializedName("username") val username: String?,
+ @SerializedName("img") val image: String?
+) : Parcelable
+
+fun ContactRemote.toDomain() = Contact(
+ id = id ?: 0,
+ name = name.orEmpty(),
+ username = username.orEmpty(),
+ image = image.orEmpty()
+)
+
+fun List.toDomain() = this.map {
+ it.toDomain()
+}
+
+fun ContactRemote.toDb() = ContactDb(
+ id = id ?: 0,
+ name = name.orEmpty(),
+ username = username.orEmpty(),
+ image = image.orEmpty()
+)
+
+fun List.toDb() = this.map {
+ it.toDb()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/data/features/contacts/repository/ContactsRepositoryImpl.kt b/app/src/main/java/com/picpay/desafio/android/data/features/contacts/repository/ContactsRepositoryImpl.kt
new file mode 100644
index 000000000..43b9540cb
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/data/features/contacts/repository/ContactsRepositoryImpl.kt
@@ -0,0 +1,28 @@
+package com.picpay.desafio.android.data.features.contacts.repository
+
+import com.picpay.desafio.android.data.features.contacts.local.dao.ContactDao
+import com.picpay.desafio.android.data.features.contacts.local.entities.database.toDomain
+import com.picpay.desafio.android.data.features.contacts.model.toDb
+import com.picpay.desafio.android.data.features.contacts.service.ContactsService
+import com.picpay.desafio.android.data.utils.networkAdapter
+import com.picpay.desafio.android.domain.features.contacts.model.Contact
+import com.picpay.desafio.android.domain.features.contacts.repository.ContactsRepository
+import com.picpay.desafio.android.domain.utils.State
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+class ContactsRepositoryImpl(
+ private val service: ContactsService,
+ private val dao: ContactDao,
+) : ContactsRepository {
+ override suspend fun getContacts(endpoint: String): Flow>> =
+ networkAdapter(
+ query = { dao.queryContacts().map { it.toDomain() } },
+ get = { service.getContacts(endpoint) },
+ saveGetResult = { listContactDb ->
+ dao.deleteAll()
+ dao.insertAll(listContactDb.toDb())
+ },
+ )
+}
+
diff --git a/app/src/main/java/com/picpay/desafio/android/data/features/contacts/service/ContactsService.kt b/app/src/main/java/com/picpay/desafio/android/data/features/contacts/service/ContactsService.kt
new file mode 100644
index 000000000..3213ef446
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/data/features/contacts/service/ContactsService.kt
@@ -0,0 +1,13 @@
+package com.picpay.desafio.android.data.features.contacts.service
+
+import com.picpay.desafio.android.data.features.contacts.model.ContactRemote
+import retrofit2.http.GET
+import retrofit2.http.Path
+
+interface ContactsService {
+ @GET("{endpoint}")
+ suspend fun getContacts(
+ @Path("endpoint")
+ endpoint: String
+ ): List
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/data/utils/Constants.kt b/app/src/main/java/com/picpay/desafio/android/data/utils/Constants.kt
new file mode 100644
index 000000000..c7602bf87
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/data/utils/Constants.kt
@@ -0,0 +1,6 @@
+package com.picpay.desafio.android.data.utils
+
+object Constants {
+ internal const val BASE_URL = "https://609a908e0f5a13001721b74e.mockapi.io/picpay/api/"
+ internal const val OK_HTTP = "Ok Http"
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/data/utils/NetworkAdapter.kt b/app/src/main/java/com/picpay/desafio/android/data/utils/NetworkAdapter.kt
new file mode 100644
index 000000000..d109a382e
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/data/utils/NetworkAdapter.kt
@@ -0,0 +1,41 @@
+package com.picpay.desafio.android.data.utils
+
+import com.picpay.desafio.android.domain.utils.State
+import com.picpay.desafio.android.domain.utils.toErrorInformation
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flow
+
+inline fun networkAdapter(
+ crossinline query: () -> Flow,
+ crossinline get: suspend () -> Request,
+ crossinline saveGetResult: suspend (Request) -> Unit,
+): Flow> = flow {
+ var data = query().first()
+
+ try {
+ saveGetResult(get())
+ data = query().first()
+
+ } catch (error: Exception) {
+ if (data != emptyList() && data != null) {
+ emit(
+ State.ErrorWithCache(
+ data,
+ error.toErrorInformation(
+ "Não foi possível conectar ao servidor. Exibindo dados em cache."
+ ),
+ )
+ )
+ } else {
+ emit(
+ State.Error(
+ error = error.toErrorInformation(
+ "Não foi possível conectar ao servidor."
+ ),
+ )
+ )
+ }
+ }
+ emit(State.Success(data))
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/domain/di/DomainModule.kt b/app/src/main/java/com/picpay/desafio/android/domain/di/DomainModule.kt
new file mode 100644
index 000000000..8798a1bfa
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/domain/di/DomainModule.kt
@@ -0,0 +1,13 @@
+package com.picpay.desafio.android.domain.di
+
+import com.picpay.desafio.android.domain.features.contacts.usecase.GetContactsUseCase
+import org.koin.core.context.loadKoinModules
+import org.koin.dsl.module
+
+object DomainModule {
+ fun load() = loadKoinModules(useCaseModule)
+
+ private val useCaseModule = module {
+ factory { GetContactsUseCase(get()) }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/domain/features/contacts/model/Contact.kt b/app/src/main/java/com/picpay/desafio/android/domain/features/contacts/model/Contact.kt
new file mode 100644
index 000000000..b65057c7a
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/domain/features/contacts/model/Contact.kt
@@ -0,0 +1,19 @@
+package com.picpay.desafio.android.domain.features.contacts.model
+
+import androidx.compose.runtime.Immutable
+import com.picpay.desafio.android.domain.utils.State
+import com.picpay.desafio.android.domain.utils.StateBearer
+import java.io.Serializable
+
+data class ContactsScreen(
+ override val state: State = State.Loading,
+ val contacts: List? = null
+) : Serializable, StateBearer
+
+@Immutable
+data class Contact(
+ val id: Int,
+ val name: String,
+ val username: String,
+ val image: String
+) : Serializable
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/domain/features/contacts/repository/ContactsRepository.kt b/app/src/main/java/com/picpay/desafio/android/domain/features/contacts/repository/ContactsRepository.kt
new file mode 100644
index 000000000..cc6d43588
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/domain/features/contacts/repository/ContactsRepository.kt
@@ -0,0 +1,9 @@
+package com.picpay.desafio.android.domain.features.contacts.repository
+
+import com.picpay.desafio.android.domain.features.contacts.model.Contact
+import com.picpay.desafio.android.domain.utils.State
+import kotlinx.coroutines.flow.Flow
+
+interface ContactsRepository {
+ suspend fun getContacts(endpoint: String): Flow>>
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/domain/features/contacts/usecase/GetContactsUseCase.kt b/app/src/main/java/com/picpay/desafio/android/domain/features/contacts/usecase/GetContactsUseCase.kt
new file mode 100644
index 000000000..22872d24a
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/domain/features/contacts/usecase/GetContactsUseCase.kt
@@ -0,0 +1,52 @@
+package com.picpay.desafio.android.domain.features.contacts.usecase
+
+import com.picpay.desafio.android.domain.features.contacts.model.ContactsScreen
+import com.picpay.desafio.android.domain.features.contacts.repository.ContactsRepository
+import com.picpay.desafio.android.domain.utils.State
+import com.picpay.desafio.android.domain.utils.UseCase
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flow
+
+class GetContactsUseCase(
+ private val repository: ContactsRepository
+) : UseCase() {
+ override suspend fun execute(
+ param: String,
+ currentState: ContactsScreen,
+ ): Flow = flow {
+ emit(currentState.copy(state = State.Loading))
+
+ when (val result = repository.getContacts(param).first()) {
+ is State.Success -> {
+ emit(
+ currentState.copy(
+ state = State.Success(data = result.data),
+ contacts = result.data,
+ )
+ )
+ }
+
+ is State.Error -> {
+ emit(
+ currentState.copy(
+ state = State.Error(error = result.error),
+ )
+ )
+ }
+
+ is State.ErrorWithCache -> {
+ emit(
+ currentState.copy(
+ state = State.ErrorWithCache(data = result.data, error = result.error),
+ contacts = result.data,
+ )
+ )
+ }
+
+ else -> {
+ emit(currentState.copy(state = State.Loading))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/domain/utils/State.kt b/app/src/main/java/com/picpay/desafio/android/domain/utils/State.kt
new file mode 100644
index 000000000..a4ecc355d
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/domain/utils/State.kt
@@ -0,0 +1,32 @@
+package com.picpay.desafio.android.domain.utils
+
+sealed class State {
+ data object Loading : State()
+
+ data class Success(val data: T) : State()
+
+ data class Error(val error: ErrorInformation) : State()
+
+ data class ErrorWithCache(
+ val data: T? = null,
+ val error: ErrorInformation = ErrorInformation(
+ "Não foi possível conectar ao servidor. Exibindo dados em cache."
+ ),
+ ) : State()
+}
+
+data class ErrorInformation(
+ val message: String? = null,
+ val cause: Throwable? = null,
+)
+
+fun Exception.toErrorInformation(message: String? = null) = ErrorInformation(
+ message = message
+ ?: localizedMessage
+ ?: "Ocorreu um erro. Tente novamente.",
+ cause = this
+)
+
+interface StateBearer {
+ val state: State
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/domain/utils/UseCase.kt b/app/src/main/java/com/picpay/desafio/android/domain/utils/UseCase.kt
new file mode 100644
index 000000000..8d5874529
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/domain/utils/UseCase.kt
@@ -0,0 +1,15 @@
+package com.picpay.desafio.android.domain.utils
+
+import kotlinx.coroutines.flow.Flow
+
+abstract class UseCase {
+ abstract suspend fun execute(
+ param: K,
+ currentState: T,
+ ): Flow
+
+ suspend operator fun invoke(
+ param: K,
+ currentState: T
+ ) = execute(param, currentState)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/presentation/MainActivity.kt b/app/src/main/java/com/picpay/desafio/android/presentation/MainActivity.kt
new file mode 100644
index 000000000..041fafd0a
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/presentation/MainActivity.kt
@@ -0,0 +1,30 @@
+package com.picpay.desafio.android.presentation
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.collectAsState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.picpay.desafio.android.presentation.ui.features.contacts.ContactsViewModel
+import com.picpay.desafio.android.presentation.ui.features.contacts.compose.ContactsScreenComposable
+import org.koin.androidx.viewmodel.ext.android.viewModel
+
+class MainActivity : ComponentActivity() {
+ private val viewModel: ContactsViewModel by viewModel()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ ContactsScreenComposable(
+ modifier = Modifier
+ .padding(horizontal = 24.dp)
+ .fillMaxSize(),
+ screen = viewModel.screen.collectAsState().value,
+ listener = viewModel
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/com/picpay/desafio/android/presentation/di/PresentationModule.kt b/app/src/main/java/com/picpay/desafio/android/presentation/di/PresentationModule.kt
new file mode 100644
index 000000000..acb56d363
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/presentation/di/PresentationModule.kt
@@ -0,0 +1,20 @@
+package com.picpay.desafio.android.presentation.di
+
+import androidx.lifecycle.SavedStateHandle
+import com.picpay.desafio.android.presentation.ui.features.contacts.ContactsViewModel
+import org.koin.androidx.viewmodel.dsl.viewModel
+import org.koin.core.context.loadKoinModules
+import org.koin.dsl.module
+
+object PresentationModule {
+ fun load() = loadKoinModules(viewModelModule,)
+
+ private val viewModelModule = module {
+ viewModel { (savedState: SavedStateHandle) ->
+ ContactsViewModel(
+ getContactsUseCase = get(),
+ savedState = savedState,
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/presentation/ui/features/contacts/ContactsListener.kt b/app/src/main/java/com/picpay/desafio/android/presentation/ui/features/contacts/ContactsListener.kt
new file mode 100644
index 000000000..c07e47f67
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/presentation/ui/features/contacts/ContactsListener.kt
@@ -0,0 +1,9 @@
+package com.picpay.desafio.android.presentation.ui.features.contacts
+
+import com.picpay.desafio.android.presentation.ui.utils.FeedbackListener
+
+interface ContactsListener : FeedbackListener
+
+object DummyContactsListener : ContactsListener {
+ override fun onButtonClicked() = Unit
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/presentation/ui/features/contacts/ContactsViewModel.kt b/app/src/main/java/com/picpay/desafio/android/presentation/ui/features/contacts/ContactsViewModel.kt
new file mode 100644
index 000000000..084e4d405
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/presentation/ui/features/contacts/ContactsViewModel.kt
@@ -0,0 +1,54 @@
+package com.picpay.desafio.android.presentation.ui.features.contacts
+
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.picpay.desafio.android.domain.features.contacts.model.ContactsScreen
+import com.picpay.desafio.android.domain.features.contacts.usecase.GetContactsUseCase
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+
+const val CONTACTS_STATE = "CONTACTS_STATE"
+const val CONTACTS_PARAM = "users"
+
+class ContactsViewModel(
+ private val getContactsUseCase: GetContactsUseCase,
+ private val savedState: SavedStateHandle,
+) : ViewModel(), ContactsListener {
+ private val _screen = MutableStateFlow(ContactsScreen())
+ val screen: StateFlow = _screen.asStateFlow()
+
+ init {
+ if (savedState.contains(CONTACTS_STATE).not()) {
+ getUsers()
+ } else {
+ savedState.get(CONTACTS_STATE)?.let {
+ viewModelScope.launch {
+ _screen.emit(it)
+ }
+ }
+ }
+ }
+
+ private fun getUsers() {
+ viewModelScope.launch {
+ getContactsUseCase(
+ CONTACTS_PARAM,
+ screen.value
+ )
+ .onEach {
+ savedState[CONTACTS_STATE] = it
+ }
+ .collect {
+ _screen.emit(it)
+ }
+ }
+ }
+
+ override fun onButtonClicked() {
+ getUsers()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/presentation/ui/features/contacts/compose/ContactComposable.kt b/app/src/main/java/com/picpay/desafio/android/presentation/ui/features/contacts/compose/ContactComposable.kt
new file mode 100644
index 000000000..6bbb6cd3c
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/presentation/ui/features/contacts/compose/ContactComposable.kt
@@ -0,0 +1,88 @@
+package com.picpay.desafio.android.presentation.ui.features.contacts.compose
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.shape.CircleShape
+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.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
+import coil3.compose.AsyncImage
+import coil3.request.CachePolicy
+import coil3.request.ImageRequest
+import coil3.request.transformations
+import coil3.transform.RoundedCornersTransformation
+import com.picpay.desafio.android.R
+import com.picpay.desafio.android.domain.features.contacts.model.Contact
+import com.picpay.desafio.android.presentation.ui.features.contacts.compose.fakedata.ContactFakeData
+
+const val IMAGE_TAG = "image"
+const val USERNAME_TAG = "username"
+const val NAME_TAG = "name"
+
+@Composable
+fun ContactComposable(
+ contact: Contact,
+ modifier: Modifier = Modifier,
+) {
+ Row(
+ modifier = modifier,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ AsyncImage(
+ modifier = Modifier
+ .size(52.dp)
+ .clip(CircleShape)
+ .testTag(IMAGE_TAG),
+ model = ImageRequest.Builder(LocalContext.current)
+ .data(contact.image)
+ .memoryCachePolicy(CachePolicy.ENABLED)
+ .transformations(RoundedCornersTransformation(radius = 16f))
+ .build(),
+ placeholder = painterResource(R.drawable.ic_round_account_circle),
+ contentDescription = stringResource(R.string.image_profile_description),
+ contentScale = ContentScale.Crop,
+ )
+
+ Column(
+ modifier = Modifier
+ .padding(start = 16.dp)
+ ) {
+ Text(
+ modifier = Modifier.testTag(USERNAME_TAG),
+ text = contact.username,
+ color = Color.White
+ )
+ Text(
+ modifier = Modifier.testTag(NAME_TAG),
+ text = contact.name,
+ color = colorResource(R.color.colorDetail)
+ )
+ }
+ }
+}
+
+@Composable
+@Preview("ContactPreview")
+private fun ContactPreview(
+ @PreviewParameter(ContactFakeData::class) contact: Contact
+) {
+ ContactComposable(
+ modifier = Modifier.wrapContentSize(),
+ contact = contact
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/presentation/ui/features/contacts/compose/ContactsScreenComposable.kt b/app/src/main/java/com/picpay/desafio/android/presentation/ui/features/contacts/compose/ContactsScreenComposable.kt
new file mode 100644
index 000000000..da9c9f7c1
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/presentation/ui/features/contacts/compose/ContactsScreenComposable.kt
@@ -0,0 +1,91 @@
+package com.picpay.desafio.android.presentation.ui.features.contacts.compose
+
+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.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+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.platform.testTag
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.picpay.desafio.android.R
+import com.picpay.desafio.android.domain.features.contacts.model.ContactsScreen
+import com.picpay.desafio.android.presentation.ui.features.contacts.ContactsListener
+import com.picpay.desafio.android.presentation.ui.features.contacts.DummyContactsListener
+import com.picpay.desafio.android.presentation.ui.features.contacts.compose.fakedata.ContactsScreenFakeData
+import com.picpay.desafio.android.presentation.ui.utils.compose.ScreenComposable
+
+internal const val ERROR_TAG = "error"
+internal const val LOADING_TAG = "loading"
+internal const val SUCCESS_TAG = "success"
+internal const val LOADING_IMAGE_TAG = "loading_image"
+internal const val CACHE_TAG = "cache_tag"
+
+@Composable
+fun ContactsScreenComposable(
+ screen: ContactsScreen,
+ listener: ContactsListener,
+ modifier: Modifier = Modifier,
+) {
+ ScreenComposable(
+ screenState = screen.state,
+ listener = listener,
+ toolbar = {
+ Text(
+ modifier = Modifier.padding(24.dp),
+ text = stringResource(R.string.title),
+ color = Color.White,
+ fontSize = 28.sp,
+ fontWeight = FontWeight.Bold,
+ )
+ },
+ successContent = {
+ Column(
+ modifier = modifier
+ .testTag(SUCCESS_TAG)
+ ) {
+ screen.contacts?.let { contacts ->
+ LazyColumn(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ items(
+ items = contacts,
+ key = { it.id }
+ ) { contact ->
+ ContactComposable(
+ modifier = Modifier
+ .padding(vertical = 12.dp)
+ .fillMaxWidth(),
+ contact = contact,
+ )
+ }
+ }
+ }
+ }
+ }
+ )
+}
+
+
+@Composable
+@Preview("ContactsScreenPreview")
+private fun ContactsScreenPreview(
+ @PreviewParameter(ContactsScreenFakeData::class) data: ContactsScreen
+) {
+ ContactsScreenComposable(
+ modifier = Modifier
+ .padding(horizontal = 24.dp)
+ .fillMaxSize(),
+ screen = data,
+ listener = DummyContactsListener,
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/presentation/ui/features/contacts/compose/fakedata/ContactFakeData.kt b/app/src/main/java/com/picpay/desafio/android/presentation/ui/features/contacts/compose/fakedata/ContactFakeData.kt
new file mode 100644
index 000000000..0fc027c92
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/presentation/ui/features/contacts/compose/fakedata/ContactFakeData.kt
@@ -0,0 +1,20 @@
+package com.picpay.desafio.android.presentation.ui.features.contacts.compose.fakedata
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import com.picpay.desafio.android.domain.features.contacts.model.Contact
+
+class ContactFakeData : PreviewParameterProvider {
+ private fun contact(id: Int): Contact {
+ return Contact(
+ id = id,
+ name = "Rodrigo Murta",
+ username = "rodrigo.mmurta$id",
+ image = "https://randomuser.me/api/portraits/men/${id + 1}.jpg"
+ )
+ }
+
+ override val values: Sequence
+ get() = List(size = 10) { index ->
+ contact(index)
+ }.asSequence()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/presentation/ui/features/contacts/compose/fakedata/ContactsScreenFakeData.kt b/app/src/main/java/com/picpay/desafio/android/presentation/ui/features/contacts/compose/fakedata/ContactsScreenFakeData.kt
new file mode 100644
index 000000000..9196903b1
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/presentation/ui/features/contacts/compose/fakedata/ContactsScreenFakeData.kt
@@ -0,0 +1,54 @@
+package com.picpay.desafio.android.presentation.ui.features.contacts.compose.fakedata
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import com.picpay.desafio.android.domain.features.contacts.model.Contact
+import com.picpay.desafio.android.domain.features.contacts.model.ContactsScreen
+import com.picpay.desafio.android.domain.utils.ErrorInformation
+import com.picpay.desafio.android.domain.utils.State
+
+class ContactsScreenFakeData : PreviewParameterProvider {
+ private fun contact(id: Int): Contact {
+ return Contact(
+ id = id,
+ name = "Rodrigo Murta",
+ username = "rodrigo.mmurta$id",
+ image = "https://randomuser.me/api/portraits/men/${id + 1}.jpg"
+ )
+ }
+
+ private fun listContacts(size: Int): List = List(size) { index ->
+ contact(index)
+ }
+
+ private val successScreen = ContactsScreen(
+ state = State.Success(data = listContacts(size = 10)),
+ contacts = listContacts(size = 10)
+ )
+
+ private val loadingScreen = successScreen.copy(state = State.Loading)
+
+ private val errorWithCacheScreen = successScreen.copy(
+ state = State.ErrorWithCache(
+ error = ErrorInformation(
+ message = "Não foi possível conectar ao servidor. Exibindo dados em cache."
+ )
+ )
+ )
+
+ private val errorWithoutCacheScreen = ContactsScreen(
+ state = State.Error(
+ error = ErrorInformation(
+ message = "Ocorreu um erro, tente novamente."
+ )
+ ),
+ contacts = null,
+ )
+
+ override val values: Sequence
+ get() = sequenceOf(
+ successScreen,
+ loadingScreen,
+ errorWithCacheScreen,
+ errorWithoutCacheScreen
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/presentation/ui/utils/FeedbackListener.kt b/app/src/main/java/com/picpay/desafio/android/presentation/ui/utils/FeedbackListener.kt
new file mode 100644
index 000000000..299bb99e2
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/presentation/ui/utils/FeedbackListener.kt
@@ -0,0 +1,5 @@
+package com.picpay.desafio.android.presentation.ui.utils
+
+interface FeedbackListener {
+ fun onButtonClicked()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/presentation/ui/utils/compose/CacheAlertComposable.kt b/app/src/main/java/com/picpay/desafio/android/presentation/ui/utils/compose/CacheAlertComposable.kt
new file mode 100644
index 000000000..607954547
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/presentation/ui/utils/compose/CacheAlertComposable.kt
@@ -0,0 +1,64 @@
+package com.picpay.desafio.android.presentation.ui.utils.compose
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardColors
+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.platform.testTag
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.picpay.desafio.android.R
+import com.picpay.desafio.android.domain.utils.ErrorInformation
+
+const val TEXT_TAG = "text_tag"
+
+@Composable
+fun CacheAlertComposable(
+ error: ErrorInformation,
+ modifier: Modifier = Modifier,
+) {
+ Card(
+ modifier = modifier,
+ colors = CardColors(
+ Color.Transparent,
+ Color.Transparent,
+ Color.Transparent,
+ Color.Transparent,
+ ),
+ border = BorderStroke(
+ width = 1.dp,
+ color = colorResource(R.color.colorAccent)
+ )
+ ) {
+ Text(
+ modifier = Modifier
+ .padding(8.dp)
+ .testTag(TEXT_TAG),
+ text = error.message.orEmpty(),
+ color = Color.White,
+ fontSize = 14.sp,
+ textAlign = TextAlign.Center,
+ style = TextStyle()
+ )
+ }
+}
+
+@Composable
+@Preview
+private fun CacheAlertPreview() {
+ CacheAlertComposable(
+ modifier = Modifier.padding(top = 12.dp),
+ error = ErrorInformation(
+ stringResource(R.string.errorWithCache)
+ )
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/presentation/ui/utils/compose/ErrorFeedbackComposable.kt b/app/src/main/java/com/picpay/desafio/android/presentation/ui/utils/compose/ErrorFeedbackComposable.kt
new file mode 100644
index 000000000..f668942b4
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/presentation/ui/utils/compose/ErrorFeedbackComposable.kt
@@ -0,0 +1,81 @@
+package com.picpay.desafio.android.presentation.ui.utils.compose
+
+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.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonColors
+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.platform.testTag
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.picpay.desafio.android.R
+import com.picpay.desafio.android.presentation.ui.features.contacts.DummyContactsListener
+import com.picpay.desafio.android.presentation.ui.utils.FeedbackListener
+
+const val ERROR_TEXT_TAG = "errorText"
+const val ERROR_BUTTON_TAG = "errorButton"
+
+@Composable
+fun ErrorFeedbackComposable(
+ errorMessage: String = stringResource(R.string.error),
+ buttonTitle: String = stringResource(R.string.reload),
+ listener: FeedbackListener,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.CenterVertically),
+ ) {
+ Text(
+ modifier = Modifier.testTag(ERROR_TEXT_TAG),
+ text = errorMessage,
+ color = Color.White,
+ fontSize = 16.sp,
+ textAlign = TextAlign.Center,
+ )
+
+ Button(
+ modifier = Modifier
+ .padding(horizontal = 24.dp)
+ .testTag(ERROR_BUTTON_TAG),
+ colors = ButtonColors(
+ containerColor = colorResource(R.color.colorAccent),
+ contentColor = Color.White,
+ disabledContentColor = colorResource(R.color.colorDetail),
+ disabledContainerColor = colorResource(R.color.colorPrimary),
+ ),
+ shape = RoundedCornerShape(8.dp),
+ onClick = {
+ listener.onButtonClicked()
+ },
+ ) {
+ Text(
+ text = buttonTitle,
+ fontSize = 14.sp,
+ fontWeight = FontWeight.Bold,
+ )
+ }
+ }
+}
+
+@Composable
+@Preview
+private fun ErrorFeedbackComposablePreview() {
+ ErrorFeedbackComposable(
+ modifier = Modifier.fillMaxSize(),
+ listener = DummyContactsListener
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/presentation/ui/utils/compose/ScreenComposable.kt b/app/src/main/java/com/picpay/desafio/android/presentation/ui/utils/compose/ScreenComposable.kt
new file mode 100644
index 000000000..35e0d030d
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/presentation/ui/utils/compose/ScreenComposable.kt
@@ -0,0 +1,111 @@
+package com.picpay.desafio.android.presentation.ui.utils.compose
+
+import androidx.compose.foundation.clickable
+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.size
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.res.colorResource
+import androidx.compose.ui.unit.dp
+import com.picpay.desafio.android.R
+import com.picpay.desafio.android.domain.utils.State
+import com.picpay.desafio.android.presentation.ui.features.contacts.compose.CACHE_TAG
+import com.picpay.desafio.android.presentation.ui.features.contacts.compose.ERROR_TAG
+import com.picpay.desafio.android.presentation.ui.features.contacts.compose.LOADING_IMAGE_TAG
+import com.picpay.desafio.android.presentation.ui.features.contacts.compose.LOADING_TAG
+import com.picpay.desafio.android.presentation.ui.utils.FeedbackListener
+
+@Composable
+fun ScreenComposable(
+ screenState: State = State.Loading,
+ listener: FeedbackListener,
+ toolbar: @Composable () -> Unit,
+ loadingContent: @Composable () -> Unit = {
+ LoadingGeneric(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 24.dp),
+ )
+ },
+ errorContent: @Composable () -> Unit = {
+ ErrorGeneric(
+ screenState = screenState,
+ listener = listener
+ )
+ },
+ successContent: @Composable () -> Unit,
+) {
+ Scaffold(
+ containerColor = colorResource(R.color.colorPrimaryDark)
+ ) { paddingValues ->
+ Column(modifier = Modifier.padding(paddingValues)) {
+ toolbar()
+
+ val errorWithoutCache = screenState is State.Error
+
+ if (errorWithoutCache) {
+ errorContent()
+ } else if (screenState is State.Loading) {
+ loadingContent()
+ } else {
+ (screenState as? State.ErrorWithCache)?.let { errorWithCache ->
+ CacheAlertComposable(
+ modifier = Modifier
+ .padding(
+ top = 12.dp,
+ start = 24.dp,
+ end = 24.dp,
+ )
+ .testTag(CACHE_TAG)
+ .clickable { listener.onButtonClicked() },
+ error = errorWithCache.error
+ )
+ }
+
+ successContent()
+ }
+ }
+ }
+}
+
+@Composable
+private fun LoadingGeneric(
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier
+ .testTag(LOADING_TAG),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ CircularProgressIndicator(
+ modifier = Modifier
+ .size(48.dp)
+ .testTag(LOADING_IMAGE_TAG),
+ color = colorResource(R.color.colorAccent)
+ )
+ }
+}
+
+@Composable
+private fun ErrorGeneric(
+ screenState: State,
+ listener: FeedbackListener,
+) {
+ val state = screenState as? State.Error
+ ErrorFeedbackComposable(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(horizontal = 24.dp)
+ .testTag(ERROR_TAG),
+ errorMessage = state?.error?.message.orEmpty(),
+ listener = listener,
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
deleted file mode 100644
index 487ac549e..000000000
--- a/app/src/main/res/layout/activity_main.xml
+++ /dev/null
@@ -1,60 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/list_item_user.xml b/app/src/main/res/layout/list_item_user.xml
deleted file mode 100644
index 587d40cc8..000000000
--- a/app/src/main/res/layout/list_item_user.xml
+++ /dev/null
@@ -1,63 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 39df3169e..4cf08d3e8 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -2,5 +2,8 @@
PicPay
Contatos
Ocorreu um erro. Tente novamente.
+ Não foi possível conectar ao servidor. Exibindo dados em cache.
+ Recarregar
+ Imagem do perfil de um contato
diff --git a/app/src/test/java/com/picpay/desafio/android/ExampleService.kt b/app/src/test/java/com/picpay/desafio/android/ExampleService.kt
deleted file mode 100644
index 0199c5e4a..000000000
--- a/app/src/test/java/com/picpay/desafio/android/ExampleService.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.picpay.desafio.android
-
-class ExampleService(
- private val service: PicPayService
-) {
-
- fun example(): List {
- val users = service.getUsers().execute()
-
- return users.body() ?: emptyList()
- }
-}
\ No newline at end of file
diff --git a/app/src/test/java/com/picpay/desafio/android/ExampleServiceTest.kt b/app/src/test/java/com/picpay/desafio/android/ExampleServiceTest.kt
deleted file mode 100644
index 843c0e776..000000000
--- a/app/src/test/java/com/picpay/desafio/android/ExampleServiceTest.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.picpay.desafio.android
-
-import com.nhaarman.mockitokotlin2.mock
-import com.nhaarman.mockitokotlin2.whenever
-import junit.framework.Assert.assertEquals
-import org.junit.Test
-import retrofit2.Call
-import retrofit2.Response
-
-class ExampleServiceTest {
-
- private val api = mock()
-
- private val service = ExampleService(api)
-
- @Test
- fun exampleTest() {
- // given
- val call = mock>>()
- val expectedUsers = emptyList()
-
- whenever(call.execute()).thenReturn(Response.success(expectedUsers))
- whenever(api.getUsers()).thenReturn(call)
-
- // when
- val users = service.example()
-
- // then
- assertEquals(users, expectedUsers)
- }
-}
\ No newline at end of file
diff --git a/app/src/test/java/com/picpay/desafio/android/data/features/contacts/model/ContactRemoteTest.kt b/app/src/test/java/com/picpay/desafio/android/data/features/contacts/model/ContactRemoteTest.kt
new file mode 100644
index 000000000..dd992f60b
--- /dev/null
+++ b/app/src/test/java/com/picpay/desafio/android/data/features/contacts/model/ContactRemoteTest.kt
@@ -0,0 +1,78 @@
+package com.picpay.desafio.android.data.features.contacts.model
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class ContactRemoteTest {
+
+ @Test
+ fun `toDomain should map ContactRemote to Contact with non-null values`() {
+ // Given
+ val contactRemote = ContactRemote(
+ id = 1,
+ name = "Rodrigo M",
+ username = "rodrigom",
+ image = "https://example.com/rodrigom.jpg"
+ )
+
+ // When
+ val contact = contactRemote.toDomain()
+
+ // Then
+ assertEquals(1, contact.id)
+ assertEquals("Rodrigo M", contact.name)
+ assertEquals("rodrigom", contact.username)
+ assertEquals("https://example.com/rodrigom.jpg", contact.image)
+ }
+
+ @Test
+ fun `toDomain should map ContactRemote to Contact with null values`() {
+ // Given
+ val contactRemote = ContactRemote(
+ id = null,
+ name = null,
+ username = null,
+ image = null
+ )
+
+ // When
+ val contact = contactRemote.toDomain()
+
+ // Then
+ assertEquals(0, contact.id)
+ assertEquals("", contact.name)
+ assertEquals("", contact.username)
+ assertEquals("", contact.image)
+ }
+
+ @Test
+ fun `toDomain should map list of ContactRemote to list of Contact`() {
+ // Given
+ val contactRemoteList = listOf(
+ ContactRemote(1, "Rodrigo M", "rodrigom", "https://example.com/rodrigom.jpg"),
+ ContactRemote(2, "Rodrigo MM", "rodrigomm", "https://example.com/rodrigomm.jpg"),
+ ContactRemote(null, null, null, null)
+ )
+
+ // When
+ val contactList = contactRemoteList.toDomain()
+
+ // Then
+ assertEquals(3, contactList.size)
+
+ assertEquals(1, contactList[0].id)
+ assertEquals("Rodrigo M", contactList[0].name)
+ assertEquals("rodrigom", contactList[0].username)
+ assertEquals("https://example.com/rodrigom.jpg", contactList[0].image)
+
+ assertEquals(2, contactList[1].id)
+ assertEquals("Rodrigo MM", contactList[1].name)
+ assertEquals("rodrigomm", contactList[1].username)
+ assertEquals("https://example.com/rodrigomm.jpg", contactList[1].image)
+
+ assertEquals(0, contactList[2].id)
+ assertEquals("", contactList[2].name)
+ assertEquals("", contactList[2].username)
+ assertEquals("", contactList[2].image)
+ }
+}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 7d1b94f34..58bbc43f4 100644
--- a/build.gradle
+++ b/build.gradle
@@ -2,56 +2,46 @@
buildscript {
ext {
- kotlin_version = '1.3.61'
+ kotlin_version = '2.0.21'
- appcompat_version = '1.1.0'
- core_ktx_version = '1.2.0'
- core_testing_version = '2.1.0'
- constraintlayout_version = '1.1.3'
- material_version = "1.1.0"
- moshi_version = '1.8.0'
- retrofit_version = '2.7.1'
- okhttp_version = '4.3.1'
- picasso_version = '2.71828'
- circleimageview_version = '3.0.0'
-
- junit_version = '4.12'
- mockito_version = '2.27.0'
- mockito_kotlin_version = '2.1.0'
-
- test_runner_version = '1.1.1'
- espresso_version = '3.1.1'
-
- koin_version = "2.0.1"
- dagger_version = "2.23.2"
- lifecycle_version = "2.2.0"
+ core_ktx_version = '1.13.1'
+ koin_version = "3.5.6"
+ lifecycle_version = "2.8.7"
coroutines_version = "1.3.3"
rxjava_version = "2.2.17"
rxandroid_version = "2.1.1"
- core_ktx_test_version = "1.2.0"
+ gson_version = "2.10.1"
+ retrofit_version = '2.7.1'
+ okhttp_version = '4.12.0'
+ junit_version = '4.12'
+ core_testing_version = '2.2.0'
+ compose_version = "2024.10.01"
+ activity_compose_version = "1.9.3"
+ lifecycle_viewmodel_compose_version = "2.8.5"
+ coil_version = "3.0.2"
+ room_version = "2.6.1"
+ mockk_version = '1.13.16'
+ detekt_version = '1.23.1'
}
repositories {
google()
- jcenter()
-
+ mavenCentral()
}
+
dependencies {
- classpath 'com.android.tools.build:gradle:3.5.3'
+ classpath 'com.android.tools.build:gradle:8.7.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
-allprojects {
- repositories {
- google()
- jcenter()
-
- }
+plugins {
+ id("org.jetbrains.kotlin.plugin.compose") version "$kotlin_version" apply false
+ id("io.gitlab.arturbosch.detekt") version "$detekt_version" apply false
}
-task clean(type: Delete) {
+tasks.register('clean', Delete) {
delete rootProject.buildDir
}
diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml
new file mode 100644
index 000000000..b73357402
--- /dev/null
+++ b/config/detekt/detekt.yml
@@ -0,0 +1,788 @@
+build:
+ maxIssues: 0
+ excludeCorrectable: false
+ weights:
+ # complexity: 2
+ # LongParameterList: 1
+ # style: 1
+ # comments: 1
+
+config:
+ validation: true
+ warningsAsErrors: false
+ checkExhaustiveness: false
+ # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]'
+ excludes: ''
+
+processors:
+ active: true
+ exclude:
+ - 'DetektProgressListener'
+ # - 'KtFileCountProcessor'
+ # - 'PackageCountProcessor'
+ # - 'ClassCountProcessor'
+ # - 'FunctionCountProcessor'
+ # - 'PropertyCountProcessor'
+ # - 'ProjectComplexityProcessor'
+ # - 'ProjectCognitiveComplexityProcessor'
+ # - 'ProjectLLOCProcessor'
+ # - 'ProjectCLOCProcessor'
+ # - 'ProjectLOCProcessor'
+ # - 'ProjectSLOCProcessor'
+ # - 'LicenseHeaderLoaderExtension'
+
+console-reports:
+ active: true
+ exclude:
+ - 'ProjectStatisticsReport'
+ - 'ComplexityReport'
+ - 'NotificationReport'
+ - 'FindingsReport'
+ - 'FileBasedFindingsReport'
+ # - 'LiteFindingsReport'
+
+output-reports:
+ active: true
+ exclude:
+ # - 'TxtOutputReport'
+ # - 'XmlOutputReport'
+ # - 'HtmlOutputReport'
+ # - 'MdOutputReport'
+ # - 'SarifOutputReport'
+
+comments:
+ active: true
+ AbsentOrWrongFileLicense:
+ active: false
+ licenseTemplateFile: 'license.template'
+ licenseTemplateIsRegex: false
+ CommentOverPrivateFunction:
+ active: false
+ CommentOverPrivateProperty:
+ active: false
+ DeprecatedBlockTag:
+ active: false
+ EndOfSentenceFormat:
+ active: false
+ endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)'
+ KDocReferencesNonPublicProperty:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ OutdatedDocumentation:
+ active: false
+ matchTypeParameters: true
+ matchDeclarationsOrder: true
+ allowParamOnConstructorProperties: false
+ UndocumentedPublicClass:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ searchInNestedClass: true
+ searchInInnerClass: true
+ searchInInnerObject: true
+ searchInInnerInterface: true
+ searchInProtectedClass: false
+ UndocumentedPublicFunction:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ searchProtectedFunction: false
+ UndocumentedPublicProperty:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ searchProtectedProperty: false
+
+complexity:
+ active: true
+ CognitiveComplexMethod:
+ active: false
+ threshold: 15
+ ComplexCondition:
+ active: true
+ threshold: 4
+ ComplexInterface:
+ active: false
+ threshold: 10
+ includeStaticDeclarations: false
+ includePrivateDeclarations: false
+ ignoreOverloaded: false
+ CyclomaticComplexMethod:
+ active: true
+ threshold: 15
+ ignoreSingleWhenExpression: false
+ ignoreSimpleWhenEntries: false
+ ignoreNestingFunctions: false
+ nestingFunctions:
+ - 'also'
+ - 'apply'
+ - 'forEach'
+ - 'isNotNull'
+ - 'ifNull'
+ - 'let'
+ - 'run'
+ - 'use'
+ - 'with'
+ LabeledExpression:
+ active: false
+ ignoredLabels: []
+ LargeClass:
+ active: true
+ threshold: 600
+ LongMethod:
+ active: true
+ threshold: 60
+ ignoreAnnotated: ['Composable']
+ LongParameterList:
+ active: true
+ functionThreshold: 6
+ constructorThreshold: 7
+ ignoreDefaultParameters: false
+ ignoreDataClasses: true
+ ignoreAnnotatedParameter: []
+ ignoreAnnotated: ['Composable']
+ MethodOverloading:
+ active: false
+ threshold: 6
+ NamedArguments:
+ active: false
+ threshold: 3
+ ignoreArgumentsMatchingNames: false
+ NestedBlockDepth:
+ active: true
+ threshold: 4
+ NestedScopeFunctions:
+ active: false
+ threshold: 1
+ functions:
+ - 'kotlin.apply'
+ - 'kotlin.run'
+ - 'kotlin.with'
+ - 'kotlin.let'
+ - 'kotlin.also'
+ ReplaceSafeCallChainWithRun:
+ active: false
+ StringLiteralDuplication:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ threshold: 3
+ ignoreAnnotation: true
+ excludeStringsWithLessThan5Characters: true
+ ignoreStringsRegex: '$^'
+ TooManyFunctions:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ thresholdInFiles: 11
+ thresholdInClasses: 11
+ thresholdInInterfaces: 11
+ thresholdInObjects: 11
+ thresholdInEnums: 11
+ ignoreDeprecated: false
+ ignorePrivate: false
+ ignoreOverridden: false
+
+coroutines:
+ active: true
+ GlobalCoroutineUsage:
+ active: false
+ InjectDispatcher:
+ active: true
+ dispatcherNames:
+ - 'IO'
+ - 'Default'
+ - 'Unconfined'
+ RedundantSuspendModifier:
+ active: true
+ SleepInsteadOfDelay:
+ active: true
+ SuspendFunSwallowedCancellation:
+ active: false
+ SuspendFunWithCoroutineScopeReceiver:
+ active: false
+ SuspendFunWithFlowReturnType:
+ active: true
+
+empty-blocks:
+ active: true
+ EmptyCatchBlock:
+ active: true
+ allowedExceptionNameRegex: '_|(ignore|expected).*'
+ EmptyClassBlock:
+ active: true
+ EmptyDefaultConstructor:
+ active: true
+ EmptyDoWhileBlock:
+ active: true
+ EmptyElseBlock:
+ active: true
+ EmptyFinallyBlock:
+ active: true
+ EmptyForBlock:
+ active: true
+ EmptyFunctionBlock:
+ active: true
+ ignoreOverridden: false
+ EmptyIfBlock:
+ active: true
+ EmptyInitBlock:
+ active: true
+ EmptyKtFile:
+ active: true
+ EmptySecondaryConstructor:
+ active: true
+ EmptyTryBlock:
+ active: true
+ EmptyWhenBlock:
+ active: true
+ EmptyWhileBlock:
+ active: true
+
+exceptions:
+ active: true
+ ExceptionRaisedInUnexpectedLocation:
+ active: true
+ methodNames:
+ - 'equals'
+ - 'finalize'
+ - 'hashCode'
+ - 'toString'
+ InstanceOfCheckForException:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ NotImplementedDeclaration:
+ active: false
+ ObjectExtendsThrowable:
+ active: false
+ PrintStackTrace:
+ active: true
+ RethrowCaughtException:
+ active: true
+ ReturnFromFinally:
+ active: true
+ ignoreLabeled: false
+ SwallowedException:
+ active: true
+ ignoredExceptionTypes:
+ - 'InterruptedException'
+ - 'MalformedURLException'
+ - 'NumberFormatException'
+ - 'ParseException'
+ allowedExceptionNameRegex: '_|(ignore|expected).*'
+ ThrowingExceptionFromFinally:
+ active: true
+ ThrowingExceptionInMain:
+ active: false
+ ThrowingExceptionsWithoutMessageOrCause:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ exceptions:
+ - 'ArrayIndexOutOfBoundsException'
+ - 'Exception'
+ - 'IllegalArgumentException'
+ - 'IllegalMonitorStateException'
+ - 'IllegalStateException'
+ - 'IndexOutOfBoundsException'
+ - 'NullPointerException'
+ - 'RuntimeException'
+ - 'Throwable'
+ ThrowingNewInstanceOfSameException:
+ active: true
+ TooGenericExceptionCaught:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ exceptionNames:
+ - 'ArrayIndexOutOfBoundsException'
+ - 'Error'
+ - 'Exception'
+ - 'IllegalMonitorStateException'
+ - 'IndexOutOfBoundsException'
+ - 'NullPointerException'
+ - 'RuntimeException'
+ - 'Throwable'
+ allowedExceptionNameRegex: '_|(ignore|expected).*'
+ TooGenericExceptionThrown:
+ active: false
+ exceptionNames:
+ - 'Error'
+ - 'Exception'
+ - 'RuntimeException'
+ - 'Throwable'
+
+naming:
+ active: true
+ BooleanPropertyNaming:
+ active: false
+ allowedPattern: '^(is|has|are)'
+ ClassNaming:
+ active: true
+ classPattern: '[A-Z][a-zA-Z0-9]*'
+ ConstructorParameterNaming:
+ active: true
+ parameterPattern: '[a-z][A-Za-z0-9]*'
+ privateParameterPattern: '[a-z][A-Za-z0-9]*'
+ excludeClassPattern: '$^'
+ EnumNaming:
+ active: true
+ enumEntryPattern: '[A-Z][_a-zA-Z0-9]*'
+ ForbiddenClassName:
+ active: false
+ forbiddenName: []
+ FunctionMaxLength:
+ active: false
+ maximumFunctionNameLength: 30
+ FunctionMinLength:
+ active: false
+ minimumFunctionNameLength: 3
+ FunctionNaming:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ functionPattern: '[a-z][a-zA-Z0-9]*'
+ excludeClassPattern: '$^'
+ ignoreAnnotated: ['Composable']
+ FunctionParameterNaming:
+ active: true
+ parameterPattern: '[a-z][A-Za-z0-9]*'
+ excludeClassPattern: '$^'
+ InvalidPackageDeclaration:
+ active: true
+ rootPackage: ''
+ requireRootInDeclaration: false
+ LambdaParameterNaming:
+ active: false
+ parameterPattern: '[a-z][A-Za-z0-9]*|_'
+ MatchingDeclarationName:
+ active: true
+ mustBeFirst: true
+ MemberNameEqualsClassName:
+ active: true
+ ignoreOverridden: true
+ NoNameShadowing:
+ active: true
+ NonBooleanPropertyPrefixedWithIs:
+ active: false
+ ObjectPropertyNaming:
+ active: true
+ constantPattern: '[A-Za-z][_A-Za-z0-9]*'
+ propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
+ privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*'
+ PackageNaming:
+ active: true
+ packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
+ TopLevelPropertyNaming:
+ active: true
+ constantPattern: '[A-Z][_A-Z0-9]*'
+ propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
+ privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
+ VariableMaxLength:
+ active: false
+ maximumVariableNameLength: 64
+ VariableMinLength:
+ active: false
+ minimumVariableNameLength: 1
+ VariableNaming:
+ active: true
+ variablePattern: '[a-z][A-Za-z0-9]*'
+ privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
+ excludeClassPattern: '$^'
+
+performance:
+ active: true
+ ArrayPrimitive:
+ active: true
+ CouldBeSequence:
+ active: false
+ threshold: 3
+ ForEachOnRange:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ SpreadOperator:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ UnnecessaryPartOfBinaryExpression:
+ active: false
+ UnnecessaryTemporaryInstantiation:
+ active: true
+
+potential-bugs:
+ active: true
+ AvoidReferentialEquality:
+ active: true
+ forbiddenTypePatterns:
+ - 'kotlin.String'
+ CastNullableToNonNullableType:
+ active: false
+ CastToNullableType:
+ active: false
+ Deprecation:
+ active: false
+ DontDowncastCollectionTypes:
+ active: false
+ DoubleMutabilityForCollection:
+ active: true
+ mutableTypes:
+ - 'kotlin.collections.MutableList'
+ - 'kotlin.collections.MutableMap'
+ - 'kotlin.collections.MutableSet'
+ - 'java.util.ArrayList'
+ - 'java.util.LinkedHashSet'
+ - 'java.util.HashSet'
+ - 'java.util.LinkedHashMap'
+ - 'java.util.HashMap'
+ ElseCaseInsteadOfExhaustiveWhen:
+ active: false
+ ignoredSubjectTypes: []
+ EqualsAlwaysReturnsTrueOrFalse:
+ active: true
+ EqualsWithHashCodeExist:
+ active: true
+ ExitOutsideMain:
+ active: false
+ ExplicitGarbageCollectionCall:
+ active: true
+ HasPlatformType:
+ active: true
+ IgnoredReturnValue:
+ active: true
+ restrictToConfig: true
+ returnValueAnnotations:
+ - 'CheckResult'
+ - '*.CheckResult'
+ - 'CheckReturnValue'
+ - '*.CheckReturnValue'
+ ignoreReturnValueAnnotations:
+ - 'CanIgnoreReturnValue'
+ - '*.CanIgnoreReturnValue'
+ returnValueTypes:
+ - 'kotlin.sequences.Sequence'
+ - 'kotlinx.coroutines.flow.*Flow'
+ - 'java.util.stream.*Stream'
+ ignoreFunctionCall: []
+ ImplicitDefaultLocale:
+ active: true
+ ImplicitUnitReturnType:
+ active: false
+ allowExplicitReturnType: true
+ InvalidRange:
+ active: true
+ IteratorHasNextCallsNextMethod:
+ active: true
+ IteratorNotThrowingNoSuchElementException:
+ active: true
+ LateinitUsage:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ ignoreOnClassesPattern: ''
+ MapGetWithNotNullAssertionOperator:
+ active: true
+ MissingPackageDeclaration:
+ active: false
+ excludes: ['**/*.kts']
+ NullCheckOnMutableProperty:
+ active: false
+ NullableToStringCall:
+ active: false
+ PropertyUsedBeforeDeclaration:
+ active: false
+ UnconditionalJumpStatementInLoop:
+ active: false
+ UnnecessaryNotNullCheck:
+ active: false
+ UnnecessaryNotNullOperator:
+ active: true
+ UnnecessarySafeCall:
+ active: true
+ UnreachableCatchBlock:
+ active: true
+ UnreachableCode:
+ active: true
+ UnsafeCallOnNullableType:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
+ UnsafeCast:
+ active: true
+ UnusedUnaryOperator:
+ active: true
+ UselessPostfixExpression:
+ active: true
+ WrongEqualsTypeParameter:
+ active: true
+
+style:
+ active: true
+ AlsoCouldBeApply:
+ active: true
+ BracesOnIfStatements:
+ active: false
+ singleLine: 'never'
+ multiLine: 'always'
+ BracesOnWhenStatements:
+ active: false
+ singleLine: 'necessary'
+ multiLine: 'consistent'
+ CanBeNonNullable:
+ active: false
+ CascadingCallWrapping:
+ active: true
+ includeElvis: true
+ ClassOrdering:
+ active: false
+ CollapsibleIfStatements:
+ active: true
+ DataClassContainsFunctions:
+ active: false
+ conversionFunctionPrefix:
+ - 'to'
+ allowOperators: false
+ DataClassShouldBeImmutable:
+ active: false
+ DestructuringDeclarationWithTooManyEntries:
+ active: true
+ maxDestructuringEntries: 3
+ DoubleNegativeLambda:
+ active: false
+ negativeFunctions:
+ - reason: 'Use `takeIf` instead.'
+ value: 'takeUnless'
+ - reason: 'Use `all` instead.'
+ value: 'none'
+ negativeFunctionNameParts:
+ - 'not'
+ - 'non'
+ EqualsNullCall:
+ active: true
+ EqualsOnSignatureLine:
+ active: false
+ ExplicitCollectionElementAccessMethod:
+ active: false
+ ExplicitItLambdaParameter:
+ active: true
+ ExpressionBodySyntax:
+ active: false
+ includeLineWrapping: false
+ ForbiddenAnnotation:
+ active: false
+ annotations:
+ - reason: 'it is a java annotation. Use `Suppress` instead.'
+ value: 'java.lang.SuppressWarnings'
+ - reason: 'it is a java annotation. Use `kotlin.Deprecated` instead.'
+ value: 'java.lang.Deprecated'
+ - reason: 'it is a java annotation. Use `kotlin.annotation.MustBeDocumented` instead.'
+ value: 'java.lang.annotation.Documented'
+ - reason: 'it is a java annotation. Use `kotlin.annotation.Target` instead.'
+ value: 'java.lang.annotation.Target'
+ - reason: 'it is a java annotation. Use `kotlin.annotation.Retention` instead.'
+ value: 'java.lang.annotation.Retention'
+ - reason: 'it is a java annotation. Use `kotlin.annotation.Repeatable` instead.'
+ value: 'java.lang.annotation.Repeatable'
+ - reason: 'Kotlin does not support @Inherited annotation, see https://youtrack.jetbrains.com/issue/KT-22265'
+ value: 'java.lang.annotation.Inherited'
+ ForbiddenComment:
+ active: true
+ comments:
+ - reason: 'Forbidden FIXME todo marker in comment, please fix the problem.'
+ value: 'FIXME:'
+ - reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.'
+ value: 'STOPSHIP:'
+ - reason: 'Forbidden TODO todo marker in comment, please do the changes.'
+ value: 'TODO:'
+ allowedPatterns: ''
+ ForbiddenImport:
+ active: false
+ imports: []
+ forbiddenPatterns: ''
+ ForbiddenMethodCall:
+ active: false
+ methods:
+ - reason: 'print does not allow you to configure the output stream. Use a logger instead.'
+ value: 'kotlin.io.print'
+ - reason: 'println does not allow you to configure the output stream. Use a logger instead.'
+ value: 'kotlin.io.println'
+ ForbiddenSuppress:
+ active: true
+ rules: []
+ ForbiddenVoid:
+ active: true
+ ignoreOverridden: false
+ ignoreUsageInGenerics: false
+ FunctionOnlyReturningConstant:
+ active: true
+ ignoreOverridableFunction: true
+ ignoreActualFunction: true
+ excludedFunctions: []
+ LoopWithTooManyJumpStatements:
+ active: true
+ maxJumpCount: 1
+ MagicNumber:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts']
+ ignoreNumbers:
+ - '-1'
+ - '0'
+ - '1'
+ - '2'
+ ignoreHashCodeFunction: true
+ ignorePropertyDeclaration: false
+ ignoreLocalVariableDeclaration: false
+ ignoreConstantDeclaration: true
+ ignoreCompanionObjectPropertyDeclaration: true
+ ignoreAnnotation: false
+ ignoreNamedArgument: true
+ ignoreEnums: false
+ ignoreRanges: false
+ ignoreExtensionFunctions: true
+ MandatoryBracesLoops:
+ active: false
+ MaxChainedCallsOnSameLine:
+ active: false
+ maxChainedCalls: 5
+ MaxLineLength:
+ active: true
+ maxLineLength: 140
+ excludePackageStatements: true
+ excludeImportStatements: true
+ excludeCommentStatements: false
+ excludeRawStrings: true
+ MayBeConst:
+ active: true
+ ModifierOrder:
+ active: true
+ MultilineLambdaItParameter:
+ active: false
+ MultilineRawStringIndentation:
+ active: false
+ indentSize: 4
+ trimmingMethods:
+ - 'trimIndent'
+ - 'trimMargin'
+ NestedClassesVisibility:
+ active: true
+ NewLineAtEndOfFile:
+ active: false
+ NoTabs:
+ active: false
+ NullableBooleanCheck:
+ active: false
+ ObjectLiteralToLambda:
+ active: true
+ OptionalAbstractKeyword:
+ active: true
+ OptionalUnit:
+ active: false
+ PreferToOverPairSyntax:
+ active: false
+ ProtectedMemberInFinalClass:
+ active: true
+ RedundantExplicitType:
+ active: false
+ RedundantHigherOrderMapUsage:
+ active: true
+ RedundantVisibilityModifierRule:
+ active: false
+ ReturnCount:
+ active: true
+ max: 2
+ excludedFunctions:
+ - 'equals'
+ excludeLabeled: false
+ excludeReturnFromLambda: true
+ excludeGuardClauses: false
+ SafeCast:
+ active: true
+ SerialVersionUIDInSerializableClass:
+ active: false
+ SpacingBetweenPackageAndImports:
+ active: false
+ StringShouldBeRawString:
+ active: false
+ maxEscapedCharacterCount: 2
+ ignoredCharacters: []
+ ThrowsCount:
+ active: true
+ max: 2
+ excludeGuardClauses: false
+ TrailingWhitespace:
+ active: true
+ TrimMultilineRawString:
+ active: false
+ trimmingMethods:
+ - 'trimIndent'
+ - 'trimMargin'
+ UnderscoresInNumericLiterals:
+ active: false
+ acceptableLength: 4
+ allowNonStandardGrouping: false
+ UnnecessaryAbstractClass:
+ active: true
+ UnnecessaryAnnotationUseSiteTarget:
+ active: false
+ UnnecessaryApply:
+ active: true
+ UnnecessaryBackticks:
+ active: false
+ UnnecessaryBracesAroundTrailingLambda:
+ active: false
+ UnnecessaryFilter:
+ active: true
+ UnnecessaryInheritance:
+ active: true
+ UnnecessaryInnerClass:
+ active: false
+ UnnecessaryLet:
+ active: false
+ UnnecessaryParentheses:
+ active: false
+ allowForUnclearPrecedence: false
+ UntilInsteadOfRangeTo:
+ active: false
+ UnusedImports:
+ active: true
+ UnusedParameter:
+ active: true
+ allowedNames: 'ignored|expected'
+ UnusedPrivateClass:
+ active: true
+ UnusedPrivateMember:
+ active: true
+ allowedNames: ''
+ ignoreAnnotated: ['Preview']
+ UnusedPrivateProperty:
+ active: false
+ allowedNames: '_|ignored|expected|serialVersionUID'
+ UseAnyOrNoneInsteadOfFind:
+ active: true
+ UseArrayLiteralsInAnnotations:
+ active: true
+ UseCheckNotNull:
+ active: true
+ UseCheckOrError:
+ active: true
+ UseDataClass:
+ active: false
+ allowVars: false
+ UseEmptyCounterpart:
+ active: false
+ UseIfEmptyOrIfBlank:
+ active: false
+ UseIfInsteadOfWhen:
+ active: false
+ ignoreWhenContainingVariableDeclaration: false
+ UseIsNullOrEmpty:
+ active: true
+ UseLet:
+ active: false
+ UseOrEmpty:
+ active: true
+ UseRequire:
+ active: true
+ UseRequireNotNull:
+ active: true
+ UseSumOfInsteadOfFlatMapSize:
+ active: false
+ UselessCallOnNotNull:
+ active: true
+ UtilityClassWithPublicConstructor:
+ active: true
+ VarCouldBeVal:
+ active: true
+ ignoreLateinitVar: false
+ WildcardImport:
+ active: true
+ excludeImports:
+ - 'java.util.*'
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 31680f1d6..1d7fbaf23 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Sun Feb 16 19:36:17 BRT 2020
+#Mon Feb 17 22:32:20 BRT 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
diff --git a/gradlew b/gradlew
old mode 100644
new mode 100755
diff --git a/settings.gradle b/settings.gradle
index e7b4def49..ff6d84fe7 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1 +1,11 @@
+rootProject.name = "desafio-android"
+
include ':app'
+
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
\ No newline at end of file