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/app/.gitignore b/app/.gitignore
index 796b96d1c..42afabfd2 100644
--- a/app/.gitignore
+++ b/app/.gitignore
@@ -1 +1 @@
-/build
+/build
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
deleted file mode 100644
index a7fbdc0e9..000000000
--- a/app/build.gradle
+++ /dev/null
@@ -1,92 +0,0 @@
-apply plugin: 'com.android.application'
-
-apply plugin: 'kotlin-android'
-
-apply plugin: 'kotlin-android-extensions'
-
-apply plugin: 'kotlin-kapt'
-
-android {
- compileSdkVersion 29
- defaultConfig {
- applicationId "com.picpay.desafio.android"
- minSdkVersion 21
- targetSdkVersion 29
- versionCode 1
- versionName "1.0"
-
- vectorDrawables.useSupportLibrary = true
-
- testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
- }
- buildTypes {
- debug {}
-
- release {
- minifyEnabled true
- shrinkResources true
- proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
- }
- }
-
- compileOptions {
- sourceCompatibility 1.8
- targetCompatibility 1.8
- }
-
- kotlinOptions {
- jvmTarget = JavaVersion.VERSION_1_8.toString()
- }
-}
-
-dependencies {
- implementation fileTree(dir: 'libs', include: ['*.jar'])
-
- implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
-
- 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 "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
- implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
- implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
-
- implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
- implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
- testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
-
- 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.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"
-
- 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"
-}
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 000000000..d77ed7038
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,74 @@
+import com.picpay.desafio.android.Libraries
+import com.picpay.desafio.android.Modules
+import com.picpay.desafio.android.config.AppConfig
+
+
+plugins {
+ id("com.android.application")
+ id("org.jetbrains.kotlin.android")
+}
+
+android {
+ defaultConfig {
+ applicationId = AppConfig.applicationId
+
+ versionCode = AppConfig.versionCode
+ versionName = AppConfig.versionName
+
+ setProperty("archivesBaseName", AppConfig.baseName)
+ }
+
+ testOptions {
+ unitTests {
+ isIncludeAndroidResources = true
+ }
+ }
+
+ buildFeatures {
+ compose = true
+ }
+
+ buildTypes {
+ getByName("release") {
+ isMinifyEnabled = true
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"), "consumer-rules.pro"
+ )
+ }
+
+ getByName("debug") {
+ isMinifyEnabled = false
+ }
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = AppConfig.ComposeOptions.kotlinCompilerExtensionVersion
+ }
+}
+
+dependencies {
+ implementation(project(Modules.ui))
+ implementation(project(Modules.network))
+ implementation(project(Modules.contactListUseCaseImpl))
+ implementation(project(Modules.contactListUseCase))
+ implementation(project(Modules.contactListRepository))
+ implementation(project(Modules.contactListRepositoryImpl))
+ implementation(project(Modules.contactListRemoteDataSource))
+ implementation(project(Modules.contactListInternalDataSource))
+ implementation(project(Modules.contactListPresentation))
+ implementation(Libraries.ThirdParty.Koin.core)
+ implementation(Libraries.ThirdParty.Koin.compose)
+ implementation(Libraries.AndroidX.Compose.activity)
+ implementation(Libraries.AndroidX.Compose.material)
+ implementation(Libraries.AndroidX.Compose.viewModelLifecycle)
+
+ testImplementation(Libraries.AndroidX.Tests.jUnit)
+ testImplementation(Libraries.Jetbrains.KotlinX.Tests.coroutines)
+ testImplementation(Libraries.AndroidX.Tests.runner)
+ testImplementation(Libraries.Google.Truth.truth)
+ testImplementation(Libraries.AndroidX.Compose.jUnit)
+ testImplementation(Libraries.ThirdParty.Mockk.mockk)
+ testImplementation(Libraries.ThirdParty.Koin.jUnit)
+ testImplementation(Libraries.ThirdParty.UiTest.roboletric)
+ debugImplementation(Libraries.AndroidX.Compose.testManifest)
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index e5bd70971..ff59496d8 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -1,6 +1,6 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
-# proguardFiles setting in build.gradle.
+# proguardFiles setting in build.gradle.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
@@ -18,117 +18,4 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
-#-renamesourcefileattribute SourceFile
-
-#######################
-# MOSHI
-#######################
-
-# JSR 305 annotations are for embedding nullability information.
--dontwarn javax.annotation.**
-
--keepclasseswithmembers class * {
- @com.squareup.moshi.* ;
-}
-
--keep @com.squareup.moshi.JsonQualifier interface *
-
-# Enum field names are used by the integrated EnumJsonAdapter.
-# Annotate enums with @JsonClass(generateAdapter = false) to use them with Moshi.
--keepclassmembers @com.squareup.moshi.JsonClass class * extends java.lang.Enum {
- ;
-}
-
-# The name of @JsonClass types is used to look up the generated adapter.
--keepnames @com.squareup.moshi.JsonClass class *
-
-# Retain generated JsonAdapters if annotated type is retained.
--if @com.squareup.moshi.JsonClass class *
--keep class <1>JsonAdapter {
- (...);
- ;
-}
--if @com.squareup.moshi.JsonClass class **$*
--keep class <1>_<2>JsonAdapter {
- (...);
- ;
-}
--if @com.squareup.moshi.JsonClass class **$*$*
--keep class <1>_<2>_<3>JsonAdapter {
- (...);
- ;
-}
--if @com.squareup.moshi.JsonClass class **$*$*$*
--keep class <1>_<2>_<3>_<4>JsonAdapter {
- (...);
- ;
-}
--if @com.squareup.moshi.JsonClass class **$*$*$*$*
--keep class <1>_<2>_<3>_<4>_<5>JsonAdapter {
- (...);
- ;
-}
--if @com.squareup.moshi.JsonClass class **$*$*$*$*$*
--keep class <1>_<2>_<3>_<4>_<5>_<6>JsonAdapter {
- (...);
- ;
-}
-
-#######################
-# MOSHI KOTLIN
-#######################
-
--keep class kotlin.reflect.jvm.internal.impl.builtins.BuiltInsLoaderImpl
-
--keepclassmembers class kotlin.Metadata {
- public ;
-}
-
-#######################
-# OKHTTP
-#######################
-
-# JSR 305 annotations are for embedding nullability information.
--dontwarn javax.annotation.**
-
-# A resource is loaded with a relative path so the package of this class must be preserved.
--keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
-
-# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java.
--dontwarn org.codehaus.mojo.animal_sniffer.*
-
-# OkHttp platform used only on JVM and when Conscrypt dependency is available.
--dontwarn okhttp3.internal.platform.ConscryptPlatform
-
-#######################
-# RETROFIT
-#######################
-
-# Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and
-# EnclosingMethod is required to use InnerClasses.
--keepattributes Signature, InnerClasses, EnclosingMethod
-
-# Retrofit does reflection on method and parameter annotations.
--keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations
-
-# Retain service method parameters when optimizing.
--keepclassmembers,allowshrinking,allowobfuscation interface * {
- @retrofit2.http.* ;
-}
-
-# Ignore annotation used for build tooling.
--dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-
-# Ignore JSR 305 annotations for embedding nullability information.
--dontwarn javax.annotation.**
-
-# Guarded by a NoClassDefFoundError try/catch and only used when on the classpath.
--dontwarn kotlin.Unit
-
-# Top-level functions that can only be used by Kotlin.
--dontwarn retrofit2.KotlinExtensions
-
-# With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy
-# and replaces all potential values with null. Explicitly keeping the interfaces prevents this.
--if interface * { @retrofit2.http.* ; }
--keep,allowobfuscation interface <1>
\ No newline at end of file
+#-renamesourcefileattribute SourceFile
\ 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/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7bdf2ce38..3f17baa9d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,19 +2,24 @@
-
-
+
-
+ android:theme="@style/Theme.Desafioandroid"
+ tools:targetApi="31">
+
diff --git a/app/src/main/java/com/picpay/desafio/android/CustomApp.kt b/app/src/main/java/com/picpay/desafio/android/CustomApp.kt
new file mode 100644
index 000000000..3aadc6c39
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/CustomApp.kt
@@ -0,0 +1,32 @@
+package com.picpay.desafio.android
+
+import android.app.Application
+import com.picpay.desafio.contact_list.presentation.di.contactListPresentationModule
+import com.picpay.desafio.contactlist.datasource.remote.impl.di.contactListDatasourceModule
+import com.picpay.desafio.contactlist.usecase.impl.contactListDomainModule
+import com.picpay.desafio.feature.contactlist.datasource.internal.di.internalDatasourceModule
+import com.picpay.desafio.network.di.networkModule
+import com.picpay.desafio.feature.contactlist.repository.impl.di.contactListRepositoryModule
+import com.picpay.desafio.ui.theme.di.uiModule
+import org.koin.android.ext.koin.androidContext
+import org.koin.core.context.startKoin
+
+class CustomApp : Application() {
+
+ override fun onCreate() {
+ super.onCreate()
+ startKoin {
+ androidContext(this@CustomApp)
+ modules(
+ networkModule,
+ contactListRepositoryModule,
+ contactListDomainModule,
+ contactListPresentationModule,
+ contactListDatasourceModule,
+ internalDatasourceModule,
+ uiModule
+ )
+ }
+ }
+
+}
\ 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
index 2447de98d..95365c126 100644
--- a/app/src/main/java/com/picpay/desafio/android/MainActivity.kt
+++ b/app/src/main/java/com/picpay/desafio/android/MainActivity.kt
@@ -1,75 +1,18 @@
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()!!
- }
- })
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import com.picpay.desafio.contact_list.presentation.ContactScreen
+import com.picpay.desafio.ui.theme.DesafioandroidTheme
+
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ DesafioandroidTheme {
+ ContactScreen()
+ }
+ }
}
-}
+}
\ No newline at end of file
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/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/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
index 6348baae3..2b068d114 100644
--- a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
+++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -1,34 +1,30 @@
-
+ xmlns:aapt="http://schemas.android.com/aapt"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+
+ android:endX="85.84757"
+ android:endY="92.4963"
+ android:startX="42.9492"
+ android:startY="49.59793"
+ android:type="linear">
+ android:color="#44000000"
+ android:offset="0.0" />
+ android:color="#00000000"
+ android:offset="1.0" />
-
+ android:fillColor="#FFFFFF"
+ android:fillType="nonZero"
+ android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
+ android:strokeWidth="1"
+ android:strokeColor="#00000000" />
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
index a0ad202f9..07d5da9cb 100644
--- a/app/src/main/res/drawable/ic_launcher_background.xml
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -1,74 +1,170 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index bbd3e0212..eca70cfe5 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -1,5 +1,5 @@
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index bbd3e0212..eca70cfe5 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -1,5 +1,5 @@
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
deleted file mode 100644
index 898f3ed59..000000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 000000000..c209e78ec
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
deleted file mode 100644
index dffca3601..000000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..b2dfe3d1b
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
deleted file mode 100644
index 64ba76f75..000000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 000000000..4f0f1d64e
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
deleted file mode 100644
index dae5e0823..000000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..62b611da0
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100644
index e5ed46597..000000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 000000000..948a3070f
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
deleted file mode 100644
index 14ed0af35..000000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..1b9a6956b
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100644
index b0907cac3..000000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 000000000..28d4b77f9
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
deleted file mode 100644
index d8ae03154..000000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..9287f5083
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100644
index 2c18de9e6..000000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 000000000..aa7d6427e
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
deleted file mode 100644
index beed3cdd2..000000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 000000000..9126ae37c
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index fe7f39aca..f8c6127d3 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -1,8 +1,10 @@
- #2B2C2F
- #1D1E20
- #11C76F
- #ACB1BD
- #80FFFFFF
-
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
\ 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..019f15e68 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,6 +1,3 @@
- PicPay
- Contatos
- Ocorreu um erro. Tente novamente.
-
-
+ desafio-android
+
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
deleted file mode 100644
index edde96c27..000000000
--- a/app/src/main/res/values/styles.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 000000000..e97fb4d92
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 000000000..fa0f996d2
--- /dev/null
+++ b/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 000000000..9ee9997b0
--- /dev/null
+++ b/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml
deleted file mode 100644
index e61f7137e..000000000
--- a/app/src/main/res/xml/network_security_config.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/app/src/test/java/com/picpay/desafio/android/ContactListScreenTest.kt b/app/src/test/java/com/picpay/desafio/android/ContactListScreenTest.kt
new file mode 100644
index 000000000..7ea428a96
--- /dev/null
+++ b/app/src/test/java/com/picpay/desafio/android/ContactListScreenTest.kt
@@ -0,0 +1,227 @@
+package com.picpay.desafio.android
+
+import androidx.compose.ui.test.*
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.core.app.ActivityScenario
+import com.picpay.desafio.contact_list.presentation.ContactScreen
+import com.picpay.desafio.contact_list.presentation.di.contactListPresentationModule
+import com.picpay.desafio.feature.contactlist.usecase.GetUsersUseCase
+import com.picpay.desafio.feature.contactlist.usecase.UserDomain
+import com.picpay.desafio.ui.theme.DesafioandroidTheme
+import io.mockk.coEvery
+import io.mockk.mockk
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.koin.core.context.*
+import org.koin.core.module.Module
+import org.koin.dsl.koinApplication
+import org.koin.dsl.module
+import org.koin.test.KoinTest
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.shadows.ShadowLog
+
+@Config(instrumentedPackages = ["androidx.loader.content"])
+@RunWith(RobolectricTestRunner::class)
+class ContactListScreenTest : KoinTest {
+
+ @get:Rule
+ var composeRule = createComposeRule()
+
+ private lateinit var mockedUseCaseModule: Module
+ lateinit var mockedUseCase: GetUsersUseCase
+
+ @Before
+ fun setup() {
+ mockedUseCase = mockk()
+ mockedUseCaseModule = module(override = true) {
+ single { mockedUseCase }
+ }
+
+ if (GlobalContext.getOrNull() == null) {
+ startKoin {
+ modules(mockedUseCaseModule, contactListPresentationModule)
+ }
+ }
+
+ loadKoinModules(mockedUseCaseModule)
+ }
+
+
+ @Test
+ fun verifyIfShowContactListWhenUseCaseReturnsSucess() {
+ mockUseCaseResult()
+
+ composeRule.setContent {
+ DesafioandroidTheme {
+ ContactScreen()
+ }
+ }
+
+ composeRule.onNodeWithTag("ContactList").assertIsDisplayed()
+ composeRule.onNodeWithTag("ContactListTitle").assertIsDisplayed()
+ composeRule.onNodeWithTag("ContactListTitle").assertTextEquals("Contatos")
+ }
+
+ @Test
+ fun verifyIfNotShowContactListAndTitleWhenUseCaseReturnsFailure() {
+ mockUseCaseResult(true)
+
+ composeRule.setContent {
+ DesafioandroidTheme {
+ ContactScreen()
+ }
+ }
+
+ composeRule.onNodeWithTag("ContactListError").assertIsDisplayed()
+ .assertTextEquals("Erro desconhecido, tente novamente mais tarde.")
+ composeRule.onNodeWithTag("ContactList").assertDoesNotExist()
+ composeRule.onNodeWithTag("ContactListTitle").assertDoesNotExist()
+ }
+
+ @Test
+ fun verifyIfListMakeScroll() {
+ mockUseCaseResult()
+
+ composeRule.setContent {
+ DesafioandroidTheme {
+ ContactScreen()
+ }
+ }
+
+ val itemList = composeRule.onNodeWithTag("ContactList")
+ .performScrollToIndex(11)
+ .onChildAt(1)
+
+ val itemListName = itemList.onChildAt(2)
+ val itemUserName = itemList.onChildAt(1)
+
+ itemListName.assertTextEquals("José Maria")
+ itemUserName.assertTextEquals("@JoseMaria")
+ }
+
+ @Test
+ fun testChangeOrientationWithSaveState() {
+ mockUseCaseResult()
+
+ ActivityScenario.launch(MainActivity::class.java).use { scenario ->
+ scenario.onActivity { activity ->
+ val itemList = composeRule.onNodeWithTag("ContactList")
+ .performScrollToIndex(11)
+ .onChildAt(1)
+
+ activity.recreate()
+
+ val itemName = itemList.onChildAt(2)
+ val itemUserName = itemList.onChildAt(1)
+
+ itemName.assertTextEquals("José Maria")
+ itemUserName.assertTextEquals("@JoseMaria")
+ }
+ }
+ }
+
+ @After
+ fun tearDown() {
+ unloadKoinModules(mockedUseCaseModule)
+ stopKoin()
+ }
+
+ private fun mockUseCaseResult(hasError: Boolean = false) {
+ if (hasError) {
+ coEvery { mockedUseCase() } returns Result.failure(Exception())
+ } else {
+ coEvery { mockedUseCase() } returns Result.success(
+ listOf(
+ UserDomain(
+ "https://randomuser.me/api/portraits/men/1.jpg",
+ "José Silva",
+ 1,
+ "@JoseSilva"
+ ),
+ UserDomain(
+ "https://randomuser.me/api/portraits/men/1.jpg",
+ "José Silva",
+ 1,
+ "@JoseSilva"
+ ),
+ UserDomain(
+ "https://randomuser.me/api/portraits/men/1.jpg",
+ "José Silva",
+ 1,
+ "@JoseSilva"
+ ),
+ UserDomain(
+ "https://randomuser.me/api/portraits/men/1.jpg",
+ "José Silva",
+ 1,
+ "@JoseSilva"
+ ),
+ UserDomain(
+ "https://randomuser.me/api/portraits/men/1.jpg",
+ "José Silva",
+ 1,
+ "@JoseSilva"
+ ),
+ UserDomain(
+ "https://randomuser.me/api/portraits/men/1.jpg",
+ "José Silva",
+ 1,
+ "@JoseSilva"
+ ),
+ UserDomain(
+ "https://randomuser.me/api/portraits/men/1.jpg",
+ "José Silva",
+ 1,
+ "@JoseSilva"
+ ),
+ UserDomain(
+ "https://randomuser.me/api/portraits/men/1.jpg",
+ "José Silva",
+ 1,
+ "@JoseSilva"
+ ),
+ UserDomain(
+ "https://randomuser.me/api/portraits/men/1.jpg",
+ "José Silva",
+ 1,
+ "@JoseSilva"
+ ),
+ UserDomain(
+ "https://randomuser.me/api/portraits/men/1.jpg",
+ "José Silva",
+ 1,
+ "@JoseSilva"
+ ),
+ UserDomain(
+ "https://randomuser.me/api/portraits/men/1.jpg",
+ "José Silva",
+ 1,
+ "@JoseSilva"
+ ),
+ UserDomain(
+ "https://randomuser.me/api/portraits/men/1.jpg",
+ "José Maria",
+ 1,
+ "@JoseMaria"
+ ),
+ UserDomain(
+ "https://randomuser.me/api/portraits/men/1.jpg",
+ "José Silva",
+ 1,
+ "@JoseSilva"
+ ),
+ UserDomain(
+ "https://randomuser.me/api/portraits/men/1.jpg",
+ "José Silva",
+ 1,
+ "@JoseSilva"
+ ),
+ )
+ )
+ }
+ }
+}
\ No newline at end of file
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/build.gradle b/build.gradle
deleted file mode 100644
index 7d1b94f34..000000000
--- a/build.gradle
+++ /dev/null
@@ -1,57 +0,0 @@
-// Top-level build file where you can add configuration options common to all sub-projects/modules.
-
-buildscript {
- ext {
- kotlin_version = '1.3.61'
-
- 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"
- coroutines_version = "1.3.3"
- rxjava_version = "2.2.17"
- rxandroid_version = "2.1.1"
- core_ktx_test_version = "1.2.0"
- }
-
- repositories {
- google()
- jcenter()
-
- }
- dependencies {
- classpath 'com.android.tools.build:gradle:3.5.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()
-
- }
-}
-
-task clean(type: Delete) {
- delete rootProject.buildDir
-}
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 000000000..12550ee3b
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,53 @@
+import com.picpay.desafio.android.config.AppConfig
+import com.android.build.gradle.BaseExtension
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ id("com.android.application") version com.picpay.desafio.android.Versions.Android.gradle apply false
+ id("com.android.library") version com.picpay.desafio.android.Versions.Android.gradle apply false
+ id("org.jetbrains.kotlin.android") version com.picpay.desafio.android.Versions.JetBrains.Kotlin.gradle apply false
+}
+
+allprojects {
+
+ configurations.all {
+ resolutionStrategy {
+ force("org.intellij:annotations:13.0")
+ exclude(group = "com.intellij", module = "annotations")
+ }
+ }
+
+ afterEvaluate {
+ val androidExtensions = this.extensions.findByName("android")
+ (androidExtensions as? BaseExtension)?.apply {
+ compileSdkVersion(AppConfig.compileSdk)
+
+ defaultConfig {
+ minSdk = AppConfig.minSdk
+ targetSdk = AppConfig.targetSdk
+
+ testInstrumentationRunner =
+ AppConfig.androidTestInstrumentation
+
+ }
+
+ compileOptions {
+ sourceCompatibility =
+ AppConfig.CompileOptions.javaSourceCompatibility
+ targetCompatibility =
+ AppConfig.CompileOptions.javaSourceCompatibility
+ }
+
+ tasks.withType {
+ kotlinOptions {
+ jvmTarget = AppConfig.CompileOptions.kotlinJvmTarget
+ }
+ }
+ }
+ }
+}
+
+tasks.register("clean", Delete::class) {
+ delete(rootProject.buildDir)
+}
\ No newline at end of file
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
new file mode 100644
index 000000000..202aba3be
--- /dev/null
+++ b/buildSrc/build.gradle.kts
@@ -0,0 +1,9 @@
+plugins {
+ `java-gradle-plugin`
+ `kotlin-dsl`
+ `kotlin-dsl-precompiled-script-plugins`
+}
+
+repositories {
+ mavenCentral()
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/java/com/picpay/desafio/android/Libraries.kt b/buildSrc/src/main/java/com/picpay/desafio/android/Libraries.kt
new file mode 100644
index 000000000..c190cb1de
--- /dev/null
+++ b/buildSrc/src/main/java/com/picpay/desafio/android/Libraries.kt
@@ -0,0 +1,152 @@
+package com.picpay.desafio.android
+
+object Libraries {
+
+ object Jetbrains {
+ object Kotlin {
+ const val stdLib =
+ "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${Versions.JetBrains.Kotlin.stdLib}"
+ }
+
+ object KotlinX {
+ const val coroutines =
+ "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.JetBrains.KotlinX.coroutines}"
+
+ object Tests {
+ const val coroutines =
+ "org.jetbrains.kotlinx:kotlinx-coroutines-test:${Versions.JetBrains.KotlinX.coroutines}"
+ }
+ }
+ }
+
+ object Google {
+ object Truth {
+ const val truth = "com.google.truth:truth:${Versions.Google.Truth.truth}"
+ }
+ }
+
+ object Android {
+ const val material =
+ "com.google.android.material:material:${Versions.Android.material}"
+ }
+
+ object AndroidX {
+ const val startup =
+ "androidx.startup:startup-runtime:${Versions.AndroidX.startup}"
+ const val appCompat =
+ "androidx.appcompat:appcompat:${Versions.AndroidX.appCompat}"
+ const val coreKtx =
+ "androidx.core:core-ktx:${Versions.AndroidX.coreKtx}"
+
+ object Navigation {
+ const val ui =
+ "androidx.navigation:navigation-com.picpay.desafio.contact_list.presentation.ui-ktx:${Versions.AndroidX.Navigation.ui}"
+ const val compose =
+ "androidx.navigation:navigation-compose:${Versions.AndroidX.Navigation.compose}"
+ }
+
+ object Compose {
+ const val activity =
+ "androidx.activity:activity-compose:${Versions.AndroidX.Compose.activity}"
+ const val material =
+ "androidx.compose.material:material:${Versions.AndroidX.Compose.material}"
+ const val animation =
+ "androidx.compose.animation:animation:${Versions.AndroidX.Compose.animation}"
+ const val uiTooling =
+ "androidx.compose.ui:ui-tooling:${Versions.AndroidX.Compose.uiTooling}"
+ const val viewModelLifecycle =
+ "androidx.lifecycle:lifecycle-viewmodel-compose:${Versions.AndroidX.Compose.viewModelLifecycle}"
+ const val runtimeLiveData =
+ "androidx.compose.runtime:runtime-livedata:${Versions.AndroidX.Compose.runtimeLiveData}"
+ const val jUnit =
+ "androidx.compose.ui:ui-test-junit4:${Versions.AndroidX.Compose.jUnit}"
+ const val testManifest =
+ "androidx.compose.ui:ui-test-manifest:${Versions.AndroidX.Compose.jUnit}"
+ }
+
+ object Lifecycle {
+ const val runtime =
+ "androidx.lifecycle:lifecycle-runtime:${Versions.AndroidX.Lifecycle.runtime}"
+ const val runtimeKtx =
+ "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.AndroidX.Lifecycle.runtimeKtx}"
+ const val commonJava8 =
+ "androidx.lifecycle:lifecycle-common-java8:${Versions.AndroidX.Lifecycle.commonJava8}"
+ const val liveData =
+ "androidx.lifecycle:lifecycle-livedata:${Versions.AndroidX.Lifecycle.liveData}"
+ const val liveDataKtx =
+ "androidx.lifecycle:lifecycle-livedata-ktx:${Versions.AndroidX.Lifecycle.liveDataKtx}"
+ const val viewModel =
+ "androidx.lifecycle:lifecycle-viewmodel:${Versions.AndroidX.Lifecycle.viewModel}"
+ const val viewModelKtx =
+ "androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.AndroidX.Lifecycle.viewModelKtx}"
+ }
+
+ object Room {
+ const val runtime = "androidx.room:room-runtime:${Versions.AndroidX.Room.runtime}"
+ const val compiler = "androidx.room:room-compiler:${Versions.AndroidX.Room.compiler}"
+ const val ktx = "androidx.room:room-ktx:${Versions.AndroidX.Room.ktx}"
+ val all = listOf(runtime, compiler, ktx)
+ }
+
+ object Tests {
+ const val runner =
+ "androidx.test:runner:${Versions.AndroidX.Tests.runner}"
+ const val rules =
+ "androidx.test:rules:${Versions.AndroidX.Tests.rules}"
+ const val core =
+ "androidx.test:core:${Versions.AndroidX.Tests.core}"
+ const val jUnit =
+ "junit:junit:${Versions.AndroidX.Tests.jUnit}"
+ const val extjUnit =
+ "androidx.test.ext:junit:${Versions.AndroidX.Tests.extjUnit}"
+ const val archCoreTesting =
+ "androidx.arch.core:core-testing:${Versions.AndroidX.Tests.archCoreTesting}"
+ }
+ }
+
+ object ThirdParty {
+ const val coil = "io.coil-kt:coil-compose:${Versions.ThirdParty.coil}"
+
+ object Koin {
+ const val core =
+ "io.insert-koin:koin-core:${Versions.ThirdParty.Koin.koin}"
+ const val android =
+ "io.insert-koin:koin-android:${Versions.ThirdParty.Koin.koin}"
+ const val compose =
+ "io.insert-koin:koin-androidx-compose:${Versions.ThirdParty.Koin.koin}"
+ const val jUnit =
+ "io.insert-koin:koin-test-junit4:${Versions.ThirdParty.Koin.koin}"
+ }
+
+ object Retrofit {
+ const val retrofit =
+ "com.squareup.retrofit2:retrofit:${Versions.ThirdParty.Retrofit.version}"
+ const val moshiConverter =
+ "com.squareup.retrofit2:converter-moshi:${Versions.ThirdParty.Retrofit.moshiConverter}"
+ const val gsonConverter =
+ "com.squareup.retrofit2:converter-gson:${Versions.ThirdParty.Retrofit.gsonConverter}"
+ const val scalarsConverter =
+ "com.squareup.retrofit2:converter-scalars:${Versions.ThirdParty.Retrofit.scalarsConverter}"
+ }
+
+ object Okhttp {
+ const val okhttp =
+ "com.squareup.okhttp3:okhttp:${Versions.ThirdParty.Okhttp.version}"
+ const val loggingInterceptor =
+ "com.squareup.okhttp3:logging-interceptor:${Versions.ThirdParty.Okhttp.loggingInterceptor}"
+ }
+
+ object UiTest {
+ const val roboletric =
+ "org.robolectric:robolectric:${Versions.ThirdParty.UiTest.roboletric}"
+ }
+
+ object Mockk {
+ const val mockk = "io.mockk:mockk:${Versions.ThirdParty.Mockk.mockk}"
+ const val mockkAndroid = "io.mockk:mockk-android:${Versions.ThirdParty.Mockk.mockk}"
+ const val mockkWebService =
+ "com.squareup.okhttp3:mockwebserver:${Versions.ThirdParty.Mockk.mockkWebService}"
+ const val mockkAgentJvm = "io.mockk:mockk-agent-jvm:${Versions.ThirdParty.Mockk.mockk}"
+ }
+ }
+}
diff --git a/buildSrc/src/main/java/com/picpay/desafio/android/Modules.kt b/buildSrc/src/main/java/com/picpay/desafio/android/Modules.kt
new file mode 100644
index 000000000..0fa69038a
--- /dev/null
+++ b/buildSrc/src/main/java/com/picpay/desafio/android/Modules.kt
@@ -0,0 +1,16 @@
+package com.picpay.desafio.android
+
+object Modules {
+ const val testLib = ":testlib"
+ const val app = ":app"
+ const val ui = ":ui"
+ const val network = ":network"
+
+ const val contactListPresentation = ":feature:contactlist:presentation"
+ const val contactListInternalDataSource = ":feature:contactlist:datasource:internal"
+ const val contactListRemoteDataSource = ":feature:contactlist:datasource:remote"
+ const val contactListRepositoryImpl = ":feature:contactlist:repository:impl"
+ const val contactListRepository = ":feature:contactlist:repository:public"
+ const val contactListUseCaseImpl = ":feature:contactlist:usecase:impl"
+ const val contactListUseCase = ":feature:contactlist:usecase:public"
+}
diff --git a/buildSrc/src/main/java/com/picpay/desafio/android/Versions.kt b/buildSrc/src/main/java/com/picpay/desafio/android/Versions.kt
new file mode 100644
index 000000000..a599a0c31
--- /dev/null
+++ b/buildSrc/src/main/java/com/picpay/desafio/android/Versions.kt
@@ -0,0 +1,149 @@
+package com.picpay.desafio.android
+
+object Versions {
+
+ object JetBrains {
+ object Kotlin {
+ const val stdLib = "1.6.10"
+ const val gradle = "1.6.10"
+ }
+
+ object KotlinX {
+ const val coroutines = "1.6.1"
+ }
+ }
+
+ object Google {
+ const val services = "4.3.10"
+
+ object Firebase {
+ const val bom = "29.0.4"
+ const val crashlyticsGradle = "2.8.1"
+ }
+
+ object Gson {
+ const val gson = "2.8.7"
+ }
+
+ object Truth {
+ const val truth = "1.1.3"
+ }
+ }
+
+ object Android {
+ const val gradle = "7.1.3"
+ const val material = "1.5.0"
+ const val databinding = "3.1.4"
+ }
+
+ object AndroidX {
+ const val startup = "1.1.1"
+ const val appCompat = "1.4.1"
+ const val constraintLayout = "2.1.3"
+ const val fragment = "1.4.0"
+ const val lifecycleCommonJava = "2.3.1"
+ const val lifecycleExtensions = "2.3.1"
+ const val coreKtx = "1.7.0"
+ const val navigation = "2.3.5"
+
+ object Navigation {
+ const val fragment = "2.3.5"
+ const val ui = "2.3.5"
+ const val compose = "2.4.2"
+ }
+
+ object Compose {
+ const val activity = "1.4.0"
+ const val material = "1.1.1"
+ const val animation = "1.1.1"
+ const val uiTooling = "1.1.1"
+ const val viewModelLifecycle = "2.4.1"
+ const val runtimeLiveData = "1.1.1"
+ const val jUnit = "1.1.1"
+ }
+
+ object Lifecycle {
+ const val runtime = "2.3.1"
+ const val runtimeKtx = "2.4.0"
+ const val commonJava8 = "2.3.1"
+ const val liveData = "2.3.1"
+ const val liveDataKtx = "2.4.0"
+ const val viewModel = "2.3.1"
+ const val viewModelKtx = "2.4.0"
+ }
+
+ object Room {
+ const val runtime = "2.4.2"
+ const val compiler = "2.4.2"
+ const val ktx = "2.4.2"
+ }
+
+ object Tests {
+ const val runner = "1.4.0"
+ const val rules = "1.4.0"
+ const val core = "1.4.0"
+ const val jUnit = "4.13.2"
+ const val extjUnit = "1.1.3"
+ const val archCoreTesting = "2.1.0"
+ }
+ }
+
+ object ThirdParty {
+ const val coil = "2.1.0"
+
+ object UiTest {
+ const val roboletric = "4.8"
+ }
+
+ object Koin {
+ const val koin = "3.1.5"
+ }
+
+ object Retrofit {
+ const val version = "2.9.0"
+ const val moshiConverter = "2.9.0"
+ const val gsonConverter = "2.9.0"
+ const val scalarsConverter = "2.9.0"
+ }
+
+ object Okhttp {
+ const val version = "4.9.3"
+ const val loggingInterceptor = "4.9.3"
+ }
+
+ object Okio {
+ const val version = "3.0.0"
+ }
+
+ object Karumi {
+ const val dexter = "6.2.3"
+ }
+
+ object Airbnb {
+ const val lottie = "5.0.3"
+ }
+
+ object Binaryfork {
+ const val spanny = "1.0.4"
+ }
+
+ object Tests {
+ object Espresso {
+ const val rules = "1.4.0"
+ const val core = "3.4.0"
+ }
+ }
+
+ object Bumptech {
+ object Glide {
+ const val glide = "4.13.0"
+ const val compiler = "4.13.0"
+ }
+ }
+
+ object Mockk {
+ const val mockkWebService = "4.10.0"
+ const val mockk = "1.12.0"
+ }
+ }
+}
diff --git a/buildSrc/src/main/java/com/picpay/desafio/android/config/AppConfig.kt b/buildSrc/src/main/java/com/picpay/desafio/android/config/AppConfig.kt
new file mode 100644
index 000000000..b456e5d53
--- /dev/null
+++ b/buildSrc/src/main/java/com/picpay/desafio/android/config/AppConfig.kt
@@ -0,0 +1,45 @@
+package com.picpay.desafio.android.config
+
+import org.gradle.api.JavaVersion
+
+object AppConfig {
+ const val compileSdk = 31
+ const val targetSdk = 31
+ const val minSdk = 28
+
+ const val applicationId = "com.picpay.desafio.android"
+
+ object Version {
+ const val major = 0
+ const val minor = 2
+ const val patch = 0
+ const val build = 0
+ }
+
+ const val versionCode =
+ Version.major * 1000000 + Version.minor * 10000 + Version.patch * 100 + Version.build
+
+ const val versionName =
+ "${Version.major}.${Version.minor}.${Version.patch}-${Version.build}"
+
+ const val androidTestInstrumentation = "androidx.test.runner.AndroidJUnitRunner"
+
+ var baseName = "vibra-pos-$versionName"
+
+ object CompileOptions {
+ val javaSourceCompatibility = JavaVersion.VERSION_1_8
+ const val kotlinJvmTarget = "1.8"
+ }
+
+ object ComposeOptions {
+ const val kotlinCompilerExtensionVersion = "1.1.1"
+ }
+
+ object BuildConfigField {
+ val BASE_URL = ConfigField(
+ type = "String",
+ name = "API_BASE_URL",
+ value = "\"https://609a908e0f5a13001721b74e.mockapi.io/picpay/api/\""
+ )
+ }
+}
diff --git a/buildSrc/src/main/java/com/picpay/desafio/android/config/ConfigField.kt b/buildSrc/src/main/java/com/picpay/desafio/android/config/ConfigField.kt
new file mode 100644
index 000000000..e4089a167
--- /dev/null
+++ b/buildSrc/src/main/java/com/picpay/desafio/android/config/ConfigField.kt
@@ -0,0 +1,3 @@
+package com.picpay.desafio.android.config
+
+data class ConfigField(val type: String, val name: String, val value: String)
\ No newline at end of file
diff --git a/buildSrc/src/main/java/com/picpay/desafio/android/config/FlavorDimension.kt b/buildSrc/src/main/java/com/picpay/desafio/android/config/FlavorDimension.kt
new file mode 100644
index 000000000..8fed90873
--- /dev/null
+++ b/buildSrc/src/main/java/com/picpay/desafio/android/config/FlavorDimension.kt
@@ -0,0 +1,3 @@
+package com.picpay.desafio.android.config
+
+data class FlavorDimension(val name: String)
diff --git a/buildSrc/src/main/java/com/picpay/desafio/android/config/ProductFlavor.kt b/buildSrc/src/main/java/com/picpay/desafio/android/config/ProductFlavor.kt
new file mode 100644
index 000000000..a53e777f2
--- /dev/null
+++ b/buildSrc/src/main/java/com/picpay/desafio/android/config/ProductFlavor.kt
@@ -0,0 +1,9 @@
+package com.picpay.desafio.android.config
+
+data class ProductFlavor(
+ val name: String,
+ val dimension: FlavorDimension,
+ val applicationIdSuffix: String = "",
+ val versionNameSuffix: String = "",
+ val buildConfigFields: List = listOf(),
+)
diff --git a/buildSrc/src/main/java/com/picpay/desafio/android/extensions/Dependencies.kt b/buildSrc/src/main/java/com/picpay/desafio/android/extensions/Dependencies.kt
new file mode 100644
index 000000000..247b1698f
--- /dev/null
+++ b/buildSrc/src/main/java/com/picpay/desafio/android/extensions/Dependencies.kt
@@ -0,0 +1,34 @@
+package com.picpay.desafio.android.extensions
+
+import org.gradle.api.artifacts.dsl.DependencyHandler
+
+//util functions for adding the different type dependencies from build.gradle file
+fun DependencyHandler.kapt(list: List) {
+ list.forEach { dependency ->
+ add("kapt", dependency)
+ }
+}
+
+fun DependencyHandler.api(list: List) {
+ list.forEach { dependency ->
+ add("api", dependency)
+ }
+}
+
+fun DependencyHandler.implementation(list: List) {
+ list.forEach { dependency ->
+ add("implementation", dependency)
+ }
+}
+
+fun DependencyHandler.androidTestImplementation(list: List) {
+ list.forEach { dependency ->
+ add("androidTestImplementation", dependency)
+ }
+}
+
+fun DependencyHandler.testImplementation(list: List) {
+ list.forEach { dependency ->
+ add("testImplementation", dependency)
+ }
+}
\ No newline at end of file
diff --git a/feature/contactlist/datasource/internal/build.gradle.kts b/feature/contactlist/datasource/internal/build.gradle.kts
new file mode 100644
index 000000000..4a6200570
--- /dev/null
+++ b/feature/contactlist/datasource/internal/build.gradle.kts
@@ -0,0 +1,46 @@
+import com.picpay.desafio.android.Libraries
+import com.picpay.desafio.android.Libraries.AndroidX.Room
+import com.picpay.desafio.android.Modules
+import com.picpay.desafio.android.config.AppConfig
+
+plugins {
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+ id("kotlin-kapt")
+}
+
+android {
+ compileSdk = AppConfig.compileSdk
+
+ defaultConfig {
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+}
+
+dependencies {
+ kapt(Room.compiler)
+ implementation(Room.ktx)
+ implementation(Room.runtime)
+ implementation(Libraries.ThirdParty.Koin.core)
+ implementation(Libraries.ThirdParty.Koin.android)
+
+ implementation(project(Modules.contactListUseCase))
+ implementation(project(Modules.contactListRepository))
+
+ testImplementation(Libraries.AndroidX.Tests.jUnit)
+ testImplementation(Libraries.AndroidX.Tests.runner)
+ testImplementation(Libraries.Jetbrains.KotlinX.Tests.coroutines)
+ testImplementation(Libraries.AndroidX.Tests.runner)
+ testImplementation(Libraries.Google.Truth.truth)
+ testImplementation(Libraries.ThirdParty.Mockk.mockk)
+ testImplementation(Libraries.AndroidX.Tests.extjUnit)
+
+ androidTestImplementation(Libraries.AndroidX.Tests.runner)
+ androidTestImplementation(Libraries.AndroidX.Tests.extjUnit)
+ androidTestImplementation(Libraries.Google.Truth.truth)
+ androidTestImplementation(Libraries.Jetbrains.KotlinX.Tests.coroutines)
+ androidTestImplementation(Libraries.AndroidX.Tests.archCoreTesting)
+ androidTestImplementation(Libraries.AndroidX.Tests.core)
+ androidTestImplementation(Libraries.AndroidX.Tests.jUnit)
+}
\ No newline at end of file
diff --git a/feature/contactlist/datasource/internal/consumer-rules.pro b/feature/contactlist/datasource/internal/consumer-rules.pro
new file mode 100644
index 000000000..e69de29bb
diff --git a/feature/contactlist/datasource/internal/proguard-rules.pro b/feature/contactlist/datasource/internal/proguard-rules.pro
new file mode 100644
index 000000000..481bb4348
--- /dev/null
+++ b/feature/contactlist/datasource/internal/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/feature/contactlist/datasource/internal/src/androidTest/java/com/picpay/desafio/feature/contactlist/datasource/internal/ContactListDatabaseTest.kt b/feature/contactlist/datasource/internal/src/androidTest/java/com/picpay/desafio/feature/contactlist/datasource/internal/ContactListDatabaseTest.kt
new file mode 100644
index 000000000..4f203f2d5
--- /dev/null
+++ b/feature/contactlist/datasource/internal/src/androidTest/java/com/picpay/desafio/feature/contactlist/datasource/internal/ContactListDatabaseTest.kt
@@ -0,0 +1,56 @@
+package com.picpay.desafio.feature.contactlist.datasource.internal
+
+import android.content.Context
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.room.Room
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+internal class ContactListDatabaseTest {
+
+ @get:Rule
+ var instantTaskExecutorRule = InstantTaskExecutorRule()
+
+ private lateinit var database: ContactListDatabase
+ private lateinit var useDao: UserDao
+
+ @Before
+ fun setup() {
+ val context = ApplicationProvider.getApplicationContext()
+ database = Room.inMemoryDatabaseBuilder(
+ context, ContactListDatabase::class.java
+ ).allowMainThreadQueries()
+ .build()
+ useDao = database.userDao()
+ }
+
+ @Test
+ fun insertUserAndGetAllUser() = runTest {
+ val userEntity = UserEntity(
+ id = 1,
+ img = "img",
+ name = "name",
+ username = "username"
+ )
+
+ useDao.insertAll(userEntity)
+ val actual = useDao.getAllUser()
+
+ Truth.assertThat(actual).contains(userEntity)
+ }
+
+ @After
+ fun closeDb() {
+ database.close()
+ }
+}
\ No newline at end of file
diff --git a/feature/contactlist/datasource/internal/src/main/AndroidManifest.xml b/feature/contactlist/datasource/internal/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..6fc66846f
--- /dev/null
+++ b/feature/contactlist/datasource/internal/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/feature/contactlist/datasource/internal/src/main/java/com/picpay/desafio/feature/contactlist/datasource/internal/ContactListDatabase.kt b/feature/contactlist/datasource/internal/src/main/java/com/picpay/desafio/feature/contactlist/datasource/internal/ContactListDatabase.kt
new file mode 100644
index 000000000..50e01bb81
--- /dev/null
+++ b/feature/contactlist/datasource/internal/src/main/java/com/picpay/desafio/feature/contactlist/datasource/internal/ContactListDatabase.kt
@@ -0,0 +1,19 @@
+package com.picpay.desafio.feature.contactlist.datasource.internal
+
+import android.content.Context
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+
+@Database(entities = [UserEntity::class], version = 1)
+abstract class ContactListDatabase : RoomDatabase() {
+ abstract fun userDao(): UserDao
+
+ companion object {
+ fun createDatabase(context: Context): ContactListDatabase {
+ return Room
+ .databaseBuilder(context, ContactListDatabase::class.java, "contact-db")
+ .build()
+ }
+ }
+}
\ No newline at end of file
diff --git a/feature/contactlist/datasource/internal/src/main/java/com/picpay/desafio/feature/contactlist/datasource/internal/InternalUserDataSourceImpl.kt b/feature/contactlist/datasource/internal/src/main/java/com/picpay/desafio/feature/contactlist/datasource/internal/InternalUserDataSourceImpl.kt
new file mode 100644
index 000000000..799012f32
--- /dev/null
+++ b/feature/contactlist/datasource/internal/src/main/java/com/picpay/desafio/feature/contactlist/datasource/internal/InternalUserDataSourceImpl.kt
@@ -0,0 +1,24 @@
+package com.picpay.desafio.feature.contactlist.datasource.internal
+
+import com.picpay.desafio.feature.contactlist.repository.UserInternalDataSource
+import com.picpay.desafio.feature.contactlist.usecase.UserDomain
+
+class UserInternalDataSourceImpl(
+ private val userDao: UserDao,
+ private val userMapper: UserEntityMapperWithDomain
+) : UserInternalDataSource {
+ override suspend fun getUsers(): List {
+ return userDao.getAllUser().map {
+ userMapper.mapFromDomain(it)
+ }
+ }
+
+ override suspend fun insertUsers(users: List) {
+ val userEntities = users.map {
+ userMapper.mapToEntity(it)
+ }
+ userEntities.forEach {
+ userDao.insertAll(it)
+ }
+ }
+}
\ No newline at end of file
diff --git a/feature/contactlist/datasource/internal/src/main/java/com/picpay/desafio/feature/contactlist/datasource/internal/UserDao.kt b/feature/contactlist/datasource/internal/src/main/java/com/picpay/desafio/feature/contactlist/datasource/internal/UserDao.kt
new file mode 100644
index 000000000..ae10c0953
--- /dev/null
+++ b/feature/contactlist/datasource/internal/src/main/java/com/picpay/desafio/feature/contactlist/datasource/internal/UserDao.kt
@@ -0,0 +1,14 @@
+package com.picpay.desafio.feature.contactlist.datasource.internal
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.Query
+
+@Dao
+interface UserDao {
+ @Query("Select * from user")
+ suspend fun getAllUser(): List
+
+ @Insert
+ suspend fun insertAll(vararg users: UserEntity)
+}
\ No newline at end of file
diff --git a/feature/contactlist/datasource/internal/src/main/java/com/picpay/desafio/feature/contactlist/datasource/internal/UserEntity.kt b/feature/contactlist/datasource/internal/src/main/java/com/picpay/desafio/feature/contactlist/datasource/internal/UserEntity.kt
new file mode 100644
index 000000000..5c89b6ac9
--- /dev/null
+++ b/feature/contactlist/datasource/internal/src/main/java/com/picpay/desafio/feature/contactlist/datasource/internal/UserEntity.kt
@@ -0,0 +1,17 @@
+package com.picpay.desafio.feature.contactlist.datasource.internal
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity(tableName = "user")
+data class UserEntity(
+ @PrimaryKey
+ val id: Int,
+ @ColumnInfo(name = "image_url")
+ val img: String,
+ @ColumnInfo(name = "name")
+ val name: String,
+ @ColumnInfo(name = "username")
+ val username: String
+)
\ No newline at end of file
diff --git a/feature/contactlist/datasource/internal/src/main/java/com/picpay/desafio/feature/contactlist/datasource/internal/UserEntityMapperWithDomain.kt b/feature/contactlist/datasource/internal/src/main/java/com/picpay/desafio/feature/contactlist/datasource/internal/UserEntityMapperWithDomain.kt
new file mode 100644
index 000000000..4b1ce7c92
--- /dev/null
+++ b/feature/contactlist/datasource/internal/src/main/java/com/picpay/desafio/feature/contactlist/datasource/internal/UserEntityMapperWithDomain.kt
@@ -0,0 +1,19 @@
+package com.picpay.desafio.feature.contactlist.datasource.internal
+
+import com.picpay.desafio.feature.contactlist.usecase.UserDomain
+
+class UserEntityMapperWithDomain() {
+ fun mapFromDomain(userEntity: UserEntity) = UserDomain(
+ img = userEntity.img,
+ name = userEntity.name,
+ id = userEntity.id,
+ username = userEntity.username
+ )
+
+ fun mapToEntity(userDomain: UserDomain) = UserEntity(
+ id = userDomain.id,
+ img = userDomain.img,
+ name = userDomain.name,
+ username = userDomain.username
+ )
+}
\ No newline at end of file
diff --git a/feature/contactlist/datasource/internal/src/main/java/com/picpay/desafio/feature/contactlist/datasource/internal/di/internalDatasourceModule.kt b/feature/contactlist/datasource/internal/src/main/java/com/picpay/desafio/feature/contactlist/datasource/internal/di/internalDatasourceModule.kt
new file mode 100644
index 000000000..d8da9599c
--- /dev/null
+++ b/feature/contactlist/datasource/internal/src/main/java/com/picpay/desafio/feature/contactlist/datasource/internal/di/internalDatasourceModule.kt
@@ -0,0 +1,31 @@
+package com.picpay.desafio.feature.contactlist.datasource.internal.di
+
+import com.picpay.desafio.feature.contactlist.datasource.internal.ContactListDatabase
+import com.picpay.desafio.feature.contactlist.datasource.internal.UserDao
+import com.picpay.desafio.feature.contactlist.datasource.internal.UserEntityMapperWithDomain
+import com.picpay.desafio.feature.contactlist.datasource.internal.UserInternalDataSourceImpl
+import com.picpay.desafio.feature.contactlist.repository.UserInternalDataSource
+import org.koin.android.ext.koin.androidApplication
+import org.koin.dsl.module
+
+val internalDatasourceModule = module {
+ single {
+ ContactListDatabase.createDatabase(androidApplication())
+ }
+
+ factory {
+ provideUserDao(get())
+ }
+
+ factory {
+ UserEntityMapperWithDomain()
+ }
+
+ factory {
+ UserInternalDataSourceImpl(get(), get())
+ }
+}
+
+private fun provideUserDao(database: ContactListDatabase): UserDao {
+ return database.userDao()
+}
\ No newline at end of file
diff --git a/feature/contactlist/datasource/remote/build.gradle.kts b/feature/contactlist/datasource/remote/build.gradle.kts
new file mode 100644
index 000000000..dc2cb335b
--- /dev/null
+++ b/feature/contactlist/datasource/remote/build.gradle.kts
@@ -0,0 +1,27 @@
+import com.picpay.desafio.android.Libraries
+import com.picpay.desafio.android.Libraries.ThirdParty.Koin
+import com.picpay.desafio.android.Libraries.ThirdParty.Retrofit
+import com.picpay.desafio.android.Modules
+import com.picpay.desafio.android.extensions.testImplementation
+
+plugins {
+ id("java-library")
+ id("org.jetbrains.kotlin.jvm")
+}
+
+dependencies {
+ implementation(Koin.core)
+ implementation(Retrofit.retrofit)
+ implementation(Retrofit.gsonConverter)
+
+ testImplementation(Retrofit.gsonConverter)
+ testImplementation(Libraries.AndroidX.Tests.jUnit)
+ testImplementation(Libraries.Jetbrains.KotlinX.Tests.coroutines)
+ testImplementation(Libraries.AndroidX.Tests.runner)
+ testImplementation(Libraries.Google.Truth.truth)
+ testImplementation(Libraries.ThirdParty.Mockk.mockk)
+ testImplementation(Libraries.ThirdParty.Mockk.mockkWebService)
+
+ implementation(project(Modules.contactListUseCase))
+ implementation(project(Modules.contactListRepository))
+}
\ No newline at end of file
diff --git a/feature/contactlist/datasource/remote/src/main/java/com/picpay/desafio/contactlist/datasource/remote/impl/UserRemoteDataSourceImpl.kt b/feature/contactlist/datasource/remote/src/main/java/com/picpay/desafio/contactlist/datasource/remote/impl/UserRemoteDataSourceImpl.kt
new file mode 100644
index 000000000..9c5936ce0
--- /dev/null
+++ b/feature/contactlist/datasource/remote/src/main/java/com/picpay/desafio/contactlist/datasource/remote/impl/UserRemoteDataSourceImpl.kt
@@ -0,0 +1,15 @@
+package com.picpay.desafio.contactlist.datasource.remote.impl
+
+import com.picpay.desafio.feature.contactlist.repository.UserRemoteDataSource
+import com.picpay.desafio.feature.contactlist.usecase.UserDomain
+
+class UserRemoteDataSourceImpl(
+ private val userService: UserService,
+ private val mapper: UserResponseToUserDomainMapper
+) : UserRemoteDataSource {
+ override suspend fun getUsers(): List {
+ return userService.getUsers().map {
+ mapper.mapToDomain(it)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/User.kt b/feature/contactlist/datasource/remote/src/main/java/com/picpay/desafio/contactlist/datasource/remote/impl/UserResponse.kt
similarity index 61%
rename from app/src/main/java/com/picpay/desafio/android/User.kt
rename to feature/contactlist/datasource/remote/src/main/java/com/picpay/desafio/contactlist/datasource/remote/impl/UserResponse.kt
index aa28171c9..b0d0ddbca 100644
--- a/app/src/main/java/com/picpay/desafio/android/User.kt
+++ b/feature/contactlist/datasource/remote/src/main/java/com/picpay/desafio/contactlist/datasource/remote/impl/UserResponse.kt
@@ -1,13 +1,10 @@
-package com.picpay.desafio.android
+package com.picpay.desafio.contactlist.datasource.remote.impl
-import android.os.Parcelable
import com.google.gson.annotations.SerializedName
-import kotlinx.android.parcel.Parcelize
-@Parcelize
-data class User(
+data class UserResponse(
@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
+)
\ No newline at end of file
diff --git a/feature/contactlist/datasource/remote/src/main/java/com/picpay/desafio/contactlist/datasource/remote/impl/UserResponseToUserDomainMapper.kt b/feature/contactlist/datasource/remote/src/main/java/com/picpay/desafio/contactlist/datasource/remote/impl/UserResponseToUserDomainMapper.kt
new file mode 100644
index 000000000..b37e30ee3
--- /dev/null
+++ b/feature/contactlist/datasource/remote/src/main/java/com/picpay/desafio/contactlist/datasource/remote/impl/UserResponseToUserDomainMapper.kt
@@ -0,0 +1,13 @@
+package com.picpay.desafio.contactlist.datasource.remote.impl
+
+import com.picpay.desafio.feature.contactlist.usecase.UserDomain
+
+class UserResponseToUserDomainMapper {
+ fun mapToDomain(response: UserResponse) =
+ UserDomain(
+ img = response.img,
+ name = response.name,
+ id = response.id,
+ username = response.username
+ )
+}
\ No newline at end of file
diff --git a/feature/contactlist/datasource/remote/src/main/java/com/picpay/desafio/contactlist/datasource/remote/impl/UserService.kt b/feature/contactlist/datasource/remote/src/main/java/com/picpay/desafio/contactlist/datasource/remote/impl/UserService.kt
new file mode 100644
index 000000000..259f72844
--- /dev/null
+++ b/feature/contactlist/datasource/remote/src/main/java/com/picpay/desafio/contactlist/datasource/remote/impl/UserService.kt
@@ -0,0 +1,8 @@
+package com.picpay.desafio.contactlist.datasource.remote.impl
+
+import retrofit2.http.GET
+
+interface UserService {
+ @GET("users")
+ suspend fun getUsers(): List
+}
\ No newline at end of file
diff --git a/feature/contactlist/datasource/remote/src/main/java/com/picpay/desafio/contactlist/datasource/remote/impl/di/ContactListDatasourceModule.kt b/feature/contactlist/datasource/remote/src/main/java/com/picpay/desafio/contactlist/datasource/remote/impl/di/ContactListDatasourceModule.kt
new file mode 100644
index 000000000..095423b58
--- /dev/null
+++ b/feature/contactlist/datasource/remote/src/main/java/com/picpay/desafio/contactlist/datasource/remote/impl/di/ContactListDatasourceModule.kt
@@ -0,0 +1,14 @@
+package com.picpay.desafio.contactlist.datasource.remote.impl.di
+
+import com.picpay.desafio.contactlist.datasource.remote.impl.UserRemoteDataSourceImpl
+import com.picpay.desafio.contactlist.datasource.remote.impl.UserResponseToUserDomainMapper
+import com.picpay.desafio.contactlist.datasource.remote.impl.UserService
+import com.picpay.desafio.feature.contactlist.repository.UserRemoteDataSource
+import org.koin.dsl.module
+import retrofit2.Retrofit
+
+val contactListDatasourceModule = module {
+ factory { get().create(UserService::class.java) }
+ factory { UserResponseToUserDomainMapper() }
+ factory { UserRemoteDataSourceImpl(get(), get()) }
+}
\ No newline at end of file
diff --git a/feature/contactlist/datasource/remote/src/test/kotlin/com/picpay/desafio/contactlist/datasource/remote/impl/UserRemoteDataSourceTest.kt b/feature/contactlist/datasource/remote/src/test/kotlin/com/picpay/desafio/contactlist/datasource/remote/impl/UserRemoteDataSourceTest.kt
new file mode 100644
index 000000000..2902f7564
--- /dev/null
+++ b/feature/contactlist/datasource/remote/src/test/kotlin/com/picpay/desafio/contactlist/datasource/remote/impl/UserRemoteDataSourceTest.kt
@@ -0,0 +1,127 @@
+package com.picpay.desafio.contactlist.datasource.remote.impl
+
+import com.google.common.truth.Truth
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.MockWebServer
+import okio.buffer
+import okio.source
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import retrofit2.HttpException
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+
+class UserRemoteDataSourceTest {
+
+ private lateinit var mockWebServer: MockWebServer
+
+ private lateinit var api: UserService
+
+ @Before
+ fun setup() {
+ mockWebServer = MockWebServer()
+ api = Retrofit.Builder()
+ .baseUrl(mockWebServer.url("/"))
+ .addConverterFactory(GsonConverterFactory.create())
+ .build()
+ .create(UserService::class.java)
+
+ }
+
+ @After
+ fun tearDown() {
+ mockWebServer.shutdown()
+ }
+
+ @Test
+ fun getUser_sentRequest_getExpectedResult() = runTest {
+ enqueueMockRespose("get_users_list.json")
+
+ val expected = listOfUserResponseExpected()
+ val actual = api.getUsers()
+
+ Truth.assertThat(actual).isEqualTo(expected)
+ }
+
+ @Test
+ fun getSearchedResult_sentRequest_verifyPathAndMethodIsExpected() {
+ runBlocking {
+
+ enqueueMockRespose("get_users_empty.json")
+
+ val responseBody = api.getUsers()
+
+ val request = mockWebServer.takeRequest()
+ Truth.assertThat(responseBody).isNotNull()
+ Truth.assertThat(request.path).isEqualTo("/users")
+ Truth.assertThat(request.method).isEqualTo("GET")
+ }
+ }
+
+ @Test
+ fun getUser_sentRequest_getEmptyList() = runTest {
+ enqueueMockRespose("get_users_empty.json")
+
+ val expected = listOf()
+ val actual = api.getUsers()
+
+ Truth.assertThat(actual).isEqualTo(expected)
+ }
+
+ @Test()
+ fun getUser_sentRequest_getNotFoundError() = runTest {
+
+ mockWebServer.enqueue(MockResponse().setResponseCode(404))
+
+ val actual = kotlin.runCatching {
+ api.getUsers()
+ }.onFailure {
+ Truth.assertThat(it).isInstanceOf(HttpException::class.java)
+ }
+
+ Truth.assertThat(actual.isFailure).isTrue()
+ Truth.assertThat(actual.exceptionOrNull()?.message)
+ .isEqualTo("HTTP 404 Client Error")
+ }
+
+ private fun listOfUserResponseExpected() = listOf(
+ UserResponse(
+ img = "https://randomuser.me/api/portraits/men/1.jpg",
+ name = "Sandrine Spinka",
+ id = 1,
+ username = "Tod86"
+ ),
+ UserResponse(
+ img = "https://randomuser.me/api/portraits/men/2.jpg",
+ name = "Carli Carroll",
+ id = 2,
+ username = "Constantin_Sawayn"
+ ),
+ UserResponse(
+ img = "https://randomuser.me/api/portraits/men/3.jpg",
+ name = "Annabelle Reilly",
+ id = 3,
+ username = "Lawrence_Nader62"
+ ),
+ UserResponse(
+ img = "https://randomuser.me/api/portraits/men/4.jpg",
+ name = "Mrs. Hilton Welch",
+ id = 4,
+ username = "Tatyana_Ullrich"
+ )
+ )
+
+
+ private fun enqueueMockRespose(fileName: String) {
+ javaClass.classLoader?.let {
+ val inputStream = it.getResourceAsStream(fileName)
+ val source = inputStream.source().buffer()
+ val mockResponse = MockResponse()
+ mockResponse.setBody(source.readString(Charsets.UTF_8))
+ mockWebServer.enqueue(mockResponse)
+ }
+ }
+}
\ No newline at end of file
diff --git a/feature/contactlist/datasource/remote/src/test/kotlin/com/picpay/desafio/contactlist/datasource/remote/impl/UserResponseToUserDomainMapperTest.kt b/feature/contactlist/datasource/remote/src/test/kotlin/com/picpay/desafio/contactlist/datasource/remote/impl/UserResponseToUserDomainMapperTest.kt
new file mode 100644
index 000000000..3573f464e
--- /dev/null
+++ b/feature/contactlist/datasource/remote/src/test/kotlin/com/picpay/desafio/contactlist/datasource/remote/impl/UserResponseToUserDomainMapperTest.kt
@@ -0,0 +1,31 @@
+package com.picpay.desafio.contactlist.datasource.remote.impl
+
+import com.google.common.truth.Truth
+import com.picpay.desafio.feature.contactlist.usecase.UserDomain
+import org.junit.Test
+
+class UserResponseToUserDomainMapperTest {
+
+ private val mapper = UserResponseToUserDomainMapper()
+
+ @Test
+ fun map_receiveUserResponse_expectedUserDomain() {
+ val expected = UserDomain(
+ img = "img-url",
+ name = "name",
+ id = 1,
+ username = "username"
+ )
+
+ val actual = mapper.mapToDomain(
+ UserResponse(
+ img = "img-url",
+ name = "name",
+ id = 1,
+ username = "username"
+ )
+ )
+
+ Truth.assertThat(actual).isEqualTo(expected)
+ }
+}
\ No newline at end of file
diff --git a/feature/contactlist/datasource/remote/src/test/resources/get_users_empty.json b/feature/contactlist/datasource/remote/src/test/resources/get_users_empty.json
new file mode 100644
index 000000000..0637a088a
--- /dev/null
+++ b/feature/contactlist/datasource/remote/src/test/resources/get_users_empty.json
@@ -0,0 +1 @@
+[]
\ No newline at end of file
diff --git a/feature/contactlist/datasource/remote/src/test/resources/get_users_list.json b/feature/contactlist/datasource/remote/src/test/resources/get_users_list.json
new file mode 100644
index 000000000..730225f5b
--- /dev/null
+++ b/feature/contactlist/datasource/remote/src/test/resources/get_users_list.json
@@ -0,0 +1,21 @@
+[{
+ "id": "1",
+ "name": "Sandrine Spinka",
+ "img": "https://randomuser.me/api/portraits/men/1.jpg",
+ "username": "Tod86"
+}, {
+ "id": "2",
+ "name": "Carli Carroll",
+ "img": "https://randomuser.me/api/portraits/men/2.jpg",
+ "username": "Constantin_Sawayn"
+}, {
+ "id": "3",
+ "name": "Annabelle Reilly",
+ "img": "https://randomuser.me/api/portraits/men/3.jpg",
+ "username": "Lawrence_Nader62"
+}, {
+ "id": "4",
+ "name": "Mrs. Hilton Welch",
+ "img": "https://randomuser.me/api/portraits/men/4.jpg",
+ "username": "Tatyana_Ullrich"
+}]
\ No newline at end of file
diff --git a/feature/contactlist/presentation/.gitignore b/feature/contactlist/presentation/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/feature/contactlist/presentation/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/feature/contactlist/presentation/build.gradle.kts b/feature/contactlist/presentation/build.gradle.kts
new file mode 100644
index 000000000..ed912dc4f
--- /dev/null
+++ b/feature/contactlist/presentation/build.gradle.kts
@@ -0,0 +1,44 @@
+import com.picpay.desafio.android.Libraries
+import com.picpay.desafio.android.Modules
+import com.picpay.desafio.android.config.AppConfig
+
+plugins {
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+ id("kotlin-parcelize")
+}
+
+android {
+ compileSdk = AppConfig.compileSdk
+
+ buildFeatures {
+ compose = true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = AppConfig.ComposeOptions.kotlinCompilerExtensionVersion
+ }
+}
+
+dependencies {
+ implementation(Libraries.AndroidX.Compose.activity)
+ implementation(Libraries.AndroidX.Compose.material)
+ implementation(Libraries.AndroidX.Compose.uiTooling)
+ implementation(project(Modules.ui))
+ implementation(project(Modules.contactListUseCase))
+ implementation(project(Modules.testLib))
+ implementation(Libraries.ThirdParty.coil)
+ implementation(Libraries.ThirdParty.Koin.core)
+ implementation(Libraries.ThirdParty.Koin.android)
+ implementation(Libraries.ThirdParty.Koin.compose)
+
+ testImplementation(Libraries.AndroidX.Tests.jUnit)
+ testImplementation(Libraries.Jetbrains.KotlinX.Tests.coroutines)
+ testImplementation(Libraries.AndroidX.Tests.runner)
+ testImplementation(Libraries.Google.Truth.truth)
+ testImplementation(Libraries.AndroidX.Compose.jUnit)
+ testImplementation(Libraries.ThirdParty.Mockk.mockk)
+ testImplementation(Libraries.ThirdParty.Koin.jUnit)
+ testImplementation(Libraries.ThirdParty.UiTest.roboletric)
+ debugImplementation(Libraries.AndroidX.Compose.testManifest)
+}
\ No newline at end of file
diff --git a/feature/contactlist/presentation/consumer-rules.pro b/feature/contactlist/presentation/consumer-rules.pro
new file mode 100644
index 000000000..e69de29bb
diff --git a/feature/contactlist/presentation/proguard-rules.pro b/feature/contactlist/presentation/proguard-rules.pro
new file mode 100644
index 000000000..481bb4348
--- /dev/null
+++ b/feature/contactlist/presentation/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/feature/contactlist/presentation/src/main/AndroidManifest.xml b/feature/contactlist/presentation/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..69e68d692
--- /dev/null
+++ b/feature/contactlist/presentation/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/feature/contactlist/presentation/src/main/java/com/picpay/desafio/contact_list/presentation/ContactScreen.kt b/feature/contactlist/presentation/src/main/java/com/picpay/desafio/contact_list/presentation/ContactScreen.kt
new file mode 100644
index 000000000..a61dcea68
--- /dev/null
+++ b/feature/contactlist/presentation/src/main/java/com/picpay/desafio/contact_list/presentation/ContactScreen.kt
@@ -0,0 +1,94 @@
+package com.picpay.desafio.contact_list.presentation
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.saveable.rememberSaveable
+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.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.picpay.desafio.contact_list.presentation.components.Loading
+import com.picpay.desafio.contact_list.presentation.viewmodel.ContactUiError
+import com.picpay.desafio.contact_list.presentation.viewmodel.ContactUiState
+import com.picpay.desafio.contact_list.presentation.viewmodel.ContactUserEvent
+import com.picpay.desafio.contact_list.presentation.viewmodel.ContactViewModel
+import org.koin.androidx.compose.getViewModel
+
+@Composable
+fun ContactScreen(viewModel: ContactViewModel = getViewModel()) {
+
+ val uiState = viewModel.uiState.collectAsState().value
+
+ LaunchedEffect(key1 = Unit, block = {
+ if(uiState.userList == null && uiState.error == null)
+ viewModel.setUserEvent(ContactUserEvent.OnInitScreen)
+ })
+
+ Surface(
+ modifier = Modifier
+ .fillMaxSize()
+ .testTag("ContactListScreen"), color = MaterialTheme.colors.primaryVariant
+ ) {
+ ContactScreenContent(uiState = uiState)
+ }
+}
+
+@Composable
+private fun ContactScreenContent(uiState: ContactUiState) {
+ if (uiState.error != null) {
+ Box(contentAlignment = Alignment.Center) {
+ Text(
+ text = getErrorMessage(uiState.error),
+ fontSize = 28.sp,
+ color = Color.Red,
+ fontWeight = FontWeight.Bold,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.testTag("ContactListError")
+ )
+ }
+ } else {
+ Column {
+ Text(
+ text = stringResource(id = R.string.contacts),
+ fontSize = 28.sp,
+ color = Color.White,
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier
+ .padding(top = 48.dp, start = 24.dp, bottom = 24.dp)
+ .testTag("ContactListTitle")
+ )
+ if (uiState.loading) {
+ Loading()
+ } else {
+ LazyColumn(content = {
+ items(items = uiState.userList ?: listOf()) { item ->
+ ContactItem(imageUrl = item.img, userName = item.username, name = item.name)
+ }
+ }, modifier = Modifier.testTag("ContactList"))
+ }
+ }
+ }
+}
+
+@Composable
+private fun getErrorMessage(uiError: ContactUiError): String {
+ return when (uiError) {
+ ContactUiError.InternetConnection -> stringResource(id = R.string.internet_connection_error)
+ ContactUiError.UnknownError -> stringResource(id = R.string.unknown_error)
+ }
+}
diff --git a/feature/contactlist/presentation/src/main/java/com/picpay/desafio/contact_list/presentation/components/ContactItem.kt b/feature/contactlist/presentation/src/main/java/com/picpay/desafio/contact_list/presentation/components/ContactItem.kt
new file mode 100644
index 000000000..a6d2bc75b
--- /dev/null
+++ b/feature/contactlist/presentation/src/main/java/com/picpay/desafio/contact_list/presentation/components/ContactItem.kt
@@ -0,0 +1,58 @@
+package com.picpay.desafio.contact_list.presentation
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+import com.picpay.desafio.ui.theme.Detail
+
+@Composable
+fun ContactItem(imageUrl: String, userName: String, name: String) {
+ Surface(color = MaterialTheme.colors.primaryVariant, modifier = Modifier.fillMaxWidth().semantics{
+ contentDescription = "ContactItem"
+ }) {
+ Row {
+
+ AsyncImage(
+ model = imageUrl,
+ contentDescription = "",
+ modifier = Modifier
+ .size(52.dp)
+ .padding(start = 24.dp, top = 12.dp, bottom = 12.dp)
+ .clip(CircleShape)
+ .semantics {
+ contentDescription = "ContactItemImg"
+ }
+ )
+
+ Column(modifier = Modifier.padding(start = 16.dp, top = 8.dp, bottom = 8.dp)) {
+ Text(userName, color = Color.White, modifier = Modifier.semantics {
+ contentDescription = "ContactItemName"
+ })
+ Text(name, color = Detail, modifier = Modifier.semantics {
+ contentDescription = "ContactItemUserName"
+ })
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+fun ContactItemPreview() {
+ ContactItem(
+ imageUrl = "https://randomuser.me/api/portraits/men/1.jpg",
+ userName = "@kitute",
+ name = "bruno henrique"
+ )
+}
\ No newline at end of file
diff --git a/feature/contactlist/presentation/src/main/java/com/picpay/desafio/contact_list/presentation/components/Loading.kt b/feature/contactlist/presentation/src/main/java/com/picpay/desafio/contact_list/presentation/components/Loading.kt
new file mode 100644
index 000000000..a07add414
--- /dev/null
+++ b/feature/contactlist/presentation/src/main/java/com/picpay/desafio/contact_list/presentation/components/Loading.kt
@@ -0,0 +1,22 @@
+package com.picpay.desafio.contact_list.presentation.components
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.CircularProgressIndicator
+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.unit.dp
+
+@Composable
+fun Loading() {
+ Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
+ CircularProgressIndicator(
+ modifier = Modifier.size(50.dp),
+ strokeWidth = 5.dp,
+ color = Color.White
+ )
+ }
+}
\ No newline at end of file
diff --git a/feature/contactlist/presentation/src/main/java/com/picpay/desafio/contact_list/presentation/di/modules.kt b/feature/contactlist/presentation/src/main/java/com/picpay/desafio/contact_list/presentation/di/modules.kt
new file mode 100644
index 000000000..d486a1682
--- /dev/null
+++ b/feature/contactlist/presentation/src/main/java/com/picpay/desafio/contact_list/presentation/di/modules.kt
@@ -0,0 +1,13 @@
+package com.picpay.desafio.contact_list.presentation.di
+
+import com.picpay.desafio.contact_list.presentation.model.mapper.UserDomainToUserUiMapper
+import com.picpay.desafio.contact_list.presentation.viewmodel.ContactUiState
+import com.picpay.desafio.contact_list.presentation.viewmodel.ContactViewModel
+import org.koin.androidx.viewmodel.dsl.viewModel
+import org.koin.dsl.module
+
+val contactListPresentationModule = module {
+ factory { ContactUiState() }
+ factory { UserDomainToUserUiMapper() }
+ viewModel { ContactViewModel(get(), get(), get()) }
+}
\ No newline at end of file
diff --git a/feature/contactlist/presentation/src/main/java/com/picpay/desafio/contact_list/presentation/model/UserUi.kt b/feature/contactlist/presentation/src/main/java/com/picpay/desafio/contact_list/presentation/model/UserUi.kt
new file mode 100644
index 000000000..65098eeb6
--- /dev/null
+++ b/feature/contactlist/presentation/src/main/java/com/picpay/desafio/contact_list/presentation/model/UserUi.kt
@@ -0,0 +1,8 @@
+package com.picpay.desafio.contact_list.presentation.model
+
+data class UserUi(
+ val img: String,
+ val name: String,
+ val id: Int,
+ val username: String
+)
\ No newline at end of file
diff --git a/feature/contactlist/presentation/src/main/java/com/picpay/desafio/contact_list/presentation/model/mapper/UserDomainToUserUiMapper.kt b/feature/contactlist/presentation/src/main/java/com/picpay/desafio/contact_list/presentation/model/mapper/UserDomainToUserUiMapper.kt
new file mode 100644
index 000000000..5ab5a0e97
--- /dev/null
+++ b/feature/contactlist/presentation/src/main/java/com/picpay/desafio/contact_list/presentation/model/mapper/UserDomainToUserUiMapper.kt
@@ -0,0 +1,13 @@
+package com.picpay.desafio.contact_list.presentation.model.mapper
+
+import com.picpay.desafio.contact_list.presentation.model.UserUi
+import com.picpay.desafio.feature.contactlist.usecase.UserDomain
+
+class UserDomainToUserUiMapper {
+ fun mapToUi(domain: UserDomain) = UserUi(
+ img = domain.img,
+ name = domain.name,
+ id = domain.id,
+ username = domain.username
+ )
+}
\ No newline at end of file
diff --git a/feature/contactlist/presentation/src/main/java/com/picpay/desafio/contact_list/presentation/viewmodel/ContactUiError.kt b/feature/contactlist/presentation/src/main/java/com/picpay/desafio/contact_list/presentation/viewmodel/ContactUiError.kt
new file mode 100644
index 000000000..097fc5cb8
--- /dev/null
+++ b/feature/contactlist/presentation/src/main/java/com/picpay/desafio/contact_list/presentation/viewmodel/ContactUiError.kt
@@ -0,0 +1,6 @@
+package com.picpay.desafio.contact_list.presentation.viewmodel
+
+sealed class ContactUiError {
+ object UnknownError : ContactUiError()
+ object InternetConnection : ContactUiError()
+}
\ No newline at end of file
diff --git a/feature/contactlist/presentation/src/main/java/com/picpay/desafio/contact_list/presentation/viewmodel/ContactUiState.kt b/feature/contactlist/presentation/src/main/java/com/picpay/desafio/contact_list/presentation/viewmodel/ContactUiState.kt
new file mode 100644
index 000000000..9f1cb400a
--- /dev/null
+++ b/feature/contactlist/presentation/src/main/java/com/picpay/desafio/contact_list/presentation/viewmodel/ContactUiState.kt
@@ -0,0 +1,9 @@
+package com.picpay.desafio.contact_list.presentation.viewmodel
+
+import com.picpay.desafio.contact_list.presentation.model.UserUi
+
+data class ContactUiState(
+ val userList: List? = null,
+ val loading: Boolean = false,
+ val error: ContactUiError? = null
+)
\ No newline at end of file
diff --git a/feature/contactlist/presentation/src/main/java/com/picpay/desafio/contact_list/presentation/viewmodel/ContactUserEvent.kt b/feature/contactlist/presentation/src/main/java/com/picpay/desafio/contact_list/presentation/viewmodel/ContactUserEvent.kt
new file mode 100644
index 000000000..f797b3de4
--- /dev/null
+++ b/feature/contactlist/presentation/src/main/java/com/picpay/desafio/contact_list/presentation/viewmodel/ContactUserEvent.kt
@@ -0,0 +1,5 @@
+package com.picpay.desafio.contact_list.presentation.viewmodel
+
+sealed class ContactUserEvent {
+ object OnInitScreen: ContactUserEvent()
+}
\ No newline at end of file
diff --git a/feature/contactlist/presentation/src/main/java/com/picpay/desafio/contact_list/presentation/viewmodel/ContactViewModel.kt b/feature/contactlist/presentation/src/main/java/com/picpay/desafio/contact_list/presentation/viewmodel/ContactViewModel.kt
new file mode 100644
index 000000000..fe5e90f4c
--- /dev/null
+++ b/feature/contactlist/presentation/src/main/java/com/picpay/desafio/contact_list/presentation/viewmodel/ContactViewModel.kt
@@ -0,0 +1,60 @@
+package com.picpay.desafio.contact_list.presentation.viewmodel
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import coil.network.HttpException
+import com.picpay.desafio.contact_list.presentation.model.mapper.UserDomainToUserUiMapper
+import com.picpay.desafio.feature.contactlist.usecase.GetUsersUseCase
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+import java.net.HttpRetryException
+import java.net.UnknownHostException
+
+class ContactViewModel(
+ private val getUsersUseCase: GetUsersUseCase,
+ userUiState: ContactUiState,
+ private val mapper: UserDomainToUserUiMapper,
+) : ViewModel() {
+ private val _uiState = MutableStateFlow(userUiState)
+ val uiState = _uiState.asStateFlow()
+
+ fun setUserEvent(userEvent: ContactUserEvent) {
+ when (userEvent) {
+ ContactUserEvent.OnInitScreen -> {
+ handleInitScreen()
+ }
+ }
+ }
+
+ private fun handleInitScreen() {
+ _uiState.value = uiState.value.copy(loading = true)
+ viewModelScope.launch {
+ getUsersUseCase().fold(onSuccess = { userList ->
+ _uiState.value = uiState.value.copy(
+ userList = userList.map {
+ mapper.mapToUi(it)
+ }, loading = false
+ )
+ }, onFailure = {
+ handleError(it)
+ })
+ }
+ }
+
+ private fun handleError(it: Throwable) {
+ when (it) {
+ is UnknownHostException -> _uiState.value = uiState.value.copy(
+ loading = false,
+ userList = null,
+ error = ContactUiError.InternetConnection
+ )
+ else -> _uiState.value = uiState.value.copy(
+ loading = false,
+ userList = null,
+ error = ContactUiError.UnknownError
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_round_account_circle.xml b/feature/contactlist/presentation/src/main/res/drawable/ic_round_account_circle.xml
similarity index 91%
rename from app/src/main/res/drawable/ic_round_account_circle.xml
rename to feature/contactlist/presentation/src/main/res/drawable/ic_round_account_circle.xml
index b9664bae4..0ead19e36 100644
--- a/app/src/main/res/drawable/ic_round_account_circle.xml
+++ b/feature/contactlist/presentation/src/main/res/drawable/ic_round_account_circle.xml
@@ -4,6 +4,6 @@
android:viewportWidth="24"
android:viewportHeight="24">
diff --git a/feature/contactlist/presentation/src/main/res/values/strings.xml b/feature/contactlist/presentation/src/main/res/values/strings.xml
new file mode 100644
index 000000000..906b70ef6
--- /dev/null
+++ b/feature/contactlist/presentation/src/main/res/values/strings.xml
@@ -0,0 +1,6 @@
+
+
+ Problema de conexão, tente novamente mais tarde
+ Erro desconhecido, tente novamente mais tarde.
+ Contatos
+
\ No newline at end of file
diff --git a/feature/contactlist/presentation/src/test/java/com/picpay/desafio/contact_list/presentation/ContactViewModelTest.kt b/feature/contactlist/presentation/src/test/java/com/picpay/desafio/contact_list/presentation/ContactViewModelTest.kt
new file mode 100644
index 000000000..466ee126b
--- /dev/null
+++ b/feature/contactlist/presentation/src/test/java/com/picpay/desafio/contact_list/presentation/ContactViewModelTest.kt
@@ -0,0 +1,149 @@
+package com.picpay.desafio.contact_list.presentation
+
+import com.google.common.truth.Truth
+import com.picpay.desafio.contact_list.presentation.model.UserUi
+import com.picpay.desafio.contact_list.presentation.model.mapper.UserDomainToUserUiMapper
+import com.picpay.desafio.contact_list.presentation.viewmodel.ContactUiError
+import com.picpay.desafio.contact_list.presentation.viewmodel.ContactUiState
+import com.picpay.desafio.contact_list.presentation.viewmodel.ContactUserEvent
+import com.picpay.desafio.contact_list.presentation.viewmodel.ContactViewModel
+import com.picpay.desafio.feature.contactlist.usecase.GetUsersUseCase
+import com.picpay.desafio.feature.contactlist.usecase.UserDomain
+import com.picpay.desafio.testlib.MainDispatcherRule
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Rule
+import org.junit.Test
+import java.net.UnknownHostException
+
+@ExperimentalCoroutinesApi
+class ContactViewModelTest {
+
+ @get:Rule
+ val mainDispatcherRule = MainDispatcherRule()
+
+ private val getUsersUseCase: GetUsersUseCase = mockk()
+ private val mapper: UserDomainToUserUiMapper = mockk()
+
+ private val viewModel = ContactViewModel(getUsersUseCase, ContactUiState(), mapper)
+
+ @Test
+ fun onCreateViewModel_withoutReceiveEvent_UiStateIsEmpty() {
+
+ val expected = ContactUiState(
+ userList = null,
+ loading = false,
+ error = null
+ )
+ val actual = viewModel.uiState.value
+
+ Truth.assertThat(actual).isEqualTo(expected)
+ }
+
+ @Test
+ fun setUserState_receiveOnInit_sendUserListToUi() = runTest {
+ val mapperResult = listOf(
+ UserUi(img = "img-url", name = "name", id = 1, username = "username"),
+ UserUi(img = "img-url", name = "name", id = 2, username = "username"),
+ UserUi(img = "img-url", name = "name", id = 3, username = "username"),
+ )
+
+ val userCaseResult = Result.success(
+ listOf(
+ UserDomain(img = "img-url", name = "name", id = 1, username = "username"),
+ UserDomain(img = "img-url", name = "name", id = 2, username = "username"),
+ UserDomain(img = "img-url", name = "name", id = 3, username = "username"),
+ )
+ )
+
+ prepareScenario(
+ mapperResult = mapperResult,
+ useCaseResult = userCaseResult
+ )
+
+ val expected = ContactUiState(
+ loading = false,
+ userList = mapperResult,
+ error = null
+ )
+
+ viewModel.setUserEvent(ContactUserEvent.OnInitScreen)
+ runCurrent()
+
+ val actual = viewModel.uiState.first()
+
+ Truth.assertThat(actual).isEqualTo(expected)
+ verify(exactly = 3) { mapper.mapToUi(any()) }
+ }
+
+ @Test
+ fun setUserState_receiveOnInit_sendUserEmptyListToUi() = runTest {
+ prepareScenario()
+
+ val expected = ContactUiState(
+ loading = false,
+ userList = listOf(),
+ error = null
+ )
+
+ viewModel.setUserEvent(ContactUserEvent.OnInitScreen)
+ runCurrent()
+
+ val actual = viewModel.uiState.first()
+
+ Truth.assertThat(actual).isEqualTo(expected)
+ verify(exactly = 0) { mapper.mapToUi(any()) }
+ }
+
+ @Test
+ fun setUserState_receiveOnInit_sendUserUnknownUiErrorToUi() = runTest {
+ prepareScenario(useCaseResult = Result.failure(Exception()))
+
+ val expected = ContactUiState(
+ loading = false,
+ userList = null,
+ error = ContactUiError.UnknownError
+ )
+
+ viewModel.setUserEvent(ContactUserEvent.OnInitScreen)
+ runCurrent()
+
+ val actual = viewModel.uiState.first()
+
+ Truth.assertThat(actual).isEqualTo(expected)
+ verify(exactly = 0) { mapper.mapToUi(any()) }
+ }
+
+ @Test
+ fun setUserState_receiveOnInit_sendUserInternetUiErrorToUi() = runTest {
+ prepareScenario(useCaseResult = Result.failure(UnknownHostException()))
+
+ val expected = ContactUiState(
+ loading = false,
+ userList = null,
+ error = ContactUiError.InternetConnection
+ )
+
+ viewModel.setUserEvent(ContactUserEvent.OnInitScreen)
+ runCurrent()
+
+ val actual = viewModel.uiState.first()
+
+ Truth.assertThat(actual).isEqualTo(expected)
+ verify(exactly = 0) { mapper.mapToUi(any()) }
+ }
+
+ private fun prepareScenario(
+ useCaseResult: Result> = Result.success(listOf()),
+ mapperResult: List = listOf(UserUi("", "", 1, ""))
+ ) {
+ every { mapper.mapToUi(any()) } returnsMany mapperResult
+ coEvery { getUsersUseCase() } returns useCaseResult
+ }
+}
\ No newline at end of file
diff --git a/feature/contactlist/repository/impl/.gitignore b/feature/contactlist/repository/impl/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/feature/contactlist/repository/impl/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/feature/contactlist/repository/impl/build.gradle.kts b/feature/contactlist/repository/impl/build.gradle.kts
new file mode 100644
index 000000000..10c451905
--- /dev/null
+++ b/feature/contactlist/repository/impl/build.gradle.kts
@@ -0,0 +1,21 @@
+import com.picpay.desafio.android.Modules
+import com.picpay.desafio.android.Libraries.ThirdParty.Koin
+import com.picpay.desafio.android.extensions.testImplementation
+import com.picpay.desafio.android.Libraries
+
+plugins {
+ id("java-library")
+ id("org.jetbrains.kotlin.jvm")
+}
+
+dependencies {
+ implementation(Koin.core)
+ implementation(project(Modules.contactListRepository))
+ implementation(project(Modules.contactListUseCase))
+
+ testImplementation(Libraries.AndroidX.Tests.jUnit)
+ testImplementation(Libraries.Jetbrains.KotlinX.Tests.coroutines)
+ testImplementation(Libraries.AndroidX.Tests.runner)
+ testImplementation(Libraries.Google.Truth.truth)
+ testImplementation(Libraries.ThirdParty.Mockk.mockk)
+}
\ No newline at end of file
diff --git a/feature/contactlist/repository/impl/src/main/java/com/picpay/desafio/feature/contactlist/repository/impl/UserRepositoryImpl.kt b/feature/contactlist/repository/impl/src/main/java/com/picpay/desafio/feature/contactlist/repository/impl/UserRepositoryImpl.kt
new file mode 100644
index 000000000..d6cba9da9
--- /dev/null
+++ b/feature/contactlist/repository/impl/src/main/java/com/picpay/desafio/feature/contactlist/repository/impl/UserRepositoryImpl.kt
@@ -0,0 +1,35 @@
+package com.picpay.desafio.feature.contactlist.repository.impl
+
+import com.picpay.desafio.feature.contactlist.repository.UserInternalDataSource
+import com.picpay.desafio.feature.contactlist.repository.UserRemoteDataSource
+import com.picpay.desafio.feature.contactlist.repository.UserRepository
+import com.picpay.desafio.feature.contactlist.usecase.UserDomain
+
+class UserRepositoryImpl(
+ private val remoteDataSource: UserRemoteDataSource,
+ private val internalDataSource: UserInternalDataSource
+) : UserRepository {
+ override suspend fun getUsers(): Result> {
+ return try {
+ val usersFromDataBase = internalDataSource.getUsers()
+ if (usersFromDataBase.isEmpty()) {
+ val usersFromRemote = remoteDataSource.getUsers()
+ internalDataSource.insertUsers(usersFromRemote)
+ Result.success(usersFromRemote)
+ } else {
+ Result.success(usersFromDataBase)
+ }
+ } catch (e: Exception) {
+ Result.failure(e)
+ }
+ }
+
+ override suspend fun insertUsers(users: List): Result {
+ return try {
+ internalDataSource.insertUsers(users)
+ Result.success(Unit)
+ } catch (e: Exception) {
+ Result.failure(e)
+ }
+ }
+}
\ No newline at end of file
diff --git a/feature/contactlist/repository/impl/src/main/java/com/picpay/desafio/feature/contactlist/repository/impl/di/ContactListRepositoryModule.kt b/feature/contactlist/repository/impl/src/main/java/com/picpay/desafio/feature/contactlist/repository/impl/di/ContactListRepositoryModule.kt
new file mode 100644
index 000000000..40afa7e6d
--- /dev/null
+++ b/feature/contactlist/repository/impl/src/main/java/com/picpay/desafio/feature/contactlist/repository/impl/di/ContactListRepositoryModule.kt
@@ -0,0 +1,9 @@
+package com.picpay.desafio.feature.contactlist.repository.impl.di
+
+import com.picpay.desafio.feature.contactlist.repository.UserRepository
+import com.picpay.desafio.feature.contactlist.repository.impl.UserRepositoryImpl
+import org.koin.dsl.module
+
+val contactListRepositoryModule = module {
+ factory { UserRepositoryImpl(get(), get()) }
+}
\ No newline at end of file
diff --git a/feature/contactlist/repository/impl/src/test/kotlin/com/picpay/desafio/feature/contactlist/repository/impl/UserRepositoryTest.kt b/feature/contactlist/repository/impl/src/test/kotlin/com/picpay/desafio/feature/contactlist/repository/impl/UserRepositoryTest.kt
new file mode 100644
index 000000000..83b33c1bd
--- /dev/null
+++ b/feature/contactlist/repository/impl/src/test/kotlin/com/picpay/desafio/feature/contactlist/repository/impl/UserRepositoryTest.kt
@@ -0,0 +1,177 @@
+package com.picpay.desafio.feature.contactlist.repository.impl
+
+import com.google.common.truth.Truth
+import com.picpay.desafio.feature.contactlist.repository.UserInternalDataSource
+import com.picpay.desafio.feature.contactlist.repository.UserRemoteDataSource
+import com.picpay.desafio.feature.contactlist.usecase.UserDomain
+import io.mockk.*
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class UserRepositoryTest {
+ private val userRemoteDataSource: UserRemoteDataSource = mockk()
+ private val userInternalDataSource: UserInternalDataSource = mockk()
+
+ private val repository = UserRepositoryImpl(userRemoteDataSource, userInternalDataSource)
+
+ @Test
+ fun getUsers_getUserRemoteDataSource_ExpectedListOfUser() = runTest {
+ val expected = listOf(
+ UserDomain(
+ img = "img",
+ name = "name",
+ id = 1,
+ username = "username"
+ ),
+ UserDomain(
+ img = "img",
+ name = "name",
+ id = 2,
+ username = "username"
+ ),
+ UserDomain(
+ img = "img",
+ name = "name",
+ id = 3,
+ username = "username"
+ )
+ )
+
+ prepareScenario(
+ userDataRemoteSourceResult = listUserResponse(),
+ )
+
+ val actual = repository.getUsers()
+
+ Truth.assertThat(actual).isEqualTo(Result.success(expected))
+ coVerify(exactly = 1) { userInternalDataSource.insertUsers(expected) }
+ coVerify(exactly = 1) { userInternalDataSource.getUsers() }
+ }
+
+ @Test
+ fun getUsers_getUserInternalDataSource_ExpectedListOfUser() = runTest {
+ val expected = listOf(
+ UserDomain(
+ img = "img",
+ name = "name",
+ id = 1,
+ username = "username"
+ ),
+ UserDomain(
+ img = "img",
+ name = "name",
+ id = 2,
+ username = "username"
+ ),
+ UserDomain(
+ img = "img",
+ name = "name",
+ id = 3,
+ username = "username"
+ )
+ )
+
+ prepareScenario(
+ userDataInternalSourceResult = listUserResponse(),
+ )
+
+ val actual = repository.getUsers()
+
+ Truth.assertThat(actual).isEqualTo(Result.success(expected))
+ coVerify(exactly = 0) { userRemoteDataSource.getUsers() }
+ coVerify(exactly = 0) { userInternalDataSource.insertUsers(any()) }
+ }
+
+ @Test
+ fun getUsers_ExternalDataSourceError_returnResultFailure() = runTest {
+ val exception = Exception("")
+ prepareScenario(userDataSourceError = exception)
+
+ val expected = Result.failure(exception)
+ val actual = repository.getUsers()
+
+ Truth.assertThat(actual).isEqualTo(expected)
+
+ coVerify(exactly = 1) { userRemoteDataSource.getUsers() }
+ coVerify(exactly = 1) { userInternalDataSource.getUsers() }
+ coVerify(exactly = 0) { userInternalDataSource.insertUsers(any()) }
+ }
+
+ @Test
+ fun getUsers_InternalDataSourceInsertUserError_returnResultFailure() = runTest {
+ val exception = Exception("")
+ prepareScenario(insertUserInternalDataSourceError = exception)
+
+ val expected = Result.failure(exception)
+ val actual = repository.getUsers()
+
+ Truth.assertThat(actual).isEqualTo(expected)
+ coVerify(exactly = 1) { userRemoteDataSource.getUsers() }
+ coVerify(exactly = 1) { userInternalDataSource.getUsers() }
+ }
+
+ @Test
+ fun getUsers_InternalDataSourceGetUserError_returnResultFailure() = runTest {
+ val exception = Exception("")
+ prepareScenario(getUserInternalDataSourceError = exception)
+
+ val expected = Result.failure(exception)
+ val actual = repository.getUsers()
+
+ Truth.assertThat(actual).isEqualTo(expected)
+ coVerify(exactly = 0) { userRemoteDataSource.getUsers() }
+ coVerify(exactly = 0) { userInternalDataSource.insertUsers(any()) }
+ coVerify(exactly = 1) { userInternalDataSource.getUsers() }
+ }
+
+ private fun prepareScenario(
+ userDataSourceError: Throwable? = null,
+ getUserInternalDataSourceError: Throwable? = null,
+ userDataRemoteSourceResult: List = listOf(),
+ userDataInternalSourceResult: List = listOf(),
+ insertUserInternalDataSourceError: Throwable? = null,
+ ) {
+ userDataSourceError?.let {
+ coEvery { userRemoteDataSource.getUsers() } throws userDataSourceError
+ } ?: run {
+ coEvery { userRemoteDataSource.getUsers() } returns userDataRemoteSourceResult
+ }
+
+ getUserInternalDataSourceError?.let {
+ coEvery { userInternalDataSource.getUsers() } throws getUserInternalDataSourceError
+ } ?: run {
+ coEvery { userInternalDataSource.getUsers() } returns userDataInternalSourceResult
+ }
+
+ insertUserInternalDataSourceError?.let {
+ coEvery {
+ userInternalDataSource.insertUsers(any())
+ } throws insertUserInternalDataSourceError
+ } ?: run {
+ coEvery {
+ userInternalDataSource.insertUsers(any())
+ } just runs
+ }
+ }
+
+ private fun listUserResponse() = listOf(
+ UserDomain(
+ img = "img",
+ name = "name",
+ id = 1,
+ username = "username"
+ ),
+ UserDomain(
+ img = "img",
+ name = "name",
+ id = 2,
+ username = "username"
+ ),
+ UserDomain(
+ img = "img",
+ name = "name",
+ id = 3,
+ username = "username"
+ )
+ )
+}
\ No newline at end of file
diff --git a/feature/contactlist/repository/public/.gitignore b/feature/contactlist/repository/public/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/feature/contactlist/repository/public/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/feature/contactlist/repository/public/build.gradle.kts b/feature/contactlist/repository/public/build.gradle.kts
new file mode 100644
index 000000000..514a64f86
--- /dev/null
+++ b/feature/contactlist/repository/public/build.gradle.kts
@@ -0,0 +1,10 @@
+import com.picpay.desafio.android.Modules
+
+plugins {
+ id("java-library")
+ id("org.jetbrains.kotlin.jvm")
+}
+
+dependencies {
+ implementation(project(Modules.contactListUseCase))
+}
\ No newline at end of file
diff --git a/feature/contactlist/repository/public/src/main/java/com/picpay/desafio/feature/contactlist/repository/UserDataSource.kt b/feature/contactlist/repository/public/src/main/java/com/picpay/desafio/feature/contactlist/repository/UserDataSource.kt
new file mode 100644
index 000000000..3d3998142
--- /dev/null
+++ b/feature/contactlist/repository/public/src/main/java/com/picpay/desafio/feature/contactlist/repository/UserDataSource.kt
@@ -0,0 +1,12 @@
+package com.picpay.desafio.feature.contactlist.repository
+
+import com.picpay.desafio.feature.contactlist.usecase.UserDomain
+
+interface UserRemoteDataSource {
+ suspend fun getUsers(): List
+}
+
+interface UserInternalDataSource {
+ suspend fun getUsers(): List
+ suspend fun insertUsers(users: List)
+}
\ No newline at end of file
diff --git a/feature/contactlist/repository/public/src/main/java/com/picpay/desafio/feature/contactlist/repository/UserRepository.kt b/feature/contactlist/repository/public/src/main/java/com/picpay/desafio/feature/contactlist/repository/UserRepository.kt
new file mode 100644
index 000000000..314d7887d
--- /dev/null
+++ b/feature/contactlist/repository/public/src/main/java/com/picpay/desafio/feature/contactlist/repository/UserRepository.kt
@@ -0,0 +1,8 @@
+package com.picpay.desafio.feature.contactlist.repository
+
+import com.picpay.desafio.feature.contactlist.usecase.UserDomain
+
+interface UserRepository {
+ suspend fun getUsers(): Result>
+ suspend fun insertUsers(users: List): Result
+}
\ No newline at end of file
diff --git a/feature/contactlist/usecase/impl/.gitignore b/feature/contactlist/usecase/impl/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/feature/contactlist/usecase/impl/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/feature/contactlist/usecase/impl/build.gradle.kts b/feature/contactlist/usecase/impl/build.gradle.kts
new file mode 100644
index 000000000..35291de4f
--- /dev/null
+++ b/feature/contactlist/usecase/impl/build.gradle.kts
@@ -0,0 +1,21 @@
+import com.picpay.desafio.android.Libraries
+import com.picpay.desafio.android.Libraries.ThirdParty.Koin
+import com.picpay.desafio.android.Modules
+
+plugins {
+ id("java-library")
+ id("org.jetbrains.kotlin.jvm")
+}
+
+dependencies {
+ implementation(project(Modules.contactListUseCase))
+ implementation(project(Modules.contactListRepository))
+ implementation(Koin.core)
+
+ testImplementation(Libraries.AndroidX.Tests.jUnit)
+ testImplementation(Libraries.Jetbrains.KotlinX.Tests.coroutines)
+ testImplementation(Libraries.AndroidX.Tests.runner)
+ testImplementation(Libraries.Google.Truth.truth)
+ testImplementation(Libraries.ThirdParty.Mockk.mockk)
+
+}
\ No newline at end of file
diff --git a/feature/contactlist/usecase/impl/src/main/java/com/picpay/desafio/contactlist/usecase/impl/ContactListDomainModule.kt b/feature/contactlist/usecase/impl/src/main/java/com/picpay/desafio/contactlist/usecase/impl/ContactListDomainModule.kt
new file mode 100644
index 000000000..4acc095b3
--- /dev/null
+++ b/feature/contactlist/usecase/impl/src/main/java/com/picpay/desafio/contactlist/usecase/impl/ContactListDomainModule.kt
@@ -0,0 +1,8 @@
+package com.picpay.desafio.contactlist.usecase.impl
+
+import com.picpay.desafio.feature.contactlist.usecase.GetUsersUseCase
+import org.koin.dsl.module
+
+val contactListDomainModule = module {
+ factory { GetUsersUseCaseImpl(get()) }
+}
\ No newline at end of file
diff --git a/feature/contactlist/usecase/impl/src/main/java/com/picpay/desafio/contactlist/usecase/impl/GetUsersUseCaseImpl.kt b/feature/contactlist/usecase/impl/src/main/java/com/picpay/desafio/contactlist/usecase/impl/GetUsersUseCaseImpl.kt
new file mode 100644
index 000000000..1abd0a78f
--- /dev/null
+++ b/feature/contactlist/usecase/impl/src/main/java/com/picpay/desafio/contactlist/usecase/impl/GetUsersUseCaseImpl.kt
@@ -0,0 +1,11 @@
+package com.picpay.desafio.contactlist.usecase.impl
+
+import com.picpay.desafio.feature.contactlist.repository.UserRepository
+import com.picpay.desafio.feature.contactlist.usecase.GetUsersUseCase
+import com.picpay.desafio.feature.contactlist.usecase.UserDomain
+
+class GetUsersUseCaseImpl(private val userRepository: UserRepository) : GetUsersUseCase {
+ override suspend operator fun invoke(): Result> {
+ return userRepository.getUsers()
+ }
+}
\ No newline at end of file
diff --git a/feature/contactlist/usecase/impl/src/test/kotlin/com/picpay/desafio/contactlist/usecase/impl/GetUsersUseCaseTest.kt b/feature/contactlist/usecase/impl/src/test/kotlin/com/picpay/desafio/contactlist/usecase/impl/GetUsersUseCaseTest.kt
new file mode 100644
index 000000000..7bf87fa77
--- /dev/null
+++ b/feature/contactlist/usecase/impl/src/test/kotlin/com/picpay/desafio/contactlist/usecase/impl/GetUsersUseCaseTest.kt
@@ -0,0 +1,72 @@
+package com.picpay.desafio.contactlist.usecase.impl
+
+import com.google.common.truth.Truth
+import com.picpay.desafio.feature.contactlist.repository.UserRepository
+import com.picpay.desafio.feature.contactlist.usecase.UserDomain
+import io.mockk.coEvery
+import io.mockk.mockk
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class GetUsersUseCaseTest {
+ private val userRepository: UserRepository = mockk()
+
+ private val usecase = GetUsersUseCaseImpl(userRepository)
+
+ @Test
+ fun invoke_receiveEmptyList_expectResultSuccessWithEmptyList() = runTest {
+ prepareScenario()
+ val expected = Result.success(listOf())
+
+ val actual = usecase()
+
+ Truth.assertThat(actual).isEqualTo(expected)
+ }
+
+ @Test
+ fun invoke_receiveUsersList_expectedResultSuccessUserList() = runTest {
+ val expected = Result.success(listUserResponse())
+ prepareScenario(expected)
+
+ val actual = usecase()
+
+ Truth.assertThat(actual).isEqualTo(expected)
+ }
+
+ @Test
+ fun invoke_receiveError_expectedResultFailure() = runTest {
+ val expected = Result.failure>(ArrayIndexOutOfBoundsException())
+ prepareScenario(expected)
+
+ val actual = usecase()
+
+ Truth.assertThat(actual).isEqualTo(expected)
+ }
+
+ private fun prepareScenario(
+ userRepositoryResult: Result> = Result.success(listOf())
+ ) {
+ coEvery { userRepository.getUsers() } returns userRepositoryResult
+ }
+
+ private fun listUserResponse() = listOf(
+ UserDomain(
+ img = "img",
+ name = "name",
+ id = 1,
+ username = "username"
+ ),
+ UserDomain(
+ img = "img",
+ name = "name",
+ id = 2,
+ username = "username"
+ ),
+ UserDomain(
+ img = "img",
+ name = "name",
+ id = 3,
+ username = "username"
+ )
+ )
+}
\ No newline at end of file
diff --git a/feature/contactlist/usecase/public/.gitignore b/feature/contactlist/usecase/public/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/feature/contactlist/usecase/public/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/feature/contactlist/usecase/public/build.gradle.kts b/feature/contactlist/usecase/public/build.gradle.kts
new file mode 100644
index 000000000..f4f4ad5eb
--- /dev/null
+++ b/feature/contactlist/usecase/public/build.gradle.kts
@@ -0,0 +1,4 @@
+plugins {
+ id("java-library")
+ id("org.jetbrains.kotlin.jvm")
+}
\ No newline at end of file
diff --git a/feature/contactlist/usecase/public/src/main/java/com/picpay/desafio/feature/contactlist/usecase/GetUsersUseCase.kt b/feature/contactlist/usecase/public/src/main/java/com/picpay/desafio/feature/contactlist/usecase/GetUsersUseCase.kt
new file mode 100644
index 000000000..ace4f6610
--- /dev/null
+++ b/feature/contactlist/usecase/public/src/main/java/com/picpay/desafio/feature/contactlist/usecase/GetUsersUseCase.kt
@@ -0,0 +1,5 @@
+package com.picpay.desafio.feature.contactlist.usecase
+
+interface GetUsersUseCase {
+ suspend operator fun invoke(): Result>
+}
\ No newline at end of file
diff --git a/feature/contactlist/usecase/public/src/main/java/com/picpay/desafio/feature/contactlist/usecase/UserDomain.kt b/feature/contactlist/usecase/public/src/main/java/com/picpay/desafio/feature/contactlist/usecase/UserDomain.kt
new file mode 100644
index 000000000..d2301f787
--- /dev/null
+++ b/feature/contactlist/usecase/public/src/main/java/com/picpay/desafio/feature/contactlist/usecase/UserDomain.kt
@@ -0,0 +1,8 @@
+package com.picpay.desafio.feature.contactlist.usecase
+
+data class UserDomain(
+ val img: String,
+ val name: String,
+ val id: Int,
+ val username: String
+)
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 23339e0df..de395713c 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -6,16 +6,19 @@
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
-org.gradle.jvmargs=-Xmx1536m
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
-# Android operating system, and which are packaged with your app's APK
+# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
-# Automatically convert third-party libraries to use AndroidX
-android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
+org.gradle.parallel=true
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index f6b961fd5..e708b1c02 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 31680f1d6..4a7d65ff6 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
+#Tue Jun 14 21:46:33 BRT 2022
distributionBase=GRADLE_USER_HOME
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
distributionPath=wrapper/dists
-zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
+zipStoreBase=GRADLE_USER_HOME
diff --git a/gradlew b/gradlew
old mode 100644
new mode 100755
index cccdd3d51..4f906e0c8
--- a/gradlew
+++ b/gradlew
@@ -1,5 +1,21 @@
#!/usr/bin/env sh
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
##############################################################################
##
## Gradle start up script for UN*X
@@ -28,7 +44,7 @@ APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS=""
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
@@ -66,6 +82,7 @@ esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
@@ -109,10 +126,11 @@ if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
-# For Cygwin, switch paths to Windows format before running java
-if $cygwin ; then
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
@@ -138,19 +156,19 @@ if $cygwin ; then
else
eval `echo args$i`="\"$arg\""
fi
- i=$((i+1))
+ i=`expr $i + 1`
done
case $i in
- (0) set -- ;;
- (1) set -- "$args0" ;;
- (2) set -- "$args0" "$args1" ;;
- (3) set -- "$args0" "$args1" "$args2" ;;
- (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
- (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
- (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
- (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
- (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
- (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
@@ -159,14 +177,9 @@ save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
-APP_ARGS=$(save "$@")
+APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
-# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
-if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
- cd "$(dirname "$0")"
-fi
-
exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
index f9553162f..ac1b06f93 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -1,84 +1,89 @@
-@if "%DEBUG%" == "" @echo off
-@rem ##########################################################################
-@rem
-@rem Gradle startup script for Windows
-@rem
-@rem ##########################################################################
-
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
-
-set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
-set APP_BASE_NAME=%~n0
-set APP_HOME=%DIRNAME%
-
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS=
-
-@rem Find java.exe
-if defined JAVA_HOME goto findJavaFromJavaHome
-
-set JAVA_EXE=java.exe
-%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto init
-
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:findJavaFromJavaHome
-set JAVA_HOME=%JAVA_HOME:"=%
-set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-
-if exist "%JAVA_EXE%" goto init
-
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:init
-@rem Get command-line arguments, handling Windows variants
-
-if not "%OS%" == "Windows_NT" goto win9xME_args
-
-:win9xME_args
-@rem Slurp the command line arguments.
-set CMD_LINE_ARGS=
-set _SKIP=2
-
-:win9xME_args_slurp
-if "x%~1" == "x" goto execute
-
-set CMD_LINE_ARGS=%*
-
-:execute
-@rem Setup the command line
-
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
-
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
-
-:end
-@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
-
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
-
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
-
-:omega
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/network/.gitignore b/network/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/network/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/network/build.gradle.kts b/network/build.gradle.kts
new file mode 100644
index 000000000..79a5158f2
--- /dev/null
+++ b/network/build.gradle.kts
@@ -0,0 +1,29 @@
+import com.picpay.desafio.android.config.AppConfig.BuildConfigField
+import com.picpay.desafio.android.Libraries.ThirdParty
+import com.picpay.desafio.android.config.AppConfig
+
+plugins {
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+}
+
+android {
+ compileSdk = AppConfig.compileSdk
+
+ defaultConfig {
+ buildConfigField(
+ name = BuildConfigField.BASE_URL.name,
+ value = BuildConfigField.BASE_URL.value,
+ type = BuildConfigField.BASE_URL.type
+ )
+ }
+
+}
+
+dependencies {
+ implementation(ThirdParty.Koin.core)
+ implementation(ThirdParty.Retrofit.retrofit)
+ implementation(ThirdParty.Retrofit.gsonConverter)
+ implementation(ThirdParty.Okhttp.okhttp)
+ implementation(ThirdParty.Okhttp.loggingInterceptor)
+}
\ No newline at end of file
diff --git a/network/src/main/AndroidManifest.xml b/network/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..dab897f65
--- /dev/null
+++ b/network/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/network/src/main/java/com/picpay/desafio/network/NetworkClientProvider.kt b/network/src/main/java/com/picpay/desafio/network/NetworkClientProvider.kt
new file mode 100644
index 000000000..6c323ced2
--- /dev/null
+++ b/network/src/main/java/com/picpay/desafio/network/NetworkClientProvider.kt
@@ -0,0 +1,44 @@
+package com.picpay.desafio.network
+
+import okhttp3.CertificatePinner
+import okhttp3.Interceptor
+import okhttp3.OkHttpClient
+import okhttp3.logging.HttpLoggingInterceptor
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+import java.util.concurrent.TimeUnit
+
+
+object NetworkClientProvider {
+
+ object NetworkConstants {
+ val TIME_UNIT = TimeUnit.MINUTES
+ val READ_TIMEOUT = 5L
+ val CONNECTION_TIMEOUT = 5L
+ val BASE_URL = "https://brasileirao-app.herokuapp.com"
+ }
+
+ fun buildOkHttpClient(
+ pinner: CertificatePinner? = null,
+ interceptor: Interceptor? = null
+ ): OkHttpClient {
+ val client = OkHttpClient.Builder()
+ .readTimeout(NetworkConstants.READ_TIMEOUT, NetworkConstants.TIME_UNIT)
+ .connectTimeout(NetworkConstants.CONNECTION_TIMEOUT, NetworkConstants.TIME_UNIT)
+
+ client.addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
+
+ pinner?.let { client.certificatePinner(it) }
+ interceptor?.let { client.addInterceptor(it) }
+
+ return client.build()
+ }
+
+ fun buildRetrofitClient(okHttpClient: OkHttpClient, url: String): Retrofit {
+ return Retrofit.Builder()
+ .baseUrl(url)
+ .addConverterFactory(GsonConverterFactory.create())
+ .client(okHttpClient)
+ .build()
+ }
+}
\ No newline at end of file
diff --git a/network/src/main/java/com/picpay/desafio/network/di/NetworkModule.kt b/network/src/main/java/com/picpay/desafio/network/di/NetworkModule.kt
new file mode 100644
index 000000000..86c9891d8
--- /dev/null
+++ b/network/src/main/java/com/picpay/desafio/network/di/NetworkModule.kt
@@ -0,0 +1,15 @@
+package com.picpay.desafio.network.di
+
+import com.picpay.desafio.network.BuildConfig
+import com.picpay.desafio.network.NetworkClientProvider.buildOkHttpClient
+import com.picpay.desafio.network.NetworkClientProvider.buildRetrofitClient
+import org.koin.dsl.module
+
+val networkModule = module {
+ single {
+ buildRetrofitClient(
+ buildOkHttpClient(),
+ BuildConfig.API_BASE_URL
+ )
+ }
+}
diff --git a/settings.gradle b/settings.gradle
deleted file mode 100644
index e7b4def49..000000000
--- a/settings.gradle
+++ /dev/null
@@ -1 +0,0 @@
-include ':app'
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 000000000..66ab513ba
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,30 @@
+pluginManagement {
+ repositories {
+ gradlePluginPortal()
+ google()
+ mavenCentral()
+ }
+}
+
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "desafio-android"
+include(
+ ":ui",
+ ":app",
+ ":network",
+ ":feature:contactlist:presentation",
+ ":feature:contactlist:datasource:remote",
+ ":feature:contactlist:datasource:internal",
+ ":feature:contactlist:repository:impl",
+ ":feature:contactlist:repository:public",
+ ":feature:contactlist:usecase:impl",
+ ":feature:contactlist:usecase:public",
+ ":testlib"
+)
diff --git a/testlib/.gitignore b/testlib/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/testlib/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/testlib/build.gradle.kts b/testlib/build.gradle.kts
new file mode 100644
index 000000000..4531a4517
--- /dev/null
+++ b/testlib/build.gradle.kts
@@ -0,0 +1,17 @@
+import com.picpay.desafio.android.Libraries
+import com.picpay.desafio.android.config.AppConfig
+
+plugins {
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+}
+
+android {
+ compileSdk = AppConfig.compileSdk
+}
+
+dependencies {
+ implementation(Libraries.Jetbrains.KotlinX.coroutines)
+ implementation(Libraries.Jetbrains.KotlinX.Tests.coroutines)
+ implementation(Libraries.AndroidX.Tests.jUnit)
+}
\ No newline at end of file
diff --git a/testlib/consumer-rules.pro b/testlib/consumer-rules.pro
new file mode 100644
index 000000000..e69de29bb
diff --git a/testlib/proguard-rules.pro b/testlib/proguard-rules.pro
new file mode 100644
index 000000000..481bb4348
--- /dev/null
+++ b/testlib/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/testlib/src/androidTest/java/com/picpay/desafio/testlib/ExampleInstrumentedTest.kt b/testlib/src/androidTest/java/com/picpay/desafio/testlib/ExampleInstrumentedTest.kt
new file mode 100644
index 000000000..b33176c1d
--- /dev/null
+++ b/testlib/src/androidTest/java/com/picpay/desafio/testlib/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.picpay.desafio.testlib
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.picpay.desafio.testlib.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/testlib/src/main/AndroidManifest.xml b/testlib/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..bd8f3a051
--- /dev/null
+++ b/testlib/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/testlib/src/main/java/com/picpay/desafio/testlib/MainDispatcherRule.kt b/testlib/src/main/java/com/picpay/desafio/testlib/MainDispatcherRule.kt
new file mode 100644
index 000000000..e365c5096
--- /dev/null
+++ b/testlib/src/main/java/com/picpay/desafio/testlib/MainDispatcherRule.kt
@@ -0,0 +1,25 @@
+package com.picpay.desafio.testlib
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.setMain
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+
+// Reusable JUnit4 TestRule to override the Main dispatcher
+@OptIn(ExperimentalCoroutinesApi::class)
+class MainDispatcherRule(
+ private val testDispatcher: TestDispatcher = StandardTestDispatcher()
+) : TestWatcher() {
+
+ override fun starting(description: Description) {
+ Dispatchers.setMain(testDispatcher)
+ }
+
+ override fun finished(description: Description) {
+ Dispatchers.resetMain()
+ }
+}
\ No newline at end of file
diff --git a/testlib/src/test/java/com/picpay/desafio/testlib/ExampleUnitTest.kt b/testlib/src/test/java/com/picpay/desafio/testlib/ExampleUnitTest.kt
new file mode 100644
index 000000000..1674534c7
--- /dev/null
+++ b/testlib/src/test/java/com/picpay/desafio/testlib/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.picpay.desafio.testlib
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/ui/.gitignore b/ui/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/ui/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/ui/build.gradle.kts b/ui/build.gradle.kts
new file mode 100644
index 000000000..55b3fe1e2
--- /dev/null
+++ b/ui/build.gradle.kts
@@ -0,0 +1,27 @@
+import com.picpay.desafio.android.Libraries.AndroidX.Compose
+import com.picpay.desafio.android.config.AppConfig
+import com.picpay.desafio.android.Libraries.ThirdParty.Koin
+plugins {
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+}
+
+android {
+ compileSdk = AppConfig.compileSdk
+
+ buildFeatures {
+ compose = true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = AppConfig.ComposeOptions.kotlinCompilerExtensionVersion
+ }
+}
+
+
+dependencies {
+ implementation(Compose.material)
+ implementation(Compose.animation)
+ implementation(Compose.uiTooling)
+ implementation(Koin.core)
+}
\ No newline at end of file
diff --git a/ui/consumer-rules.pro b/ui/consumer-rules.pro
new file mode 100644
index 000000000..e69de29bb
diff --git a/ui/proguard-rules.pro b/ui/proguard-rules.pro
new file mode 100644
index 000000000..481bb4348
--- /dev/null
+++ b/ui/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/ui/src/main/AndroidManifest.xml b/ui/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..81d8412d3
--- /dev/null
+++ b/ui/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/ui/src/main/java/com/picpay/desafio/ui/theme/Color.kt b/ui/src/main/java/com/picpay/desafio/ui/theme/Color.kt
new file mode 100644
index 000000000..de1ada31c
--- /dev/null
+++ b/ui/src/main/java/com/picpay/desafio/ui/theme/Color.kt
@@ -0,0 +1,9 @@
+package com.picpay.desafio.ui.theme
+
+import androidx.compose.ui.graphics.Color
+
+val Primary = Color(0xFF2B2C2F)
+val PrimaryDark = Color(0xFF1D1E20)
+val Accent = Color(0xFF11C76F)
+val Light = Color(0xFFACB1BD)
+val Detail = Color(0x80FFFFFF)
\ No newline at end of file
diff --git a/ui/src/main/java/com/picpay/desafio/ui/theme/Theme.kt b/ui/src/main/java/com/picpay/desafio/ui/theme/Theme.kt
new file mode 100644
index 000000000..1b0859daf
--- /dev/null
+++ b/ui/src/main/java/com/picpay/desafio/ui/theme/Theme.kt
@@ -0,0 +1,46 @@
+package com.picpay.desafio.ui.theme
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.darkColors
+import androidx.compose.material.lightColors
+import androidx.compose.runtime.Composable
+
+private val DarkColorPalette = darkColors(
+ primary = Primary,
+ primaryVariant = PrimaryDark,
+ secondary = Accent
+)
+
+private val LightColorPalette = lightColors(
+ primary = Primary,
+ primaryVariant = PrimaryDark,
+ secondary = Accent
+
+ /* Other default colors to override
+ background = Color.White,
+ surface = Color.White,
+ onPrimary = Color.White,
+ onSecondary = Color.Black,
+ onBackground = Color.Black,
+ onSurface = Color.Black,
+ */
+)
+
+@Composable
+fun DesafioandroidTheme(
+ darkTheme: Boolean = isSystemInDarkTheme(),
+ content: @Composable () -> Unit
+) {
+ val colors = if (darkTheme) {
+ DarkColorPalette
+ } else {
+ LightColorPalette
+ }
+
+ MaterialTheme(
+ colors = colors,
+ typography = Typography,
+ content = content
+ )
+}
\ No newline at end of file
diff --git a/ui/src/main/java/com/picpay/desafio/ui/theme/Type.kt b/ui/src/main/java/com/picpay/desafio/ui/theme/Type.kt
new file mode 100644
index 000000000..3628083e5
--- /dev/null
+++ b/ui/src/main/java/com/picpay/desafio/ui/theme/Type.kt
@@ -0,0 +1,28 @@
+package com.picpay.desafio.ui.theme
+
+import androidx.compose.material.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+
+// Set of Material typography styles to start with
+val Typography = Typography(
+ body1 = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp
+ )
+ /* Other default text styles to override
+ button = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.W500,
+ fontSize = 14.sp
+ ),
+ caption = TextStyle(
+ fontFamily = FontFamily.Default,
+ fontWeight = FontWeight.Normal,
+ fontSize = 12.sp
+ )
+ */
+)
\ No newline at end of file
diff --git a/ui/src/main/java/com/picpay/desafio/ui/theme/di/UiModule.kt b/ui/src/main/java/com/picpay/desafio/ui/theme/di/UiModule.kt
new file mode 100644
index 000000000..923cda74e
--- /dev/null
+++ b/ui/src/main/java/com/picpay/desafio/ui/theme/di/UiModule.kt
@@ -0,0 +1,11 @@
+package com.picpay.desafio.ui.theme.di
+
+import com.picpay.desafio.ui.theme.resourceprovider.StringResourceProvider
+import com.picpay.desafio.ui.theme.resourceprovider.StringResourceProviderImpl
+import org.koin.dsl.module
+
+val uiModule = module {
+ factory {
+ StringResourceProviderImpl(get())
+ }
+}
\ No newline at end of file
diff --git a/ui/src/main/java/com/picpay/desafio/ui/theme/resourceprovider/StringResourceProvider.kt b/ui/src/main/java/com/picpay/desafio/ui/theme/resourceprovider/StringResourceProvider.kt
new file mode 100644
index 000000000..db09085fc
--- /dev/null
+++ b/ui/src/main/java/com/picpay/desafio/ui/theme/resourceprovider/StringResourceProvider.kt
@@ -0,0 +1,10 @@
+package com.picpay.desafio.ui.theme.resourceprovider
+
+import androidx.annotation.StringRes
+
+interface StringResourceProvider {
+ fun getString(
+ @StringRes
+ stringId: Int
+ ): String
+}
\ No newline at end of file
diff --git a/ui/src/main/java/com/picpay/desafio/ui/theme/resourceprovider/StringResourceProviderImpl.kt b/ui/src/main/java/com/picpay/desafio/ui/theme/resourceprovider/StringResourceProviderImpl.kt
new file mode 100644
index 000000000..7b3ee2566
--- /dev/null
+++ b/ui/src/main/java/com/picpay/desafio/ui/theme/resourceprovider/StringResourceProviderImpl.kt
@@ -0,0 +1,9 @@
+package com.picpay.desafio.ui.theme.resourceprovider
+
+import android.content.Context
+
+class StringResourceProviderImpl(private val context: Context) : StringResourceProvider {
+ override fun getString(stringId: Int): String {
+ return context.getString(stringId)
+ }
+}
\ No newline at end of file