From fb8eec59d5c2e8ef0bc446a0d6ef7c80ae16076c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Toledo=20Gonz=C3=A1lez?= Date: Mon, 13 Jun 2022 17:08:31 -0400 Subject: [PATCH 01/10] Removing non traceable files, updating gitignore file to avoid those kind of files --- .gitignore | 7 +- .idea/codeStyles/Project.xml | 125 ----------------------------------- .idea/gradle.xml | 18 ----- .idea/misc.xml | 14 ---- .idea/runConfigurations.xml | 12 ---- 5 files changed, 1 insertion(+), 175 deletions(-) delete mode 100644 .idea/codeStyles/Project.xml delete mode 100644 .idea/gradle.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/runConfigurations.xml 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 From f9fb98f399b8f0cf5e2dc373feea8bc7ee7a2e5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Toledo=20Gonz=C3=A1lez?= Date: Mon, 13 Jun 2022 20:42:26 -0400 Subject: [PATCH 02/10] Setting project structure - Organizing main files on packages. - Separating responsibilities into corresponding file/packages. - Setting and start koin dependency injection. --- app/build.gradle | 5 +- app/src/main/AndroidManifest.xml | 6 +- .../picpay/desafio/android/MainActivity.kt | 69 ------------------- .../com/picpay/desafio/android/MainApp.kt | 20 ++++++ .../picpay/desafio/android/PicPayService.kt | 11 --- .../java/com/picpay/desafio/android/User.kt | 13 ---- .../picpay/desafio/android/UserListAdapter.kt | 34 --------- .../desafio/android/UserListDiffCallback.kt | 26 ------- .../desafio/android/UserListItemViewHolder.kt | 30 -------- .../android/data/entities/UserEntity.kt | 13 ++++ .../desafio/android/data/mapper/UserMapper.kt | 8 +++ .../android/data/remote/PicPayService.kt | 14 ++++ .../data/remote/UserRemoteDataSource.kt | 15 ++++ .../data/repository/UserDataRepository.kt | 13 ++++ .../picpay/desafio/android/di/MainModule.kt | 53 ++++++++++++++ .../desafio/android/domain/model/UserModel.kt | 21 ++++++ .../domain/repository/UserRepository.kt | 8 +++ .../domain/useCases/ListContactsUseCase.kt | 8 +++ .../useCases/ListContactsUseCaseImpl.kt | 9 +++ .../presentation/adapters/UserListAdapter.kt | 17 +++++ .../presentation/fragments/ContactFragment.kt | 50 ++++++++++++++ .../viewHolders/UserListItemViewHolder.kt | 39 +++++++++++ .../viewModels/ContactViewModel.kt | 39 +++++++++++ app/src/main/res/layout/activity_main.xml | 58 +++------------- app/src/main/res/layout/frag_contact.xml | 59 ++++++++++++++++ app/src/main/res/layout/list_item_user.xml | 30 +++----- app/src/main/res/values/dimens.xml | 13 ++++ .../picpay/desafio/android/ExampleService.kt | 5 +- .../desafio/android/ExampleServiceTest.kt | 6 +- 29 files changed, 435 insertions(+), 257 deletions(-) create mode 100644 app/src/main/java/com/picpay/desafio/android/MainApp.kt delete mode 100644 app/src/main/java/com/picpay/desafio/android/PicPayService.kt delete mode 100644 app/src/main/java/com/picpay/desafio/android/User.kt delete mode 100644 app/src/main/java/com/picpay/desafio/android/UserListAdapter.kt delete mode 100644 app/src/main/java/com/picpay/desafio/android/UserListDiffCallback.kt delete mode 100644 app/src/main/java/com/picpay/desafio/android/UserListItemViewHolder.kt create mode 100644 app/src/main/java/com/picpay/desafio/android/data/entities/UserEntity.kt create mode 100644 app/src/main/java/com/picpay/desafio/android/data/mapper/UserMapper.kt create mode 100644 app/src/main/java/com/picpay/desafio/android/data/remote/PicPayService.kt create mode 100644 app/src/main/java/com/picpay/desafio/android/data/remote/UserRemoteDataSource.kt create mode 100644 app/src/main/java/com/picpay/desafio/android/data/repository/UserDataRepository.kt create mode 100644 app/src/main/java/com/picpay/desafio/android/di/MainModule.kt create mode 100644 app/src/main/java/com/picpay/desafio/android/domain/model/UserModel.kt create mode 100644 app/src/main/java/com/picpay/desafio/android/domain/repository/UserRepository.kt create mode 100644 app/src/main/java/com/picpay/desafio/android/domain/useCases/ListContactsUseCase.kt create mode 100644 app/src/main/java/com/picpay/desafio/android/domain/useCases/ListContactsUseCaseImpl.kt create mode 100644 app/src/main/java/com/picpay/desafio/android/presentation/adapters/UserListAdapter.kt create mode 100644 app/src/main/java/com/picpay/desafio/android/presentation/fragments/ContactFragment.kt create mode 100644 app/src/main/java/com/picpay/desafio/android/presentation/viewHolders/UserListItemViewHolder.kt create mode 100644 app/src/main/java/com/picpay/desafio/android/presentation/viewModels/ContactViewModel.kt create mode 100644 app/src/main/res/layout/frag_contact.xml create mode 100644 app/src/main/res/values/dimens.xml diff --git a/app/build.gradle b/app/build.gradle index a7fbdc0e9..22657c80b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,11 +7,11 @@ apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' android { - compileSdkVersion 29 + compileSdkVersion 31 defaultConfig { applicationId "com.picpay.desafio.android" minSdkVersion 21 - targetSdkVersion 29 + targetSdkVersion 31 versionCode 1 versionName "1.0" @@ -45,6 +45,7 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "androidx.core:core-ktx:$core_ktx_version" + implementation "androidx.fragment:fragment-ktx:$core_ktx_version" implementation "androidx.appcompat:appcompat:$appcompat_version" implementation "androidx.constraintlayout:constraintlayout:$constraintlayout_version" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7bdf2ce38..d6f15c511 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..f17f6dab3 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,6 @@ package com.picpay.desafio.android -import android.view.View -import android.widget.ProgressBar -import android.widget.Toast import androidx.appcompat.app.AppCompatActivity -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.google.gson.Gson -import com.google.gson.GsonBuilder -import okhttp3.OkHttpClient -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response -import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory class MainActivity : AppCompatActivity(R.layout.activity_main) { - - private lateinit var recyclerView: RecyclerView - private lateinit var progressBar: ProgressBar - private lateinit var adapter: UserListAdapter - - private val url = "https://609a908e0f5a13001721b74e.mockapi.io/picpay/api/" - - private val gson: Gson by lazy { GsonBuilder().create() } - - private val okHttp: OkHttpClient by lazy { - OkHttpClient.Builder() - .build() - } - - private val retrofit: Retrofit by lazy { - Retrofit.Builder() - .baseUrl(url) - .client(okHttp) - .addConverterFactory(GsonConverterFactory.create(gson)) - .build() - } - - private val service: PicPayService by lazy { - retrofit.create(PicPayService::class.java) - } - - override fun onResume() { - super.onResume() - - recyclerView = findViewById(R.id.recyclerView) - progressBar = findViewById(R.id.user_list_progress_bar) - - adapter = UserListAdapter() - recyclerView.adapter = adapter - recyclerView.layoutManager = LinearLayoutManager(this) - - progressBar.visibility = View.VISIBLE - service.getUsers() - .enqueue(object : Callback> { - override fun onFailure(call: Call>, t: Throwable) { - val message = getString(R.string.error) - - progressBar.visibility = View.GONE - recyclerView.visibility = View.GONE - - Toast.makeText(this@MainActivity, message, Toast.LENGTH_SHORT) - .show() - } - - override fun onResponse(call: Call>, response: Response>) { - progressBar.visibility = View.GONE - - adapter.users = response.body()!! - } - }) - } } diff --git a/app/src/main/java/com/picpay/desafio/android/MainApp.kt b/app/src/main/java/com/picpay/desafio/android/MainApp.kt new file mode 100644 index 000000000..b5812fc06 --- /dev/null +++ b/app/src/main/java/com/picpay/desafio/android/MainApp.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 MainApp : Application() { + + override fun onCreate() { + super.onCreate() + startKoin { + androidLogger() + androidContext(this@MainApp) + } + 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/User.kt b/app/src/main/java/com/picpay/desafio/android/User.kt deleted file mode 100644 index aa28171c9..000000000 --- a/app/src/main/java/com/picpay/desafio/android/User.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.picpay.desafio.android - -import android.os.Parcelable -import com.google.gson.annotations.SerializedName -import kotlinx.android.parcel.Parcelize - -@Parcelize -data class User( - @SerializedName("img") val img: String, - @SerializedName("name") val name: String, - @SerializedName("id") val id: Int, - @SerializedName("username") val username: String -) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/picpay/desafio/android/UserListAdapter.kt b/app/src/main/java/com/picpay/desafio/android/UserListAdapter.kt deleted file mode 100644 index 538c98a4a..000000000 --- a/app/src/main/java/com/picpay/desafio/android/UserListAdapter.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.picpay.desafio.android - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView - -class UserListAdapter : RecyclerView.Adapter() { - - var users = emptyList() - set(value) { - val result = DiffUtil.calculateDiff( - UserListDiffCallback( - field, - value - ) - ) - result.dispatchUpdatesTo(this) - field = value - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserListItemViewHolder { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.list_item_user, parent, false) - - return UserListItemViewHolder(view) - } - - override fun onBindViewHolder(holder: UserListItemViewHolder, position: Int) { - holder.bind(users[position]) - } - - override fun getItemCount(): Int = users.size -} \ No newline at end of file diff --git a/app/src/main/java/com/picpay/desafio/android/UserListDiffCallback.kt b/app/src/main/java/com/picpay/desafio/android/UserListDiffCallback.kt deleted file mode 100644 index 7c734d37b..000000000 --- a/app/src/main/java/com/picpay/desafio/android/UserListDiffCallback.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.picpay.desafio.android - -import androidx.recyclerview.widget.DiffUtil -import com.picpay.desafio.android.User - -class UserListDiffCallback( - private val oldList: List, - private val newList: List -) : DiffUtil.Callback() { - - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - return oldList[oldItemPosition].username.equals(newList[newItemPosition].username) - } - - override fun getOldListSize(): Int { - return oldList.size - } - - override fun getNewListSize(): Int { - return newList.size - } - - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - return true - } -} \ No newline at end of file diff --git a/app/src/main/java/com/picpay/desafio/android/UserListItemViewHolder.kt b/app/src/main/java/com/picpay/desafio/android/UserListItemViewHolder.kt deleted file mode 100644 index 1d8240eb3..000000000 --- a/app/src/main/java/com/picpay/desafio/android/UserListItemViewHolder.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.picpay.desafio.android - -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import com.squareup.picasso.Callback -import com.squareup.picasso.Picasso -import kotlinx.android.synthetic.main.list_item_user.view.* - -class UserListItemViewHolder( - itemView: View -) : RecyclerView.ViewHolder(itemView) { - - fun bind(user: User) { - itemView.name.text = user.name - itemView.username.text = user.username - itemView.progressBar.visibility = View.VISIBLE - Picasso.get() - .load(user.img) - .error(R.drawable.ic_round_account_circle) - .into(itemView.picture, object : Callback { - override fun onSuccess() { - itemView.progressBar.visibility = View.GONE - } - - override fun onError(e: Exception?) { - itemView.progressBar.visibility = View.GONE - } - }) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/picpay/desafio/android/data/entities/UserEntity.kt b/app/src/main/java/com/picpay/desafio/android/data/entities/UserEntity.kt new file mode 100644 index 000000000..15e90a889 --- /dev/null +++ b/app/src/main/java/com/picpay/desafio/android/data/entities/UserEntity.kt @@ -0,0 +1,13 @@ +package com.picpay.desafio.android.data.entities + +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import kotlinx.android.parcel.Parcelize + +@Parcelize +data class UserEntity( + @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..7d5eae667 --- /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.UserModel + +fun UserEntity.toModel() = UserModel(img = img, name = name, username = username) + +fun List.toListModel() = map { it.toModel() } \ No newline at end of file diff --git a/app/src/main/java/com/picpay/desafio/android/data/remote/PicPayService.kt b/app/src/main/java/com/picpay/desafio/android/data/remote/PicPayService.kt new file mode 100644 index 000000000..c50180413 --- /dev/null +++ b/app/src/main/java/com/picpay/desafio/android/data/remote/PicPayService.kt @@ -0,0 +1,14 @@ +package com.picpay.desafio.android.data.remote + +import com.picpay.desafio.android.data.entities.UserEntity +import retrofit2.http.GET + +interface PicPayService { + + @GET("users") + suspend fun getUsers(): List + + companion object { + const val BASE_URL = "https://609a908e0f5a13001721b74e.mockapi.io/picpay/api/" + } +} \ 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..961be55be --- /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: PicPayService) { + + 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/UserDataRepository.kt b/app/src/main/java/com/picpay/desafio/android/data/repository/UserDataRepository.kt new file mode 100644 index 000000000..27e1d224f --- /dev/null +++ b/app/src/main/java/com/picpay/desafio/android/data/repository/UserDataRepository.kt @@ -0,0 +1,13 @@ +package com.picpay.desafio.android.data.repository + +import com.picpay.desafio.android.data.mapper.toListModel +import com.picpay.desafio.android.data.remote.UserRemoteDataSource +import com.picpay.desafio.android.domain.model.UserModel +import com.picpay.desafio.android.domain.repository.UserRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class UserDataRepository(private val remoteDataSource: UserRemoteDataSource) : UserRepository { + + override fun getUsers(): Flow> = remoteDataSource.getUsers().map { it.toListModel() } +} \ 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..ee31ad89c --- /dev/null +++ b/app/src/main/java/com/picpay/desafio/android/di/MainModule.kt @@ -0,0 +1,53 @@ +package com.picpay.desafio.android.di + +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.picpay.desafio.android.data.remote.PicPayService +import com.picpay.desafio.android.data.remote.UserRemoteDataSource +import com.picpay.desafio.android.data.repository.UserDataRepository +import com.picpay.desafio.android.domain.repository.UserRepository +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() + +val networkModule = module { + fun provideGson() = GsonBuilder().create() + fun provideOkHttp() = OkHttpClient.Builder().build() + fun provideRetrofit(okHttp: OkHttpClient, gson: Gson) = Retrofit.Builder() + .baseUrl(PicPayService.BASE_URL) + .client(okHttp) + .addConverterFactory(GsonConverterFactory.create(gson)) + .build() + single { provideGson() } + single { provideOkHttp() } + single { provideRetrofit(get(), get()) } + single { getRetrofit().create(PicPayService::class.java) } +} + +val dataModule = module { + single { UserRemoteDataSource(get()) } + single { UserDataRepository(get()) } +} + +val useCasesModule = module { + factory { ListContactsUseCaseImpl(get()) } +} + +val viewModelsModule = module { + viewModel { ContactViewModel(get()) } +} + +object MainModuleInitializer { + 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/UserModel.kt b/app/src/main/java/com/picpay/desafio/android/domain/model/UserModel.kt new file mode 100644 index 000000000..233842db7 --- /dev/null +++ b/app/src/main/java/com/picpay/desafio/android/domain/model/UserModel.kt @@ -0,0 +1,21 @@ +package com.picpay.desafio.android.domain.model + +import androidx.recyclerview.widget.DiffUtil + +data class UserModel(val img: String, val name: String, val username: String) { + + companion object { + + val DIFF_UTIL_CALLBACK = object : DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: UserModel, newItem: UserModel): Boolean { + return oldItem.username == newItem.username + } + + override fun areContentsTheSame(oldItem: UserModel, newItem: UserModel): Boolean { + return oldItem == newItem + } + + } + } +} diff --git a/app/src/main/java/com/picpay/desafio/android/domain/repository/UserRepository.kt b/app/src/main/java/com/picpay/desafio/android/domain/repository/UserRepository.kt new file mode 100644 index 000000000..198b358db --- /dev/null +++ b/app/src/main/java/com/picpay/desafio/android/domain/repository/UserRepository.kt @@ -0,0 +1,8 @@ +package com.picpay.desafio.android.domain.repository + +import com.picpay.desafio.android.domain.model.UserModel +import kotlinx.coroutines.flow.Flow + +interface UserRepository { + fun getUsers(): 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..e9506dcbf --- /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.UserModel +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..3553f1a49 --- /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.UserModel +import com.picpay.desafio.android.domain.repository.UserRepository +import kotlinx.coroutines.flow.Flow + +class ListContactsUseCaseImpl(private val userRepository: UserRepository) : ListContactsUseCase { + override fun invoke(): Flow> = userRepository.getUsers() +} \ No newline at end of file diff --git a/app/src/main/java/com/picpay/desafio/android/presentation/adapters/UserListAdapter.kt b/app/src/main/java/com/picpay/desafio/android/presentation/adapters/UserListAdapter.kt new file mode 100644 index 000000000..589688a33 --- /dev/null +++ b/app/src/main/java/com/picpay/desafio/android/presentation/adapters/UserListAdapter.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.UserModel +import com.picpay.desafio.android.presentation.viewHolders.UserListItemViewHolder + +class UserListAdapter : ListAdapter(UserModel.DIFF_UTIL_CALLBACK) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserListItemViewHolder { + return UserListItemViewHolder.newInstance(parent) + } + + override fun onBindViewHolder(holder: UserListItemViewHolder, 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..01e1c944e --- /dev/null +++ b/app/src/main/java/com/picpay/desafio/android/presentation/fragments/ContactFragment.kt @@ -0,0 +1,50 @@ +package com.picpay.desafio.android.presentation.fragments + +import android.os.Bundle +import android.view.View +import android.widget.ProgressBar +import android.widget.Toast +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.lifecycle.observe +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.picpay.desafio.android.R +import com.picpay.desafio.android.presentation.adapters.UserListAdapter +import com.picpay.desafio.android.presentation.viewModels.ContactViewModel +import org.koin.androidx.viewmodel.ext.android.viewModel + +class ContactFragment : Fragment(R.layout.frag_contact) { + + private lateinit var recyclerView: RecyclerView + private lateinit var progressBar: ProgressBar + private val listAdapter: UserListAdapter by lazy { UserListAdapter() } + private val contactViewModel: ContactViewModel by viewModel() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + progressBar = view.findViewById(R.id.user_list_progress_bar) + recyclerView = view.findViewById(R.id.recyclerView).apply { + adapter = listAdapter + layoutManager = LinearLayoutManager(view.context) + } + initObservers() + } + + private fun initObservers() { + with(contactViewModel) { + isLoading.observe(viewLifecycleOwner) { progressBar.isVisible = it } + messages.observe(viewLifecycleOwner) { + recyclerView.visibility = View.GONE + Toast.makeText(this@ContactFragment.context, getString(it), Toast.LENGTH_SHORT) + .show() + } + contacts.observe(viewLifecycleOwner) { + 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/UserListItemViewHolder.kt b/app/src/main/java/com/picpay/desafio/android/presentation/viewHolders/UserListItemViewHolder.kt new file mode 100644 index 000000000..76b98de5d --- /dev/null +++ b/app/src/main/java/com/picpay/desafio/android/presentation/viewHolders/UserListItemViewHolder.kt @@ -0,0 +1,39 @@ +package com.picpay.desafio.android.presentation.viewHolders + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.picpay.desafio.android.R +import com.picpay.desafio.android.domain.model.UserModel +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(userModel: UserModel) { + itemView.name.text = userModel.name + itemView.username.text = userModel.username + itemView.progressBar.visibility = View.VISIBLE + Picasso.get() + .load(userModel.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 + } + }) + } + + companion object { + + fun newInstance(parent: ViewGroup) = UserListItemViewHolder( + itemView = LayoutInflater.from(parent.context).inflate(R.layout.list_item_user, 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..9167d29f8 --- /dev/null +++ b/app/src/main/java/com/picpay/desafio/android/presentation/viewModels/ContactViewModel.kt @@ -0,0 +1,39 @@ +package com.picpay.desafio.android.presentation.viewModels + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.picpay.desafio.android.R +import com.picpay.desafio.android.domain.model.UserModel +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) : ViewModel() { + + private val _isLoading: MutableLiveData = MutableLiveData(false) + val isLoading: LiveData get() = _isLoading + + private val _messages: MutableLiveData = MutableLiveData() + val messages: LiveData get() = _messages + + private val _contacts: MutableLiveData> = MutableLiveData() + val contacts: LiveData> get() = _contacts + + fun loadContacts() { + viewModelScope.launch { + listContactsUseCase() + .onStart { _isLoading.postValue(true) } + .catch { + _isLoading.postValue(false) + _messages.postValue(R.string.error) + } + .onCompletion { _isLoading.postValue(false) } + .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..8395efc9e 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,5 +1,5 @@ - - + android:layout_marginTop="@dimen/medium_margin" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> - - - - - - - - - - - - - \ No newline at end of file + \ 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..c7e248f31 --- /dev/null +++ b/app/src/main/res/layout/frag_contact.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + \ 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 index 0199c5e4a..56f675849 100644 --- a/app/src/test/java/com/picpay/desafio/android/ExampleService.kt +++ b/app/src/test/java/com/picpay/desafio/android/ExampleService.kt @@ -1,10 +1,13 @@ package com.picpay.desafio.android +import com.picpay.desafio.android.data.entities.UserEntity +import com.picpay.desafio.android.data.remote.PicPayService + class ExampleService( private val service: PicPayService ) { - fun example(): List { + fun example(): List { val users = service.getUsers().execute() return users.body() ?: emptyList() diff --git a/app/src/test/java/com/picpay/desafio/android/ExampleServiceTest.kt b/app/src/test/java/com/picpay/desafio/android/ExampleServiceTest.kt index 843c0e776..3475eb7cd 100644 --- a/app/src/test/java/com/picpay/desafio/android/ExampleServiceTest.kt +++ b/app/src/test/java/com/picpay/desafio/android/ExampleServiceTest.kt @@ -2,6 +2,8 @@ package com.picpay.desafio.android import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.whenever +import com.picpay.desafio.android.data.entities.UserEntity +import com.picpay.desafio.android.data.remote.PicPayService import junit.framework.Assert.assertEquals import org.junit.Test import retrofit2.Call @@ -16,8 +18,8 @@ class ExampleServiceTest { @Test fun exampleTest() { // given - val call = mock>>() - val expectedUsers = emptyList() + val call = mock>>() + val expectedUsers = emptyList() whenever(call.execute()).thenReturn(Response.success(expectedUsers)) whenever(api.getUsers()).thenReturn(call) From cb688e9cf95b86895ae9f14f5a4e1d2cb73013f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Toledo=20Gonz=C3=A1lez?= Date: Tue, 14 Jun 2022 16:48:40 -0400 Subject: [PATCH 03/10] Update gradle files - Updating dependencies versions, removing unused dependencies. - Adding viewBinding build feature. - Updating related files and creating some bases and extensions files. --- app/build.gradle | 42 ++++++++---------- app/src/main/AndroidManifest.xml | 2 +- .../picpay/desafio/android/MainActivity.kt | 7 ++- .../android/{MainApp.kt => PicPayApp.kt} | 4 +- .../desafio/android/bases/BaseActivity.kt | 18 ++++++++ .../desafio/android/bases/BaseFragment.kt | 31 +++++++++++++ .../desafio/android/bases/BaseViewModel.kt | 10 +++++ .../android/data/entities/UserEntity.kt | 10 ++--- .../android/extensions/ActivityExtensions.kt | 18 ++++++++ .../android/extensions/ContextExtensions.kt | 24 ++++++++++ .../android/extensions/FragmentExtensions.kt | 18 ++++++++ .../presentation/fragments/ContactFragment.kt | 44 ++++++++----------- .../viewHolders/UserListItemViewHolder.kt | 26 ++++++----- .../viewModels/ContactViewModel.kt | 7 ++- build.gradle | 41 ++++++++--------- gradle/wrapper/gradle-wrapper.properties | 2 +- 16 files changed, 206 insertions(+), 98 deletions(-) rename app/src/main/java/com/picpay/desafio/android/{MainApp.kt => PicPayApp.kt} (84%) create mode 100644 app/src/main/java/com/picpay/desafio/android/bases/BaseActivity.kt create mode 100644 app/src/main/java/com/picpay/desafio/android/bases/BaseFragment.kt create mode 100644 app/src/main/java/com/picpay/desafio/android/bases/BaseViewModel.kt create mode 100644 app/src/main/java/com/picpay/desafio/android/extensions/ActivityExtensions.kt create mode 100644 app/src/main/java/com/picpay/desafio/android/extensions/ContextExtensions.kt create mode 100644 app/src/main/java/com/picpay/desafio/android/extensions/FragmentExtensions.kt diff --git a/app/build.gradle b/app/build.gradle index 22657c80b..e3b4b24a0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,10 +1,9 @@ -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 31 @@ -29,6 +28,10 @@ android { } } + buildFeatures { + viewBinding true + } + compileOptions { sourceCompatibility 1.8 targetCompatibility 1.8 @@ -45,33 +48,24 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "androidx.core:core-ktx:$core_ktx_version" - implementation "androidx.fragment:fragment-ktx:$core_ktx_version" + implementation "androidx.fragment:fragment-ktx:$fragment_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 "com.google.code.gson:gson:$gson_version" + implementation "com.google.android.material:material:$material_version" + + implementation "io.insert-koin:koin-android:$koin_version" + implementation "io.insert-koin:koin-androidx-viewmodel:$koin_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" @@ -85,7 +79,7 @@ dependencies { 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" + implementation "io.insert-koin:koin-test:$koin_version" androidTestImplementation "androidx.test:runner:$test_runner_version" androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d6f15c511..d1433da20 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,7 +6,7 @@ () { + override val binding by viewBinding(ActivityMainBinding::inflate) } diff --git a/app/src/main/java/com/picpay/desafio/android/MainApp.kt b/app/src/main/java/com/picpay/desafio/android/PicPayApp.kt similarity index 84% rename from app/src/main/java/com/picpay/desafio/android/MainApp.kt rename to app/src/main/java/com/picpay/desafio/android/PicPayApp.kt index b5812fc06..87c487c12 100644 --- a/app/src/main/java/com/picpay/desafio/android/MainApp.kt +++ b/app/src/main/java/com/picpay/desafio/android/PicPayApp.kt @@ -6,13 +6,13 @@ import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidLogger import org.koin.core.context.startKoin -class MainApp : Application() { +class PicPayApp : Application() { override fun onCreate() { super.onCreate() startKoin { androidLogger() - androidContext(this@MainApp) + androidContext(this@PicPayApp) } MainModuleInitializer.init() } 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..4a13ccc8c --- /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.viewModelClass +import org.koin.androidx.viewmodel.ext.android.getViewModel + +abstract class BaseActivity : AppCompatActivity() { + + abstract val binding: ViewBinding + val viewModel: V by lazy { getViewModel(clazz = viewModelClass()) } + + 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..91509d7c9 --- /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.viewModelClass +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 = viewModelClass()) } + + 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..d1a11199c --- /dev/null +++ b/app/src/main/java/com/picpay/desafio/android/bases/BaseViewModel.kt @@ -0,0 +1,10 @@ +package com.picpay.desafio.android.bases + +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlin.coroutines.CoroutineContext + +abstract class BaseViewModel : ViewModel(), CoroutineScope { + override val coroutineContext: CoroutineContext = Dispatchers.Main +} \ No newline at end of file diff --git a/app/src/main/java/com/picpay/desafio/android/data/entities/UserEntity.kt b/app/src/main/java/com/picpay/desafio/android/data/entities/UserEntity.kt index 15e90a889..1878752d9 100644 --- a/app/src/main/java/com/picpay/desafio/android/data/entities/UserEntity.kt +++ b/app/src/main/java/com/picpay/desafio/android/data/entities/UserEntity.kt @@ -2,12 +2,12 @@ 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 UserEntity( - @SerializedName("img") val img: String, - @SerializedName("name") val name: String, - @SerializedName("id") val id: Int, - @SerializedName("username") val username: String + @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/extensions/ActivityExtensions.kt b/app/src/main/java/com/picpay/desafio/android/extensions/ActivityExtensions.kt new file mode 100644 index 000000000..f2501e660 --- /dev/null +++ b/app/src/main/java/com/picpay/desafio/android/extensions/ActivityExtensions.kt @@ -0,0 +1,18 @@ +package com.picpay.desafio.android.extensions + +import android.view.LayoutInflater +import androidx.appcompat.app.AppCompatActivity +import androidx.viewbinding.ViewBinding +import java.lang.reflect.ParameterizedType +import kotlin.reflect.KClass + +inline fun AppCompatActivity.viewBinding( + crossinline bindingInflater: (LayoutInflater) -> T +) = lazy(LazyThreadSafetyMode.NONE) { bindingInflater.invoke(layoutInflater) } + +@Suppress("UNCHECKED_CAST") +internal fun AppCompatActivity.viewModelClass(): KClass { + val type = javaClass.genericSuperclass as ParameterizedType + val result = type.actualTypeArguments[0] as Class + return result.kotlin +} \ 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..9d53e5617 --- /dev/null +++ b/app/src/main/java/com/picpay/desafio/android/extensions/FragmentExtensions.kt @@ -0,0 +1,18 @@ +package com.picpay.desafio.android.extensions + +import android.view.LayoutInflater +import androidx.fragment.app.Fragment +import androidx.viewbinding.ViewBinding +import java.lang.reflect.ParameterizedType +import kotlin.reflect.KClass + +inline fun Fragment.viewBinding( + crossinline bindingInflater: (LayoutInflater) -> T +) = lazy(LazyThreadSafetyMode.NONE) { bindingInflater.invoke(layoutInflater) } + +@Suppress("UNCHECKED_CAST") +internal fun Fragment.viewModelClass(): KClass { + val type = javaClass.genericSuperclass as ParameterizedType + val result = type.actualTypeArguments[0] as Class + return result.kotlin +} \ 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 index 01e1c944e..304edd610 100644 --- 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 @@ -1,46 +1,38 @@ package com.picpay.desafio.android.presentation.fragments -import android.os.Bundle import android.view.View -import android.widget.ProgressBar -import android.widget.Toast import androidx.core.view.isVisible -import androidx.fragment.app.Fragment -import androidx.lifecycle.observe import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.picpay.desafio.android.R +import com.picpay.desafio.android.bases.BaseFragment +import com.picpay.desafio.android.databinding.FragContactBinding +import com.picpay.desafio.android.extensions.showToastShortText +import com.picpay.desafio.android.extensions.viewBinding import com.picpay.desafio.android.presentation.adapters.UserListAdapter import com.picpay.desafio.android.presentation.viewModels.ContactViewModel -import org.koin.androidx.viewmodel.ext.android.viewModel -class ContactFragment : Fragment(R.layout.frag_contact) { +class ContactFragment : BaseFragment() { - private lateinit var recyclerView: RecyclerView - private lateinit var progressBar: ProgressBar + override val binding by viewBinding(FragContactBinding::inflate) private val listAdapter: UserListAdapter by lazy { UserListAdapter() } - private val contactViewModel: ContactViewModel by viewModel() - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - progressBar = view.findViewById(R.id.user_list_progress_bar) - recyclerView = view.findViewById(R.id.recyclerView).apply { - adapter = listAdapter - layoutManager = LinearLayoutManager(view.context) + override fun initComponents() { + with(binding) { + recyclerView.apply { + adapter = listAdapter + layoutManager = LinearLayoutManager(context) + } } - initObservers() } - private fun initObservers() { - with(contactViewModel) { - isLoading.observe(viewLifecycleOwner) { progressBar.isVisible = it } + override fun initObservers() { + with(viewModel) { + isLoading.observe(viewLifecycleOwner) { binding.userListProgressBar.isVisible = it } messages.observe(viewLifecycleOwner) { - recyclerView.visibility = View.GONE - Toast.makeText(this@ContactFragment.context, getString(it), Toast.LENGTH_SHORT) - .show() + binding.recyclerView.visibility = View.GONE + this@ContactFragment.context?.showToastShortText(getString(it)) } contacts.observe(viewLifecycleOwner) { - recyclerView.visibility = View.VISIBLE + binding.recyclerView.visibility = View.VISIBLE listAdapter.submitList(it) } loadContacts() diff --git a/app/src/main/java/com/picpay/desafio/android/presentation/viewHolders/UserListItemViewHolder.kt b/app/src/main/java/com/picpay/desafio/android/presentation/viewHolders/UserListItemViewHolder.kt index 76b98de5d..da9e43b6f 100644 --- a/app/src/main/java/com/picpay/desafio/android/presentation/viewHolders/UserListItemViewHolder.kt +++ b/app/src/main/java/com/picpay/desafio/android/presentation/viewHolders/UserListItemViewHolder.kt @@ -1,39 +1,43 @@ package com.picpay.desafio.android.presentation.viewHolders -import android.view.LayoutInflater 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.UserModel +import com.picpay.desafio.android.extensions.toLayoutInflater 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) { +class UserListItemViewHolder( + private val binding: ListItemUserBinding +) : RecyclerView.ViewHolder(binding.root) { fun bind(userModel: UserModel) { - itemView.name.text = userModel.name - itemView.username.text = userModel.username - itemView.progressBar.visibility = View.VISIBLE - Picasso.get() + with(binding) { + name.text = userModel.name + username.text = userModel.username + progressBar.visibility = View.VISIBLE + Picasso.get() .load(userModel.img) .error(R.drawable.ic_round_account_circle) - .into(itemView.picture, object : Callback { + .into(picture, object : Callback { override fun onSuccess() { - itemView.progressBar.visibility = View.GONE + progressBar.visibility = View.GONE } override fun onError(e: Exception?) { - itemView.progressBar.visibility = View.GONE + progressBar.visibility = View.GONE } }) + } } companion object { fun newInstance(parent: ViewGroup) = UserListItemViewHolder( - itemView = LayoutInflater.from(parent.context).inflate(R.layout.list_item_user, parent, false) + 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 index 9167d29f8..626d341de 100644 --- 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 @@ -2,9 +2,8 @@ package com.picpay.desafio.android.presentation.viewModels import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import com.picpay.desafio.android.R +import com.picpay.desafio.android.bases.BaseViewModel import com.picpay.desafio.android.domain.model.UserModel import com.picpay.desafio.android.domain.useCases.ListContactsUseCase import kotlinx.coroutines.flow.catch @@ -13,7 +12,7 @@ import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch -class ContactViewModel(private val listContactsUseCase: ListContactsUseCase) : ViewModel() { +class ContactViewModel(private val listContactsUseCase: ListContactsUseCase) : BaseViewModel() { private val _isLoading: MutableLiveData = MutableLiveData(false) val isLoading: LiveData get() = _isLoading @@ -25,7 +24,7 @@ class ContactViewModel(private val listContactsUseCase: ListContactsUseCase) : V val contacts: LiveData> get() = _contacts fun loadContacts() { - viewModelScope.launch { + launch { listContactsUseCase() .onStart { _isLoading.postValue(true) } .catch { diff --git a/build.gradle b/build.gradle index 7d1b94f34..8497589fc 100644 --- a/build.gradle +++ b/build.gradle @@ -2,42 +2,40 @@ buildscript { ext { - kotlin_version = '1.3.61' + gradle_version = '7.0.4' + kotlin_version = '1.6.20' - appcompat_version = '1.1.0' - core_ktx_version = '1.2.0' + appcompat_version = '1.4.2' + 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.6" + 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' + junit_version = '4.13.2' mockito_version = '2.27.0' mockito_kotlin_version = '2.1.0' - test_runner_version = '1.1.1' - espresso_version = '3.1.1' + test_runner_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 +45,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 From 126c122911f3d3585aaf8e37d86c40c112b6644d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Toledo=20Gonz=C3=A1lez?= Date: Tue, 14 Jun 2022 16:48:40 -0400 Subject: [PATCH 04/10] Update gradle files - Updating dependencies versions, removing unused dependencies. - Adding viewBinding build feature. - Updating related files and creating some bases and extensions files. --- app/build.gradle | 2 +- build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index e3b4b24a0..6edc6d609 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -45,7 +45,7 @@ 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.fragment:fragment-ktx:$fragment_ktx_version" diff --git a/build.gradle b/build.gradle index 8497589fc..821950171 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext { - gradle_version = '7.0.4' + gradle_version = '7.1.3' kotlin_version = '1.6.20' appcompat_version = '1.4.2' From e81d4f24a42c3b00324e7560cca6e6570d6d6975 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Toledo=20Gonz=C3=A1lez?= Date: Tue, 14 Jun 2022 17:30:19 -0400 Subject: [PATCH 05/10] Update gradle files --- app/build.gradle | 4 ++-- build.gradle | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 6edc6d609..25922b032 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -33,8 +33,8 @@ android { } compileOptions { - sourceCompatibility 1.8 - targetCompatibility 1.8 + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { diff --git a/build.gradle b/build.gradle index 821950171..5bc8d7204 100644 --- a/build.gradle +++ b/build.gradle @@ -3,14 +3,14 @@ buildscript { ext { gradle_version = '7.1.3' - kotlin_version = '1.6.20' + kotlin_version = '1.6.21' appcompat_version = '1.4.2' core_ktx_version = '1.8.0' fragment_ktx_version = '1.4.1' core_testing_version = '2.1.0' constraintlayout_version = '2.1.4' - gson_version = "2.8.6" + gson_version = "2.8.9" material_version = "1.6.1" retrofit_version = '2.7.1' okhttp_version = '4.8.0' From 6d17ac3531a07334a8c671f88f0d69bd55c2d541 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Toledo=20Gonz=C3=A1lez?= Date: Tue, 14 Jun 2022 19:03:39 -0400 Subject: [PATCH 06/10] Refactoring unit tests --- app/build.gradle | 14 +++---- .../remote/{PicPayService.kt => PicPayApi.kt} | 2 +- .../data/remote/UserRemoteDataSource.kt | 2 +- .../picpay/desafio/android/di/MainModule.kt | 6 +-- .../picpay/desafio/android/ExampleService.kt | 15 ------- .../desafio/android/ExampleServiceTest.kt | 33 --------------- .../picpay/desafio/android/base/BaseTest.kt | 25 +++++++++++ .../desafio/android/base/CoroutineRule.kt | 26 ++++++++++++ .../viewModels/ContactViewModelTest.kt | 41 +++++++++++++++++++ .../android/providers/ContactMockProvider.kt | 27 ++++++++++++ .../android/providers/ErrorMockProvider.kt | 7 ++++ build.gradle | 3 +- 12 files changed, 138 insertions(+), 63 deletions(-) rename app/src/main/java/com/picpay/desafio/android/data/remote/{PicPayService.kt => PicPayApi.kt} (92%) delete mode 100644 app/src/test/java/com/picpay/desafio/android/ExampleService.kt delete mode 100644 app/src/test/java/com/picpay/desafio/android/ExampleServiceTest.kt create mode 100644 app/src/test/java/com/picpay/desafio/android/base/BaseTest.kt create mode 100644 app/src/test/java/com/picpay/desafio/android/base/CoroutineRule.kt create mode 100644 app/src/test/java/com/picpay/desafio/android/presentation/viewModels/ContactViewModelTest.kt create mode 100644 app/src/test/java/com/picpay/desafio/android/providers/ContactMockProvider.kt create mode 100644 app/src/test/java/com/picpay/desafio/android/providers/ErrorMockProvider.kt diff --git a/app/build.gradle b/app/build.gradle index 25922b032..f3a2d6dbd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -48,13 +48,12 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "androidx.core:core-ktx:$core_ktx_version" - implementation "androidx.fragment:fragment-ktx:$fragment_ktx_version" - implementation "androidx.appcompat:appcompat:$appcompat_version" + implementation "androidx.fragment:fragment-ktx:$fragment_ktx_version" implementation "androidx.constraintlayout:constraintlayout:$constraintlayout_version" - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" implementation "com.google.code.gson:gson:$gson_version" implementation "com.google.android.material:material:$material_version" @@ -64,7 +63,6 @@ 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 "com.squareup.retrofit2:retrofit:$retrofit_version" implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofit_version" @@ -76,12 +74,12 @@ dependencies { 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 "io.mockk:mockk:$mockk_version" + testImplementation "io.insert-koin:koin-test:$koin_version" testImplementation "androidx.arch.core:core-testing:$core_testing_version" - implementation "io.insert-koin:koin-test:$koin_version" + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version" + androidTestImplementation "androidx.test:core-ktx:$core_ktx_test_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/src/main/java/com/picpay/desafio/android/data/remote/PicPayService.kt b/app/src/main/java/com/picpay/desafio/android/data/remote/PicPayApi.kt similarity index 92% rename from app/src/main/java/com/picpay/desafio/android/data/remote/PicPayService.kt rename to app/src/main/java/com/picpay/desafio/android/data/remote/PicPayApi.kt index c50180413..83551bf24 100644 --- a/app/src/main/java/com/picpay/desafio/android/data/remote/PicPayService.kt +++ b/app/src/main/java/com/picpay/desafio/android/data/remote/PicPayApi.kt @@ -3,7 +3,7 @@ package com.picpay.desafio.android.data.remote import com.picpay.desafio.android.data.entities.UserEntity import retrofit2.http.GET -interface PicPayService { +interface PicPayApi { @GET("users") suspend fun getUsers(): List 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 index 961be55be..fa1a2cbde 100644 --- 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 @@ -5,7 +5,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.flow -class UserRemoteDataSource(private val api: PicPayService) { +class UserRemoteDataSource(private val api: PicPayApi) { private fun call(block: suspend FlowCollector.() -> T): Flow = flow { emit(block()) 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 index ee31ad89c..4240fa761 100644 --- a/app/src/main/java/com/picpay/desafio/android/di/MainModule.kt +++ b/app/src/main/java/com/picpay/desafio/android/di/MainModule.kt @@ -2,7 +2,7 @@ package com.picpay.desafio.android.di import com.google.gson.Gson import com.google.gson.GsonBuilder -import com.picpay.desafio.android.data.remote.PicPayService +import com.picpay.desafio.android.data.remote.PicPayApi import com.picpay.desafio.android.data.remote.UserRemoteDataSource import com.picpay.desafio.android.data.repository.UserDataRepository import com.picpay.desafio.android.domain.repository.UserRepository @@ -23,14 +23,14 @@ val networkModule = module { fun provideGson() = GsonBuilder().create() fun provideOkHttp() = OkHttpClient.Builder().build() fun provideRetrofit(okHttp: OkHttpClient, gson: Gson) = Retrofit.Builder() - .baseUrl(PicPayService.BASE_URL) + .baseUrl(PicPayApi.BASE_URL) .client(okHttp) .addConverterFactory(GsonConverterFactory.create(gson)) .build() single { provideGson() } single { provideOkHttp() } single { provideRetrofit(get(), get()) } - single { getRetrofit().create(PicPayService::class.java) } + single { getRetrofit().create(PicPayApi::class.java) } } val dataModule = module { 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 56f675849..000000000 --- a/app/src/test/java/com/picpay/desafio/android/ExampleService.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.picpay.desafio.android - -import com.picpay.desafio.android.data.entities.UserEntity -import com.picpay.desafio.android.data.remote.PicPayService - -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 3475eb7cd..000000000 --- a/app/src/test/java/com/picpay/desafio/android/ExampleServiceTest.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.picpay.desafio.android - -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.whenever -import com.picpay.desafio.android.data.entities.UserEntity -import com.picpay.desafio.android.data.remote.PicPayService -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..c0338ef52 --- /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 kotlinx.coroutines.flow.flow +import org.junit.Before +import org.junit.Rule +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +abstract class BaseTest { + + @get: Rule + val coroutineRule = CoroutineRule() + + @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/CoroutineRule.kt b/app/src/test/java/com/picpay/desafio/android/base/CoroutineRule.kt new file mode 100644 index 000000000..9c39d15d5 --- /dev/null +++ b/app/src/test/java/com/picpay/desafio/android/base/CoroutineRule.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.TestCoroutineScope +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.rules.TestWatcher +import org.junit.runner.Description + +@ExperimentalCoroutinesApi +class CoroutineRule( + private val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher() +) : TestWatcher(), TestCoroutineScope by TestCoroutineScope(dispatcher) { + override fun starting(description: Description?) { + super.starting(description) + Dispatchers.setMain(dispatcher) + } + + override fun finished(description: Description?) { + super.finished(description) + cleanupTestCoroutines() + Dispatchers.resetMain() + } +} \ 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..413ffb4c6 --- /dev/null +++ b/app/src/test/java/com/picpay/desafio/android/presentation/viewModels/ContactViewModelTest.kt @@ -0,0 +1,41 @@ +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.providers.ContactMockProvider +import com.picpay.desafio.android.providers.ErrorMockProvider +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 listContacts = mockk(relaxed = true) + + override fun setup() { + super.setup() + viewModel = ContactViewModel(listContacts) + } + + @Test + fun loadContactsTest() { + viewModel.run { + coEvery { listContacts() } returns ContactMockProvider.mockedFlowContacts() + loadContacts() + coVerify { listContacts() } + assert(contacts.value?.isNotEmpty() ?: false) + } + } + + @Test + fun loadContactsTestError() { + viewModel.run { + coEvery { listContacts() } returns ErrorMockProvider.mockErrorFlow() + loadContacts() + coVerify { listContacts() } + assert(messages.value != null) + } + } +} \ No newline at end of file diff --git a/app/src/test/java/com/picpay/desafio/android/providers/ContactMockProvider.kt b/app/src/test/java/com/picpay/desafio/android/providers/ContactMockProvider.kt new file mode 100644 index 000000000..10916cc2a --- /dev/null +++ b/app/src/test/java/com/picpay/desafio/android/providers/ContactMockProvider.kt @@ -0,0 +1,27 @@ +package com.picpay.desafio.android.providers + +import com.picpay.desafio.android.domain.model.UserModel +import kotlinx.coroutines.flow.flowOf + +object ContactMockProvider { + + fun mockedFlowContacts() = flowOf(mockedContacts()) + private fun mockedContacts() = internalMockedContacts() + private fun internalMockedContacts() = listOf( + UserModel( + img = "https://randomuser.me/api/portraits/men/1.jpg", + name = "Eduardo Santos", + username = "@eduardo.santos" + ), + UserModel( + img = "https://randomuser.me/api/portraits/woman/2.jpg", + name = "Marina Coelho", + username = "@marina.coelho" + ), + UserModel( + img = "https://randomuser.me/api/portraits/woman/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/ErrorMockProvider.kt b/app/src/test/java/com/picpay/desafio/android/providers/ErrorMockProvider.kt new file mode 100644 index 000000000..95b4a3162 --- /dev/null +++ b/app/src/test/java/com/picpay/desafio/android/providers/ErrorMockProvider.kt @@ -0,0 +1,7 @@ +package com.picpay.desafio.android.providers + +import kotlinx.coroutines.flow.flow + +object ErrorMockProvider { + fun mockErrorFlow() = flow { emit(throw Exception()) } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 5bc8d7204..27cf0fa5b 100644 --- a/build.gradle +++ b/build.gradle @@ -18,8 +18,7 @@ buildscript { circleimageview_version = '3.0.0' junit_version = '4.13.2' - mockito_version = '2.27.0' - mockito_kotlin_version = '2.1.0' + mockk_version = '1.12.0' test_runner_version = '1.4.0' espresso_version = '3.4.0' From d919ad337e9f9d295bd610e217f47b93ec3b0ce5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Toledo=20Gonz=C3=A1lez?= Date: Thu, 16 Jun 2022 15:30:08 -0400 Subject: [PATCH 07/10] Refactoring app tests - Improving unit tests. - Setting instrumented tests. - Adjusting related files. --- app/build.gradle | 22 +++++-- .../desafio/android/MainActivityTest.kt | 64 +++---------------- .../picpay/desafio/android/PicPayAppTest.kt | 20 ++++++ .../desafio/android/PicPayAppTestRunner.kt | 14 ++++ .../android/data/server/MockWebServerRule.kt | 19 ++++++ .../picpay/desafio/android/di/TestModule.kt | 56 ++++++++++++++++ .../android/extensions/AnyExtensions.kt | 12 ++++ .../fragments/ContactFragmentTest.kt | 50 +++++++++++++++ .../providers/MockAndroidContactProvider.kt | 30 +++++++++ .../providers/MockAndroidUserProvider.kt | 31 +++++++++ .../android/providers/MockResponseProvider.kt | 15 +++++ .../RecyclerViewMatcherProvider.kt} | 16 ++--- .../desafio/android/bases/BaseActivity.kt | 4 +- .../desafio/android/bases/BaseFragment.kt | 4 +- .../desafio/android/bases/BaseViewModel.kt | 43 +++++++++++-- .../android/data/entities/UserEntity.kt | 2 +- .../desafio/android/data/mapper/UserMapper.kt | 6 +- .../desafio/android/data/remote/PicPayApi.kt | 4 -- .../data/repository/ContactDataRepository.kt | 13 ++++ .../data/repository/UserDataRepository.kt | 13 ---- .../picpay/desafio/android/di/MainModule.kt | 61 +++++++++--------- .../model/{UserModel.kt => ContactModel.kt} | 8 +-- .../domain/repository/ContactRepository.kt | 8 +++ .../domain/repository/UserRepository.kt | 8 --- .../domain/useCases/ListContactsUseCase.kt | 4 +- .../useCases/ListContactsUseCaseImpl.kt | 8 +-- .../android/extensions/ActivityExtensions.kt | 9 +-- .../android/extensions/AnyExtensions.kt | 9 +++ .../android/extensions/BooleanExtensions.kt | 5 ++ .../android/extensions/FragmentExtensions.kt | 9 +-- .../android/extensions/IntExtensions.kt | 7 ++ .../android/extensions/StringsExtensions.kt | 5 ++ .../adapters/ContactListAdapter.kt | 17 +++++ .../presentation/adapters/UserListAdapter.kt | 17 ----- .../presentation/fragments/ContactFragment.kt | 8 +-- ...Holder.kt => ContactListItemViewHolder.kt} | 14 ++-- .../viewModels/ContactViewModel.kt | 24 +++---- .../picpay/desafio/android/base/BaseTest.kt | 6 +- ...{CoroutineRule.kt => CoroutineTestRule.kt} | 10 +-- .../android/data/mapper/UserMapperTest.kt | 21 ++++++ .../useCases/ListContactsUseCaseTest.kt | 37 +++++++++++ .../viewModels/ContactViewModelTest.kt | 25 ++++---- .../android/providers/ContactMockProvider.kt | 27 -------- .../android/providers/MockContactProvider.kt | 30 +++++++++ ...orMockProvider.kt => MockErrorProvider.kt} | 2 +- .../android/providers/MockUserProvider.kt | 31 +++++++++ build.gradle | 2 + 47 files changed, 595 insertions(+), 255 deletions(-) create mode 100644 app/src/androidTest/java/com/picpay/desafio/android/PicPayAppTest.kt create mode 100644 app/src/androidTest/java/com/picpay/desafio/android/PicPayAppTestRunner.kt create mode 100644 app/src/androidTest/java/com/picpay/desafio/android/data/server/MockWebServerRule.kt create mode 100644 app/src/androidTest/java/com/picpay/desafio/android/di/TestModule.kt create mode 100644 app/src/androidTest/java/com/picpay/desafio/android/extensions/AnyExtensions.kt create mode 100644 app/src/androidTest/java/com/picpay/desafio/android/presentation/fragments/ContactFragmentTest.kt create mode 100644 app/src/androidTest/java/com/picpay/desafio/android/providers/MockAndroidContactProvider.kt create mode 100644 app/src/androidTest/java/com/picpay/desafio/android/providers/MockAndroidUserProvider.kt create mode 100644 app/src/androidTest/java/com/picpay/desafio/android/providers/MockResponseProvider.kt rename app/src/androidTest/java/com/picpay/desafio/android/{RecyclerViewMatchers.kt => providers/RecyclerViewMatcherProvider.kt} (75%) create mode 100644 app/src/main/java/com/picpay/desafio/android/data/repository/ContactDataRepository.kt delete mode 100644 app/src/main/java/com/picpay/desafio/android/data/repository/UserDataRepository.kt rename app/src/main/java/com/picpay/desafio/android/domain/model/{UserModel.kt => ContactModel.kt} (52%) create mode 100644 app/src/main/java/com/picpay/desafio/android/domain/repository/ContactRepository.kt delete mode 100644 app/src/main/java/com/picpay/desafio/android/domain/repository/UserRepository.kt create mode 100644 app/src/main/java/com/picpay/desafio/android/extensions/AnyExtensions.kt create mode 100644 app/src/main/java/com/picpay/desafio/android/extensions/BooleanExtensions.kt create mode 100644 app/src/main/java/com/picpay/desafio/android/extensions/IntExtensions.kt create mode 100644 app/src/main/java/com/picpay/desafio/android/extensions/StringsExtensions.kt create mode 100644 app/src/main/java/com/picpay/desafio/android/presentation/adapters/ContactListAdapter.kt delete mode 100644 app/src/main/java/com/picpay/desafio/android/presentation/adapters/UserListAdapter.kt rename app/src/main/java/com/picpay/desafio/android/presentation/viewHolders/{UserListItemViewHolder.kt => ContactListItemViewHolder.kt} (76%) rename app/src/test/java/com/picpay/desafio/android/base/{CoroutineRule.kt => CoroutineTestRule.kt} (71%) create mode 100644 app/src/test/java/com/picpay/desafio/android/data/mapper/UserMapperTest.kt create mode 100644 app/src/test/java/com/picpay/desafio/android/domain/useCases/ListContactsUseCaseTest.kt delete mode 100644 app/src/test/java/com/picpay/desafio/android/providers/ContactMockProvider.kt create mode 100644 app/src/test/java/com/picpay/desafio/android/providers/MockContactProvider.kt rename app/src/test/java/com/picpay/desafio/android/providers/{ErrorMockProvider.kt => MockErrorProvider.kt} (84%) create mode 100644 app/src/test/java/com/picpay/desafio/android/providers/MockUserProvider.kt diff --git a/app/build.gradle b/app/build.gradle index f3a2d6dbd..d45a23d35 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -15,8 +15,7 @@ android { versionName "1.0" vectorDrawables.useSupportLibrary = true - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunner "com.picpay.desafio.android.PicPayAppTestRunner" } buildTypes { debug {} @@ -52,34 +51,47 @@ dependencies { implementation "androidx.fragment:fragment-ktx:$fragment_ktx_version" implementation "androidx.constraintlayout:constraintlayout:$constraintlayout_version" + // Lifecycle dependencies implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" implementation "com.google.code.gson:gson:$gson_version" implementation "com.google.android.material:material:$material_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" + testImplementation "io.insert-koin:koin-test-junit4:$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" + // 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 "io.mockk:mockk:$mockk_version" - testImplementation "io.insert-koin:koin-test:$koin_version" + + debugImplementation "androidx.fragment:fragment-testing:$fragment_ktx_version" testImplementation "androidx.arch.core:core-testing:$core_testing_version" - testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_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.espresso:espresso-core:$espresso_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..451dbc746 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,24 @@ 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 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 - - @Test - fun shouldDisplayTitle() { - launchActivity().apply { - val expectedTitle = context.getString(R.string.title) - - moveToState(Lifecycle.State.RESUMED) - - onView(withText(expectedTitle)).check(matches(isDisplayed())) - } - } + @get: Rule + val rule = activityScenarioRule() @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..283135374 --- /dev/null +++ b/app/src/androidTest/java/com/picpay/desafio/android/presentation/fragments/ContactFragmentTest.kt @@ -0,0 +1,50 @@ +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.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.* +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 shouldDisplayTitle() { + onView(withId(R.id.title)).check(matches(isDisplayed())) + onView(withText(R.string.title)).check(matches(isDisplayed())) + } + + @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..9c57ee1ab --- /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/woman/2.jpg", + name = "Marina Coelho", + username = "@marina.coelho" + ), + ContactModel( + image = "https://randomuser.me/api/portraits/woman/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..3aacb6da9 --- /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/woman/2.jpg", + name = "Marina Coelho", + username = "@marina.coelho" + ), + UserEntity( + id = 3, + img = "https://randomuser.me/api/portraits/woman/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/java/com/picpay/desafio/android/bases/BaseActivity.kt b/app/src/main/java/com/picpay/desafio/android/bases/BaseActivity.kt index 4a13ccc8c..ada62ff63 100644 --- a/app/src/main/java/com/picpay/desafio/android/bases/BaseActivity.kt +++ b/app/src/main/java/com/picpay/desafio/android/bases/BaseActivity.kt @@ -3,13 +3,13 @@ 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.viewModelClass +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 = viewModelClass()) } + val viewModel: V by lazy { getViewModel(clazz = getViewModelClass()) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) 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 index 91509d7c9..832c6a5a8 100644 --- a/app/src/main/java/com/picpay/desafio/android/bases/BaseFragment.kt +++ b/app/src/main/java/com/picpay/desafio/android/bases/BaseFragment.kt @@ -6,7 +6,7 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.viewbinding.ViewBinding -import com.picpay.desafio.android.extensions.viewModelClass +import com.picpay.desafio.android.extensions.getViewModelClass import org.koin.androidx.viewmodel.ext.android.getViewModel abstract class BaseFragment : Fragment() { @@ -14,7 +14,7 @@ abstract class BaseFragment : Fragment() { abstract val binding: ViewBinding abstract fun initComponents() abstract fun initObservers() - val viewModel: V by lazy { getViewModel(clazz = viewModelClass()) } + val viewModel: V by lazy { getViewModel(clazz = getViewModelClass()) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? 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 index d1a11199c..423306f4a 100644 --- a/app/src/main/java/com/picpay/desafio/android/bases/BaseViewModel.kt +++ b/app/src/main/java/com/picpay/desafio/android/bases/BaseViewModel.kt @@ -1,10 +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 kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlin.coroutines.CoroutineContext +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(), CoroutineScope { - override val coroutineContext: CoroutineContext = Dispatchers.Main +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/data/entities/UserEntity.kt b/app/src/main/java/com/picpay/desafio/android/data/entities/UserEntity.kt index 1878752d9..59954651b 100644 --- a/app/src/main/java/com/picpay/desafio/android/data/entities/UserEntity.kt +++ b/app/src/main/java/com/picpay/desafio/android/data/entities/UserEntity.kt @@ -6,8 +6,8 @@ import kotlinx.parcelize.Parcelize @Parcelize 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 index 7d5eae667..3faa71126 100644 --- 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 @@ -1,8 +1,8 @@ package com.picpay.desafio.android.data.mapper import com.picpay.desafio.android.data.entities.UserEntity -import com.picpay.desafio.android.domain.model.UserModel +import com.picpay.desafio.android.domain.model.ContactModel -fun UserEntity.toModel() = UserModel(img = img, name = name, username = username) +fun UserEntity.toContactModel() = ContactModel(image = img, name = name, username = username) -fun List.toListModel() = map { it.toModel() } \ No newline at end of file +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 index 83551bf24..61a20e9d3 100644 --- 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 @@ -7,8 +7,4 @@ interface PicPayApi { @GET("users") suspend fun getUsers(): List - - companion object { - const val BASE_URL = "https://609a908e0f5a13001721b74e.mockapi.io/picpay/api/" - } } \ 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/data/repository/UserDataRepository.kt b/app/src/main/java/com/picpay/desafio/android/data/repository/UserDataRepository.kt deleted file mode 100644 index 27e1d224f..000000000 --- a/app/src/main/java/com/picpay/desafio/android/data/repository/UserDataRepository.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.picpay.desafio.android.data.repository - -import com.picpay.desafio.android.data.mapper.toListModel -import com.picpay.desafio.android.data.remote.UserRemoteDataSource -import com.picpay.desafio.android.domain.model.UserModel -import com.picpay.desafio.android.domain.repository.UserRepository -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map - -class UserDataRepository(private val remoteDataSource: UserRemoteDataSource) : UserRepository { - - override fun getUsers(): Flow> = remoteDataSource.getUsers().map { it.toListModel() } -} \ 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 index 4240fa761..17efde5a1 100644 --- a/app/src/main/java/com/picpay/desafio/android/di/MainModule.kt +++ b/app/src/main/java/com/picpay/desafio/android/di/MainModule.kt @@ -4,8 +4,8 @@ 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.UserDataRepository -import com.picpay.desafio.android.domain.repository.UserRepository +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 @@ -19,34 +19,37 @@ import retrofit2.converter.gson.GsonConverterFactory fun Scope.getRetrofit() = get() -val networkModule = module { - fun provideGson() = GsonBuilder().create() - fun provideOkHttp() = OkHttpClient.Builder().build() - fun provideRetrofit(okHttp: OkHttpClient, gson: Gson) = Retrofit.Builder() - .baseUrl(PicPayApi.BASE_URL) - .client(okHttp) - .addConverterFactory(GsonConverterFactory.create(gson)) - .build() - single { provideGson() } - single { provideOkHttp() } - single { provideRetrofit(get(), get()) } - single { getRetrofit().create(PicPayApi::class.java) } -} - -val dataModule = module { - single { UserRemoteDataSource(get()) } - single { UserDataRepository(get()) } -} - -val useCasesModule = module { - factory { ListContactsUseCaseImpl(get()) } -} - -val viewModelsModule = module { - viewModel { ContactViewModel(get()) } -} - object MainModuleInitializer { + + private val networkModule = module { + fun provideBaseUrl() = "https://609a908e0f5a13001721b74e.mockapi.io/picpay/api/" + 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) ) diff --git a/app/src/main/java/com/picpay/desafio/android/domain/model/UserModel.kt b/app/src/main/java/com/picpay/desafio/android/domain/model/ContactModel.kt similarity index 52% rename from app/src/main/java/com/picpay/desafio/android/domain/model/UserModel.kt rename to app/src/main/java/com/picpay/desafio/android/domain/model/ContactModel.kt index 233842db7..915f25555 100644 --- a/app/src/main/java/com/picpay/desafio/android/domain/model/UserModel.kt +++ b/app/src/main/java/com/picpay/desafio/android/domain/model/ContactModel.kt @@ -2,17 +2,17 @@ package com.picpay.desafio.android.domain.model import androidx.recyclerview.widget.DiffUtil -data class UserModel(val img: String, val name: String, val username: String) { +data class ContactModel(val image: String, val name: String, val username: String) { companion object { - val DIFF_UTIL_CALLBACK = object : DiffUtil.ItemCallback() { + val DIFF_UTIL_CALLBACK = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: UserModel, newItem: UserModel): Boolean { + override fun areItemsTheSame(oldItem: ContactModel, newItem: ContactModel): Boolean { return oldItem.username == newItem.username } - override fun areContentsTheSame(oldItem: UserModel, newItem: UserModel): Boolean { + 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/repository/UserRepository.kt b/app/src/main/java/com/picpay/desafio/android/domain/repository/UserRepository.kt deleted file mode 100644 index 198b358db..000000000 --- a/app/src/main/java/com/picpay/desafio/android/domain/repository/UserRepository.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.picpay.desafio.android.domain.repository - -import com.picpay.desafio.android.domain.model.UserModel -import kotlinx.coroutines.flow.Flow - -interface UserRepository { - fun getUsers(): 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 index e9506dcbf..071cf1a39 100644 --- 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 @@ -1,8 +1,8 @@ package com.picpay.desafio.android.domain.useCases -import com.picpay.desafio.android.domain.model.UserModel +import com.picpay.desafio.android.domain.model.ContactModel import kotlinx.coroutines.flow.Flow interface ListContactsUseCase { - operator fun invoke(): Flow> + 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 index 3553f1a49..4cab44a2b 100644 --- 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 @@ -1,9 +1,9 @@ package com.picpay.desafio.android.domain.useCases -import com.picpay.desafio.android.domain.model.UserModel -import com.picpay.desafio.android.domain.repository.UserRepository +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 userRepository: UserRepository) : ListContactsUseCase { - override fun invoke(): Flow> = userRepository.getUsers() +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 index f2501e660..327192c17 100644 --- a/app/src/main/java/com/picpay/desafio/android/extensions/ActivityExtensions.kt +++ b/app/src/main/java/com/picpay/desafio/android/extensions/ActivityExtensions.kt @@ -3,16 +3,9 @@ package com.picpay.desafio.android.extensions import android.view.LayoutInflater import androidx.appcompat.app.AppCompatActivity import androidx.viewbinding.ViewBinding -import java.lang.reflect.ParameterizedType -import kotlin.reflect.KClass inline fun AppCompatActivity.viewBinding( crossinline bindingInflater: (LayoutInflater) -> T ) = lazy(LazyThreadSafetyMode.NONE) { bindingInflater.invoke(layoutInflater) } -@Suppress("UNCHECKED_CAST") -internal fun AppCompatActivity.viewModelClass(): KClass { - val type = javaClass.genericSuperclass as ParameterizedType - val result = type.actualTypeArguments[0] as Class - return result.kotlin -} \ No newline at end of file +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/FragmentExtensions.kt b/app/src/main/java/com/picpay/desafio/android/extensions/FragmentExtensions.kt index 9d53e5617..17de78aaf 100644 --- a/app/src/main/java/com/picpay/desafio/android/extensions/FragmentExtensions.kt +++ b/app/src/main/java/com/picpay/desafio/android/extensions/FragmentExtensions.kt @@ -3,16 +3,9 @@ package com.picpay.desafio.android.extensions import android.view.LayoutInflater import androidx.fragment.app.Fragment import androidx.viewbinding.ViewBinding -import java.lang.reflect.ParameterizedType -import kotlin.reflect.KClass inline fun Fragment.viewBinding( crossinline bindingInflater: (LayoutInflater) -> T ) = lazy(LazyThreadSafetyMode.NONE) { bindingInflater.invoke(layoutInflater) } -@Suppress("UNCHECKED_CAST") -internal fun Fragment.viewModelClass(): KClass { - val type = javaClass.genericSuperclass as ParameterizedType - val result = type.actualTypeArguments[0] as Class - return result.kotlin -} \ No newline at end of file +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/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/adapters/UserListAdapter.kt b/app/src/main/java/com/picpay/desafio/android/presentation/adapters/UserListAdapter.kt deleted file mode 100644 index 589688a33..000000000 --- a/app/src/main/java/com/picpay/desafio/android/presentation/adapters/UserListAdapter.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.picpay.desafio.android.presentation.adapters - -import android.view.ViewGroup -import androidx.recyclerview.widget.ListAdapter -import com.picpay.desafio.android.domain.model.UserModel -import com.picpay.desafio.android.presentation.viewHolders.UserListItemViewHolder - -class UserListAdapter : ListAdapter(UserModel.DIFF_UTIL_CALLBACK) { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserListItemViewHolder { - return UserListItemViewHolder.newInstance(parent) - } - - override fun onBindViewHolder(holder: UserListItemViewHolder, 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 index 304edd610..3e3a80624 100644 --- 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 @@ -7,13 +7,13 @@ import com.picpay.desafio.android.bases.BaseFragment import com.picpay.desafio.android.databinding.FragContactBinding import com.picpay.desafio.android.extensions.showToastShortText import com.picpay.desafio.android.extensions.viewBinding -import com.picpay.desafio.android.presentation.adapters.UserListAdapter +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: UserListAdapter by lazy { UserListAdapter() } + private val listAdapter: ContactListAdapter by lazy { ContactListAdapter() } override fun initComponents() { with(binding) { @@ -27,9 +27,9 @@ class ContactFragment : BaseFragment() { override fun initObservers() { with(viewModel) { isLoading.observe(viewLifecycleOwner) { binding.userListProgressBar.isVisible = it } - messages.observe(viewLifecycleOwner) { + messageResource.observe(viewLifecycleOwner) { resource -> binding.recyclerView.visibility = View.GONE - this@ContactFragment.context?.showToastShortText(getString(it)) + resource?.let { this@ContactFragment.context?.showToastShortText(getString(it)) } } contacts.observe(viewLifecycleOwner) { binding.recyclerView.visibility = View.VISIBLE diff --git a/app/src/main/java/com/picpay/desafio/android/presentation/viewHolders/UserListItemViewHolder.kt b/app/src/main/java/com/picpay/desafio/android/presentation/viewHolders/ContactListItemViewHolder.kt similarity index 76% rename from app/src/main/java/com/picpay/desafio/android/presentation/viewHolders/UserListItemViewHolder.kt rename to app/src/main/java/com/picpay/desafio/android/presentation/viewHolders/ContactListItemViewHolder.kt index da9e43b6f..b210f2455 100644 --- a/app/src/main/java/com/picpay/desafio/android/presentation/viewHolders/UserListItemViewHolder.kt +++ b/app/src/main/java/com/picpay/desafio/android/presentation/viewHolders/ContactListItemViewHolder.kt @@ -5,22 +5,22 @@ 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.UserModel +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 UserListItemViewHolder( +class ContactListItemViewHolder( private val binding: ListItemUserBinding ) : RecyclerView.ViewHolder(binding.root) { - fun bind(userModel: UserModel) { + fun bind(contactModel: ContactModel) { with(binding) { - name.text = userModel.name - username.text = userModel.username + name.text = contactModel.name + username.text = contactModel.username progressBar.visibility = View.VISIBLE Picasso.get() - .load(userModel.img) + .load(contactModel.image) .error(R.drawable.ic_round_account_circle) .into(picture, object : Callback { override fun onSuccess() { @@ -36,7 +36,7 @@ class UserListItemViewHolder( companion object { - fun newInstance(parent: ViewGroup) = UserListItemViewHolder( + fun newInstance(parent: ViewGroup) = ContactListItemViewHolder( ListItemUserBinding.inflate(parent.context.toLayoutInflater(), parent, false) ) } 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 index 626d341de..2c1cea179 100644 --- 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 @@ -2,9 +2,10 @@ 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.UserModel +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 @@ -14,24 +15,15 @@ import kotlinx.coroutines.launch class ContactViewModel(private val listContactsUseCase: ListContactsUseCase) : BaseViewModel() { - private val _isLoading: MutableLiveData = MutableLiveData(false) - val isLoading: LiveData get() = _isLoading - - private val _messages: MutableLiveData = MutableLiveData() - val messages: LiveData get() = _messages - - private val _contacts: MutableLiveData> = MutableLiveData() - val contacts: LiveData> get() = _contacts + private val _contacts: MutableLiveData> = MutableLiveData() + val contacts: LiveData> get() = _contacts fun loadContacts() { - launch { + viewModelScope.launch { listContactsUseCase() - .onStart { _isLoading.postValue(true) } - .catch { - _isLoading.postValue(false) - _messages.postValue(R.string.error) - } - .onCompletion { _isLoading.postValue(false) } + .onStart { startLoading() } + .catch { setMessageResource(R.string.error) } + .onCompletion { stopLoading() } .collect { _contacts.postValue(it) } } } 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 index c0338ef52..6ce2e1ecf 100644 --- a/app/src/test/java/com/picpay/desafio/android/base/BaseTest.kt +++ b/app/src/test/java/com/picpay/desafio/android/base/BaseTest.kt @@ -2,17 +2,17 @@ package com.picpay.desafio.android.base import androidx.arch.core.executor.testing.InstantTaskExecutorRule import io.mockk.MockKAnnotations -import kotlinx.coroutines.flow.flow +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 { +abstract class BaseTest : TestCase() { @get: Rule - val coroutineRule = CoroutineRule() + val coroutineRule = CoroutineTestRule() @get: Rule val rule = InstantTaskExecutorRule() diff --git a/app/src/test/java/com/picpay/desafio/android/base/CoroutineRule.kt b/app/src/test/java/com/picpay/desafio/android/base/CoroutineTestRule.kt similarity index 71% rename from app/src/test/java/com/picpay/desafio/android/base/CoroutineRule.kt rename to app/src/test/java/com/picpay/desafio/android/base/CoroutineTestRule.kt index 9c39d15d5..fa618693f 100644 --- a/app/src/test/java/com/picpay/desafio/android/base/CoroutineRule.kt +++ b/app/src/test/java/com/picpay/desafio/android/base/CoroutineTestRule.kt @@ -3,16 +3,16 @@ package com.picpay.desafio.android.base import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineScope import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain import org.junit.rules.TestWatcher import org.junit.runner.Description @ExperimentalCoroutinesApi -class CoroutineRule( - private val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher() -) : TestWatcher(), TestCoroutineScope by TestCoroutineScope(dispatcher) { +class CoroutineTestRule : TestWatcher() { + + private val dispatcher = TestCoroutineDispatcher() + override fun starting(description: Description?) { super.starting(description) Dispatchers.setMain(dispatcher) @@ -20,7 +20,7 @@ class CoroutineRule( override fun finished(description: Description?) { super.finished(description) - cleanupTestCoroutines() + 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 index 413ffb4c6..ff1811a8a 100644 --- 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 @@ -2,8 +2,9 @@ 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.providers.ContactMockProvider -import com.picpay.desafio.android.providers.ErrorMockProvider +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 @@ -12,30 +13,30 @@ import org.junit.Test class ContactViewModelTest : BaseTest() { private lateinit var viewModel: ContactViewModel - private val listContacts = mockk(relaxed = true) + private val listContactsUseCase = mockk(relaxed = true) override fun setup() { super.setup() - viewModel = ContactViewModel(listContacts) + viewModel = ContactViewModel(listContactsUseCase) } @Test - fun loadContactsTest() { + fun shouldLoadingContacts() { viewModel.run { - coEvery { listContacts() } returns ContactMockProvider.mockedFlowContacts() + coEvery { listContactsUseCase() } returns MockContactProvider.mockedFlowContacts() loadContacts() - coVerify { listContacts() } - assert(contacts.value?.isNotEmpty() ?: false) + coVerify { listContactsUseCase() } + assertTrue(contacts.value?.isNotEmpty().orFalse()) } } @Test - fun loadContactsTestError() { + fun shouldNotLoadContacts() { viewModel.run { - coEvery { listContacts() } returns ErrorMockProvider.mockErrorFlow() + coEvery { listContactsUseCase() } returns MockErrorProvider.mockErrorFlow() loadContacts() - coVerify { listContacts() } - assert(messages.value != null) + coVerify { listContactsUseCase() } + assertNotNull(messageResource.value) } } } \ No newline at end of file diff --git a/app/src/test/java/com/picpay/desafio/android/providers/ContactMockProvider.kt b/app/src/test/java/com/picpay/desafio/android/providers/ContactMockProvider.kt deleted file mode 100644 index 10916cc2a..000000000 --- a/app/src/test/java/com/picpay/desafio/android/providers/ContactMockProvider.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.picpay.desafio.android.providers - -import com.picpay.desafio.android.domain.model.UserModel -import kotlinx.coroutines.flow.flowOf - -object ContactMockProvider { - - fun mockedFlowContacts() = flowOf(mockedContacts()) - private fun mockedContacts() = internalMockedContacts() - private fun internalMockedContacts() = listOf( - UserModel( - img = "https://randomuser.me/api/portraits/men/1.jpg", - name = "Eduardo Santos", - username = "@eduardo.santos" - ), - UserModel( - img = "https://randomuser.me/api/portraits/woman/2.jpg", - name = "Marina Coelho", - username = "@marina.coelho" - ), - UserModel( - img = "https://randomuser.me/api/portraits/woman/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/MockContactProvider.kt b/app/src/test/java/com/picpay/desafio/android/providers/MockContactProvider.kt new file mode 100644 index 000000000..28eb7606a --- /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/woman/2.jpg", + name = "Marina Coelho", + username = "@marina.coelho" + ), + ContactModel( + image = "https://randomuser.me/api/portraits/woman/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/ErrorMockProvider.kt b/app/src/test/java/com/picpay/desafio/android/providers/MockErrorProvider.kt similarity index 84% rename from app/src/test/java/com/picpay/desafio/android/providers/ErrorMockProvider.kt rename to app/src/test/java/com/picpay/desafio/android/providers/MockErrorProvider.kt index 95b4a3162..2c4b9252a 100644 --- a/app/src/test/java/com/picpay/desafio/android/providers/ErrorMockProvider.kt +++ b/app/src/test/java/com/picpay/desafio/android/providers/MockErrorProvider.kt @@ -2,6 +2,6 @@ package com.picpay.desafio.android.providers import kotlinx.coroutines.flow.flow -object ErrorMockProvider { +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..87dbcc666 --- /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/woman/2.jpg", + name = "Marina Coelho", + username = "@marina.coelho" + ), + UserEntity( + id = 3, + img = "https://randomuser.me/api/portraits/woman/3.jpg", + name = "Márcia Silva", + username = "@marcia.silva" + ) + ) +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 27cf0fa5b..1b553ac4e 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,9 @@ buildscript { junit_version = '4.13.2' mockk_version = '1.12.0' + 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.2.3" From 96dee2712558bfbac6d4fb67cc8afc3a56805618 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Toledo=20Gonz=C3=A1lez?= Date: Thu, 16 Jun 2022 16:39:12 -0400 Subject: [PATCH 08/10] Setting network response cache --- .../picpay/desafio/android/di/MainModule.kt | 54 +++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) 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 index 17efde5a1..a21fb15a4 100644 --- a/app/src/main/java/com/picpay/desafio/android/di/MainModule.kt +++ b/app/src/main/java/com/picpay/desafio/android/di/MainModule.kt @@ -1,5 +1,6 @@ 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 @@ -9,30 +10,77 @@ 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 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 provideOkHttp() = OkHttpClient.Builder().build() + + 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 { provideOkHttp() } + single { provideCache(get()) } + single { provideOkHttp(get()) } single { provideRetrofit(get(), get(), get()) } single { getRetrofit().create(PicPayApi::class.java) } } From 0bf205d1f0512ee476bb43bfae44b13afc555d9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Toledo=20Gonz=C3=A1lez?= Date: Thu, 16 Jun 2022 22:06:28 -0400 Subject: [PATCH 09/10] Add title on main activity and set swipe refresh layout on contacts fragment --- app/build.gradle | 1 + .../desafio/android/MainActivityTest.kt | 7 ++++ .../fragments/ContactFragmentTest.kt | 10 +---- .../providers/MockAndroidContactProvider.kt | 4 +- .../providers/MockAndroidUserProvider.kt | 4 +- .../SwipeRefreshLayoutExtensions.kt | 18 +++++++++ .../presentation/fragments/ContactFragment.kt | 8 ++-- app/src/main/res/layout/activity_main.xml | 16 +++++++- app/src/main/res/layout/frag_contact.xml | 40 ++++--------------- .../android/providers/MockContactProvider.kt | 4 +- .../android/providers/MockUserProvider.kt | 4 +- build.gradle | 1 + 12 files changed, 61 insertions(+), 56 deletions(-) create mode 100644 app/src/main/java/com/picpay/desafio/android/extensions/SwipeRefreshLayoutExtensions.kt diff --git a/app/build.gradle b/app/build.gradle index d45a23d35..d2035f877 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -49,6 +49,7 @@ dependencies { 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" // Lifecycle dependencies 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 451dbc746..394fc27fd 100644 --- a/app/src/androidTest/java/com/picpay/desafio/android/MainActivityTest.kt +++ b/app/src/androidTest/java/com/picpay/desafio/android/MainActivityTest.kt @@ -2,6 +2,7 @@ package com.picpay.desafio.android 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.withId import androidx.test.ext.junit.rules.activityScenarioRule @@ -16,6 +17,12 @@ class MainActivityTest { @get: Rule val rule = activityScenarioRule() + @Test + fun shouldDisplayTitle() { + onView(withId(R.id.title)).check(matches(isDisplayed())) + onView(ViewMatchers.withText(R.string.title)).check(matches(isDisplayed())) + } + @Test fun shouldDisplayFragmentContainer() { onView(withId(R.id.fcvContainer)).check(matches(isDisplayed())) 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 index 283135374..a74b3462c 100644 --- 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 @@ -3,9 +3,7 @@ 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.Espresso.onView -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.ViewMatchers.* +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 @@ -32,12 +30,6 @@ class ContactFragmentTest { scenario.moveToState(Lifecycle.State.RESUMED) } - @Test - fun shouldDisplayTitle() { - onView(withId(R.id.title)).check(matches(isDisplayed())) - onView(withText(R.string.title)).check(matches(isDisplayed())) - } - @Test fun shouldDisplayListItemText() { RecyclerViewMatcherProvider.checkRecyclerViewItem( 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 index 9c57ee1ab..bbcf1ab61 100644 --- a/app/src/androidTest/java/com/picpay/desafio/android/providers/MockAndroidContactProvider.kt +++ b/app/src/androidTest/java/com/picpay/desafio/android/providers/MockAndroidContactProvider.kt @@ -17,12 +17,12 @@ object MockAndroidContactProvider { private fun internalMockedContacts() = listOf( internalMockedContact(), ContactModel( - image = "https://randomuser.me/api/portraits/woman/2.jpg", + image = "https://randomuser.me/api/portraits/women/2.jpg", name = "Marina Coelho", username = "@marina.coelho" ), ContactModel( - image = "https://randomuser.me/api/portraits/woman/3.jpg", + image = "https://randomuser.me/api/portraits/women/3.jpg", name = "Márcia Silva", username = "@marcia.silva" ) 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 index 3aacb6da9..904acf9d1 100644 --- a/app/src/androidTest/java/com/picpay/desafio/android/providers/MockAndroidUserProvider.kt +++ b/app/src/androidTest/java/com/picpay/desafio/android/providers/MockAndroidUserProvider.kt @@ -17,13 +17,13 @@ object MockAndroidUserProvider { internalMockedUser(), UserEntity( id = 2, - img = "https://randomuser.me/api/portraits/woman/2.jpg", + img = "https://randomuser.me/api/portraits/women/2.jpg", name = "Marina Coelho", username = "@marina.coelho" ), UserEntity( id = 3, - img = "https://randomuser.me/api/portraits/woman/3.jpg", + img = "https://randomuser.me/api/portraits/women/3.jpg", name = "Márcia Silva", username = "@marcia.silva" ) 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/fragments/ContactFragment.kt b/app/src/main/java/com/picpay/desafio/android/presentation/fragments/ContactFragment.kt index 3e3a80624..47f33714d 100644 --- 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 @@ -1,12 +1,10 @@ package com.picpay.desafio.android.presentation.fragments import android.view.View -import androidx.core.view.isVisible 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.showToastShortText -import com.picpay.desafio.android.extensions.viewBinding +import com.picpay.desafio.android.extensions.* import com.picpay.desafio.android.presentation.adapters.ContactListAdapter import com.picpay.desafio.android.presentation.viewModels.ContactViewModel @@ -17,6 +15,8 @@ class ContactFragment : BaseFragment() { override fun initComponents() { with(binding) { + srlContent.setTheme() + srlContent.setOnRefreshListener(viewModel::loadContacts) recyclerView.apply { adapter = listAdapter layoutManager = LinearLayoutManager(context) @@ -26,7 +26,7 @@ class ContactFragment : BaseFragment() { override fun initObservers() { with(viewModel) { - isLoading.observe(viewLifecycleOwner) { binding.userListProgressBar.isVisible = it } + 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)) } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 8395efc9e..e0cc2748f 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -7,14 +7,26 @@ android:background="@color/colorPrimaryDark" tools:context=".MainActivity"> + + + app:layout_constraintTop_toBottomOf="@id/title" /> \ 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 index c7e248f31..14c01bf11 100644 --- a/app/src/main/res/layout/frag_contact.xml +++ b/app/src/main/res/layout/frag_contact.xml @@ -1,29 +1,16 @@ - - - - + android:layout_height="match_parent"> - - - + - \ No newline at end of file + \ 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 index 28eb7606a..3cac05874 100644 --- a/app/src/test/java/com/picpay/desafio/android/providers/MockContactProvider.kt +++ b/app/src/test/java/com/picpay/desafio/android/providers/MockContactProvider.kt @@ -17,12 +17,12 @@ object MockContactProvider { private fun internalMockedContacts() = listOf( internalMockedContact(), ContactModel( - image = "https://randomuser.me/api/portraits/woman/2.jpg", + image = "https://randomuser.me/api/portraits/women/2.jpg", name = "Marina Coelho", username = "@marina.coelho" ), ContactModel( - image = "https://randomuser.me/api/portraits/woman/3.jpg", + image = "https://randomuser.me/api/portraits/women/3.jpg", name = "Márcia Silva", username = "@marcia.silva" ) 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 index 87dbcc666..93bbb333e 100644 --- a/app/src/test/java/com/picpay/desafio/android/providers/MockUserProvider.kt +++ b/app/src/test/java/com/picpay/desafio/android/providers/MockUserProvider.kt @@ -17,13 +17,13 @@ object MockUserProvider { internalMockedUser(), UserEntity( id = 2, - img = "https://randomuser.me/api/portraits/woman/2.jpg", + img = "https://randomuser.me/api/portraits/women/2.jpg", name = "Marina Coelho", username = "@marina.coelho" ), UserEntity( id = 3, - img = "https://randomuser.me/api/portraits/woman/3.jpg", + img = "https://randomuser.me/api/portraits/women/3.jpg", name = "Márcia Silva", username = "@marcia.silva" ) diff --git a/build.gradle b/build.gradle index 1b553ac4e..260a61a0d 100644 --- a/build.gradle +++ b/build.gradle @@ -6,6 +6,7 @@ buildscript { kotlin_version = '1.6.21' 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' From 7d2145f72af825238af48ced44a390f940d63ac5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Toledo=20Gonz=C3=A1lez?= Date: Thu, 16 Jun 2022 22:13:05 -0400 Subject: [PATCH 10/10] Fix unit test --- app/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index d2035f877..1d7d1ab59 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -64,7 +64,6 @@ dependencies { 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" - testImplementation "io.insert-koin:koin-test-junit4:$koin_version" // Coroutines dependencies implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"