diff --git a/.gitignore b/.gitignore
index 2b75303ac..e1921bfc6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,13 +1,8 @@
*.iml
.gradle
/local.properties
-/.idea/caches
-/.idea/libraries
-/.idea/modules.xml
-/.idea/workspace.xml
-/.idea/navEditor.xml
-/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
+/.idea/*
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/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/app/build.gradle b/app/build.gradle
index a7fbdc0e9..1d7d1ab59 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,23 +1,21 @@
-apply plugin: 'com.android.application'
-
-apply plugin: 'kotlin-android'
-
-apply plugin: 'kotlin-android-extensions'
-
-apply plugin: 'kotlin-kapt'
+plugins {
+ id 'com.android.application'
+ id 'kotlin-android'
+ id 'kotlin-parcelize'
+ id 'kotlin-kapt'
+}
android {
- compileSdkVersion 29
+ compileSdkVersion 31
defaultConfig {
applicationId "com.picpay.desafio.android"
minSdkVersion 21
- targetSdkVersion 29
+ targetSdkVersion 31
versionCode 1
versionName "1.0"
vectorDrawables.useSupportLibrary = true
-
- testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ testInstrumentationRunner "com.picpay.desafio.android.PicPayAppTestRunner"
}
buildTypes {
debug {}
@@ -29,9 +27,13 @@ android {
}
}
+ buildFeatures {
+ viewBinding true
+ }
+
compileOptions {
- sourceCompatibility 1.8
- targetCompatibility 1.8
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
@@ -42,51 +44,54 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
- implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "androidx.core:core-ktx:$core_ktx_version"
-
implementation "androidx.appcompat:appcompat:$appcompat_version"
+ implementation "androidx.fragment:fragment-ktx:$fragment_ktx_version"
+ implementation "androidx.legacy:legacy-support-v4:$legacy_support_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"
+ // Lifecycle dependencies
+ implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
+ implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
- implementation "com.google.dagger:dagger:$dagger_version"
- kapt "com.google.dagger:dagger-compiler:$dagger_version"
+ implementation "com.google.code.gson:gson:$gson_version"
+ implementation "com.google.android.material:material:$material_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"
+ // Koin dependencies
+ implementation "io.insert-koin:koin-core:$koin_version"
+ implementation "io.insert-koin:koin-android:$koin_version"
+ implementation "io.insert-koin:koin-androidx-viewmodel:$koin_version"
+ testImplementation "io.insert-koin:koin-test:$koin_version"
+ // Coroutines dependencies
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'
-
+ // Retrofit/Network dependencies
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"
+ // Utils dependencies
implementation "com.squareup.picasso:picasso:$picasso_version"
implementation "de.hdodenhof:circleimageview:$circleimageview_version"
+ // Tests dependencies
testImplementation "junit:junit:$junit_version"
- testImplementation "org.mockito:mockito-core:$mockito_version"
- testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:$mockito_kotlin_version"
+ testImplementation "io.mockk:mockk:$mockk_version"
+
+ debugImplementation "androidx.fragment:fragment-testing:$fragment_ktx_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.ext:junit-ktx:$test_ext_junit_ktx_version"
androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version"
androidTestImplementation "androidx.test:core-ktx:$core_ktx_test_version"
+ androidTestImplementation "androidx.test:runner:$test_runner_version"
+ androidTestImplementation "androidx.test:rules:$test_rules_version"
+
}
diff --git a/app/src/androidTest/java/com/picpay/desafio/android/MainActivityTest.kt b/app/src/androidTest/java/com/picpay/desafio/android/MainActivityTest.kt
index e4a4978eb..394fc27fd 100644
--- a/app/src/androidTest/java/com/picpay/desafio/android/MainActivityTest.kt
+++ b/app/src/androidTest/java/com/picpay/desafio/android/MainActivityTest.kt
@@ -1,68 +1,31 @@
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
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 androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.ext.junit.rules.activityScenarioRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Rule
import org.junit.Test
+import org.junit.runner.RunWith
-
+@RunWith(AndroidJUnit4::class)
class MainActivityTest {
- private val server = MockWebServer()
-
- private val context = InstrumentationRegistry.getInstrumentation().targetContext
+ @get: Rule
+ val rule = activityScenarioRule()
@Test
fun shouldDisplayTitle() {
- launchActivity().apply {
- val expectedTitle = context.getString(R.string.title)
-
- moveToState(Lifecycle.State.RESUMED)
-
- onView(withText(expectedTitle)).check(matches(isDisplayed()))
- }
+ onView(withId(R.id.title)).check(matches(isDisplayed()))
+ onView(ViewMatchers.withText(R.string.title)).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()
+ fun shouldDisplayFragmentContainer() {
+ onView(withId(R.id.fcvContainer)).check(matches(isDisplayed()))
}
- 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/PicPayAppTest.kt b/app/src/androidTest/java/com/picpay/desafio/android/PicPayAppTest.kt
new file mode 100644
index 000000000..48e89886a
--- /dev/null
+++ b/app/src/androidTest/java/com/picpay/desafio/android/PicPayAppTest.kt
@@ -0,0 +1,20 @@
+package com.picpay.desafio.android
+
+import android.app.Application
+import com.picpay.desafio.android.di.TestModuleInitializer
+import org.koin.android.ext.koin.androidContext
+import org.koin.android.ext.koin.androidLogger
+import org.koin.core.context.startKoin
+
+class PicPayAppTest : Application() {
+
+ override fun onCreate() {
+ super.onCreate()
+ startKoin {
+ androidLogger()
+ androidContext(this@PicPayAppTest)
+ }
+ TestModuleInitializer.init()
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/picpay/desafio/android/PicPayAppTestRunner.kt b/app/src/androidTest/java/com/picpay/desafio/android/PicPayAppTestRunner.kt
new file mode 100644
index 000000000..4482909b1
--- /dev/null
+++ b/app/src/androidTest/java/com/picpay/desafio/android/PicPayAppTestRunner.kt
@@ -0,0 +1,14 @@
+package com.picpay.desafio.android
+
+import android.app.Application
+import android.content.Context
+import androidx.test.runner.AndroidJUnitRunner
+
+class PicPayAppTestRunner : AndroidJUnitRunner() {
+
+ override fun newApplication(
+ classLoader: ClassLoader?, name: String?, context: Context?
+ ): Application {
+ return super.newApplication(classLoader, PicPayAppTest::class.java.name, context)
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/picpay/desafio/android/data/server/MockWebServerRule.kt b/app/src/androidTest/java/com/picpay/desafio/android/data/server/MockWebServerRule.kt
new file mode 100644
index 000000000..bbe70721d
--- /dev/null
+++ b/app/src/androidTest/java/com/picpay/desafio/android/data/server/MockWebServerRule.kt
@@ -0,0 +1,19 @@
+package com.picpay.desafio.android.data.server
+
+import okhttp3.mockwebserver.MockWebServer
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+
+class MockWebServerRule : TestWatcher() {
+
+ lateinit var server: MockWebServer
+
+ override fun starting(description: Description) {
+ server = MockWebServer()
+ server.start(port = 8080)
+ }
+
+ override fun finished(description: Description) {
+ server.shutdown()
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/picpay/desafio/android/di/TestModule.kt b/app/src/androidTest/java/com/picpay/desafio/android/di/TestModule.kt
new file mode 100644
index 000000000..8d2fe80f1
--- /dev/null
+++ b/app/src/androidTest/java/com/picpay/desafio/android/di/TestModule.kt
@@ -0,0 +1,56 @@
+package com.picpay.desafio.android.di
+
+import com.google.gson.Gson
+import com.google.gson.GsonBuilder
+import com.picpay.desafio.android.data.remote.PicPayApi
+import com.picpay.desafio.android.data.remote.UserRemoteDataSource
+import com.picpay.desafio.android.data.repository.ContactDataRepository
+import com.picpay.desafio.android.domain.repository.ContactRepository
+import com.picpay.desafio.android.domain.useCases.ListContactsUseCase
+import com.picpay.desafio.android.domain.useCases.ListContactsUseCaseImpl
+import com.picpay.desafio.android.presentation.viewModels.ContactViewModel
+import okhttp3.OkHttpClient
+import org.koin.androidx.viewmodel.dsl.viewModel
+import org.koin.core.context.loadKoinModules
+import org.koin.core.scope.Scope
+import org.koin.dsl.module
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+
+fun Scope.getRetrofit() = get()
+
+object TestModuleInitializer {
+
+ private val networkModule = module {
+ fun provideBaseUrl() = "http://localhost:8080"
+ fun provideGson() = GsonBuilder().create()
+ fun provideOkHttp() = OkHttpClient.Builder().build()
+ fun provideRetrofit(baseUrl: String, okHttp: OkHttpClient, gson: Gson) = Retrofit.Builder()
+ .baseUrl(baseUrl)
+ .client(okHttp)
+ .addConverterFactory(GsonConverterFactory.create(gson))
+ .build()
+ single { provideBaseUrl() }
+ single { provideGson() }
+ single { provideOkHttp() }
+ single { provideRetrofit(get(), get(), get()) }
+ single { getRetrofit().create(PicPayApi::class.java) }
+ }
+
+ private val dataModule = module {
+ single { UserRemoteDataSource(get()) }
+ single { ContactDataRepository(get()) }
+ }
+
+ private val useCasesModule = module {
+ factory { ListContactsUseCaseImpl(get()) }
+ }
+
+ private val viewModelsModule = module {
+ viewModel { ContactViewModel(get()) }
+ }
+
+ fun init() = loadKoinModules(
+ listOf(networkModule, dataModule, useCasesModule, viewModelsModule)
+ )
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/picpay/desafio/android/extensions/AnyExtensions.kt b/app/src/androidTest/java/com/picpay/desafio/android/extensions/AnyExtensions.kt
new file mode 100644
index 000000000..790599c3f
--- /dev/null
+++ b/app/src/androidTest/java/com/picpay/desafio/android/extensions/AnyExtensions.kt
@@ -0,0 +1,12 @@
+package com.picpay.desafio.android.extensions
+
+import com.google.gson.Gson
+import java.lang.reflect.ParameterizedType
+
+internal fun T.toJson() = Gson().toJson(this)
+
+@Suppress("UNCHECKED_CAST")
+internal fun Any.toViewModelClass(): Class {
+ val type = javaClass.genericSuperclass as ParameterizedType
+ return type.actualTypeArguments[0] as Class
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/picpay/desafio/android/presentation/fragments/ContactFragmentTest.kt b/app/src/androidTest/java/com/picpay/desafio/android/presentation/fragments/ContactFragmentTest.kt
new file mode 100644
index 000000000..a74b3462c
--- /dev/null
+++ b/app/src/androidTest/java/com/picpay/desafio/android/presentation/fragments/ContactFragmentTest.kt
@@ -0,0 +1,42 @@
+package com.picpay.desafio.android.presentation.fragments
+
+import androidx.fragment.app.testing.FragmentScenario
+import androidx.fragment.app.testing.launchFragmentInContainer
+import androidx.lifecycle.Lifecycle
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.picpay.desafio.android.R
+import com.picpay.desafio.android.data.server.MockWebServerRule
+import com.picpay.desafio.android.providers.MockAndroidContactProvider
+import com.picpay.desafio.android.providers.MockResponseProvider
+import com.picpay.desafio.android.providers.RecyclerViewMatcherProvider
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ContactFragmentTest {
+
+ private lateinit var scenario: FragmentScenario
+
+ @get:Rule
+ val mockWebServerRule = MockWebServerRule()
+
+ @Before
+ fun setup() {
+ mockWebServerRule.server.enqueue(MockResponseProvider.usersMockResponse())
+ scenario = launchFragmentInContainer()
+ scenario.moveToState(Lifecycle.State.RESUMED)
+ }
+
+ @Test
+ fun shouldDisplayListItemText() {
+ RecyclerViewMatcherProvider.checkRecyclerViewItem(
+ R.id.recyclerView,
+ position = 0,
+ withText(MockAndroidContactProvider.mockedContact().name)
+ )
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/picpay/desafio/android/providers/MockAndroidContactProvider.kt b/app/src/androidTest/java/com/picpay/desafio/android/providers/MockAndroidContactProvider.kt
new file mode 100644
index 000000000..bbcf1ab61
--- /dev/null
+++ b/app/src/androidTest/java/com/picpay/desafio/android/providers/MockAndroidContactProvider.kt
@@ -0,0 +1,30 @@
+package com.picpay.desafio.android.providers
+
+import com.picpay.desafio.android.domain.model.ContactModel
+import kotlinx.coroutines.flow.flowOf
+
+object MockAndroidContactProvider {
+
+ fun mockedContact() = internalMockedContact()
+ private fun internalMockedContact() = ContactModel(
+ image = "https://randomuser.me/api/portraits/men/1.jpg",
+ name = "Eduardo Santos",
+ username = "@eduardo.santos"
+ )
+
+ fun mockedFlowContacts() = flowOf(mockedContacts())
+ fun mockedContacts() = internalMockedContacts()
+ private fun internalMockedContacts() = listOf(
+ internalMockedContact(),
+ ContactModel(
+ image = "https://randomuser.me/api/portraits/women/2.jpg",
+ name = "Marina Coelho",
+ username = "@marina.coelho"
+ ),
+ ContactModel(
+ image = "https://randomuser.me/api/portraits/women/3.jpg",
+ name = "Márcia Silva",
+ username = "@marcia.silva"
+ )
+ )
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/picpay/desafio/android/providers/MockAndroidUserProvider.kt b/app/src/androidTest/java/com/picpay/desafio/android/providers/MockAndroidUserProvider.kt
new file mode 100644
index 000000000..904acf9d1
--- /dev/null
+++ b/app/src/androidTest/java/com/picpay/desafio/android/providers/MockAndroidUserProvider.kt
@@ -0,0 +1,31 @@
+package com.picpay.desafio.android.providers
+
+import com.picpay.desafio.android.data.entities.UserEntity
+
+object MockAndroidUserProvider {
+
+ fun mockedUser() = internalMockedUser()
+ private fun internalMockedUser() = UserEntity(
+ id = 1,
+ img = "https://randomuser.me/api/portraits/men/1.jpg",
+ name = "Eduardo Santos",
+ username = "@eduardo.santos"
+ )
+
+ fun mockedUsers() = internalMockedUsers()
+ private fun internalMockedUsers() = listOf(
+ internalMockedUser(),
+ UserEntity(
+ id = 2,
+ img = "https://randomuser.me/api/portraits/women/2.jpg",
+ name = "Marina Coelho",
+ username = "@marina.coelho"
+ ),
+ UserEntity(
+ id = 3,
+ img = "https://randomuser.me/api/portraits/women/3.jpg",
+ name = "Márcia Silva",
+ username = "@marcia.silva"
+ )
+ )
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/picpay/desafio/android/providers/MockResponseProvider.kt b/app/src/androidTest/java/com/picpay/desafio/android/providers/MockResponseProvider.kt
new file mode 100644
index 000000000..816c6389b
--- /dev/null
+++ b/app/src/androidTest/java/com/picpay/desafio/android/providers/MockResponseProvider.kt
@@ -0,0 +1,15 @@
+package com.picpay.desafio.android.providers
+
+import com.picpay.desafio.android.extensions.toJson
+import okhttp3.mockwebserver.MockResponse
+
+object MockResponseProvider {
+
+ private const val SUCCESS_RESPONSE_CODE = 200
+ private const val ERROR_RESPONSE_CODE = 404
+
+ fun usersMockResponse() = MockResponse().setResponseCode(SUCCESS_RESPONSE_CODE)
+ .setBody(MockAndroidUserProvider.mockedUsers().toJson())
+
+ fun errorMockResponse() = MockResponse().setResponseCode(ERROR_RESPONSE_CODE)
+}
\ 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/providers/RecyclerViewMatcherProvider.kt
similarity index 75%
rename from app/src/androidTest/java/com/picpay/desafio/android/RecyclerViewMatchers.kt
rename to app/src/androidTest/java/com/picpay/desafio/android/providers/RecyclerViewMatcherProvider.kt
index 62be92ebd..a4c9ccb9e 100644
--- a/app/src/androidTest/java/com/picpay/desafio/android/RecyclerViewMatchers.kt
+++ b/app/src/androidTest/java/com/picpay/desafio/android/providers/RecyclerViewMatcherProvider.kt
@@ -1,4 +1,4 @@
-package com.picpay.desafio.android
+package com.picpay.desafio.android.providers
import android.view.View
import androidx.recyclerview.widget.RecyclerView
@@ -9,11 +9,10 @@ import androidx.test.espresso.matcher.ViewMatchers
import org.hamcrest.Description
import org.hamcrest.Matcher
-object RecyclerViewMatchers {
+object RecyclerViewMatcherProvider {
- fun atPosition(
- position: Int,
- itemMatcher: Matcher
+ private fun atPosition(
+ position: Int, itemMatcher: Matcher
) = object : BoundedMatcher(RecyclerView::class.java) {
override fun describeTo(description: Description?) {
description?.appendText("has item at position $position: ")
@@ -28,12 +27,7 @@ object RecyclerViewMatchers {
fun checkRecyclerViewItem(resId: Int, position: Int, withMatcher: Matcher) {
Espresso.onView(ViewMatchers.withId(resId)).check(
- ViewAssertions.matches(
- atPosition(
- position,
- ViewMatchers.hasDescendant(withMatcher)
- )
- )
+ 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..d1433da20 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -6,6 +6,7 @@
-
+
diff --git a/app/src/main/java/com/picpay/desafio/android/MainActivity.kt b/app/src/main/java/com/picpay/desafio/android/MainActivity.kt
index 2447de98d..76e9e2b30 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,9 @@
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
+import com.picpay.desafio.android.bases.BaseActivity
+import com.picpay.desafio.android.databinding.ActivityMainBinding
+import com.picpay.desafio.android.extensions.viewBinding
-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()!!
- }
- })
- }
+class MainActivity : BaseActivity() {
+ override val binding by viewBinding(ActivityMainBinding::inflate)
}
diff --git a/app/src/main/java/com/picpay/desafio/android/PicPayApp.kt b/app/src/main/java/com/picpay/desafio/android/PicPayApp.kt
new file mode 100644
index 000000000..87c487c12
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/PicPayApp.kt
@@ -0,0 +1,20 @@
+package com.picpay.desafio.android
+
+import android.app.Application
+import com.picpay.desafio.android.di.MainModuleInitializer
+import org.koin.android.ext.koin.androidContext
+import org.koin.android.ext.koin.androidLogger
+import org.koin.core.context.startKoin
+
+class PicPayApp : Application() {
+
+ override fun onCreate() {
+ super.onCreate()
+ startKoin {
+ androidLogger()
+ androidContext(this@PicPayApp)
+ }
+ MainModuleInitializer.init()
+ }
+
+}
\ 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/java/com/picpay/desafio/android/bases/BaseActivity.kt b/app/src/main/java/com/picpay/desafio/android/bases/BaseActivity.kt
new file mode 100644
index 000000000..ada62ff63
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/bases/BaseActivity.kt
@@ -0,0 +1,18 @@
+package com.picpay.desafio.android.bases
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import androidx.viewbinding.ViewBinding
+import com.picpay.desafio.android.extensions.getViewModelClass
+import org.koin.androidx.viewmodel.ext.android.getViewModel
+
+abstract class BaseActivity : AppCompatActivity() {
+
+ abstract val binding: ViewBinding
+ val viewModel: V by lazy { getViewModel(clazz = getViewModelClass()) }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(binding.root)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/bases/BaseFragment.kt b/app/src/main/java/com/picpay/desafio/android/bases/BaseFragment.kt
new file mode 100644
index 000000000..832c6a5a8
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/bases/BaseFragment.kt
@@ -0,0 +1,31 @@
+package com.picpay.desafio.android.bases
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.viewbinding.ViewBinding
+import com.picpay.desafio.android.extensions.getViewModelClass
+import org.koin.androidx.viewmodel.ext.android.getViewModel
+
+abstract class BaseFragment : Fragment() {
+
+ abstract val binding: ViewBinding
+ abstract fun initComponents()
+ abstract fun initObservers()
+ val viewModel: V by lazy { getViewModel(clazz = getViewModelClass()) }
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
+ ): View? {
+ super.onCreateView(inflater, container, savedInstanceState)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ initComponents()
+ initObservers()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/bases/BaseViewModel.kt b/app/src/main/java/com/picpay/desafio/android/bases/BaseViewModel.kt
new file mode 100644
index 000000000..423306f4a
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/bases/BaseViewModel.kt
@@ -0,0 +1,43 @@
+package com.picpay.desafio.android.bases
+
+import androidx.annotation.StringRes
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import com.picpay.desafio.android.extensions.EMPTY
+import com.picpay.desafio.android.extensions.FALSE
+import com.picpay.desafio.android.extensions.NULL
+
+abstract class BaseViewModel : ViewModel() {
+
+ private val _isLoading: MutableLiveData = MutableLiveData(Boolean.FALSE)
+ val isLoading: LiveData get() = _isLoading
+
+ private val _message: MutableLiveData = MutableLiveData(String.EMPTY)
+ val message: LiveData get() = _message
+
+ private val _messageResource: MutableLiveData = MutableLiveData(Int.NULL)
+ val messageResource: LiveData get() = _messageResource
+
+ private fun setLoading(isLoading: Boolean = true) {
+ _isLoading.postValue(isLoading)
+ }
+
+ fun startLoading() {
+ setLoading(isLoading = true)
+ }
+
+ fun stopLoading() {
+ setLoading(isLoading = false)
+ }
+
+ fun setMessage(message: String) {
+ _message.postValue(message)
+ stopLoading()
+ }
+
+ fun setMessageResource(@StringRes messageResource: Int) {
+ _messageResource.postValue(messageResource)
+ stopLoading()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/User.kt b/app/src/main/java/com/picpay/desafio/android/data/entities/UserEntity.kt
similarity index 72%
rename from app/src/main/java/com/picpay/desafio/android/User.kt
rename to app/src/main/java/com/picpay/desafio/android/data/entities/UserEntity.kt
index aa28171c9..59954651b 100644
--- a/app/src/main/java/com/picpay/desafio/android/User.kt
+++ b/app/src/main/java/com/picpay/desafio/android/data/entities/UserEntity.kt
@@ -1,13 +1,13 @@
-package com.picpay.desafio.android
+package com.picpay.desafio.android.data.entities
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
-import kotlinx.android.parcel.Parcelize
+import kotlinx.parcelize.Parcelize
@Parcelize
-data class User(
+data class UserEntity(
+ @SerializedName("id") val id: Int,
@SerializedName("img") val img: String,
@SerializedName("name") val name: String,
- @SerializedName("id") val id: Int,
@SerializedName("username") val username: String
) : Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/data/mapper/UserMapper.kt b/app/src/main/java/com/picpay/desafio/android/data/mapper/UserMapper.kt
new file mode 100644
index 000000000..3faa71126
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/data/mapper/UserMapper.kt
@@ -0,0 +1,8 @@
+package com.picpay.desafio.android.data.mapper
+
+import com.picpay.desafio.android.data.entities.UserEntity
+import com.picpay.desafio.android.domain.model.ContactModel
+
+fun UserEntity.toContactModel() = ContactModel(image = img, name = name, username = username)
+
+fun List.toListContactModel() = map { it.toContactModel() }
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/data/remote/PicPayApi.kt b/app/src/main/java/com/picpay/desafio/android/data/remote/PicPayApi.kt
new file mode 100644
index 000000000..61a20e9d3
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/data/remote/PicPayApi.kt
@@ -0,0 +1,10 @@
+package com.picpay.desafio.android.data.remote
+
+import com.picpay.desafio.android.data.entities.UserEntity
+import retrofit2.http.GET
+
+interface PicPayApi {
+
+ @GET("users")
+ suspend fun getUsers(): List
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/data/remote/UserRemoteDataSource.kt b/app/src/main/java/com/picpay/desafio/android/data/remote/UserRemoteDataSource.kt
new file mode 100644
index 000000000..fa1a2cbde
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/data/remote/UserRemoteDataSource.kt
@@ -0,0 +1,15 @@
+package com.picpay.desafio.android.data.remote
+
+import com.picpay.desafio.android.data.entities.UserEntity
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.FlowCollector
+import kotlinx.coroutines.flow.flow
+
+class UserRemoteDataSource(private val api: PicPayApi) {
+
+ private fun call(block: suspend FlowCollector.() -> T): Flow = flow {
+ emit(block())
+ }
+
+ fun getUsers(): Flow> = call { api.getUsers() }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/data/repository/ContactDataRepository.kt b/app/src/main/java/com/picpay/desafio/android/data/repository/ContactDataRepository.kt
new file mode 100644
index 000000000..673ad033d
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/data/repository/ContactDataRepository.kt
@@ -0,0 +1,13 @@
+package com.picpay.desafio.android.data.repository
+
+import com.picpay.desafio.android.data.mapper.toListContactModel
+import com.picpay.desafio.android.data.remote.UserRemoteDataSource
+import com.picpay.desafio.android.domain.repository.ContactRepository
+import kotlinx.coroutines.flow.map
+
+class ContactDataRepository(
+ private val remoteDataSource: UserRemoteDataSource
+) : ContactRepository {
+
+ override fun getContacts() = remoteDataSource.getUsers().map { it.toListContactModel() }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/di/MainModule.kt b/app/src/main/java/com/picpay/desafio/android/di/MainModule.kt
new file mode 100644
index 000000000..a21fb15a4
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/di/MainModule.kt
@@ -0,0 +1,104 @@
+package com.picpay.desafio.android.di
+
+import android.content.Context
+import com.google.gson.Gson
+import com.google.gson.GsonBuilder
+import com.picpay.desafio.android.data.remote.PicPayApi
+import com.picpay.desafio.android.data.remote.UserRemoteDataSource
+import com.picpay.desafio.android.data.repository.ContactDataRepository
+import com.picpay.desafio.android.domain.repository.ContactRepository
+import com.picpay.desafio.android.domain.useCases.ListContactsUseCase
+import com.picpay.desafio.android.domain.useCases.ListContactsUseCaseImpl
+import com.picpay.desafio.android.presentation.viewModels.ContactViewModel
+import okhttp3.*
+import org.koin.androidx.viewmodel.dsl.viewModel
+import org.koin.core.context.loadKoinModules
+import org.koin.core.scope.Scope
+import org.koin.dsl.module
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+import java.util.concurrent.TimeUnit
+
+fun Scope.getRetrofit() = get()
+
+object MainModuleInitializer {
+
+ private var CACHE_MAX_AGE_VALUE = 5
+ private var CACHE_MAX_STALE_VALUE = 1
+ private var HEADER_PRAGMA = "Pragma"
+ private var HEADER_CACHE_CONTROL = "Cache-Control"
+ private const val CACHE_SIZE = (5 * 1024 * 1024).toLong()
+
+ private val networkModule = module {
+
+ fun provideBaseUrl() = "https://609a908e0f5a13001721b74e.mockapi.io/picpay/api/"
+
+ fun provideGson() = GsonBuilder().create()
+
+ fun provideCache(context: Context) = Cache(context.cacheDir, CACHE_SIZE)
+
+ fun provideCacheInterceptor() = Interceptor { chain ->
+ val request: Request = chain.request()
+ val cacheControl = CacheControl.Builder()
+ .maxAge(CACHE_MAX_AGE_VALUE, TimeUnit.MINUTES)
+ .build()
+ request.newBuilder()
+ .removeHeader(HEADER_PRAGMA)
+ .header(HEADER_CACHE_CONTROL, cacheControl.toString())
+ .build()
+ chain.proceed(request)
+ }
+
+ fun provideOfflineCacheInterceptor() = Interceptor { chain ->
+ try {
+ chain.proceed(chain.request())
+ } catch (e: Exception) {
+ val cacheControl = CacheControl.Builder()
+ .onlyIfCached()
+ .maxStale(CACHE_MAX_STALE_VALUE, TimeUnit.DAYS)
+ .build()
+ val offlineRequest: Request = chain.request().newBuilder()
+ .cacheControl(cacheControl)
+ .removeHeader(HEADER_PRAGMA)
+ .build()
+ chain.proceed(offlineRequest)
+ }
+ }
+
+ fun provideOkHttp(cache: Cache) = OkHttpClient.Builder()
+ .addNetworkInterceptor(provideCacheInterceptor())
+ .addInterceptor(provideOfflineCacheInterceptor())
+ .cache(cache)
+ .build()
+
+ fun provideRetrofit(baseUrl: String, okHttp: OkHttpClient, gson: Gson) = Retrofit.Builder()
+ .baseUrl(baseUrl)
+ .client(okHttp)
+ .addConverterFactory(GsonConverterFactory.create(gson))
+ .build()
+
+ single { provideBaseUrl() }
+ single { provideGson() }
+ single { provideCache(get()) }
+ single { provideOkHttp(get()) }
+ single { provideRetrofit(get(), get(), get()) }
+ single { getRetrofit().create(PicPayApi::class.java) }
+ }
+
+ private val dataModule = module {
+ single { UserRemoteDataSource(get()) }
+ single { ContactDataRepository(get()) }
+ }
+
+ private val useCasesModule = module {
+ factory { ListContactsUseCaseImpl(get()) }
+ }
+
+ private val viewModelsModule = module {
+ viewModel { ContactViewModel(get()) }
+ }
+
+ fun init() = loadKoinModules(
+ listOf(networkModule, dataModule, useCasesModule, viewModelsModule)
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/domain/model/ContactModel.kt b/app/src/main/java/com/picpay/desafio/android/domain/model/ContactModel.kt
new file mode 100644
index 000000000..915f25555
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/domain/model/ContactModel.kt
@@ -0,0 +1,21 @@
+package com.picpay.desafio.android.domain.model
+
+import androidx.recyclerview.widget.DiffUtil
+
+data class ContactModel(val image: String, val name: String, val username: String) {
+
+ companion object {
+
+ val DIFF_UTIL_CALLBACK = object : DiffUtil.ItemCallback() {
+
+ override fun areItemsTheSame(oldItem: ContactModel, newItem: ContactModel): Boolean {
+ return oldItem.username == newItem.username
+ }
+
+ override fun areContentsTheSame(oldItem: ContactModel, newItem: ContactModel): Boolean {
+ return oldItem == newItem
+ }
+
+ }
+ }
+}
diff --git a/app/src/main/java/com/picpay/desafio/android/domain/repository/ContactRepository.kt b/app/src/main/java/com/picpay/desafio/android/domain/repository/ContactRepository.kt
new file mode 100644
index 000000000..09b1961fb
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/domain/repository/ContactRepository.kt
@@ -0,0 +1,8 @@
+package com.picpay.desafio.android.domain.repository
+
+import com.picpay.desafio.android.domain.model.ContactModel
+import kotlinx.coroutines.flow.Flow
+
+interface ContactRepository {
+ fun getContacts(): Flow>
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/domain/useCases/ListContactsUseCase.kt b/app/src/main/java/com/picpay/desafio/android/domain/useCases/ListContactsUseCase.kt
new file mode 100644
index 000000000..071cf1a39
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/domain/useCases/ListContactsUseCase.kt
@@ -0,0 +1,8 @@
+package com.picpay.desafio.android.domain.useCases
+
+import com.picpay.desafio.android.domain.model.ContactModel
+import kotlinx.coroutines.flow.Flow
+
+interface ListContactsUseCase {
+ operator fun invoke(): Flow>
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/domain/useCases/ListContactsUseCaseImpl.kt b/app/src/main/java/com/picpay/desafio/android/domain/useCases/ListContactsUseCaseImpl.kt
new file mode 100644
index 000000000..4cab44a2b
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/domain/useCases/ListContactsUseCaseImpl.kt
@@ -0,0 +1,9 @@
+package com.picpay.desafio.android.domain.useCases
+
+import com.picpay.desafio.android.domain.model.ContactModel
+import com.picpay.desafio.android.domain.repository.ContactRepository
+import kotlinx.coroutines.flow.Flow
+
+class ListContactsUseCaseImpl(private val contactRepository: ContactRepository) : ListContactsUseCase {
+ override fun invoke(): Flow> = contactRepository.getContacts()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/extensions/ActivityExtensions.kt b/app/src/main/java/com/picpay/desafio/android/extensions/ActivityExtensions.kt
new file mode 100644
index 000000000..327192c17
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/extensions/ActivityExtensions.kt
@@ -0,0 +1,11 @@
+package com.picpay.desafio.android.extensions
+
+import android.view.LayoutInflater
+import androidx.appcompat.app.AppCompatActivity
+import androidx.viewbinding.ViewBinding
+
+inline fun AppCompatActivity.viewBinding(
+ crossinline bindingInflater: (LayoutInflater) -> T
+) = lazy(LazyThreadSafetyMode.NONE) { bindingInflater.invoke(layoutInflater) }
+
+internal fun AppCompatActivity.getViewModelClass() = toViewModelClass().kotlin
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/extensions/AnyExtensions.kt b/app/src/main/java/com/picpay/desafio/android/extensions/AnyExtensions.kt
new file mode 100644
index 000000000..acc2306f0
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/extensions/AnyExtensions.kt
@@ -0,0 +1,9 @@
+package com.picpay.desafio.android.extensions
+
+import java.lang.reflect.ParameterizedType
+
+@Suppress("UNCHECKED_CAST")
+internal fun Any.toViewModelClass(): Class {
+ val type = javaClass.genericSuperclass as ParameterizedType
+ return type.actualTypeArguments[0] as Class
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/extensions/BooleanExtensions.kt b/app/src/main/java/com/picpay/desafio/android/extensions/BooleanExtensions.kt
new file mode 100644
index 000000000..21dea3123
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/extensions/BooleanExtensions.kt
@@ -0,0 +1,5 @@
+package com.picpay.desafio.android.extensions
+
+val Boolean.Companion.FALSE: Boolean get() = false
+
+fun Boolean?.orFalse() = this ?: Boolean.FALSE
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/extensions/ContextExtensions.kt b/app/src/main/java/com/picpay/desafio/android/extensions/ContextExtensions.kt
new file mode 100644
index 000000000..c3ed932d1
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/extensions/ContextExtensions.kt
@@ -0,0 +1,24 @@
+package com.picpay.desafio.android.extensions
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.widget.Toast
+import androidx.annotation.StringRes
+
+fun Context.toLayoutInflater(): LayoutInflater = LayoutInflater.from(this)
+
+fun Context.showToastLongText(text: String?) {
+ Toast.makeText(this, text, Toast.LENGTH_LONG).show()
+}
+
+fun Context.showToastLongText(@StringRes resId: Int) {
+ Toast.makeText(this, resId, Toast.LENGTH_LONG).show()
+}
+
+fun Context.showToastShortText(text: String?) {
+ Toast.makeText(this, text, Toast.LENGTH_SHORT).show()
+}
+
+fun Context.showToastShortText(@StringRes resId: Int) {
+ Toast.makeText(this, resId, Toast.LENGTH_SHORT).show()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/extensions/FragmentExtensions.kt b/app/src/main/java/com/picpay/desafio/android/extensions/FragmentExtensions.kt
new file mode 100644
index 000000000..17de78aaf
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/extensions/FragmentExtensions.kt
@@ -0,0 +1,11 @@
+package com.picpay.desafio.android.extensions
+
+import android.view.LayoutInflater
+import androidx.fragment.app.Fragment
+import androidx.viewbinding.ViewBinding
+
+inline fun Fragment.viewBinding(
+ crossinline bindingInflater: (LayoutInflater) -> T
+) = lazy(LazyThreadSafetyMode.NONE) { bindingInflater.invoke(layoutInflater) }
+
+internal fun Fragment.getViewModelClass() = toViewModelClass().kotlin
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/extensions/IntExtensions.kt b/app/src/main/java/com/picpay/desafio/android/extensions/IntExtensions.kt
new file mode 100644
index 000000000..d0ea9eec1
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/extensions/IntExtensions.kt
@@ -0,0 +1,7 @@
+package com.picpay.desafio.android.extensions
+
+val Int.Companion.ZERO: Int get() = 0
+
+val Int.Companion.NULL: Int? get() = null
+
+fun Int?.orZero() = this ?: Int.ZERO
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/extensions/StringsExtensions.kt b/app/src/main/java/com/picpay/desafio/android/extensions/StringsExtensions.kt
new file mode 100644
index 000000000..2f197d392
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/extensions/StringsExtensions.kt
@@ -0,0 +1,5 @@
+package com.picpay.desafio.android.extensions
+
+val String.Companion.EMPTY: String get() = ""
+
+fun String?.takeIfNotBlank() = takeIf { it?.isNotBlank().orFalse() }
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/extensions/SwipeRefreshLayoutExtensions.kt b/app/src/main/java/com/picpay/desafio/android/extensions/SwipeRefreshLayoutExtensions.kt
new file mode 100644
index 000000000..e89e4ce1e
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/extensions/SwipeRefreshLayoutExtensions.kt
@@ -0,0 +1,18 @@
+package com.picpay.desafio.android.extensions
+
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+import com.picpay.desafio.android.R
+
+fun SwipeRefreshLayout.updateRefreshing(isRefreshing: Boolean) {
+ if (this.isRefreshing != isRefreshing)
+ this.isRefreshing = isRefreshing
+}
+
+fun SwipeRefreshLayout.stopRefreshing() {
+ this.isRefreshing = false
+}
+
+fun SwipeRefreshLayout.setTheme() {
+ setColorSchemeResources(R.color.colorAccent)
+ setProgressBackgroundColorSchemeResource(R.color.colorPrimaryDark)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/presentation/adapters/ContactListAdapter.kt b/app/src/main/java/com/picpay/desafio/android/presentation/adapters/ContactListAdapter.kt
new file mode 100644
index 000000000..c9d2c5c9c
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/presentation/adapters/ContactListAdapter.kt
@@ -0,0 +1,17 @@
+package com.picpay.desafio.android.presentation.adapters
+
+import android.view.ViewGroup
+import androidx.recyclerview.widget.ListAdapter
+import com.picpay.desafio.android.domain.model.ContactModel
+import com.picpay.desafio.android.presentation.viewHolders.ContactListItemViewHolder
+
+class ContactListAdapter : ListAdapter(ContactModel.DIFF_UTIL_CALLBACK) {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ContactListItemViewHolder {
+ return ContactListItemViewHolder.newInstance(parent)
+ }
+
+ override fun onBindViewHolder(holder: ContactListItemViewHolder, position: Int) {
+ holder.bind(currentList[position])
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/presentation/fragments/ContactFragment.kt b/app/src/main/java/com/picpay/desafio/android/presentation/fragments/ContactFragment.kt
new file mode 100644
index 000000000..47f33714d
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/presentation/fragments/ContactFragment.kt
@@ -0,0 +1,42 @@
+package com.picpay.desafio.android.presentation.fragments
+
+import android.view.View
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.picpay.desafio.android.bases.BaseFragment
+import com.picpay.desafio.android.databinding.FragContactBinding
+import com.picpay.desafio.android.extensions.*
+import com.picpay.desafio.android.presentation.adapters.ContactListAdapter
+import com.picpay.desafio.android.presentation.viewModels.ContactViewModel
+
+class ContactFragment : BaseFragment() {
+
+ override val binding by viewBinding(FragContactBinding::inflate)
+ private val listAdapter: ContactListAdapter by lazy { ContactListAdapter() }
+
+ override fun initComponents() {
+ with(binding) {
+ srlContent.setTheme()
+ srlContent.setOnRefreshListener(viewModel::loadContacts)
+ recyclerView.apply {
+ adapter = listAdapter
+ layoutManager = LinearLayoutManager(context)
+ }
+ }
+ }
+
+ override fun initObservers() {
+ with(viewModel) {
+ isLoading.observe(viewLifecycleOwner) { binding.srlContent.updateRefreshing(it.orFalse()) }
+ messageResource.observe(viewLifecycleOwner) { resource ->
+ binding.recyclerView.visibility = View.GONE
+ resource?.let { this@ContactFragment.context?.showToastShortText(getString(it)) }
+ }
+ contacts.observe(viewLifecycleOwner) {
+ binding.recyclerView.visibility = View.VISIBLE
+ listAdapter.submitList(it)
+ }
+ loadContacts()
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/presentation/viewHolders/ContactListItemViewHolder.kt b/app/src/main/java/com/picpay/desafio/android/presentation/viewHolders/ContactListItemViewHolder.kt
new file mode 100644
index 000000000..b210f2455
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/presentation/viewHolders/ContactListItemViewHolder.kt
@@ -0,0 +1,43 @@
+package com.picpay.desafio.android.presentation.viewHolders
+
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import com.picpay.desafio.android.R
+import com.picpay.desafio.android.databinding.ListItemUserBinding
+import com.picpay.desafio.android.domain.model.ContactModel
+import com.picpay.desafio.android.extensions.toLayoutInflater
+import com.squareup.picasso.Callback
+import com.squareup.picasso.Picasso
+
+class ContactListItemViewHolder(
+ private val binding: ListItemUserBinding
+) : RecyclerView.ViewHolder(binding.root) {
+
+ fun bind(contactModel: ContactModel) {
+ with(binding) {
+ name.text = contactModel.name
+ username.text = contactModel.username
+ progressBar.visibility = View.VISIBLE
+ Picasso.get()
+ .load(contactModel.image)
+ .error(R.drawable.ic_round_account_circle)
+ .into(picture, object : Callback {
+ override fun onSuccess() {
+ progressBar.visibility = View.GONE
+ }
+
+ override fun onError(e: Exception?) {
+ progressBar.visibility = View.GONE
+ }
+ })
+ }
+ }
+
+ companion object {
+
+ fun newInstance(parent: ViewGroup) = ContactListItemViewHolder(
+ ListItemUserBinding.inflate(parent.context.toLayoutInflater(), parent, false)
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/picpay/desafio/android/presentation/viewModels/ContactViewModel.kt b/app/src/main/java/com/picpay/desafio/android/presentation/viewModels/ContactViewModel.kt
new file mode 100644
index 000000000..2c1cea179
--- /dev/null
+++ b/app/src/main/java/com/picpay/desafio/android/presentation/viewModels/ContactViewModel.kt
@@ -0,0 +1,30 @@
+package com.picpay.desafio.android.presentation.viewModels
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.viewModelScope
+import com.picpay.desafio.android.R
+import com.picpay.desafio.android.bases.BaseViewModel
+import com.picpay.desafio.android.domain.model.ContactModel
+import com.picpay.desafio.android.domain.useCases.ListContactsUseCase
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.onCompletion
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.launch
+
+class ContactViewModel(private val listContactsUseCase: ListContactsUseCase) : BaseViewModel() {
+
+ private val _contacts: MutableLiveData> = MutableLiveData()
+ val contacts: LiveData> get() = _contacts
+
+ fun loadContacts() {
+ viewModelScope.launch {
+ listContactsUseCase()
+ .onStart { startLoading() }
+ .catch { setMessageResource(R.string.error) }
+ .onCompletion { stopLoading() }
+ .collect { _contacts.postValue(it) }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 487ac549e..e0cc2748f 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -1,5 +1,5 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+ android:layout_marginStart="@dimen/medium_margin"
+ android:layout_marginTop="@dimen/big_margin"
+ android:text="@string/title"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/frag_contact.xml b/app/src/main/res/layout/frag_contact.xml
new file mode 100644
index 000000000..14c01bf11
--- /dev/null
+++ b/app/src/main/res/layout/frag_contact.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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
index 587d40cc8..4a105f091 100644
--- a/app/src/main/res/layout/list_item_user.xml
+++ b/app/src/main/res/layout/list_item_user.xml
@@ -9,12 +9,10 @@
+ tools:text="User name" />
+
+
+ 0dp
+ 52dp
+
+
+ 48dp
+ 24dp
+ 8dp
+ 12dp
+ 16dp
+
\ 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/app/src/test/java/com/picpay/desafio/android/base/BaseTest.kt b/app/src/test/java/com/picpay/desafio/android/base/BaseTest.kt
new file mode 100644
index 000000000..6ce2e1ecf
--- /dev/null
+++ b/app/src/test/java/com/picpay/desafio/android/base/BaseTest.kt
@@ -0,0 +1,25 @@
+package com.picpay.desafio.android.base
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import io.mockk.MockKAnnotations
+import junit.framework.TestCase
+import org.junit.Before
+import org.junit.Rule
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+abstract class BaseTest : TestCase() {
+
+ @get: Rule
+ val coroutineRule = CoroutineTestRule()
+
+ @get: Rule
+ val rule = InstantTaskExecutorRule()
+
+ @Before
+ open fun setup() {
+ MockKAnnotations.init(this)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/picpay/desafio/android/base/CoroutineTestRule.kt b/app/src/test/java/com/picpay/desafio/android/base/CoroutineTestRule.kt
new file mode 100644
index 000000000..fa618693f
--- /dev/null
+++ b/app/src/test/java/com/picpay/desafio/android/base/CoroutineTestRule.kt
@@ -0,0 +1,26 @@
+package com.picpay.desafio.android.base
+
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestCoroutineDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.setMain
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+
+@ExperimentalCoroutinesApi
+class CoroutineTestRule : TestWatcher() {
+
+ private val dispatcher = TestCoroutineDispatcher()
+
+ override fun starting(description: Description?) {
+ super.starting(description)
+ Dispatchers.setMain(dispatcher)
+ }
+
+ override fun finished(description: Description?) {
+ super.finished(description)
+ dispatcher.cleanupTestCoroutines()
+ Dispatchers.resetMain()
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/picpay/desafio/android/data/mapper/UserMapperTest.kt b/app/src/test/java/com/picpay/desafio/android/data/mapper/UserMapperTest.kt
new file mode 100644
index 000000000..733c68e78
--- /dev/null
+++ b/app/src/test/java/com/picpay/desafio/android/data/mapper/UserMapperTest.kt
@@ -0,0 +1,21 @@
+package com.picpay.desafio.android.data.mapper
+
+import com.picpay.desafio.android.base.BaseTest
+import com.picpay.desafio.android.providers.MockContactProvider
+import com.picpay.desafio.android.providers.MockUserProvider
+import org.junit.Test
+
+class UserMapperTest : BaseTest() {
+
+ @Test
+ fun shouldReturnContact() {
+ val contact = MockUserProvider.mockedUser().toContactModel()
+ assertEquals(MockContactProvider.mockedContact(), contact)
+ }
+
+ @Test
+ fun shouldReturnContacts() {
+ val contacts = MockUserProvider.mockedUsers().toListContactModel()
+ assertEquals(MockContactProvider.mockedContacts(), contacts)
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/picpay/desafio/android/domain/useCases/ListContactsUseCaseTest.kt b/app/src/test/java/com/picpay/desafio/android/domain/useCases/ListContactsUseCaseTest.kt
new file mode 100644
index 000000000..f53550a38
--- /dev/null
+++ b/app/src/test/java/com/picpay/desafio/android/domain/useCases/ListContactsUseCaseTest.kt
@@ -0,0 +1,37 @@
+package com.picpay.desafio.android.domain.useCases
+
+import com.picpay.desafio.android.base.BaseTest
+import com.picpay.desafio.android.domain.repository.ContactRepository
+import com.picpay.desafio.android.providers.MockContactProvider
+import com.picpay.desafio.android.providers.MockErrorProvider
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.mockk
+import org.junit.Test
+
+class ListContactsUseCaseTest : BaseTest() {
+
+ private val repository = mockk(relaxed = true)
+
+ private lateinit var listContactsUseCase: ListContactsUseCase
+
+ override fun setup() {
+ super.setup()
+ listContactsUseCase = ListContactsUseCaseImpl(repository)
+ }
+
+ @Test
+ fun shouldListContacts() {
+ coEvery { repository.getContacts() } returns MockContactProvider.mockedFlowContacts()
+ listContactsUseCase()
+ coVerify { repository.getContacts() }
+ }
+
+ @Test
+ fun shouldNotListContacts() {
+ coEvery { repository.getContacts() } returns MockErrorProvider.mockErrorFlow()
+ listContactsUseCase()
+ coVerify { repository.getContacts() }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/picpay/desafio/android/presentation/viewModels/ContactViewModelTest.kt b/app/src/test/java/com/picpay/desafio/android/presentation/viewModels/ContactViewModelTest.kt
new file mode 100644
index 000000000..ff1811a8a
--- /dev/null
+++ b/app/src/test/java/com/picpay/desafio/android/presentation/viewModels/ContactViewModelTest.kt
@@ -0,0 +1,42 @@
+package com.picpay.desafio.android.presentation.viewModels
+
+import com.picpay.desafio.android.base.BaseTest
+import com.picpay.desafio.android.domain.useCases.ListContactsUseCase
+import com.picpay.desafio.android.extensions.orFalse
+import com.picpay.desafio.android.providers.MockContactProvider
+import com.picpay.desafio.android.providers.MockErrorProvider
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.mockk
+import org.junit.Test
+
+class ContactViewModelTest : BaseTest() {
+
+ private lateinit var viewModel: ContactViewModel
+ private val listContactsUseCase = mockk(relaxed = true)
+
+ override fun setup() {
+ super.setup()
+ viewModel = ContactViewModel(listContactsUseCase)
+ }
+
+ @Test
+ fun shouldLoadingContacts() {
+ viewModel.run {
+ coEvery { listContactsUseCase() } returns MockContactProvider.mockedFlowContacts()
+ loadContacts()
+ coVerify { listContactsUseCase() }
+ assertTrue(contacts.value?.isNotEmpty().orFalse())
+ }
+ }
+
+ @Test
+ fun shouldNotLoadContacts() {
+ viewModel.run {
+ coEvery { listContactsUseCase() } returns MockErrorProvider.mockErrorFlow()
+ loadContacts()
+ coVerify { listContactsUseCase() }
+ assertNotNull(messageResource.value)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/picpay/desafio/android/providers/MockContactProvider.kt b/app/src/test/java/com/picpay/desafio/android/providers/MockContactProvider.kt
new file mode 100644
index 000000000..3cac05874
--- /dev/null
+++ b/app/src/test/java/com/picpay/desafio/android/providers/MockContactProvider.kt
@@ -0,0 +1,30 @@
+package com.picpay.desafio.android.providers
+
+import com.picpay.desafio.android.domain.model.ContactModel
+import kotlinx.coroutines.flow.flowOf
+
+object MockContactProvider {
+
+ fun mockedContact() = internalMockedContact()
+ private fun internalMockedContact() = ContactModel(
+ image = "https://randomuser.me/api/portraits/men/1.jpg",
+ name = "Eduardo Santos",
+ username = "@eduardo.santos"
+ )
+
+ fun mockedFlowContacts() = flowOf(mockedContacts())
+ fun mockedContacts() = internalMockedContacts()
+ private fun internalMockedContacts() = listOf(
+ internalMockedContact(),
+ ContactModel(
+ image = "https://randomuser.me/api/portraits/women/2.jpg",
+ name = "Marina Coelho",
+ username = "@marina.coelho"
+ ),
+ ContactModel(
+ image = "https://randomuser.me/api/portraits/women/3.jpg",
+ name = "Márcia Silva",
+ username = "@marcia.silva"
+ )
+ )
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/picpay/desafio/android/providers/MockErrorProvider.kt b/app/src/test/java/com/picpay/desafio/android/providers/MockErrorProvider.kt
new file mode 100644
index 000000000..2c4b9252a
--- /dev/null
+++ b/app/src/test/java/com/picpay/desafio/android/providers/MockErrorProvider.kt
@@ -0,0 +1,7 @@
+package com.picpay.desafio.android.providers
+
+import kotlinx.coroutines.flow.flow
+
+object MockErrorProvider {
+ fun mockErrorFlow() = flow { emit(throw Exception()) }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/picpay/desafio/android/providers/MockUserProvider.kt b/app/src/test/java/com/picpay/desafio/android/providers/MockUserProvider.kt
new file mode 100644
index 000000000..93bbb333e
--- /dev/null
+++ b/app/src/test/java/com/picpay/desafio/android/providers/MockUserProvider.kt
@@ -0,0 +1,31 @@
+package com.picpay.desafio.android.providers
+
+import com.picpay.desafio.android.data.entities.UserEntity
+
+object MockUserProvider {
+
+ fun mockedUser() = internalMockedUser()
+ private fun internalMockedUser() = UserEntity(
+ id = 1,
+ img = "https://randomuser.me/api/portraits/men/1.jpg",
+ name = "Eduardo Santos",
+ username = "@eduardo.santos"
+ )
+
+ fun mockedUsers() = internalMockedUsers()
+ private fun internalMockedUsers() = listOf(
+ internalMockedUser(),
+ UserEntity(
+ id = 2,
+ img = "https://randomuser.me/api/portraits/women/2.jpg",
+ name = "Marina Coelho",
+ username = "@marina.coelho"
+ ),
+ UserEntity(
+ id = 3,
+ img = "https://randomuser.me/api/portraits/women/3.jpg",
+ name = "Márcia Silva",
+ username = "@marcia.silva"
+ )
+ )
+}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 7d1b94f34..260a61a0d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -2,42 +2,42 @@
buildscript {
ext {
- kotlin_version = '1.3.61'
+ gradle_version = '7.1.3'
+ kotlin_version = '1.6.21'
- appcompat_version = '1.1.0'
- core_ktx_version = '1.2.0'
+ appcompat_version = '1.4.2'
+ legacy_support_version = '1.0.0'
+ core_ktx_version = '1.8.0'
+ fragment_ktx_version = '1.4.1'
core_testing_version = '2.1.0'
- constraintlayout_version = '1.1.3'
- material_version = "1.1.0"
- moshi_version = '1.8.0'
+ constraintlayout_version = '2.1.4'
+ gson_version = "2.8.9"
+ material_version = "1.6.1"
retrofit_version = '2.7.1'
- okhttp_version = '4.3.1'
+ okhttp_version = '4.8.0'
picasso_version = '2.71828'
circleimageview_version = '3.0.0'
- junit_version = '4.12'
- mockito_version = '2.27.0'
- mockito_kotlin_version = '2.1.0'
+ junit_version = '4.13.2'
+ mockk_version = '1.12.0'
- test_runner_version = '1.1.1'
- espresso_version = '3.1.1'
+ test_ext_junit_ktx_version = '1.1.3'
+ test_runner_version = '1.4.0'
+ test_rules_version = '1.4.0'
+ espresso_version = '3.4.0'
- 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"
+ koin_version = "2.2.3"
+ lifecycle_version = "2.4.1"
+ coroutines_version = "1.5.2"
+ core_ktx_test_version = "1.4.0"
}
repositories {
google()
- jcenter()
-
+ mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:3.5.3'
+ classpath "com.android.tools.build:gradle:$gradle_version"
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
@@ -47,8 +47,7 @@ buildscript {
allprojects {
repositories {
google()
- jcenter()
-
+ mavenCentral()
}
}
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 31680f1d6..6348c7266 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip