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