├── app ├── .gitignore ├── src │ ├── main │ │ ├── ic_launcher-web.png │ │ ├── res │ │ │ ├── font │ │ │ │ └── nunito_regular.ttf │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── values │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── dimens.xml │ │ │ │ ├── styles.xml │ │ │ │ └── strings.xml │ │ │ ├── drawable │ │ │ │ ├── splash_screen.xml │ │ │ │ ├── ic_back_to_top.xml │ │ │ │ ├── ic_close.xml │ │ │ │ ├── ic_search.xml │ │ │ │ ├── ic_octocat.xml │ │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── anim │ │ │ │ ├── layout_animation_fall_down.xml │ │ │ │ ├── slide_in_right.xml │ │ │ │ ├── slide_out_left.xml │ │ │ │ └── item_animation_fall_down.xml │ │ │ ├── layout │ │ │ │ ├── empty_view_holder_layout.xml │ │ │ │ ├── user_repositories_view_holder_layout.xml │ │ │ │ ├── loading_view_holder_layout.xml │ │ │ │ ├── custom_snackbar_layout.xml │ │ │ │ ├── header_view_holder_layout.xml │ │ │ │ ├── retry_view_holder_layout.xml │ │ │ │ ├── end_of_results_view_holder_layout.xml │ │ │ │ ├── profile_view_holder_layout.xml │ │ │ │ └── activity_profile_listing.xml │ │ │ ├── values-hdpi │ │ │ │ └── dimens.xml │ │ │ ├── values-mdpi │ │ │ │ └── dimens.xml │ │ │ ├── values-xhdpi │ │ │ │ └── dimens.xml │ │ │ ├── values-xxhdpi │ │ │ │ └── dimens.xml │ │ │ ├── values-xxxhdpi │ │ │ │ └── dimens.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── values-v29 │ │ │ │ └── styles.xml │ │ │ └── values-pt │ │ │ │ └── strings.xml │ │ ├── java │ │ │ └── githubprofilesearcher │ │ │ │ └── caiodev │ │ │ │ └── com │ │ │ │ └── br │ │ │ │ └── githubprofilesearcher │ │ │ │ └── sections │ │ │ │ ├── utils │ │ │ │ ├── base │ │ │ │ │ ├── states │ │ │ │ │ │ ├── Error.kt │ │ │ │ │ │ ├── Success.kt │ │ │ │ │ │ ├── Intermediate.kt │ │ │ │ │ │ ├── State.kt │ │ │ │ │ │ ├── Connect.kt │ │ │ │ │ │ ├── Generic.kt │ │ │ │ │ │ ├── ClientSide.kt │ │ │ │ │ │ ├── ServerSide.kt │ │ │ │ │ │ ├── Available.kt │ │ │ │ │ │ ├── Connection.kt │ │ │ │ │ │ ├── InitialError.kt │ │ │ │ │ │ ├── SSLHandshake.kt │ │ │ │ │ │ ├── SocketTimeout.kt │ │ │ │ │ │ ├── UnknownHost.kt │ │ │ │ │ │ ├── InitialSuccess.kt │ │ │ │ │ │ ├── Restoration.kt │ │ │ │ │ │ ├── Unavailable.kt │ │ │ │ │ │ ├── SearchLimitReached.kt │ │ │ │ │ │ ├── SearchQuotaReached.kt │ │ │ │ │ │ ├── ActionNotRequired.kt │ │ │ │ │ │ ├── InitialConnection.kt │ │ │ │ │ │ ├── LocalPopulation.kt │ │ │ │ │ │ ├── SuccessWithoutBody.kt │ │ │ │ │ │ ├── InitialIntermediate.kt │ │ │ │ │ │ └── SuccessWithBody.kt │ │ │ │ │ ├── string │ │ │ │ │ │ └── String.kt │ │ │ │ │ ├── interfaces │ │ │ │ │ │ ├── OnItemClicked.kt │ │ │ │ │ │ ├── LifecycleOwnerFlow.kt │ │ │ │ │ │ ├── Database.kt │ │ │ │ │ │ └── ILocalRepository.kt │ │ │ │ │ ├── repository │ │ │ │ │ │ ├── local │ │ │ │ │ │ │ ├── dataStore │ │ │ │ │ │ │ │ └── manager │ │ │ │ │ │ │ │ │ ├── IKeyValueStorageManager.kt │ │ │ │ │ │ │ │ │ └── KeyValueStorageManager.kt │ │ │ │ │ │ │ ├── db │ │ │ │ │ │ │ │ └── AppDatabase.kt │ │ │ │ │ │ │ └── LocalRepository.kt │ │ │ │ │ │ └── remote │ │ │ │ │ │ │ └── RemoteRepository.kt │ │ │ │ │ └── di │ │ │ │ │ │ └── GlobalModule.kt │ │ │ │ ├── cast │ │ │ │ │ └── ValueCasting.kt │ │ │ │ ├── extensions │ │ │ │ │ ├── Flow.kt │ │ │ │ │ ├── ViewModel.kt │ │ │ │ │ └── View.kt │ │ │ │ ├── delay │ │ │ │ │ └── Delay.kt │ │ │ │ ├── customViews │ │ │ │ │ └── snackBar │ │ │ │ │ │ ├── CustomContentViewCallback.kt │ │ │ │ │ │ └── CustomSnackBar.kt │ │ │ │ ├── rest │ │ │ │ │ └── APIConnector.kt │ │ │ │ ├── init │ │ │ │ │ └── App.kt │ │ │ │ └── network │ │ │ │ │ └── NetworkChecking.kt │ │ │ │ ├── profile │ │ │ │ ├── view │ │ │ │ │ ├── viewHolder │ │ │ │ │ │ ├── OnItemSelectedListener.kt │ │ │ │ │ │ ├── transientItemViews │ │ │ │ │ │ │ ├── EmptyViewHolder.kt │ │ │ │ │ │ │ ├── LoadingViewHolder.kt │ │ │ │ │ │ │ ├── EndOfResultsViewHolder.kt │ │ │ │ │ │ │ └── RetryViewHolder.kt │ │ │ │ │ │ ├── HeaderViewHolder.kt │ │ │ │ │ │ └── ProfileInformationViewHolder.kt │ │ │ │ │ └── adapter │ │ │ │ │ │ ├── ProfileAdapter.kt │ │ │ │ │ │ ├── HeaderAdapter.kt │ │ │ │ │ │ └── TransientViewsAdapter.kt │ │ │ │ ├── model │ │ │ │ │ ├── Profile.kt │ │ │ │ │ ├── repository │ │ │ │ │ │ ├── remote │ │ │ │ │ │ │ ├── IProfileRepository.kt │ │ │ │ │ │ │ └── ProfileRepository.kt │ │ │ │ │ │ └── local │ │ │ │ │ │ │ ├── dao │ │ │ │ │ │ │ └── ProfileDao.kt │ │ │ │ │ │ │ └── dataStore │ │ │ │ │ │ │ └── serializer │ │ │ │ │ │ │ └── ProfileSerializer.kt │ │ │ │ │ ├── callInterface │ │ │ │ │ │ └── UserProfile.kt │ │ │ │ │ ├── UserProfile.kt │ │ │ │ │ └── di │ │ │ │ │ │ └── UserProfileModule.kt │ │ │ │ └── viewModel │ │ │ │ │ └── ProfileViewModel.kt │ │ │ │ └── repository │ │ │ │ ├── viewModel │ │ │ │ └── RepositoryInformationViewModel.kt │ │ │ │ └── model │ │ │ │ ├── repository │ │ │ │ ├── IRepoInformationRepository.kt │ │ │ │ └── RepoInformationRepository.kt │ │ │ │ ├── diModules │ │ │ │ └── GithubUserRepositoryModule.kt │ │ │ │ ├── RepositoryInformation.kt │ │ │ │ └── callInterface │ │ │ │ └── UserRepository.kt │ │ ├── proto │ │ │ └── profile.proto │ │ └── AndroidManifest.xml │ ├── sharedTest │ │ └── java │ │ │ └── utils │ │ │ └── base │ │ │ ├── TestSteps.kt │ │ │ ├── coroutines │ │ │ ├── junit5 │ │ │ │ └── CoroutinesTestExtension.kt │ │ │ └── junit4 │ │ │ │ └── CoroutinesTestRule.kt │ │ │ └── api │ │ │ ├── factory │ │ │ └── RetrofitTestService.kt │ │ │ └── MockedAPIResponseProvider.kt │ ├── test │ │ └── java │ │ │ └── githubprofilesearcher │ │ │ └── caiodev │ │ │ └── com │ │ │ └── br │ │ │ └── githubprofilesearcher │ │ │ └── sections │ │ │ ├── utils │ │ │ └── cast │ │ │ │ └── ValueCastingTest.kt │ │ │ └── profile │ │ │ └── unit │ │ │ ├── viewModel │ │ │ ├── fakes │ │ │ │ └── repository │ │ │ │ │ ├── remote │ │ │ │ │ ├── FakeProfileInformationRepository.kt │ │ │ │ │ └── FakeRemoteRepository.kt │ │ │ │ │ └── local │ │ │ │ │ └── FakeLocalRepository.kt │ │ │ └── ProfileViewModelTest.kt │ │ │ └── utils │ │ │ ├── base │ │ │ └── repository │ │ │ │ ├── local │ │ │ │ ├── fakes │ │ │ │ │ ├── protoDataStore │ │ │ │ │ │ └── manager │ │ │ │ │ │ │ └── FakeKeyValueStorageManager.kt │ │ │ │ │ └── database │ │ │ │ │ │ └── FakeDatabase.kt │ │ │ │ └── LocalRepositoryTest.kt │ │ │ │ └── remote │ │ │ │ └── RemoteRepositoryTest.kt │ │ │ └── delay │ │ │ └── DelayTest.kt │ └── androidTest │ │ └── java │ │ ├── espresso │ │ └── view │ │ │ └── GithubProfileInfoObtainmentActivityEspressoTest.kt │ │ └── instrumented │ │ └── network │ │ └── NetworkCheckingTest.kt ├── proguard-rules.pro ├── schemas │ └── githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.repository.local.db.AppDatabase │ │ └── 1.json └── build.gradle ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .idea ├── caches │ └── build_file_checksums.ser ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── vcs.xml ├── compiler.xml ├── detekt.xml ├── deploymentTargetDropDown.xml ├── gradle.xml ├── dictionaries │ └── unknown.xml ├── misc.xml ├── modules.xml └── jarRepositories.xml ├── tools ├── codeChecking.gradle ├── detekt.gradle ├── lint.gradle ├── protobuf.gradle └── coverage.gradle ├── .gitignore ├── README.md ├── gradle.properties ├── LICENSE.md ├── gradlew.bat ├── gradlew └── config └── detekt └── detekt.yml /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = "GithubProfileSearcher" 2 | include ':app' -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caiodev/GithubProfileSearcher/HEAD/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caiodev/GithubProfileSearcher/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.idea/caches/build_file_checksums.ser: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caiodev/GithubProfileSearcher/HEAD/.idea/caches/build_file_checksums.ser -------------------------------------------------------------------------------- /app/src/main/res/font/nunito_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caiodev/GithubProfileSearcher/HEAD/app/src/main/res/font/nunito_regular.ttf -------------------------------------------------------------------------------- /tools/codeChecking.gradle: -------------------------------------------------------------------------------- 1 | task codeChecking(dependsOn: ['clean', 'ktlintCheck', 'ktlintFormat', 'detekt', 'jacocoReport']) //, 'connectedAndroidTest' -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caiodev/GithubProfileSearcher/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caiodev/GithubProfileSearcher/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caiodev/GithubProfileSearcher/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caiodev/GithubProfileSearcher/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caiodev/GithubProfileSearcher/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caiodev/GithubProfileSearcher/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caiodev/GithubProfileSearcher/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caiodev/GithubProfileSearcher/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caiodev/GithubProfileSearcher/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caiodev/GithubProfileSearcher/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/splash_screen.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/states/Error.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states 2 | 3 | object Error 4 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/states/Success.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states 2 | 3 | object Success 4 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/states/Intermediate.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states 2 | 3 | object Intermediate 4 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/states/State.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states 2 | 3 | sealed interface State 4 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/states/Connect.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states 2 | 3 | object Connect : State 4 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/states/Generic.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states 2 | 3 | object Generic : State 4 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/states/ClientSide.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states 2 | 3 | object ClientSide : State 4 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/states/ServerSide.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states 2 | 3 | object ServerSide : State 4 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/states/Available.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states 2 | 3 | object Available : State 4 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/states/Connection.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states 2 | 3 | object Connection : State 4 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/states/InitialError.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states 2 | 3 | object InitialError : State 4 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/states/SSLHandshake.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states 2 | 3 | object SSLHandshake : State 4 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/states/SocketTimeout.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states 2 | 3 | object SocketTimeout : State 4 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/states/UnknownHost.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states 2 | 3 | object UnknownHost : State 4 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/states/InitialSuccess.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states 2 | 3 | object InitialSuccess : State 4 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/states/Restoration.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states 2 | 3 | object Restoration : State 4 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/states/Unavailable.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states 2 | 3 | object Unavailable : State 4 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/states/SearchLimitReached.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states 2 | 3 | object SearchLimitReached : State 4 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/states/SearchQuotaReached.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states 2 | 3 | object SearchQuotaReached : State 4 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/states/ActionNotRequired.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states 2 | 3 | object ActionNotRequired : State 4 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/states/InitialConnection.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states 2 | 3 | object InitialConnection : State 4 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/states/LocalPopulation.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states 2 | 3 | object LocalPopulation : State 4 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/states/SuccessWithoutBody.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states 2 | 3 | object SuccessWithoutBody : State 4 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/states/InitialIntermediate.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states 2 | 3 | object InitialIntermediate : State 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Jan 31 14:38:57 BRST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/string/String.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.string 2 | 3 | private const val EMPTY_STRING = "" 4 | 5 | fun emptyString() = EMPTY_STRING 6 | -------------------------------------------------------------------------------- /app/src/main/res/anim/layout_animation_fall_down.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/empty_view_holder_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/interfaces/OnItemClicked.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.interfaces 2 | 3 | interface OnItemClicked { 4 | fun onItemClick(adapterPosition: Int, id: Int) 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/res/values-hdpi/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | @dimen/text_size_20 4 | @dimen/view_102dp 5 | @dimen/text_size_18 6 | -------------------------------------------------------------------------------- /app/src/main/res/values-mdpi/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | @dimen/text_size_18 4 | @dimen/view_90dp 5 | @dimen/text_size_18 6 | -------------------------------------------------------------------------------- /app/src/main/res/values-xhdpi/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | @dimen/text_size_20 4 | @dimen/view_100dp 5 | @dimen/text_size_18 6 | -------------------------------------------------------------------------------- /app/src/main/res/values-xxhdpi/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | @dimen/text_size_22 4 | @dimen/view_102dp 5 | @dimen/text_size_19 6 | -------------------------------------------------------------------------------- /app/src/main/res/values-xxxhdpi/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | @dimen/text_size_24 4 | @dimen/view_116dp 5 | @dimen/text_size_22 6 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/profile/view/viewHolder/OnItemSelectedListener.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.view.viewHolder 2 | 3 | interface OnItemSelectedListener { 4 | fun onItemSelected(text: String) 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/repository/viewModel/RepositoryInformationViewModel.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.repository.viewModel 2 | 3 | import androidx.lifecycle.ViewModel 4 | 5 | class RepositoryInformationViewModel : ViewModel() {} 6 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/interfaces/LifecycleOwnerFlow.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.interfaces 2 | 3 | interface LifecycleOwnerFlow { 4 | fun setupView() 5 | fun handleViewModel() 6 | fun setupExtras() 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/res/layout/user_repositories_view_holder_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/sharedTest/java/utils/base/TestSteps.kt: -------------------------------------------------------------------------------- 1 | package utils.base 2 | 3 | interface TestSteps { 4 | 5 | fun setupDependencies() 6 | 7 | fun given(given: () -> Unit) { 8 | given() 9 | } 10 | 11 | fun doWhen(doWhen: () -> Unit) { 12 | doWhen() 13 | } 14 | 15 | fun then(then: () -> Unit) { 16 | then() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tools/detekt.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'io.gitlab.arturbosch.detekt' 2 | 3 | detekt { 4 | allRules = true 5 | autoCorrect = true 6 | buildUponDefaultConfig = true 7 | parallel = true 8 | source = files(rootProject.projectDir) 9 | reports { 10 | html.enabled = true 11 | xml.enabled = false 12 | txt.enabled = false 13 | } 14 | } -------------------------------------------------------------------------------- /tools/lint.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "org.jlleitschuh.gradle.ktlint" 2 | 3 | ktlint { 4 | version = "$ktlintVersion" 5 | verbose.set(true) 6 | enableExperimentalRules.set(true) 7 | reporters { 8 | reporter "html" 9 | reporter "json" 10 | } 11 | filter { 12 | exclude("**/generated/**") 13 | include("**/kotlin/**") 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_back_to_top.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/cast/ValueCasting.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.cast 2 | 3 | object ValueCasting { 4 | @Suppress("UNCHECKED_CAST") 5 | fun castToNonNullable(value: Any?) = value as T 6 | 7 | inline fun castTo(value: Any?) = value as? T 8 | } 9 | -------------------------------------------------------------------------------- /tools/protobuf.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.google.protobuf' 2 | 3 | protobuf { 4 | protoc { 5 | artifact = "com.google.protobuf:protoc:$protobufVersion" 6 | } 7 | generateProtoTasks { 8 | all().each { task -> 9 | task.builtins { 10 | java { 11 | option 'lite' 12 | } 13 | } 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/profile/model/Profile.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class Profile( 8 | @SerialName("items") val profile: List 9 | ) 10 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/repository/model/repository/IRepoInformationRepository.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.repository.model.repository 2 | 3 | interface IRepoInformationRepository { 4 | suspend fun fetchUser(user: String = ""): Any 5 | suspend fun fetchRepository(user: String = ""): Any 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/repository/local/dataStore/manager/IKeyValueStorageManager.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.repository.local.dataStore.manager 2 | 3 | interface IKeyValueStorageManager { 4 | suspend fun obtainData(): T 5 | suspend fun updateData(data: T) 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/interfaces/Database.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.interfaces 2 | 3 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.repository.local.dao.ProfileDao 4 | 5 | interface Database { 6 | fun profileDao(): ProfileDao 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_in_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_out_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | -------------------------------------------------------------------------------- /.idea/detekt.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | true 6 | true 7 | true 8 | $PROJECT_DIR$/config/detekt/detekt.yml 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/extensions/Flow.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.extensions 2 | 3 | import kotlinx.coroutines.flow.MutableStateFlow 4 | import kotlinx.coroutines.runBlocking 5 | 6 | fun MutableStateFlow.emitValue(value: T) { 7 | runBlocking { 8 | this@emitValue.emit(value) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_close.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CodeFactor](https://www.codefactor.io/repository/github/caiodev/githubprofilesearcher/badge)](https://www.codefactor.io/repository/github/caiodev/githubprofilesearcher) 2 | # GithubProfileSearcher 3 | 4 | Spare time project to get information about a given Github profile - API 23 (Marshmallow) and above 5 | 6 | License 7 | ======= 8 | 9 | Check LICENSE.md -> https://github.com/caiodev/GithubProfileSearcher/blob/master/LICENSE.md 10 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/delay/Delay.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.delay 2 | 3 | import java.util.Timer 4 | import kotlin.concurrent.schedule 5 | 6 | object Delay { 7 | 8 | inline fun delayTaskBy(milliseconds: Long, crossinline action: () -> Unit) { 9 | Timer().schedule(milliseconds) { action() } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/profile/model/repository/remote/IProfileRepository.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.repository.remote 2 | 3 | interface IProfileRepository { 4 | 5 | suspend fun provideUserInformation( 6 | user: String = "", 7 | pageNumber: Int = 0, 8 | maxResultsPerPage: Int = 0 9 | ): Any 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/states/SuccessWithBody.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states 2 | 3 | @Suppress("UNCHECKED_CAST") 4 | data class SuccessWithBody( 5 | val data: T, 6 | val totalPages: Int = initialPosition 7 | ) : State { 8 | 9 | companion object { 10 | internal const val initialPosition = -1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | @color/colorPrimaryDark 6 | 7 | 8 | #000000 9 | 10 | 11 | #D32F2F 12 | 13 | 14 | #388E3C 15 | 16 | -------------------------------------------------------------------------------- /app/src/test/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/cast/ValueCastingTest.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.cast 2 | 3 | import io.kotest.core.spec.style.FunSpec 4 | import io.kotest.matchers.shouldBe 5 | 6 | internal class ValueCastingTest : FunSpec({ 7 | test("castToNonNullable returns valid value") { 8 | ValueCasting.castToNonNullable>(arrayListOf(0)).first().shouldBe(0) 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/customViews/snackBar/CustomContentViewCallback.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.customViews.snackBar 2 | 3 | class CustomContentViewCallback : com.google.android.material.snackbar.ContentViewCallback { 4 | 5 | override fun animateContentIn(delay: Int, duration: Int) { 6 | // 7 | } 8 | 9 | override fun animateContentOut(delay: Int, duration: Int) { 10 | // 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/repository/model/diModules/GithubUserRepositoryModule.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.repository.model.diModules 2 | 3 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.repository.viewModel.RepositoryInformationViewModel 4 | import org.koin.androidx.viewmodel.dsl.viewModel 5 | import org.koin.dsl.module 6 | 7 | val userRepositoryViewModel = module { 8 | viewModel { RepositoryInformationViewModel() } 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/profile/view/viewHolder/transientItemViews/EmptyViewHolder.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.view.viewHolder.transientItemViews 2 | 3 | import androidx.recyclerview.widget.RecyclerView 4 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.databinding.EmptyViewHolderLayoutBinding 5 | 6 | class EmptyViewHolder(itemBinding: EmptyViewHolderLayoutBinding) : RecyclerView.ViewHolder(itemBinding.root) { 7 | companion object { 8 | const val empty = 0 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/profile/view/viewHolder/transientItemViews/LoadingViewHolder.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.view.viewHolder.transientItemViews 2 | 3 | import androidx.recyclerview.widget.RecyclerView 4 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.databinding.LoadingViewHolderLayoutBinding 5 | 6 | class LoadingViewHolder(itemBinding: LoadingViewHolderLayoutBinding) : RecyclerView.ViewHolder(itemBinding.root) { 7 | companion object { 8 | const val loading = 2 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/res/values-v29/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/layout/loading_view_holder_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /.idea/deploymentTargetDropDown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/repository/model/RepositoryInformation.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.repository.model 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class RepositoryInformation( 8 | @SerialName("id") val repositoryId: String, 9 | @SerialName("name") val repositoryName: String?, 10 | @SerialName("html_url") val repositoryUrl: String, 11 | @SerialName("stargazers_count") val starsCount: String?, 12 | @SerialName("forks_count") val forksCount: Int 13 | ) 14 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/profile/view/viewHolder/transientItemViews/EndOfResultsViewHolder.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.view.viewHolder.transientItemViews 2 | 3 | import androidx.recyclerview.widget.RecyclerView 4 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.databinding.EndOfResultsViewHolderLayoutBinding 5 | 6 | class EndOfResultsViewHolder(itemBinding: EndOfResultsViewHolderLayoutBinding) : 7 | RecyclerView.ViewHolder(itemBinding.root) { 8 | companion object { 9 | const val endOfResults = 4 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/extensions/ViewModel.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.extensions 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import kotlinx.coroutines.launch 6 | import kotlinx.coroutines.runBlocking 7 | 8 | fun ViewModel.runTaskOnBackground(task: suspend () -> Unit) { 9 | viewModelScope.launch { 10 | task() 11 | } 12 | } 13 | 14 | @Suppress("UNUSED") 15 | fun ViewModel.runTaskOnForeground(task: suspend () -> Unit) { 16 | runBlocking { 17 | task() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/profile/model/callInterface/UserProfile.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.callInterface 2 | 3 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.Profile 4 | import retrofit2.Response 5 | import retrofit2.http.GET 6 | import retrofit2.http.Query 7 | 8 | interface UserProfile { 9 | @GET("search/users") 10 | suspend fun provideUsers( 11 | @Query("q") user: String, 12 | @Query("page") pageNumber: Int, 13 | @Query("per_page") maxQuantityPerPage: Int 14 | ): Response 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/res/layout/custom_snackbar_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/profile/view/viewHolder/HeaderViewHolder.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.view.viewHolder 2 | 3 | import androidx.annotation.StringRes 4 | import androidx.recyclerview.widget.RecyclerView 5 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.databinding.HeaderViewHolderLayoutBinding 6 | 7 | class HeaderViewHolder(private val itemBinding: HeaderViewHolderLayoutBinding) : 8 | RecyclerView.ViewHolder(itemBinding.root) { 9 | 10 | internal fun bind(@StringRes model: Int) { 11 | itemBinding.userListHeader.text = 12 | itemView.context.getString(model) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/res/layout/header_view_holder_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/profile/model/UserProfile.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | import kotlinx.serialization.SerialName 7 | import kotlinx.serialization.Serializable 8 | 9 | @Entity 10 | @Serializable 11 | data class UserProfile( 12 | @ColumnInfo 13 | @SerialName("login") 14 | val login: String = "", 15 | @ColumnInfo 16 | @SerialName("html_url") 17 | val profileUrl: String = "", 18 | @PrimaryKey 19 | @SerialName("id") 20 | val userId: Long = 0, 21 | @ColumnInfo 22 | @SerialName("avatar_url") 23 | val userImage: String = "" 24 | ) 25 | -------------------------------------------------------------------------------- /app/src/test/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/profile/unit/viewModel/fakes/repository/remote/FakeProfileInformationRepository.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.unit.viewModel.fakes.repository.remote 2 | 3 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.repository.local.GenericProfileRepository 4 | 5 | class FakeProfileInformationRepository : 6 | GenericProfileRepository { 7 | 8 | private val fakeRemoteRepository = 9 | FakeRemoteRepository() 10 | 11 | override suspend fun provideGithubUserInformation( 12 | user: String, 13 | pageNumber: Int, 14 | maxResultsPerPage: Int 15 | ) = fakeRemoteRepository.provideFakeResponse() 16 | } 17 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/dictionaries/unknown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | appname 5 | browsable 6 | caio 7 | caiodev 8 | composable 9 | coroutine 10 | coroutines 11 | couldn 12 | datas 13 | detekt 14 | dorg 15 | endlocal 16 | errorlevel 17 | githubprofilesearcher 18 | gravatar 19 | koin 20 | kotest 21 | kotlinx 22 | ktlint 23 | nlcj 24 | nunito 25 | octocat 26 | protobuf 27 | recyclerview 28 | setlocal 29 | windowz 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/repository/model/callInterface/UserRepository.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.repository.model.callInterface 2 | 3 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.UserProfile 4 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.repository.model.RepositoryInformation 5 | import retrofit2.Response 6 | import retrofit2.http.GET 7 | import retrofit2.http.Path 8 | 9 | interface UserRepository { 10 | 11 | @GET("users/{user}") 12 | suspend fun fetchUser(@Path("user") user: String): Response 13 | 14 | @GET("users/{user}/repos") 15 | suspend fun fetchRepository(@Path("user") user: String): 16 | Response 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/res/anim/item_animation_fall_down.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 9 | 10 | 15 | 16 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/profile/model/repository/local/dao/ProfileDao.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.repository.local.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.OnConflictStrategy 6 | import androidx.room.Query 7 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.UserProfile 8 | 9 | @Dao 10 | interface ProfileDao { 11 | 12 | @Query("SELECT * " + "FROM UserProfile LIMIT 20") 13 | suspend fun getProfilesFromDb(): List? 14 | 15 | @Insert(onConflict = OnConflictStrategy.REPLACE) 16 | suspend fun insertProfilesIntoDb(profileList: List) 17 | 18 | @Query(value = "DELETE FROM UserProfile") 19 | suspend fun dropProfileInformation() 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/repository/local/db/AppDatabase.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.repository.local.db 2 | 3 | import androidx.room.RoomDatabase 4 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.UserProfile 5 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.repository.local.dao.ProfileDao 6 | 7 | @androidx.room.Database(entities = [UserProfile::class], version = 1) 8 | abstract class AppDatabase : 9 | RoomDatabase(), 10 | githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.interfaces.Database { 11 | 12 | abstract override fun profileDao(): ProfileDao 13 | 14 | companion object { 15 | const val databaseName = "app-db" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/proto/profile.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option java_package = "githubprofilesearcher.caiodev.com.br.githubprofilesearcher"; 4 | option java_multiple_files = true; 5 | 6 | message ProfilePreferences { 7 | bool hasUserDeletedProfileText = 1; 8 | string profile = 2; 9 | bool shouldRecyclerViewAnimationBeExecuted = 3; 10 | string temporaryCurrentProfile = 4; 11 | string currentProfile = 5; 12 | int32 pageNumber = 6; 13 | bool hasASuccessfulCallAlreadyBeenMade = 7; 14 | bool hasLastCallBeenUnsuccessful = 8; 15 | bool isThereAnOngoingCall = 9; 16 | bool hasUserRequestedUpdatedData = 10; 17 | bool shouldASearchBePerformed = 11; 18 | bool isTextInputEditTextNotEmpty = 12; 19 | bool isHeaderVisible = 13; 20 | bool isEndOfResultsViewVisible = 14; 21 | bool isPaginationLoadingViewVisible = 15; 22 | bool isRetryViewVisible = 16; 23 | bool isLocalPopulation = 17; 24 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/retry_view_holder_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/interfaces/ILocalRepository.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.interfaces 2 | 3 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.UserProfile 4 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.repository.local.dao.ProfileDao 5 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.repository.local.dataStore.manager.IKeyValueStorageManager 6 | 7 | interface ILocalRepository : ProfileDao { 8 | 9 | fun obtainProtoDataStore(): IKeyValueStorageManager 10 | 11 | override suspend fun getProfilesFromDb(): List 12 | override suspend fun insertProfilesIntoDb(profileList: List) 13 | override suspend fun dropProfileInformation() 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/repository/local/dataStore/manager/KeyValueStorageManager.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.repository.local.dataStore.manager 2 | 3 | import androidx.datastore.core.DataStore 4 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.cast.ValueCasting.castToNonNullable 5 | import kotlinx.coroutines.flow.first 6 | 7 | @Suppress("UNCHECKED_CAST") 8 | class KeyValueStorageManager(private val keyValueStorageClient: DataStore) : 9 | IKeyValueStorageManager { 10 | 11 | override suspend fun obtainData() = castToNonNullable(keyValueStorageClient.data.first()) 12 | 13 | override suspend fun updateData(data: T) { 14 | keyValueStorageClient.updateData { 15 | castToNonNullable(data) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx2048m 13 | android.enableJetifier=true 14 | android.useAndroidX=true 15 | kotlin.code.style=official 16 | # When configured, Gradle will run in incubating parallel mode. 17 | # This option should only be used with decoupled projects. More details, visit 18 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 19 | org.gradle.parallel=true 20 | org.gradle.unsafe.configuration-cache=true -------------------------------------------------------------------------------- /app/src/main/res/layout/end_of_results_view_holder_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/test/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/profile/unit/viewModel/fakes/repository/remote/FakeRemoteRepository.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.unit.viewModel.fakes.repository.remote 2 | 3 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.UserProfile 4 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.Profile 5 | 6 | class FakeRemoteRepository { 7 | 8 | fun provideFakeResponse() = States.Success( 9 | Profile( 10 | 11 | listOf( 12 | 13 | UserProfile( 14 | "torvalds", 15 | "https://github.com/torvalds", 16 | 1024025, 17 | "https://avatars0.githubusercontent.com/u/1024025?v=4" 18 | ) 19 | ) 20 | ) 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /app/src/test/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/profile/unit/utils/base/repository/local/fakes/protoDataStore/manager/FakeKeyValueStorageManager.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.unit.utils.base.repository.local.fakes.protoDataStore.manager 2 | 3 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.repository.local.dataStore.manager.IKeyValueStorageManager 4 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.repository.local.dataStore.model.ProfilePreferences 5 | 6 | class FakeKeyValueStorageManager : IKeyValueStorageManager { 7 | 8 | private var userPreferences = ProfilePreferences() 9 | 10 | override suspend fun updateValue(profilePreferences: ProfilePreferences) { 11 | this.userPreferences = profilePreferences 12 | } 13 | 14 | override suspend fun obtainValue() = userPreferences 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/profile/view/viewHolder/transientItemViews/RetryViewHolder.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.view.viewHolder.transientItemViews 2 | 3 | import androidx.recyclerview.widget.RecyclerView 4 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.databinding.RetryViewHolderLayoutBinding 5 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.interfaces.OnItemClicked 6 | 7 | class RetryViewHolder( 8 | itemBinding: RetryViewHolderLayoutBinding, 9 | private val onItemClicked: OnItemClicked? 10 | ) : 11 | RecyclerView.ViewHolder(itemBinding.root) { 12 | init { 13 | itemBinding.retryTextView.setOnClickListener { 14 | onItemClicked?.onItemClick(layoutPosition, retry) 15 | } 16 | } 17 | 18 | companion object { 19 | const val retry = 3 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/profile/model/repository/remote/ProfileRepository.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.repository.remote 2 | 3 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.callInterface.UserProfile 4 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.repository.remote.RemoteRepository 5 | 6 | class ProfileRepository( 7 | private val remoteRepository: RemoteRepository, 8 | private val apiService: UserProfile 9 | ) : IProfileRepository { 10 | 11 | override suspend fun provideUserInformation( 12 | user: String, 13 | pageNumber: Int, 14 | maxResultsPerPage: Int 15 | ) = remoteRepository.call { 16 | apiService.provideUsers( 17 | user, 18 | pageNumber, 19 | maxResultsPerPage 20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 16 | 17 | 18 | 19 | 20 | 21 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8dp 6 | 16dp 7 | 8 | 9 | 8dp 10 | 16dp 11 | 56dp 12 | 13 | 14 | 8dp 15 | 16 | 17 | 48dp 18 | 90dp 19 | 100dp 20 | 102dp 21 | 116dp 22 | 23 | 24 | 18sp 25 | 19sp 26 | 20sp 27 | 22sp 28 | 24sp 29 | 30 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /home/unknown/Android/Sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/test/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/profile/unit/utils/base/repository/local/fakes/database/FakeDatabase.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.unit.utils.base.repository.local.fakes.database 2 | 3 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.UserProfile 4 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.repository.local.dao.ProfileDao 5 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.interfaces.Database 6 | 7 | class FakeDatabase : Database, ProfileDao { 8 | 9 | override fun profileDao(): ProfileDao { 10 | return this 11 | } 12 | 13 | override suspend fun getProfilesFromDb() = listOf() 14 | 15 | override suspend fun insertProfilesIntoDb(profileList: List) { 16 | // 17 | } 18 | 19 | override suspend fun dropProfileInformation(githubProfilesList: List) { 20 | // 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/repository/model/repository/RepoInformationRepository.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.repository.model.repository 2 | 3 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.repository.model.callInterface.UserRepository 4 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.repository.remote.RemoteRepository 5 | 6 | class RepoInformationRepository( 7 | private val remoteRepository: RemoteRepository, 8 | private val retrofitService: UserRepository 9 | ) : IRepoInformationRepository { 10 | 11 | override suspend fun fetchUser(user: String) = 12 | remoteRepository.call( 13 | call = { 14 | retrofitService.fetchUser(user) 15 | } 16 | ) 17 | 18 | override suspend fun fetchRepository(user: String) = 19 | remoteRepository.call( 20 | call = { 21 | retrofitService.fetchRepository(user) 22 | } 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 18 | 19 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/sharedTest/java/utils/base/coroutines/junit5/CoroutinesTestExtension.kt: -------------------------------------------------------------------------------- 1 | package utils.base.coroutines.junit5 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.ExperimentalCoroutinesApi 5 | import kotlinx.coroutines.test.TestCoroutineDispatcher 6 | import kotlinx.coroutines.test.TestCoroutineScope 7 | import kotlinx.coroutines.test.resetMain 8 | import kotlinx.coroutines.test.setMain 9 | import org.junit.jupiter.api.extension.AfterEachCallback 10 | import org.junit.jupiter.api.extension.BeforeEachCallback 11 | import org.junit.jupiter.api.extension.ExtensionContext 12 | 13 | @ExperimentalCoroutinesApi 14 | class CoroutinesTestExtension( 15 | private val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher() 16 | ) : BeforeEachCallback, AfterEachCallback, TestCoroutineScope by TestCoroutineScope(dispatcher) { 17 | 18 | override fun beforeEach(context: ExtensionContext?) { 19 | Dispatchers.setMain(dispatcher) 20 | } 21 | 22 | override fun afterEach(context: ExtensionContext?) { 23 | cleanupTestCoroutines() 24 | Dispatchers.resetMain() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | MIT License 3 | ======= 4 | 5 | Copyright 2018 Caio, Inc. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 8 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 9 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 10 | persons to whom the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 13 | Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 16 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 18 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /app/src/test/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/profile/unit/utils/delay/DelayTest.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.unit.utils.delay 2 | 3 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.delay.Delay.delayTaskBy 4 | import org.junit.jupiter.api.Assertions.assertEquals 5 | import org.junit.jupiter.api.Test 6 | import utils.base.TestSteps 7 | import java.util.concurrent.CountDownLatch 8 | import java.util.concurrent.TimeUnit.MILLISECONDS 9 | 10 | class DelayTest : TestSteps { 11 | 12 | override fun setupDependencies() { 13 | // 14 | } 15 | 16 | @Test 17 | fun delay_timeAndInstructionToExecute_executeAGenericOperation() { 18 | var countDownLatch: CountDownLatch? = null 19 | 20 | given { 21 | countDownLatch = CountDownLatch(1) 22 | } 23 | 24 | doWhen { 25 | delayTaskBy(100) { countDownLatch?.countDown() } 26 | } 27 | 28 | then { 29 | countDownLatch?.await(200, MILLISECONDS) 30 | assertEquals(0, countDownLatch?.count) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/test/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/profile/unit/viewModel/fakes/repository/local/FakeLocalRepository.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.unit.viewModel.fakes.repository.local 2 | 3 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.UserProfile 4 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.unit.utils.base.repository.local.fakes.protoDataStore.manager.FakeKeyValueStorageManager 5 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.interfaces.GenericLocalRepository 6 | 7 | class FakeLocalRepository : GenericLocalRepository { 8 | 9 | override fun obtainProtoDataStore() = FakeKeyValueStorageManager() 10 | 11 | override suspend fun getGithubProfilesFromDb(): List { 12 | TODO("Not yet implemented") 13 | } 14 | 15 | override suspend fun insertGithubProfilesIntoDb(githubProfilesList: List) { 16 | TODO("Not yet implemented") 17 | } 18 | 19 | override suspend fun dropGithubProfileInformationTable(githubProfilesList: List) { 20 | TODO("Not yet implemented") 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/profile/model/repository/local/dataStore/serializer/ProfileSerializer.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.repository.local.dataStore.serializer 2 | 3 | import androidx.datastore.core.CorruptionException 4 | import androidx.datastore.core.Serializer 5 | import com.google.protobuf.InvalidProtocolBufferException 6 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.ProfilePreferences 7 | import java.io.InputStream 8 | import java.io.OutputStream 9 | 10 | @Suppress("BlockingMethodInNonBlockingContext") 11 | object ProfileSerializer : Serializer { 12 | 13 | const val profileProtoFileName = "profile.proto" 14 | 15 | override val defaultValue: ProfilePreferences = ProfilePreferences.getDefaultInstance() 16 | 17 | override suspend fun readFrom(input: InputStream): ProfilePreferences { 18 | try { 19 | return ProfilePreferences.parseFrom(input) 20 | } catch (exception: InvalidProtocolBufferException) { 21 | throw CorruptionException("Cannot read proto.", exception) 22 | } 23 | } 24 | 25 | override suspend fun writeTo(t: ProfilePreferences, output: OutputStream) = t.writeTo(output) 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/sharedTest/java/utils/base/coroutines/junit4/CoroutinesTestRule.kt: -------------------------------------------------------------------------------- 1 | package utils.base.coroutines.junit4 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.ExperimentalCoroutinesApi 5 | import kotlinx.coroutines.test.TestCoroutineDispatcher 6 | import kotlinx.coroutines.test.TestCoroutineScope 7 | import kotlinx.coroutines.test.resetMain 8 | import kotlinx.coroutines.test.setMain 9 | import org.junit.rules.TestRule 10 | import org.junit.runner.Description 11 | import org.junit.runners.model.Statement 12 | import kotlin.coroutines.ContinuationInterceptor 13 | 14 | @ExperimentalCoroutinesApi 15 | class CoroutinesTestRule : TestRule, TestCoroutineScope by TestCoroutineScope() { 16 | 17 | val dispatcher = coroutineContext[ContinuationInterceptor] as TestCoroutineDispatcher 18 | 19 | override fun apply(base: Statement, description: Description): Statement { 20 | return object : Statement() { 21 | @Throws(Throwable::class) 22 | override fun evaluate() { 23 | Dispatchers.setMain(dispatcher) 24 | 25 | // everything above this happens before the test 26 | base.evaluate() 27 | // everything below this happens after the test 28 | 29 | cleanupTestCoroutines() 30 | Dispatchers.resetMain() 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | GithubProfileSearcher 3 | 4 | 5 | Type a profile 6 | ID %s 7 | User: %s 8 | Profiles 9 | Retry 10 | End of results 11 | 12 | 13 | Back online 14 | 15 | 16 | Empty field 17 | No internet connection 18 | We couldn\'t get what you\'re looking for. Please, try again later 19 | Servers are overloaded at the moment. Please, try again later 20 | The API query limit has been reached. Please, try again later 21 | You have reached the 1000th profile 🎉 22 | Please, check your internet connection and try again later 23 | The network you\'re connected to is unsafe. Please, change it and try again later 24 | An error occurred. Please, try again later 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/values-pt/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Digite um perfil 5 | Usuário: %s 6 | Perfis 7 | Tentar novamente 8 | Fim dos resultados 9 | 10 | 11 | Conexão reestabelecida 12 | 13 | 14 | Campo vazio 15 | Não há conexão com a internet 16 | Não conseguimos obter o que você está procurando. Por favor, tente novamente mais tarde 17 | Os servidores estão sobrecarregados no momento. Por favor, tente novamente mais tarde 18 | A cota máxima de buscas da API foi alcançada. Por favor, tente novamente mais tarde 19 | Você alcançou o milésimo perfil 🎉 20 | Por favor, verifique sua conexão e tente novamente mais tarde 21 | A rede na qual você está, não é segura. Por favor, mude de rede e tente novamente mais tarde 22 | Ocorreu um erro. Por favor, tente novamente mais tarde 23 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/repository/local/LocalRepository.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.repository.local 2 | 3 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.UserProfile 4 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.interfaces.Database 5 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.interfaces.ILocalRepository 6 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.repository.local.dataStore.manager.IKeyValueStorageManager 7 | 8 | @Suppress("UNCHECKED_CAST") 9 | class LocalRepository( 10 | private val keyValueStorageManager: IKeyValueStorageManager, 11 | private val appDatabase: Database 12 | ) : ILocalRepository { 13 | 14 | override fun obtainProtoDataStore() = keyValueStorageManager 15 | 16 | override suspend fun getProfilesFromDb(): List { 17 | var list = listOf() 18 | appDatabase.profileDao().getProfilesFromDb()?.let { 19 | list = it 20 | } 21 | return list 22 | } 23 | 24 | override suspend fun insertProfilesIntoDb(profileList: List) { 25 | appDatabase.profileDao() 26 | .insertProfilesIntoDb(profileList) 27 | } 28 | 29 | override suspend fun dropProfileInformation() { 30 | appDatabase.profileDao().dropProfileInformation() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/profile/view/viewHolder/ProfileInformationViewHolder.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.view.viewHolder 2 | 3 | import androidx.recyclerview.widget.RecyclerView 4 | import coil.imageLoader 5 | import coil.load 6 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.R 7 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.databinding.ProfileViewHolderLayoutBinding 8 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.UserProfile 9 | import okhttp3.internal.format 10 | 11 | class ProfileInformationViewHolder( 12 | private val itemBinding: ProfileViewHolderLayoutBinding, 13 | onItemSelected: OnItemSelectedListener 14 | ) : 15 | RecyclerView.ViewHolder(itemBinding.root) { 16 | 17 | private var profileUrl = "" 18 | 19 | init { 20 | itemBinding.parentLayout.setOnClickListener { 21 | onItemSelected.onItemSelected(profileUrl) 22 | } 23 | } 24 | 25 | fun bind(model: UserProfile) { 26 | profileUrl = model.profileUrl 27 | 28 | model.userId.let { 29 | itemBinding.userId.text = 30 | format(itemView.context.getString(R.string.uid), it.toString()) 31 | } 32 | 33 | model.login.let { 34 | itemBinding.userLogin.text = 35 | format(itemView.context.getString(R.string.login), it) 36 | } 37 | 38 | itemBinding.userAvatar.load(model.userImage, itemView.context.imageLoader) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/schemas/githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.repository.local.db.AppDatabase/1.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatVersion": 1, 3 | "database": { 4 | "version": 1, 5 | "identityHash": "6eb6d7d5a602821d09da58cd8d0991e9", 6 | "entities": [ 7 | { 8 | "tableName": "UserProfile", 9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`login` TEXT NOT NULL, `profileUrl` TEXT NOT NULL, `userId` INTEGER NOT NULL, `userImage` TEXT NOT NULL, PRIMARY KEY(`userId`))", 10 | "fields": [ 11 | { 12 | "fieldPath": "login", 13 | "columnName": "login", 14 | "affinity": "TEXT", 15 | "notNull": true 16 | }, 17 | { 18 | "fieldPath": "profileUrl", 19 | "columnName": "profileUrl", 20 | "affinity": "TEXT", 21 | "notNull": true 22 | }, 23 | { 24 | "fieldPath": "userId", 25 | "columnName": "userId", 26 | "affinity": "INTEGER", 27 | "notNull": true 28 | }, 29 | { 30 | "fieldPath": "userImage", 31 | "columnName": "userImage", 32 | "affinity": "TEXT", 33 | "notNull": true 34 | } 35 | ], 36 | "primaryKey": { 37 | "columnNames": [ 38 | "userId" 39 | ], 40 | "autoGenerate": false 41 | }, 42 | "indices": [], 43 | "foreignKeys": [] 44 | } 45 | ], 46 | "views": [], 47 | "setupQueries": [ 48 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", 49 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6eb6d7d5a602821d09da58cd8d0991e9')" 50 | ] 51 | } 52 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/espresso/view/GithubProfileInfoObtainmentActivityEspressoTest.kt: -------------------------------------------------------------------------------- 1 | package espresso.view 2 | 3 | import androidx.test.espresso.Espresso.onView 4 | import androidx.test.espresso.IdlingRegistry 5 | import androidx.test.espresso.action.ViewActions.click 6 | import androidx.test.espresso.action.ViewActions.typeText 7 | import androidx.test.espresso.idling.CountingIdlingResource 8 | import androidx.test.espresso.matcher.ViewMatchers.withId 9 | import androidx.test.ext.junit.rules.activityScenarioRule 10 | import androidx.test.ext.junit.runners.AndroidJUnit4 11 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.R 12 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.view.activity.ProfileListingActivity 13 | import org.junit.After 14 | import org.junit.Before 15 | import org.junit.Rule 16 | import org.junit.Test 17 | import org.junit.runner.RunWith 18 | 19 | @RunWith(AndroidJUnit4::class) 20 | class GithubProfileInfoObtainmentActivityEspressoTest { 21 | 22 | @get:Rule 23 | internal val rule = activityScenarioRule() 24 | 25 | private val countingIdlingResource = CountingIdlingResource("ProfileSearch") 26 | 27 | @Before 28 | fun setup() { 29 | IdlingRegistry.getInstance().register(countingIdlingResource) 30 | rule.scenario.onActivity { 31 | it.bindIdlingResource(countingIdlingResource) 32 | } 33 | } 34 | 35 | @Test 36 | fun searchProfile() { 37 | onView(withId(R.id.searchProfileTextInputEditText)) 38 | .perform(typeText("torvalds")) 39 | 40 | onView(withId(R.id.actionIconImageView)) 41 | .perform(click()) 42 | } 43 | 44 | @After 45 | fun cleanUp() { 46 | IdlingRegistry.getInstance().unregister(countingIdlingResource) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tools/coverage.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'jacoco' 2 | 3 | jacoco { 4 | toolVersion = "$jacocoVersion" 5 | reportsDirectory = file("$buildDir") 6 | } 7 | 8 | tasks.withType(Test) { 9 | jacoco.includeNoLocationClasses = true 10 | jacoco.excludes = ['jdk.internal.*'] 11 | } 12 | 13 | task jacocoReport(type: JacocoReport, dependsOn: ['testDebugUnitTest']) { 14 | 15 | def coverageSourceDirs = [ 16 | "src/main/java" 17 | ] 18 | 19 | def fileFilter = [ 20 | '**/*Activity.*', 21 | '**/*Adapter.*', 22 | '**/BuildConfig.*', 23 | '**/*Constants.*', 24 | '**/Manifest*.*', 25 | '**/R.class', 26 | '**/R$*.class', 27 | '**/*Test*.*', 28 | '**/*ViewHolder*.*', 29 | 'android/**/*.*', 30 | 'androidx/**/*.*', 31 | '**/*init/**/*.*', 32 | '**/*model/**/*.*', 33 | '**/*view/**/*.*', 34 | '**/*imageLoading/**/*.*', 35 | '**/*di/**/*.*', 36 | '**/*liveEvent/**/*.*', 37 | '**/*customViews/**/*.*', 38 | '**/*service/**/*.*', 39 | '**/*network/**/*.*', 40 | '**/ViewKt**', 41 | '**/AppDatabase**' 42 | ] 43 | 44 | def javaClasses = fileTree(dir: "$buildDir/intermediates/classes/debug", excludes: fileFilter) 45 | def kotlinClasses = fileTree(dir: "$buildDir/tmp/kotlin-classes/debug", excludes: fileFilter) 46 | 47 | classDirectories.setFrom files([javaClasses, kotlinClasses]) 48 | additionalSourceDirs.setFrom files(coverageSourceDirs) 49 | 50 | sourceDirectories.setFrom files([ 51 | "$project.projectDir/src/main/java" 52 | ]) 53 | 54 | executionData.setFrom fileTree(dir: project.projectDir, includes: ["**/*.exec"]) 55 | 56 | reports { 57 | html.required = true 58 | } 59 | } -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/profile/view/adapter/ProfileAdapter.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.view.adapter 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.RecyclerView 6 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.databinding.ProfileViewHolderLayoutBinding 7 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.UserProfile 8 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.view.viewHolder.OnItemSelectedListener 9 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.view.viewHolder.ProfileInformationViewHolder 10 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.cast.ValueCasting.castTo 11 | 12 | class ProfileAdapter(private val onItemSelectedListener: OnItemSelectedListener) : 13 | RecyclerView.Adapter() { 14 | 15 | private var dataSource = listOf() 16 | 17 | override fun getItemCount() = dataSource.size 18 | 19 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { 20 | return ProfileInformationViewHolder( 21 | ProfileViewHolderLayoutBinding.inflate( 22 | LayoutInflater.from(parent.context), 23 | parent, 24 | false 25 | ), 26 | onItemSelectedListener 27 | ) 28 | } 29 | 30 | override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) { 31 | castTo(viewHolder)?.bind(dataSource[position]) 32 | } 33 | 34 | internal fun updateDataSource(newDataSource: List) { 35 | dataSource = newDataSource 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/sharedTest/java/utils/base/api/factory/RetrofitTestService.kt: -------------------------------------------------------------------------------- 1 | package utils.base.api.factory 2 | 3 | import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory 4 | import kotlinx.serialization.ExperimentalSerializationApi 5 | import kotlinx.serialization.json.Json 6 | import okhttp3.MediaType.Companion.toMediaType 7 | import okhttp3.OkHttpClient 8 | import okhttp3.logging.HttpLoggingInterceptor 9 | import okhttp3.mockwebserver.MockWebServer 10 | import retrofit2.Retrofit 11 | import timber.log.Timber 12 | import java.util.concurrent.TimeUnit 13 | 14 | object RetrofitTestService { 15 | 16 | lateinit var mockWebServer: MockWebServer 17 | val json = Json { ignoreUnknownKeys = true } 18 | val mediaType = "application/json".toMediaType() 19 | 20 | fun setup(): MockWebServer { 21 | mockWebServer = MockWebServer() 22 | mockWebServer.start() 23 | return mockWebServer 24 | } 25 | 26 | @ExperimentalSerializationApi 27 | @PublishedApi 28 | internal inline fun newInstance() = 29 | Retrofit.Builder() 30 | .baseUrl(mockWebServer.url("/")) 31 | .client(createLoggerClient()) 32 | .addConverterFactory( 33 | json.asConverterFactory( 34 | mediaType 35 | ) 36 | ) 37 | .build().create(T::class.java) as T 38 | 39 | @PublishedApi 40 | internal fun createLoggerClient() = 41 | OkHttpClient.Builder() 42 | .addInterceptor( 43 | HttpLoggingInterceptor { message -> Timber.tag("OkHttp").d(message) }.apply { 44 | level = HttpLoggingInterceptor.Level.BODY 45 | } 46 | ) 47 | .connectTimeout(2, TimeUnit.SECONDS) 48 | .readTimeout(2, TimeUnit.SECONDS) 49 | .writeTimeout(2, TimeUnit.SECONDS) 50 | .build() 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/rest/APIConnector.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.rest 2 | 3 | import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory 4 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.BuildConfig 5 | import kotlinx.serialization.ExperimentalSerializationApi 6 | import kotlinx.serialization.json.Json 7 | import okhttp3.MediaType.Companion.toMediaType 8 | import okhttp3.OkHttpClient 9 | import okhttp3.logging.HttpLoggingInterceptor 10 | import org.koin.core.scope.Scope 11 | import retrofit2.Retrofit 12 | import timber.log.Timber 13 | import java.util.concurrent.TimeUnit 14 | 15 | object APIConnector { 16 | 17 | private const val timeout = 60L 18 | private val json = Json { ignoreUnknownKeys = true } 19 | private val mediaType = "application/json".toMediaType() 20 | 21 | @ExperimentalSerializationApi 22 | @Suppress("UNUSED") 23 | fun Scope.newInstance(baseUrl: String = BuildConfig.API_URL): Retrofit { 24 | return Retrofit.Builder() 25 | .baseUrl(baseUrl) 26 | .client(createLoggerClient()) 27 | .addConverterFactory( 28 | json.asConverterFactory(mediaType) 29 | ) 30 | .build() 31 | } 32 | 33 | private fun createLoggerClient(): OkHttpClient { 34 | val responseTag = "OkHttp" 35 | val httpLoggingInterceptor = 36 | HttpLoggingInterceptor { message -> Timber.tag(responseTag).d(message) } 37 | .apply { level = HttpLoggingInterceptor.Level.BODY } 38 | return OkHttpClient.Builder() 39 | .addInterceptor(httpLoggingInterceptor) 40 | .connectTimeout(timeout, TimeUnit.SECONDS) 41 | .readTimeout(timeout, TimeUnit.SECONDS) 42 | .writeTimeout(timeout, TimeUnit.SECONDS) 43 | .build() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/sharedTest/java/utils/base/api/MockedAPIResponseProvider.kt: -------------------------------------------------------------------------------- 1 | package utils.base.api 2 | 3 | object MockedAPIResponseProvider { 4 | 5 | const val profileInfoCallResult = 6 | "{\n" + 7 | " \"total_count\": 156,\n" + 8 | " \"incomplete_results\": false,\n" + 9 | " \"items\": [\n" + 10 | " {\n" + 11 | " \"login\": \"torvalds\",\n" + 12 | " \"id\": 1024025,\n" + 13 | " \"node_id\": \"MDQ6VXNlcjEwMjQwMjU=\",\n" + 14 | " \"avatar_url\": \"https://avatars0.githubusercontent.com/u/1024025?v=4\",\n" + 15 | " \"gravatar_id\": \"\",\n" + 16 | " \"url\": \"https://api.github.com/users/torvalds\",\n" + 17 | " \"html_url\": \"https://github.com/torvalds\",\n" + 18 | " \"followers_url\": \"https://api.github.com/users/torvalds/followers\",\n" + 19 | " \"following_url\": \"https://api.github.com/users/torvalds/following{/other_user}\",\n" + 20 | " \"gists_url\": \"https://api.github.com/users/torvalds/gists{/gist_id}\",\n" + 21 | " \"starred_url\": \"https://api.github.com/users/torvalds/starred{/owner}{/repo}\",\n" + 22 | " \"subscriptions_url\": \"https://api.github.com/users/torvalds/subscriptions\",\n" + 23 | " \"organizations_url\": \"https://api.github.com/users/torvalds/orgs\",\n" + 24 | " \"repos_url\": \"https://api.github.com/users/torvalds/repos\",\n" + 25 | " \"events_url\": \"https://api.github.com/users/torvalds/events{/privacy}\",\n" + 26 | " \"received_events_url\": \"https://api.github.com/users/torvalds/received_events\",\n" + 27 | " \"type\": \"User\",\n" + 28 | " \"site_admin\": false,\n" + 29 | " \"score\": 1624.8047\n" + 30 | " }\n" + 31 | " ]\n" + 32 | "}" 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/di/GlobalModule.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.di 2 | 3 | import android.content.Context 4 | import android.net.ConnectivityManager 5 | import androidx.room.Room 6 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.interfaces.Database 7 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.interfaces.ILocalRepository 8 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.repository.local.LocalRepository 9 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.repository.local.db.AppDatabase 10 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.repository.remote.RemoteRepository 11 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.network.NetworkChecking 12 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.rest.APIConnector.newInstance 13 | import kotlinx.serialization.ExperimentalSerializationApi 14 | import org.koin.android.ext.koin.androidContext 15 | import org.koin.dsl.module 16 | 17 | @ExperimentalSerializationApi 18 | val global = module { 19 | single { 20 | Room.databaseBuilder( 21 | androidContext(), 22 | AppDatabase::class.java, 23 | AppDatabase.databaseName 24 | ).build() 25 | } 26 | 27 | factory { 28 | LocalRepository( 29 | get(), 30 | get() 31 | ) 32 | } 33 | 34 | single { 35 | NetworkChecking( 36 | androidContext() 37 | .getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager 38 | ) 39 | } 40 | 41 | single { 42 | newInstance() 43 | } 44 | 45 | single { 46 | RemoteRepository() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/profile/view/adapter/HeaderAdapter.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.view.adapter 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.RecyclerView 6 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.databinding.EmptyViewHolderLayoutBinding 7 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.databinding.HeaderViewHolderLayoutBinding 8 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.view.viewHolder.HeaderViewHolder 9 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.view.viewHolder.transientItemViews.EmptyViewHolder 10 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.view.viewHolder.transientItemViews.EmptyViewHolder.Companion.empty 11 | 12 | class HeaderAdapter(private val headerName: Int) : RecyclerView.Adapter() { 13 | 14 | private var viewType = empty 15 | 16 | internal fun updateViewState(viewType: Int) { 17 | this.viewType = viewType 18 | } 19 | 20 | override fun getItemViewType(position: Int) = viewType 21 | 22 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { 23 | val inflater = LayoutInflater.from(parent.context) 24 | return if (viewType == empty) { 25 | EmptyViewHolder( 26 | EmptyViewHolderLayoutBinding.inflate( 27 | inflater, 28 | parent, 29 | false 30 | ) 31 | ) 32 | } else { 33 | HeaderViewHolder( 34 | HeaderViewHolderLayoutBinding.inflate( 35 | inflater, 36 | parent, 37 | false 38 | ) 39 | ) 40 | } 41 | } 42 | 43 | override fun getItemCount() = 1 44 | 45 | override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) { 46 | if (viewHolder is HeaderViewHolder) viewHolder.bind(headerName) 47 | } 48 | 49 | companion object { 50 | const val header = 5 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/profile/model/di/UserProfileModule.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.di 2 | 3 | import androidx.datastore.core.DataStoreFactory 4 | import androidx.datastore.dataStoreFile 5 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.callInterface.UserProfile 6 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.repository.local.dataStore.serializer.ProfileSerializer 7 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.repository.local.dataStore.serializer.ProfileSerializer.profileProtoFileName 8 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.repository.remote.IProfileRepository 9 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.repository.remote.ProfileRepository 10 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.viewModel.ProfileViewModel 11 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.repository.local.dataStore.manager.IKeyValueStorageManager 12 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.repository.local.dataStore.manager.KeyValueStorageManager 13 | import kotlinx.serialization.ExperimentalSerializationApi 14 | import org.koin.android.ext.koin.androidContext 15 | import org.koin.androidx.viewmodel.dsl.viewModel 16 | import org.koin.dsl.module 17 | import retrofit2.Retrofit 18 | 19 | @ExperimentalSerializationApi 20 | val userProfileViewModel = module { 21 | 22 | viewModel { 23 | ProfileViewModel( 24 | networkChecking = get(), 25 | localRepository = get(), 26 | remoteRepository = get() 27 | ) 28 | } 29 | 30 | factory { 31 | KeyValueStorageManager( 32 | keyValueStorageClient = DataStoreFactory.create( 33 | serializer = ProfileSerializer, 34 | produceFile = { androidContext().dataStoreFile(profileProtoFileName) } 35 | ) 36 | ) 37 | } 38 | 39 | factory { 40 | ProfileRepository( 41 | remoteRepository = get(), 42 | apiService = get().create(UserProfile::class.java) 43 | ) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/init/App.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.init 2 | 3 | import android.app.Application 4 | import coil.ImageLoader 5 | import coil.ImageLoaderFactory 6 | import coil.disk.DiskCache 7 | import coil.memory.MemoryCache 8 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.BuildConfig 9 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.R 10 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.di.userProfileViewModel 11 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.repository.model.diModules.userRepositoryViewModel 12 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.di.global 13 | import kotlinx.serialization.ExperimentalSerializationApi 14 | import org.koin.android.ext.koin.androidContext 15 | import org.koin.android.ext.koin.androidLogger 16 | import org.koin.core.context.startKoin 17 | import org.koin.core.logger.Level 18 | import timber.log.Timber 19 | 20 | @Suppress("Unused") 21 | @OptIn(ExperimentalSerializationApi::class) 22 | class App : Application(), ImageLoaderFactory { 23 | override fun onCreate() { 24 | super.onCreate() 25 | startKoin { 26 | androidContext(this@App) 27 | androidLogger(Level.DEBUG) 28 | modules(global, userProfileViewModel, userRepositoryViewModel) 29 | } 30 | if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) 31 | } 32 | 33 | override fun newImageLoader(): ImageLoader { 34 | return ImageLoader.Builder(applicationContext) 35 | .crossfade(true) 36 | .diskCache { 37 | DiskCache.Builder() 38 | .directory(applicationContext.cacheDir.resolve(coilCacheDir)) 39 | .maxSizePercent(diskCacheCap) 40 | .build() 41 | } 42 | .memoryCache { 43 | MemoryCache.Builder(applicationContext) 44 | .maxSizePercent(memoryCacheCap) 45 | .build() 46 | } 47 | .placeholder(R.mipmap.ic_launcher) 48 | .error(R.mipmap.ic_launcher) 49 | .build() 50 | } 51 | 52 | companion object { 53 | private const val coilCacheDir = "image_cache" 54 | private const val diskCacheCap = 0.02 55 | private const val memoryCacheCap = 0.25 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/customViews/snackBar/CustomSnackBar.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.customViews.snackBar 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.annotation.ColorInt 6 | import androidx.annotation.StringRes 7 | import com.google.android.material.snackbar.BaseTransientBottomBar 8 | import com.google.android.material.snackbar.Snackbar 9 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.databinding.CustomSnackbarLayoutBinding 10 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.delay.Delay.delayTaskBy 11 | 12 | class CustomSnackBar( 13 | content: ViewGroup, 14 | private val viewBinding: CustomSnackbarLayoutBinding, 15 | callback: com.google.android.material.snackbar.ContentViewCallback 16 | ) : BaseTransientBottomBar(content, viewBinding.root, callback) { 17 | private var hasSnackBarBeenRequestedToDismiss = false 18 | 19 | fun setText(@StringRes text: Int): CustomSnackBar { 20 | viewBinding.snackBarTextView.text = viewBinding.root.context.getString(text) 21 | return this 22 | } 23 | 24 | fun setBackgroundColor(@ColorInt backgroundColor: Int): CustomSnackBar { 25 | viewBinding.snackBarParentLinearLayout.setBackgroundColor(backgroundColor) 26 | return this 27 | } 28 | 29 | override fun dismiss() { 30 | if (!hasSnackBarBeenRequestedToDismiss) { 31 | hasSnackBarBeenRequestedToDismiss = true 32 | delayTaskBy(taskDelay) { 33 | super.dismiss() 34 | hasSnackBarBeenRequestedToDismiss = false 35 | } 36 | } 37 | } 38 | 39 | companion object { 40 | private const val taskDelay = 3000L 41 | 42 | fun make(parent: ViewGroup): CustomSnackBar { 43 | val content = CustomSnackbarLayoutBinding.inflate( 44 | LayoutInflater.from(parent.context), 45 | parent, 46 | false 47 | ) 48 | return CustomSnackBar( 49 | parent, 50 | content, 51 | CustomContentViewCallback() 52 | ).run { 53 | getView().setPadding(0, 0, 0, 0) 54 | this.duration = Snackbar.LENGTH_INDEFINITE 55 | this.animationMode = Snackbar.ANIMATION_MODE_SLIDE 56 | this 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/network/NetworkChecking.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.network 2 | 3 | import android.net.ConnectivityManager 4 | import android.net.Network 5 | import android.net.NetworkCapabilities 6 | import android.net.NetworkRequest 7 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states.* 8 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.extensions.emitValue 9 | import kotlinx.coroutines.flow.MutableStateFlow 10 | import kotlinx.coroutines.flow.StateFlow 11 | 12 | class NetworkChecking(private val manager: ConnectivityManager) { 13 | 14 | private val _networkStateFlow = MutableStateFlow>( 15 | InitialConnection 16 | ) 17 | private val networkStateFlow: StateFlow> 18 | get() = _networkStateFlow 19 | 20 | private val networkRequest = NetworkRequest.Builder().apply { 21 | addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) 22 | addTransportType(NetworkCapabilities.TRANSPORT_WIFI) 23 | }.build() 24 | 25 | private val connectivityCallback = object : ConnectivityManager.NetworkCallback() { 26 | 27 | override fun onAvailable(network: Network) { 28 | _networkStateFlow.emitValue(Available) 29 | } 30 | 31 | override fun onLost(network: Network) { 32 | _networkStateFlow.emitValue(Unavailable) 33 | } 34 | } 35 | 36 | fun obtainConnectionObserver(): StateFlow> { 37 | manager.registerNetworkCallback(networkRequest, connectivityCallback) 38 | return networkStateFlow 39 | } 40 | 41 | fun checkIfConnectionIsAvailable(): State { 42 | manager.run { 43 | getNetworkCapabilities(activeNetwork)?.run { 44 | val isAnyTransportMethodAvailable = 45 | hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) || 46 | hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || 47 | hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) || 48 | hasTransport(NetworkCapabilities.TRANSPORT_VPN) 49 | return if (isAnyTransportMethodAvailable) { 50 | Available 51 | } else { 52 | Unavailable 53 | } 54 | } ?: run { 55 | return Unavailable 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_octocat.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 15 | -------------------------------------------------------------------------------- /app/src/androidTest/java/instrumented/network/NetworkCheckingTest.kt: -------------------------------------------------------------------------------- 1 | package instrumented.network 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import kotlinx.coroutines.ExperimentalCoroutinesApi 6 | import kotlinx.coroutines.runBlocking 7 | import org.junit.Assert.assertEquals 8 | import org.junit.Test 9 | import org.junit.runner.RunWith 10 | import utils.base.TestSteps 11 | 12 | @RunWith(AndroidJUnit4::class) 13 | class NetworkCheckingTest : TestSteps { 14 | 15 | override fun setupDependencies() { 16 | // detekt : Empty Method 17 | } 18 | 19 | @Test 20 | fun checkIfInternetConnectionIsAvailable_applicationContext_isOffline() { 21 | var isOffline = false 22 | 23 | runBlocking { 24 | checkIfInternetConnectionIsAvailable( 25 | InstrumentationRegistry.getInstrumentation().targetContext, 26 | {}, 27 | { isOffline = true } 28 | ) 29 | } 30 | 31 | assertEquals( 32 | true, 33 | isOffline 34 | ) 35 | } 36 | 37 | @ExperimentalCoroutinesApi 38 | @Test 39 | fun internetConnectionAvailabilityObservable_applicationContext_isOffline() { 40 | var isOffline = false 41 | 42 | doWhen { 43 | runBlocking { 44 | isOffline = observeInternetConnectionAvailability( 45 | InstrumentationRegistry.getInstrumentation().targetContext 46 | ).drop(1).first() 47 | } 48 | } 49 | 50 | assertEquals( 51 | false, 52 | isOffline 53 | ) 54 | } 55 | 56 | @Test 57 | fun checkIfInternetConnectionIsAvailable_applicationContext_isOnline() { 58 | var isOnline = false 59 | 60 | runBlocking { 61 | checkIfInternetConnectionIsAvailable( 62 | InstrumentationRegistry.getInstrumentation().targetContext, 63 | { isOnline = true }, 64 | {} 65 | ) 66 | } 67 | 68 | assertEquals( 69 | true, 70 | isOnline 71 | ) 72 | } 73 | 74 | @ExperimentalCoroutinesApi 75 | @Test 76 | fun internetConnectionAvailabilityObservable_applicationContext_isOnline() { 77 | var isOnline = false 78 | 79 | doWhen { 80 | runBlocking { 81 | isOnline = observeInternetConnectionAvailability( 82 | InstrumentationRegistry.getInstrumentation().targetContext 83 | ).drop(1).first() 84 | } 85 | } 86 | 87 | assertEquals( 88 | true, 89 | isOnline 90 | ) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /app/src/main/res/layout/profile_view_holder_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 19 | 20 | 24 | 25 | 35 | 36 | 49 | 50 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /app/src/test/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/profile/unit/viewModel/ProfileViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.unit.viewModel 2 | 3 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.UserProfile 4 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.unit.viewModel.fakes.repository.local.FakeLocalRepository 5 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.unit.viewModel.fakes.repository.remote.FakeProfileInformationRepository 6 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.viewModel.ProfileViewModel 7 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.extensions.castValue 8 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.extensions.runTaskOnBackground 9 | import kotlinx.coroutines.ExperimentalCoroutinesApi 10 | import kotlinx.coroutines.runBlocking 11 | import org.junit.jupiter.api.Assertions.assertEquals 12 | import org.junit.jupiter.api.BeforeEach 13 | import org.junit.jupiter.api.Test 14 | import org.junit.jupiter.api.extension.ExtendWith 15 | import utils.base.TestSteps 16 | import utils.base.coroutines.junit5.CoroutinesTestExtension 17 | import utils.base.liveData.junit4.LiveDataTestUtil 18 | import utils.base.liveData.junit5.LiveDataTestExtension 19 | import java.io.Writer 20 | 21 | @ExperimentalCoroutinesApi 22 | @ExtendWith(CoroutinesTestExtension::class, LiveDataTestExtension::class) 23 | class ProfileViewModelTest : TestSteps { 24 | 25 | private lateinit var viewModel: ProfileViewModel 26 | 27 | @BeforeEach 28 | override fun setupDependencies() { 29 | viewModel = 30 | ProfileViewModel(FakeLocalRepository(), FakeProfileInformationRepository()) 31 | } 32 | 33 | @Test 34 | fun requestUpdatedGithubProfiles() = runBlocking { 35 | var githubInfo: UserProfile? = null 36 | 37 | given { 38 | viewModel.requestUpdatedProfiles() 39 | } 40 | 41 | doWhen { 42 | githubInfo = 43 | LiveDataTestUtil.getValue(viewModel.successStateFlow).first() 44 | } 45 | 46 | then { 47 | assertEquals("torvalds", githubInfo?.login) 48 | } 49 | } 50 | 51 | @Test 52 | fun requestMoreGithubProfiles() = runBlocking { 53 | given { 54 | // 55 | } 56 | 57 | doWhen { 58 | // 59 | } 60 | 61 | then { 62 | // 63 | } 64 | } 65 | 66 | @Test 67 | fun runTaskOnBackground_executeAGivenOperationOnBackground() { 68 | var value = 0 69 | 70 | doWhen { 71 | runBlocking { 72 | viewModel.runTaskOnBackground { 73 | value = 1 74 | } 75 | } 76 | } 77 | 78 | then { 79 | assertEquals(1, value) 80 | } 81 | } 82 | 83 | @Test 84 | fun castAttributeThroughViewModel_castTheGivenAttributeToTheGivenType() { 85 | val text: CharSequence = "blah" 86 | 87 | doWhen { 88 | viewModel.castValue(text) 89 | } 90 | 91 | then { 92 | assertEquals(true, text is String) 93 | assertEquals(false, text is Writer) 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/extensions/View.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.extensions 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import android.view.inputmethod.InputMethodManager 6 | import android.widget.EditText 7 | import android.widget.ImageView 8 | import androidx.annotation.ColorRes 9 | import androidx.annotation.DrawableRes 10 | import androidx.annotation.StringRes 11 | import androidx.core.content.ContextCompat 12 | import androidx.lifecycle.Lifecycle 13 | import androidx.lifecycle.LifecycleOwner 14 | import androidx.lifecycle.lifecycleScope 15 | import androidx.lifecycle.repeatOnLifecycle 16 | import androidx.swiperefreshlayout.widget.SwipeRefreshLayout 17 | import com.google.android.material.snackbar.Snackbar 18 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.R 19 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.cast.ValueCasting.castTo 20 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.customViews.snackBar.CustomSnackBar 21 | import kotlinx.coroutines.launch 22 | 23 | fun CustomSnackBar.showInternetConnectionStatusSnackBar( 24 | isConnectionAvailable: Boolean 25 | ) { 26 | if (isConnectionAvailable) { 27 | setText(R.string.back_online).setBackgroundColor( 28 | ContextCompat.getColor( 29 | context, 30 | R.color.green_700 31 | ) 32 | ) 33 | if (isShown) { 34 | dismiss() 35 | } 36 | } else { 37 | setText(R.string.no_connection).setBackgroundColor( 38 | ContextCompat.getColor( 39 | context, 40 | R.color.red_700 41 | ) 42 | ) 43 | show() 44 | } 45 | } 46 | 47 | fun EditText.hideKeyboard() { 48 | castTo(context.getSystemService(Context.INPUT_METHOD_SERVICE)) 49 | ?.hideSoftInputFromWindow(applicationWindowToken, 0) 50 | } 51 | 52 | fun ImageView.changeDrawable(@DrawableRes newDrawable: Int) { 53 | setImageDrawable( 54 | ContextCompat.getDrawable( 55 | context, 56 | newDrawable 57 | ) 58 | ) 59 | } 60 | 61 | @Suppress("UNUSED") 62 | fun LifecycleOwner.runTaskOnBackground(task: suspend () -> Unit) { 63 | lifecycleScope.launch { 64 | repeatOnLifecycle(Lifecycle.State.STARTED) { task() } 65 | } 66 | } 67 | 68 | fun View.applyBackgroundColor(@ColorRes color: Int) { 69 | setBackgroundColor(ContextCompat.getColor(context, color)) 70 | } 71 | 72 | @Suppress("UNUSED") 73 | fun View.applyViewVisibility(visibility: Int) { 74 | this.visibility = visibility 75 | } 76 | 77 | @Suppress("UNUSED") 78 | inline fun Snackbar.showErrorSnackBar( 79 | @StringRes message: Int, 80 | crossinline onDismissed: (() -> Any) = {} 81 | ) { 82 | setText(message) 83 | addCallback( 84 | object : Snackbar.Callback() { 85 | override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { 86 | super.onDismissed(transientBottomBar, event) 87 | onDismissed() 88 | } 89 | } 90 | ) 91 | show() 92 | } 93 | 94 | @Suppress("UNUSED") 95 | fun SwipeRefreshLayout.applySwipeRefreshVisibilityAttributes( 96 | isSwipeEnabled: Boolean = true 97 | ) { 98 | isEnabled = isSwipeEnabled 99 | } 100 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/profile/view/adapter/TransientViewsAdapter.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.view.adapter 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.RecyclerView 6 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.databinding.EmptyViewHolderLayoutBinding 7 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.databinding.EndOfResultsViewHolderLayoutBinding 8 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.databinding.LoadingViewHolderLayoutBinding 9 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.databinding.RetryViewHolderLayoutBinding 10 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.view.viewHolder.transientItemViews.EmptyViewHolder 11 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.view.viewHolder.transientItemViews.EmptyViewHolder.Companion.empty 12 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.view.viewHolder.transientItemViews.EndOfResultsViewHolder 13 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.view.viewHolder.transientItemViews.EndOfResultsViewHolder.Companion.endOfResults 14 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.view.viewHolder.transientItemViews.LoadingViewHolder 15 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.view.viewHolder.transientItemViews.LoadingViewHolder.Companion.loading 16 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.view.viewHolder.transientItemViews.RetryViewHolder 17 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.view.viewHolder.transientItemViews.RetryViewHolder.Companion.retry 18 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.interfaces.OnItemClicked 19 | 20 | class TransientViewsAdapter : RecyclerView.Adapter() { 21 | 22 | private lateinit var itemClicked: OnItemClicked 23 | private var viewType = empty 24 | 25 | override fun getItemViewType(position: Int) = viewType 26 | 27 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { 28 | val inflater = LayoutInflater.from(parent.context) 29 | return when (viewType) { 30 | loading -> { 31 | LoadingViewHolder( 32 | LoadingViewHolderLayoutBinding.inflate( 33 | inflater, 34 | parent, 35 | false 36 | ) 37 | ) 38 | } 39 | 40 | retry -> { 41 | RetryViewHolder( 42 | RetryViewHolderLayoutBinding.inflate( 43 | inflater, 44 | parent, 45 | false 46 | ), 47 | itemClicked 48 | ) 49 | } 50 | 51 | endOfResults -> EndOfResultsViewHolder( 52 | EndOfResultsViewHolderLayoutBinding.inflate( 53 | inflater, 54 | parent, 55 | false 56 | ) 57 | ) 58 | 59 | else -> 60 | EmptyViewHolder( 61 | EmptyViewHolderLayoutBinding.inflate( 62 | inflater, 63 | parent, 64 | false 65 | ) 66 | ) 67 | } 68 | } 69 | 70 | override fun getItemCount() = 1 71 | 72 | internal fun updateViewState(newState: Int) { 73 | viewType = newState 74 | } 75 | 76 | internal fun setOnItemClicked(onItemClicked: OnItemClicked) { 77 | itemClicked = onItemClicked 78 | } 79 | 80 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { 81 | // detekt : Empty block 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 125 | -------------------------------------------------------------------------------- /app/src/test/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/profile/unit/utils/base/repository/local/LocalRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.unit.utils.base.repository.local 2 | 3 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.unit.utils.base.repository.local.fakes.database.FakeDatabase 4 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.unit.utils.base.repository.local.fakes.protoDataStore.manager.FakeKeyValueStorageManager 5 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.repository.local.LocalRepository 6 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.repository.local.dataStore.model.ProfilePreferences 7 | import kotlinx.coroutines.ExperimentalCoroutinesApi 8 | import kotlinx.coroutines.test.runBlockingTest 9 | import org.junit.jupiter.api.Assertions.assertEquals 10 | import org.junit.jupiter.api.BeforeEach 11 | import org.junit.jupiter.api.Test 12 | import org.junit.jupiter.api.extension.ExtendWith 13 | import utils.base.TestSteps 14 | import utils.base.coroutines.junit5.CoroutinesTestExtension 15 | 16 | @ExperimentalCoroutinesApi 17 | @ExtendWith(CoroutinesTestExtension::class) 18 | class LocalRepositoryTest : TestSteps { 19 | 20 | private lateinit var fakeProtoDataStore: FakeKeyValueStorageManager 21 | private lateinit var fakeDatabase: FakeDatabase 22 | private lateinit var localRepository: LocalRepository 23 | 24 | @BeforeEach 25 | override fun setupDependencies() { 26 | fakeProtoDataStore = FakeKeyValueStorageManager() 27 | fakeDatabase = FakeDatabase() 28 | localRepository = LocalRepository(fakeProtoDataStore, fakeDatabase) 29 | } 30 | 31 | @Test 32 | fun obtainValueFromDataStore_valuesOfVariousTypes_returnDefaultValues() { 33 | then { 34 | runBlockingTest { 35 | assertEquals( 36 | 0, 37 | localRepository.obtainProtoDataStore().obtainData().pageNumber 38 | ) 39 | assertEquals( 40 | false, 41 | localRepository.obtainProtoDataStore().obtainData().isHeaderVisible 42 | ) 43 | assertEquals( 44 | "", 45 | localRepository.obtainProtoDataStore().obtainData().currentProfile 46 | ) 47 | } 48 | } 49 | } 50 | 51 | @Test 52 | fun updateDataStoreValue_valuesOfVariousTypes_returnChangedValues() { 53 | doWhen { 54 | localRepository.obtainProtoDataStore().apply { 55 | runBlockingTest { 56 | updateData(ProfilePreferences(pageNumber = 7)) 57 | updateData(obtainData().copy(isHeaderVisible = true)) 58 | updateData(obtainData().copy(currentProfile = "torvalds")) 59 | } 60 | } 61 | } 62 | 63 | then { 64 | runBlockingTest { 65 | assertEquals( 66 | 7, 67 | localRepository.obtainProtoDataStore().obtainData().pageNumber 68 | ) 69 | assertEquals( 70 | true, 71 | localRepository.obtainProtoDataStore().obtainData().isHeaderVisible 72 | ) 73 | assertEquals( 74 | "torvalds", 75 | localRepository.obtainProtoDataStore().obtainData().currentProfile 76 | ) 77 | } 78 | } 79 | } 80 | 81 | @Test 82 | fun saveValueToDataStore_commandToClearAllValuesInsideProtoDataStore_clearAllValuesInsideProtoDataStore() { 83 | given { 84 | runBlockingTest { 85 | localRepository.obtainProtoDataStore().updateData(ProfilePreferences(pageNumber = 7)) 86 | } 87 | } 88 | 89 | doWhen { 90 | runBlockingTest { 91 | localRepository.obtainProtoDataStore().updateData(ProfilePreferences()) 92 | } 93 | } 94 | 95 | then { 96 | runBlockingTest { 97 | assertEquals(0, localRepository.obtainProtoDataStore().obtainData().pageNumber) 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/utils/base/repository/remote/RemoteRepository.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.repository.remote 2 | 3 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states.ClientSide 4 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states.Connect 5 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states.Error 6 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states.Generic 7 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states.SSLHandshake 8 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states.SearchLimitReached 9 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states.SearchQuotaReached 10 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states.ServerSide 11 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states.SocketTimeout 12 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states.State 13 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states.Success 14 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states.SuccessWithBody 15 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states.SuccessWithoutBody 16 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states.UnknownHost 17 | import retrofit2.Response 18 | import java.io.IOException 19 | import java.net.ConnectException 20 | import java.net.SocketTimeoutException 21 | import java.net.UnknownHostException 22 | import javax.net.ssl.SSLHandshakeException 23 | 24 | class RemoteRepository { 25 | 26 | suspend fun call( 27 | call: suspend () -> Response 28 | ): State<*> { 29 | return try { 30 | val response = call() 31 | if (response.isSuccessful) { 32 | handleSuccess(response) 33 | } else { 34 | handleHttpError(response.code()) 35 | } 36 | } catch (exception: IOException) { 37 | handleException(exception) 38 | } 39 | } 40 | 41 | private fun handleSuccess(response: Response<*>): State { 42 | with(response) { 43 | body()?.let { apiResponse -> 44 | return SuccessWithBody(apiResponse, obtainTotalPages(headers())) 45 | } ?: run { 46 | return SuccessWithoutBody 47 | } 48 | } 49 | } 50 | 51 | private fun handleHttpError(responseCode: Int): State { 52 | return when (responseCode) { 53 | in Error400..Error402, in Error403..Error404 -> ClientSide 54 | Error422 -> SearchQuotaReached 55 | Error451 -> SearchLimitReached 56 | in Error500..Error511 -> ServerSide 57 | else -> Generic 58 | } 59 | } 60 | 61 | private fun handleException(exception: IOException): State { 62 | return when (exception) { 63 | is ConnectException -> Connect 64 | is SocketTimeoutException -> SocketTimeout 65 | is SSLHandshakeException -> SSLHandshake 66 | is UnknownHostException -> UnknownHost 67 | else -> Generic 68 | } 69 | } 70 | 71 | private fun obtainTotalPages(headers: okhttp3.Headers): Int { 72 | var totalPages = 0 73 | headers[headerName]?.let { header -> 74 | if (header.isNotEmpty()) { 75 | totalPages = Regex(headerPattern).findAll(header) 76 | .map(MatchResult::value) 77 | .toList()[headerListIndex].toInt() 78 | } 79 | } 80 | return totalPages 81 | } 82 | 83 | companion object { 84 | private const val headerName = "link" 85 | private val headerPattern = "\\d+".toPattern().toString() 86 | private const val headerListIndex = 2 87 | private const val Error400 = 400 88 | private const val Error402 = 402 89 | private const val Error403 = 403 90 | private const val Error404 = 404 91 | private const val Error422 = 422 92 | private const val Error451 = 451 93 | private const val Error500 = 500 94 | private const val Error511 = 511 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_profile_listing.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 18 | 19 | 26 | 27 | 28 | 29 | 41 | 42 | 53 | 54 | 55 | 56 | 66 | 67 | 80 | 81 | 93 | 94 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'com.google.devtools.ksp' 4 | id 'com.google.protobuf' version "$protobufPlugin" 5 | id 'kotlin-android' 6 | id 'kotlinx-serialization' 7 | } 8 | apply from: "${toolsPath}codeChecking.gradle" 9 | apply from: "${toolsPath}coverage.gradle" 10 | apply from: "${toolsPath}detekt.gradle" 11 | apply from: "${toolsPath}lint.gradle" 12 | apply from: "${toolsPath}protobuf.gradle" 13 | 14 | android { 15 | compileSdkVersion 33 16 | defaultConfig { 17 | applicationId "githubprofilesearcher.caiodev.com.br.githubprofilesearcher" 18 | minSdkVersion 23 19 | targetSdkVersion 33 20 | versionCode 1 21 | versionName "1.0" 22 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 23 | vectorDrawables.useSupportLibrary = true 24 | } 25 | 26 | ksp { arg("room.schemaLocation", "$projectDir/schemas") } 27 | 28 | buildTypes { 29 | debug { 30 | minifyEnabled false 31 | debuggable = true 32 | testCoverageEnabled = true 33 | signingConfig signingConfigs.debug 34 | buildConfigField "String", "API_URL", "$apiUrl" 35 | sourceSets { 36 | main { 37 | java { srcDirs = ["build/generated/ksp/debug/kotlin"] } 38 | } 39 | } 40 | } 41 | 42 | release { 43 | minifyEnabled true 44 | debuggable = false 45 | testCoverageEnabled false 46 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 47 | buildConfigField "String", "API_URL", "$apiUrl" 48 | sourceSets { 49 | main { 50 | java { srcDirs = ["build/generated/ksp/release/kotlin"] } 51 | } 52 | } 53 | } 54 | } 55 | 56 | android.buildFeatures.viewBinding = true 57 | 58 | compileOptions { 59 | sourceCompatibility JavaVersion.VERSION_11 60 | targetCompatibility JavaVersion.VERSION_11 61 | } 62 | 63 | kotlinOptions { 64 | freeCompilerArgs += ['-opt-in=kotlin.RequiresOptIn'] 65 | jvmTarget = JavaVersion.VERSION_11 66 | } 67 | 68 | buildFeatures { 69 | compose true 70 | } 71 | 72 | composeOptions { 73 | kotlinCompilerExtensionVersion "$compiler" 74 | } 75 | 76 | testOptions { 77 | unitTests.all { useJUnitPlatform() } 78 | } 79 | 80 | sourceSets { 81 | androidTest { java.srcDirs += "$sharedTestCodePath" } 82 | test { java.srcDirs += "$sharedTestCodePath" } 83 | } 84 | packagingOptions { 85 | resources { 86 | excludes += '/META-INF/{AL2.0,LGPL2.1,LICENSE.md,LICENSE-notice.md}' 87 | } 88 | } 89 | 90 | namespace 'githubprofilesearcher.caiodev.com.br.githubprofilesearcher' 91 | } 92 | 93 | dependencies { 94 | implementation fileTree(dir: 'libs', include: ['*.jar']) 95 | 96 | //Annotation 97 | implementation "androidx.annotation:annotation:$annotation" 98 | 99 | //AppCompat 100 | implementation "androidx.appcompat:appcompat:$appCompat" 101 | 102 | //Coil 103 | implementation "io.coil-kt:coil:$coil" 104 | 105 | //Compose 106 | implementation "androidx.activity:activity-compose:$activity" 107 | implementation "androidx.compose.foundation:foundation:$composeCore" 108 | implementation "androidx.compose.material:material:$composeCore" 109 | implementation "androidx.compose.material:material-icons-core:$composeCore" 110 | implementation "androidx.compose.material:material-icons-extended:$composeCore" 111 | implementation "androidx.compose.ui:ui:$composeCore" 112 | implementation "androidx.compose.ui:ui-tooling-preview:$composeCore" 113 | implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle" 114 | debugImplementation "androidx.compose.ui:ui-tooling:$composeCore" 115 | 116 | //Core 117 | implementation "androidx.core:core-ktx:$ktxCore" 118 | 119 | //DataStore 120 | implementation "androidx.datastore:datastore:$dataStore" 121 | 122 | //Koin 123 | implementation "io.insert-koin:koin-android:$koin" 124 | 125 | //LeakCanary 126 | debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanary" 127 | 128 | //Lifecycle section 129 | implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle" 130 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle" 131 | 132 | //Logging section 133 | implementation "com.squareup.okhttp3:logging-interceptor:$okHttp" 134 | implementation "com.jakewharton.timber:timber:$timber" 135 | 136 | //Material design 137 | implementation "com.google.android.material:material:$materialDesign" 138 | 139 | //Protobuf 140 | implementation "com.google.protobuf:protobuf-javalite:$protobufVersion" 141 | 142 | //APIConnector 143 | implementation "com.squareup.retrofit2:retrofit:$retrofit" 144 | implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:$serializationConverter" 145 | 146 | //Room 147 | implementation "androidx.room:room-runtime:$room" 148 | ksp "androidx.room:room-compiler:$room" 149 | implementation "androidx.room:room-ktx:$room" 150 | 151 | //Serialization 152 | implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization" 153 | 154 | //Views/ViewGroups 155 | implementation "androidx.constraintlayout:constraintlayout:$constraintLayout" 156 | implementation "androidx.recyclerview:recyclerview:$recyclerView" 157 | implementation "androidx.swiperefreshlayout:swiperefreshlayout:$swipeRefreshLayout" 158 | 159 | /*Testing section*/ 160 | 161 | //JVM 162 | //ArchTestCore 163 | testImplementation "androidx.arch.core:core-testing:$archTest" 164 | 165 | //Coroutines 166 | testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines" 167 | 168 | //Jacoco 169 | testImplementation "org.jacoco:org.jacoco.ant:$jacocoVersion" 170 | 171 | //JUnit 172 | testImplementation "junit:junit:$junit" 173 | testImplementation "org.junit.jupiter:junit-jupiter:$junit5" 174 | 175 | //Kotest 176 | testImplementation "io.kotest:kotest-assertions-core:$kotest" 177 | testImplementation "io.kotest:kotest-property:$kotest" 178 | testImplementation "io.kotest:kotest-runner-junit5:$kotest" 179 | 180 | //MockWebServer 181 | testImplementation "com.squareup.okhttp3:mockwebserver:$okHttp" 182 | 183 | //Test core 184 | testImplementation "androidx.test:core-ktx:$testCore" 185 | 186 | //Instrumented/UI 187 | //ArchTestCore 188 | androidTestImplementation "androidx.arch.core:core-testing:$archTest" 189 | 190 | //Compose 191 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:$composeCore" 192 | debugImplementation "androidx.compose.ui:ui-test-manifest:$composeCore" 193 | 194 | //Coroutines 195 | androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines" 196 | 197 | //Espresso 198 | implementation "androidx.test.espresso:espresso-idling-resource:$espresso" 199 | androidTestImplementation "androidx.test.espresso:espresso-accessibility:$espresso" 200 | androidTestImplementation "androidx.test.espresso:espresso-contrib:$espresso" 201 | androidTestImplementation "androidx.test.espresso:espresso-core:$espresso" 202 | androidTestImplementation "androidx.test.espresso:espresso-intents:$espresso" 203 | 204 | //JUnit 205 | androidTestImplementation "androidx.test.ext:junit-ktx:$junitAndroidX" 206 | androidTestImplementation "org.junit.jupiter:junit-jupiter:$junit5" 207 | 208 | //MockWebServer 209 | androidTestImplementation "com.squareup.okhttp3:mockwebserver:$okHttp" 210 | 211 | //Runner 212 | androidTestImplementation "androidx.test:runner:$testCore" 213 | 214 | //Test core 215 | androidTestImplementation "androidx.test:core-ktx:$testCore" 216 | } -------------------------------------------------------------------------------- /app/src/test/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/profile/unit/utils/base/repository/remote/RemoteRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.unit.utils.base.repository.remote 2 | 3 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.Profile 4 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.callInterface.UserProfile 5 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.repository.remote.RemoteRepository 6 | import kotlinx.coroutines.ExperimentalCoroutinesApi 7 | import kotlinx.coroutines.runBlocking 8 | import kotlinx.serialization.ExperimentalSerializationApi 9 | import okhttp3.mockwebserver.MockResponse 10 | import okhttp3.mockwebserver.MockWebServer 11 | import org.junit.jupiter.api.AfterEach 12 | import org.junit.jupiter.api.Assertions.assertEquals 13 | import org.junit.jupiter.api.BeforeEach 14 | import org.junit.jupiter.api.Test 15 | import retrofit2.Response 16 | import utils.base.TestSteps 17 | import utils.base.api.MockedAPIResponseProvider.profileInfoCallResult 18 | import utils.base.api.factory.RetrofitTestService.newInstance 19 | import utils.base.api.factory.RetrofitTestService.setup 20 | import java.io.IOException 21 | import java.net.ConnectException 22 | import java.net.SocketTimeoutException 23 | import java.net.UnknownHostException 24 | import javax.net.ssl.SSLHandshakeException 25 | 26 | class RemoteRepositoryTest : TestSteps { 27 | 28 | private lateinit var mockWebServer: MockWebServer 29 | private lateinit var userProfile: UserProfile 30 | private lateinit var remoteRepository: RemoteRepository 31 | 32 | @OptIn(ExperimentalSerializationApi::class) 33 | @BeforeEach 34 | override fun setupDependencies() { 35 | mockWebServer = setup() 36 | userProfile = newInstance() 37 | remoteRepository = 38 | RemoteRepository() 39 | } 40 | 41 | @AfterEach 42 | fun teardown() { 43 | mockWebServer.shutdown() 44 | } 45 | 46 | @Suppress("UNCHECKED_CAST") 47 | @ExperimentalCoroutinesApi 48 | @Test 49 | fun callApi__returnsASuccessfulResponse200() { 50 | var response = Any() 51 | 52 | given { 53 | mockWebServer.enqueue( 54 | MockResponse() 55 | .setResponseCode(200) 56 | .setBody( 57 | profileInfoCallResult 58 | ) 59 | ) 60 | } 61 | 62 | doWhen { 63 | runBlocking { 64 | val userProfile = userProfile.provideGithubUsersListAsync("torvalds", 1, 20) 65 | response = remoteRepository.call { userProfile } 66 | } 67 | } 68 | 69 | then { 70 | var isSuccess = false 71 | if (response is States.Success<*>) isSuccess = true 72 | 73 | assertEquals(true, isSuccess) 74 | } 75 | } 76 | 77 | @Suppress("UNCHECKED_CAST") 78 | @ExperimentalCoroutinesApi 79 | @Test 80 | fun callApi__returnsASuccessfulResponse204NoContent() { 81 | var response = Any() 82 | 83 | given { 84 | mockWebServer.enqueue( 85 | MockResponse() 86 | .setResponseCode(204) 87 | .setBody( 88 | "" 89 | ) 90 | ) 91 | } 92 | 93 | doWhen { 94 | runBlocking { 95 | val userProfile = userProfile.provideGithubUsersListAsync("torvalds", 1, 20) 96 | response = remoteRepository.call { userProfile } 97 | } 98 | } 99 | 100 | then { 101 | var isSuccess = false 102 | if (response is States.Success<*>) isSuccess = true 103 | 104 | assertEquals(true, isSuccess) 105 | assertEquals(1, (response as States.Success).data) 106 | } 107 | } 108 | 109 | @ExperimentalCoroutinesApi 110 | @Test 111 | fun callApi__returnsAnUnsuccessfulResponse403Forbidden() { 112 | var response = Any() 113 | 114 | given { 115 | mockWebServer.enqueue( 116 | MockResponse() 117 | .setResponseCode(403) 118 | .setBody( 119 | "" 120 | ) 121 | ) 122 | } 123 | 124 | doWhen { 125 | runBlocking { 126 | val userProfile = userProfile.provideGithubUsersListAsync("torvalds", 1, 20) 127 | response = remoteRepository.call { userProfile } 128 | } 129 | } 130 | 131 | then { 132 | var isSuccessful = false 133 | if (response is States.Error) isSuccessful = false 134 | 135 | assertEquals(false, isSuccessful) 136 | assertEquals(9, (response as States.Error).error) 137 | } 138 | } 139 | 140 | @ExperimentalCoroutinesApi 141 | @Test 142 | fun callApi__returnsAnUnsuccessfulResponse400Till402And404Till494() { 143 | var response = Any() 144 | 145 | given { 146 | mockWebServer.enqueue( 147 | MockResponse() 148 | .setResponseCode(451) 149 | .setBody( 150 | "" 151 | ) 152 | ) 153 | } 154 | 155 | doWhen { 156 | runBlocking { 157 | val userProfile = userProfile.provideGithubUsersListAsync("torvalds", 1, 20) 158 | response = remoteRepository.call { userProfile } 159 | } 160 | } 161 | 162 | then { 163 | var isSuccessful = false 164 | if (response is States.Error) isSuccessful = false 165 | 166 | assertEquals(false, isSuccessful) 167 | assertEquals(7, (response as States.Error).error) 168 | } 169 | } 170 | 171 | @ExperimentalCoroutinesApi 172 | @Test 173 | fun callApi__returnsAnUnsuccessfulResponse500Till598() { 174 | var response = Any() 175 | 176 | given { 177 | mockWebServer.enqueue( 178 | MockResponse() 179 | .setResponseCode(530) 180 | .setBody( 181 | "" 182 | ) 183 | ) 184 | } 185 | 186 | doWhen { 187 | runBlocking { 188 | val userProfile = userProfile.provideGithubUsersListAsync("torvalds", 1, 20) 189 | response = remoteRepository.call { userProfile } 190 | } 191 | } 192 | 193 | then { 194 | var isSuccessful = false 195 | if (response is States.Error) isSuccessful = false 196 | 197 | assertEquals(false, isSuccessful) 198 | assertEquals(8, (response as States.Error).error) 199 | } 200 | } 201 | 202 | @ExperimentalCoroutinesApi 203 | @Test 204 | fun callApi__returnsAnUnsuccessfulResponseGenericError() { 205 | var response = Any() 206 | 207 | given { 208 | mockWebServer.enqueue( 209 | MockResponse() 210 | .setResponseCode(750) 211 | .setBody( 212 | "" 213 | ) 214 | ) 215 | } 216 | 217 | doWhen { 218 | runBlocking { 219 | val userProfile = userProfile.provideGithubUsersListAsync("torvalds", 1, 20) 220 | response = remoteRepository.call { userProfile } 221 | } 222 | } 223 | 224 | then { 225 | var isSuccessful = false 226 | if (response is States.Error) isSuccessful = false 227 | 228 | assertEquals(false, isSuccessful) 229 | assertEquals(10, (response as States.Error).error) 230 | } 231 | } 232 | 233 | @Test 234 | fun callApi__connectException() { 235 | var response = Any() 236 | 237 | doWhen { 238 | runBlocking { 239 | response = 240 | remoteRepository.call { setupException(ConnectException()) } 241 | } 242 | } 243 | 244 | then { 245 | assertEquals(2, (response as States.Error).error) 246 | } 247 | } 248 | 249 | @Test 250 | fun callApi__genericException() { 251 | var response = Any() 252 | 253 | doWhen { 254 | runBlocking { 255 | response = 256 | remoteRepository.call { setupException() } 257 | } 258 | } 259 | 260 | then { 261 | assertEquals(3, (response as States.Error).error) 262 | } 263 | } 264 | 265 | @Test 266 | fun callApi__socketTimeoutException() { 267 | var response = Any() 268 | 269 | doWhen { 270 | runBlocking { 271 | response = 272 | remoteRepository.call { setupException(SocketTimeoutException()) } 273 | } 274 | } 275 | 276 | then { 277 | assertEquals(4, (response as States.Error).error) 278 | } 279 | } 280 | 281 | @Test 282 | fun callApi__sslHandshakeException() { 283 | var response = Any() 284 | 285 | doWhen { 286 | runBlocking { 287 | response = 288 | remoteRepository.call { setupException(SSLHandshakeException("")) } 289 | } 290 | } 291 | 292 | then { 293 | assertEquals(5, (response as States.Error).error) 294 | } 295 | } 296 | 297 | @Test 298 | fun callApi__unknownHostException() { 299 | var response = Any() 300 | 301 | doWhen { 302 | runBlocking { 303 | response = 304 | remoteRepository.call { setupException(UnknownHostException()) } 305 | } 306 | } 307 | 308 | then { 309 | assertEquals(6, (response as States.Error).error) 310 | } 311 | } 312 | 313 | private fun setupException( 314 | exception: IOException = IOException() 315 | ): Response { 316 | throwException(exception) 317 | return listOf>()[0] 318 | } 319 | 320 | private fun throwException( 321 | exception: IOException 322 | ) { 323 | throw exception 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /app/src/main/java/githubprofilesearcher/caiodev/com/br/githubprofilesearcher/sections/profile/viewModel/ProfileViewModel.kt: -------------------------------------------------------------------------------- 1 | package githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.viewModel 2 | 3 | import androidx.lifecycle.ViewModel 4 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.ProfilePreferences 5 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.Profile 6 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.UserProfile 7 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.profile.model.repository.remote.IProfileRepository 8 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.interfaces.ILocalRepository 9 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states.ActionNotRequired 10 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states.InitialIntermediate 11 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states.InitialSuccess 12 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states.Intermediate 13 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states.LocalPopulation 14 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states.State 15 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states.Success 16 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states.SuccessWithBody 17 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.states.SuccessWithoutBody 18 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.base.string.emptyString 19 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.cast.ValueCasting 20 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.cast.ValueCasting.castTo 21 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.extensions.runTaskOnBackground 22 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.extensions.runTaskOnForeground 23 | import githubprofilesearcher.caiodev.com.br.githubprofilesearcher.sections.utils.network.NetworkChecking 24 | import kotlinx.coroutines.flow.MutableSharedFlow 25 | import kotlinx.coroutines.flow.MutableStateFlow 26 | import kotlinx.coroutines.flow.StateFlow 27 | import kotlinx.coroutines.flow.asSharedFlow 28 | 29 | internal class ProfileViewModel( 30 | private val networkChecking: NetworkChecking, 31 | private val localRepository: ILocalRepository, 32 | private val remoteRepository: IProfileRepository 33 | ) : ViewModel() { 34 | 35 | private val _successStateFlow = MutableStateFlow>(InitialSuccess) 36 | internal val successStateFlow: StateFlow> 37 | get() = _successStateFlow 38 | 39 | private val _intermediateSharedFlow = MutableSharedFlow>() 40 | internal val intermediateSharedFlow = _intermediateSharedFlow.asSharedFlow() 41 | 42 | private val _errorSharedFlow = 43 | MutableSharedFlow>() 45 | internal val errorSharedFlow = _errorSharedFlow.asSharedFlow() 46 | 47 | private val _profileInfoList = mutableListOf() 48 | private var profilesInfoList: List = _profileInfoList 49 | 50 | private var currentIntermediateState: State = InitialIntermediate 51 | 52 | private var profilePreferences = ProfilePreferences.getDefaultInstance() 53 | 54 | fun requestUpdatedProfiles(profile: String = emptyString()) { 55 | saveValueToDataStore( 56 | obtainValueFromDataStore().toBuilder().setPageNumber(initialPage).build() 57 | ) 58 | 59 | if (profile.isNotEmpty()) { 60 | saveValueToDataStore( 61 | obtainValueFromDataStore().toBuilder().setTemporaryCurrentProfile(profile).build() 62 | ) 63 | requestProfiles(profile, true) 64 | } else { 65 | requestProfiles( 66 | obtainValueFromDataStore().temporaryCurrentProfile, 67 | true 68 | ) 69 | } 70 | } 71 | 72 | fun paginateProfiles() { 73 | requestProfiles(obtainValueFromDataStore().currentProfile, false) 74 | } 75 | 76 | private fun requestProfiles( 77 | profile: String, 78 | shouldListItemsBeRemoved: Boolean 79 | ) { 80 | if (shouldListItemsBeRemoved) 81 | handleCall(profile, true) 82 | else 83 | handleCall(profile, false) 84 | } 85 | 86 | private fun handleCall( 87 | user: String, 88 | shouldListItemsBeRemoved: Boolean = false 89 | ) { 90 | runTaskOnBackground { 91 | val value = 92 | remoteRepository.provideUserInformation( 93 | user, 94 | obtainValueFromDataStore().pageNumber, 95 | itemsPerPage 96 | ) 97 | handleResult(value, shouldListItemsBeRemoved) 98 | } 99 | } 100 | 101 | private suspend fun handleResult(value: Any, shouldListItemsBeRemoved: Boolean) { 102 | when (value) { 103 | is SuccessWithBody<*> -> { 104 | saveValueToDataStore( 105 | obtainValueFromDataStore().toBuilder().setCurrentProfile(emptyString()).build() 106 | ) 107 | 108 | if (!obtainValueFromDataStore().hasASuccessfulCallAlreadyBeenMade 109 | ) { 110 | saveValueToDataStore( 111 | obtainValueFromDataStore().toBuilder() 112 | .setHasASuccessfulCallAlreadyBeenMade(true) 113 | .build() 114 | ) 115 | } 116 | 117 | if (shouldListItemsBeRemoved) { 118 | setupUpdatedList(value) 119 | } else { 120 | setupPaginationList(successWithBody = value) 121 | } 122 | } 123 | 124 | SuccessWithoutBody -> Unit 125 | 126 | else -> { 127 | handleError(ValueCasting.castTo(value)) 128 | } 129 | } 130 | } 131 | 132 | private suspend fun handleError( 133 | error: State? 135 | ) { 136 | error?.let { 137 | _errorSharedFlow.emit(error) 138 | } 139 | } 140 | 141 | private fun setupUpdatedList( 142 | successWithBody: SuccessWithBody<*> 143 | ) { 144 | runTaskOnBackground { 145 | successWithBody.apply { 146 | localRepository.dropProfileInformation() 147 | if (_profileInfoList.isNotEmpty()) { 148 | _profileInfoList.clear() 149 | } 150 | castTo(successWithBody.data)?.let { profile -> 151 | addContentToProfileInfoList(profile.profile) 152 | localRepository.insertProfilesIntoDb( 153 | profile.profile 154 | ) 155 | } 156 | saveDataAfterSuccess(this) 157 | } 158 | } 159 | } 160 | 161 | private fun setupPaginationList( 162 | shouldSavedListBeUsed: Boolean = false, 163 | successWithBody: SuccessWithBody<*> = SuccessWithBody(Any()) 164 | ) { 165 | runTaskOnBackground { 166 | successWithBody.apply { 167 | if (!shouldSavedListBeUsed) { 168 | castTo(successWithBody.data)?.let { 169 | addContentToProfileInfoList(it.profile) 170 | } 171 | localRepository.insertProfilesIntoDb(profilesInfoList) 172 | } else { 173 | addContentToProfileInfoList(localRepository.getProfilesFromDb()) 174 | } 175 | saveDataAfterSuccess(this) 176 | } 177 | } 178 | } 179 | 180 | fun obtainValueFromDataStore(): ProfilePreferences { 181 | runTaskOnForeground { 182 | profilePreferences = castTo(localRepository.obtainProtoDataStore().obtainData()) 183 | } 184 | return profilePreferences 185 | } 186 | 187 | fun saveValueToDataStore(profile: ProfilePreferences) { 188 | runTaskOnForeground { 189 | localRepository.obtainProtoDataStore().updateData( 190 | castTo(profile) 191 | ) 192 | } 193 | } 194 | 195 | private fun addContentToProfileInfoList(list: List) { 196 | _profileInfoList.addAll(list) 197 | } 198 | 199 | fun updateUIWithCache() { 200 | setupPaginationList(shouldSavedListBeUsed = true) 201 | } 202 | 203 | fun obtainConnectionState() = networkChecking.checkIfConnectionIsAvailable() 204 | 205 | fun provideConnectionObserver() = networkChecking.obtainConnectionObserver() 206 | 207 | private suspend fun saveDataAfterSuccess(successWithBody: SuccessWithBody<*>) { 208 | saveValueToDataStore( 209 | obtainValueFromDataStore().toBuilder() 210 | .setCurrentProfile(obtainValueFromDataStore().temporaryCurrentProfile) 211 | .build() 212 | ) 213 | saveValueToDataStore( 214 | obtainValueFromDataStore().toBuilder() 215 | .setHasLastCallBeenUnsuccessful(false) 216 | .build() 217 | ) 218 | saveValueToDataStore( 219 | obtainValueFromDataStore().toBuilder() 220 | .setIsThereAnOngoingCall(false) 221 | .build() 222 | ) 223 | if (obtainValueFromDataStore().hasUserDeletedProfileText && 224 | obtainValueFromDataStore().profile.isNotEmpty() 225 | ) { 226 | saveValueToDataStore( 227 | obtainValueFromDataStore().toBuilder() 228 | .setShouldASearchBePerformed(true) 229 | .build() 230 | ) 231 | } else { 232 | saveValueToDataStore( 233 | obtainValueFromDataStore().toBuilder() 234 | .setShouldASearchBePerformed(false) 235 | .build() 236 | ) 237 | } 238 | 239 | obtainValueFromDataStore().apply { 240 | saveValueToDataStore( 241 | toBuilder().setPageNumber( 242 | pageNumber.plus( 243 | initialPage 244 | ) 245 | ).build() 246 | ) 247 | } 248 | 249 | _successStateFlow.emit(InitialSuccess) 250 | _successStateFlow.emit( 251 | SuccessWithBody( 252 | data = profilesInfoList, 253 | successWithBody.totalPages 254 | ) 255 | ) 256 | } 257 | 258 | fun checkDataAtStartup() { 259 | runTaskOnBackground { 260 | if (successStateFlow.value == InitialSuccess && 261 | localRepository.getProfilesFromDb().isEmpty() 262 | ) { 263 | postIntermediateState(ActionNotRequired) 264 | } else if (localRepository.getProfilesFromDb().isNotEmpty() && 265 | profilesInfoList.isEmpty() 266 | ) { 267 | saveValueToDataStore( 268 | obtainValueFromDataStore() 269 | .toBuilder().setIsLocalPopulation(true).build() 270 | ) 271 | saveValueToDataStore( 272 | obtainValueFromDataStore().toBuilder() 273 | .setHasASuccessfulCallAlreadyBeenMade(false) 274 | .build() 275 | ) 276 | postIntermediateState(LocalPopulation) 277 | } 278 | } 279 | } 280 | 281 | private suspend fun postIntermediateState(state: State) { 282 | if (state != currentIntermediateState) { 283 | currentIntermediateState = state 284 | _intermediateSharedFlow.emit(state) 285 | } 286 | } 287 | 288 | internal inline fun castTo(value: Any) = ValueCasting.castTo(value) 289 | 290 | companion object { 291 | const val itemsPerPage = 20 292 | private const val initialPage = 1 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /config/detekt/detekt.yml: -------------------------------------------------------------------------------- 1 | build: 2 | maxIssues: 0 3 | excludeCorrectable: false 4 | weights: 5 | # complexity: 2 6 | # LongParameterList: 1 7 | # style: 1 8 | # comments: 1 9 | 10 | config: 11 | validation: true 12 | warningsAsErrors: false 13 | # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]' 14 | excludes: '' 15 | 16 | processors: 17 | active: true 18 | exclude: 19 | - 'DetektProgressListener' 20 | # - 'FunctionCountProcessor' 21 | # - 'PropertyCountProcessor' 22 | # - 'ClassCountProcessor' 23 | # - 'PackageCountProcessor' 24 | # - 'KtFileCountProcessor' 25 | 26 | console-reports: 27 | active: true 28 | exclude: 29 | - 'ProjectStatisticsReport' 30 | - 'ComplexityReport' 31 | - 'NotificationReport' 32 | # - 'FindingsReport' 33 | - 'FileBasedFindingsReport' 34 | 35 | output-reports: 36 | active: true 37 | exclude: 38 | - 'HtmlOutputReport' 39 | 40 | comments: 41 | active: true 42 | AbsentOrWrongFileLicense: 43 | active: false 44 | licenseTemplateFile: 'license.template' 45 | CommentOverPrivateFunction: 46 | active: false 47 | CommentOverPrivateProperty: 48 | active: false 49 | EndOfSentenceFormat: 50 | active: false 51 | endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)' 52 | UndocumentedPublicClass: 53 | active: false 54 | searchInNestedClass: true 55 | searchInInnerClass: true 56 | searchInInnerObject: true 57 | searchInInnerInterface: true 58 | UndocumentedPublicFunction: 59 | active: false 60 | UndocumentedPublicProperty: 61 | active: false 62 | 63 | complexity: 64 | active: true 65 | ComplexCondition: 66 | active: true 67 | threshold: 4 68 | ComplexInterface: 69 | active: false 70 | threshold: 10 71 | includeStaticDeclarations: false 72 | includePrivateDeclarations: false 73 | ComplexMethod: 74 | active: true 75 | threshold: 15 76 | ignoreSingleWhenExpression: false 77 | ignoreSimpleWhenEntries: false 78 | ignoreNestingFunctions: false 79 | nestingFunctions: [run, let, apply, with, also, use, forEach, isNotNull, ifNull] 80 | LabeledExpression: 81 | active: false 82 | ignoredLabels: [] 83 | LargeClass: 84 | active: true 85 | threshold: 600 86 | LongMethod: 87 | active: true 88 | threshold: 60 89 | LongParameterList: 90 | active: true 91 | functionThreshold: 6 92 | constructorThreshold: 7 93 | ignoreDefaultParameters: false 94 | ignoreDataClasses: true 95 | ignoreAnnotated: [] 96 | MethodOverloading: 97 | active: false 98 | threshold: 6 99 | NamedArguments: 100 | active: false 101 | threshold: 3 102 | NestedBlockDepth: 103 | active: true 104 | threshold: 4 105 | ReplaceSafeCallChainWithRun: 106 | active: false 107 | StringLiteralDuplication: 108 | active: false 109 | threshold: 3 110 | ignoreAnnotation: true 111 | excludeStringsWithLessThan5Characters: true 112 | ignoreStringsRegex: '$^' 113 | TooManyFunctions: 114 | active: true 115 | thresholdInFiles: 11 116 | thresholdInClasses: 40 117 | thresholdInInterfaces: 11 118 | thresholdInObjects: 11 119 | thresholdInEnums: 11 120 | ignoreDeprecated: false 121 | ignorePrivate: false 122 | ignoreOverridden: false 123 | 124 | coroutines: 125 | active: true 126 | GlobalCoroutineUsage: 127 | active: true 128 | RedundantSuspendModifier: 129 | active: true 130 | SuspendFunWithFlowReturnType: 131 | active: true 132 | 133 | empty-blocks: 134 | active: true 135 | EmptyCatchBlock: 136 | active: true 137 | allowedExceptionNameRegex: '_|(ignore|expected).*' 138 | EmptyClassBlock: 139 | active: true 140 | EmptyDefaultConstructor: 141 | active: true 142 | EmptyDoWhileBlock: 143 | active: true 144 | EmptyElseBlock: 145 | active: true 146 | EmptyFinallyBlock: 147 | active: true 148 | EmptyForBlock: 149 | active: true 150 | EmptyFunctionBlock: 151 | active: true 152 | ignoreOverridden: false 153 | EmptyIfBlock: 154 | active: true 155 | EmptyInitBlock: 156 | active: true 157 | EmptyKtFile: 158 | active: true 159 | EmptySecondaryConstructor: 160 | active: true 161 | EmptyTryBlock: 162 | active: true 163 | EmptyWhenBlock: 164 | active: true 165 | EmptyWhileBlock: 166 | active: true 167 | 168 | exceptions: 169 | active: true 170 | ExceptionRaisedInUnexpectedLocation: 171 | active: false 172 | methodNames: [toString, hashCode, equals, finalize] 173 | InstanceOfCheckForException: 174 | active: false 175 | NotImplementedDeclaration: 176 | active: false 177 | PrintStackTrace: 178 | active: false 179 | RethrowCaughtException: 180 | active: false 181 | ReturnFromFinally: 182 | active: false 183 | ignoreLabeled: false 184 | SwallowedException: 185 | active: false 186 | ignoredExceptionTypes: 187 | - InterruptedException 188 | - NumberFormatException 189 | - ParseException 190 | - MalformedURLException 191 | allowedExceptionNameRegex: '_|(ignore|expected).*' 192 | ThrowingExceptionFromFinally: 193 | active: false 194 | ThrowingExceptionInMain: 195 | active: false 196 | ThrowingExceptionsWithoutMessageOrCause: 197 | active: false 198 | exceptions: 199 | - IllegalArgumentException 200 | - IllegalStateException 201 | - IOException 202 | ThrowingNewInstanceOfSameException: 203 | active: false 204 | TooGenericExceptionCaught: 205 | active: false 206 | exceptionNames: 207 | - ArrayIndexOutOfBoundsException 208 | - Error 209 | - Exception 210 | - IllegalMonitorStateException 211 | - NullPointerException 212 | - IndexOutOfBoundsException 213 | - RuntimeException 214 | - Throwable 215 | allowedExceptionNameRegex: '_|(ignore|expected).*' 216 | TooGenericExceptionThrown: 217 | active: true 218 | exceptionNames: 219 | - Error 220 | - Exception 221 | - Throwable 222 | - RuntimeException 223 | 224 | formatting: 225 | active: true 226 | android: false 227 | autoCorrect: true 228 | AnnotationOnSeparateLine: 229 | active: false 230 | autoCorrect: true 231 | AnnotationSpacing: 232 | active: false 233 | autoCorrect: true 234 | ArgumentListWrapping: 235 | active: false 236 | autoCorrect: true 237 | ChainWrapping: 238 | active: true 239 | autoCorrect: true 240 | CommentSpacing: 241 | active: true 242 | autoCorrect: true 243 | EnumEntryNameCase: 244 | active: false 245 | autoCorrect: true 246 | Filename: 247 | active: true 248 | FinalNewline: 249 | active: true 250 | autoCorrect: true 251 | insertFinalNewLine: true 252 | ImportOrdering: 253 | active: false 254 | autoCorrect: true 255 | layout: 'idea' 256 | Indentation: 257 | active: false 258 | autoCorrect: true 259 | indentSize: 4 260 | continuationIndentSize: 4 261 | MaximumLineLength: 262 | active: true 263 | maxLineLength: 120 264 | ModifierOrdering: 265 | active: true 266 | autoCorrect: true 267 | MultiLineIfElse: 268 | active: true 269 | autoCorrect: true 270 | NoBlankLineBeforeRbrace: 271 | active: true 272 | autoCorrect: true 273 | NoConsecutiveBlankLines: 274 | active: true 275 | autoCorrect: true 276 | NoEmptyClassBody: 277 | active: true 278 | autoCorrect: true 279 | NoEmptyFirstLineInMethodBlock: 280 | active: false 281 | autoCorrect: true 282 | NoLineBreakAfterElse: 283 | active: true 284 | autoCorrect: true 285 | NoLineBreakBeforeAssignment: 286 | active: true 287 | autoCorrect: true 288 | NoMultipleSpaces: 289 | active: true 290 | autoCorrect: true 291 | NoSemicolons: 292 | active: true 293 | autoCorrect: true 294 | NoTrailingSpaces: 295 | active: true 296 | autoCorrect: true 297 | NoUnitReturn: 298 | active: true 299 | autoCorrect: true 300 | NoUnusedImports: 301 | active: true 302 | autoCorrect: true 303 | NoWildcardImports: 304 | active: true 305 | PackageName: 306 | active: true 307 | autoCorrect: true 308 | ParameterListWrapping: 309 | active: true 310 | autoCorrect: true 311 | indentSize: 4 312 | SpacingAroundColon: 313 | active: true 314 | autoCorrect: true 315 | SpacingAroundComma: 316 | active: true 317 | autoCorrect: true 318 | SpacingAroundCurly: 319 | active: true 320 | autoCorrect: true 321 | SpacingAroundDot: 322 | active: true 323 | autoCorrect: true 324 | SpacingAroundDoubleColon: 325 | active: false 326 | autoCorrect: true 327 | SpacingAroundKeyword: 328 | active: true 329 | autoCorrect: true 330 | SpacingAroundOperators: 331 | active: true 332 | autoCorrect: true 333 | SpacingAroundParens: 334 | active: true 335 | autoCorrect: true 336 | SpacingAroundRangeOperator: 337 | active: true 338 | autoCorrect: true 339 | SpacingBetweenDeclarationsWithAnnotations: 340 | active: false 341 | autoCorrect: true 342 | SpacingBetweenDeclarationsWithComments: 343 | active: false 344 | autoCorrect: true 345 | StringTemplate: 346 | active: true 347 | autoCorrect: true 348 | 349 | naming: 350 | active: true 351 | ClassNaming: 352 | active: true 353 | classPattern: '[A-Z][a-zA-Z0-9]*' 354 | ConstructorParameterNaming: 355 | active: true 356 | parameterPattern: '[a-z][A-Za-z0-9]*' 357 | privateParameterPattern: '[a-z][A-Za-z0-9]*' 358 | excludeClassPattern: '$^' 359 | ignoreOverridden: true 360 | EnumNaming: 361 | active: true 362 | enumEntryPattern: '[A-Z][_a-zA-Z0-9]*' 363 | ForbiddenClassName: 364 | active: false 365 | forbiddenName: [] 366 | FunctionMaxLength: 367 | active: false 368 | maximumFunctionNameLength: 30 369 | FunctionMinLength: 370 | active: false 371 | minimumFunctionNameLength: 3 372 | FunctionNaming: 373 | active: true 374 | functionPattern: '([a-z][a-zA-Z0-9]*)|(`.*`)' 375 | excludeClassPattern: '$^' 376 | ignoreOverridden: true 377 | ignoreAnnotated: ['Composable'] 378 | FunctionParameterNaming: 379 | active: true 380 | parameterPattern: '[a-z][A-Za-z0-9]*' 381 | excludeClassPattern: '$^' 382 | ignoreOverridden: true 383 | InvalidPackageDeclaration: 384 | active: false 385 | rootPackage: '' 386 | MatchingDeclarationName: 387 | active: true 388 | mustBeFirst: true 389 | MemberNameEqualsClassName: 390 | active: true 391 | ignoreOverridden: true 392 | NonBooleanPropertyPrefixedWithIs: 393 | active: false 394 | ObjectPropertyNaming: 395 | active: true 396 | constantPattern: '[A-Za-z][_A-Za-z0-9]*' 397 | propertyPattern: '[A-Za-z][_A-Za-z0-9]*' 398 | privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' 399 | PackageNaming: 400 | active: true 401 | packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*' 402 | TopLevelPropertyNaming: 403 | active: true 404 | constantPattern: '[A-Z][_A-Z0-9]*' 405 | propertyPattern: '[A-Za-z][_A-Za-z0-9]*' 406 | privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*' 407 | VariableMaxLength: 408 | active: false 409 | maximumVariableNameLength: 64 410 | VariableMinLength: 411 | active: false 412 | minimumVariableNameLength: 1 413 | VariableNaming: 414 | active: true 415 | variablePattern: '[a-z][A-Za-z0-9]*' 416 | privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' 417 | excludeClassPattern: '$^' 418 | ignoreOverridden: true 419 | 420 | performance: 421 | active: true 422 | ArrayPrimitive: 423 | active: true 424 | ForEachOnRange: 425 | active: true 426 | SpreadOperator: 427 | active: true 428 | UnnecessaryTemporaryInstantiation: 429 | active: true 430 | 431 | potential-bugs: 432 | active: true 433 | Deprecation: 434 | active: false 435 | DuplicateCaseInWhenExpression: 436 | active: true 437 | EqualsAlwaysReturnsTrueOrFalse: 438 | active: true 439 | EqualsWithHashCodeExist: 440 | active: true 441 | ExplicitGarbageCollectionCall: 442 | active: true 443 | HasPlatformType: 444 | active: false 445 | IgnoredReturnValue: 446 | active: false 447 | restrictToAnnotatedMethods: true 448 | returnValueAnnotations: ['*.CheckReturnValue', '*.CheckResult'] 449 | ImplicitDefaultLocale: 450 | active: false 451 | ImplicitUnitReturnType: 452 | active: false 453 | allowExplicitReturnType: true 454 | InvalidRange: 455 | active: true 456 | IteratorHasNextCallsNextMethod: 457 | active: true 458 | IteratorNotThrowingNoSuchElementException: 459 | active: true 460 | LateinitUsage: 461 | active: false 462 | excludeAnnotatedProperties: [] 463 | ignoreOnClassesPattern: '' 464 | MapGetWithNotNullAssertionOperator: 465 | active: false 466 | MissingWhenCase: 467 | active: true 468 | allowElseExpression: true 469 | NullableToStringCall: 470 | active: false 471 | RedundantElseInWhen: 472 | active: true 473 | UnconditionalJumpStatementInLoop: 474 | active: false 475 | UnnecessaryNotNullOperator: 476 | active: false 477 | UnnecessarySafeCall: 478 | active: false 479 | UnreachableCode: 480 | active: true 481 | UnsafeCallOnNullableType: 482 | active: true 483 | UnsafeCast: 484 | active: false 485 | UselessPostfixExpression: 486 | active: false 487 | WrongEqualsTypeParameter: 488 | active: true 489 | 490 | style: 491 | active: true 492 | ClassOrdering: 493 | active: false 494 | CollapsibleIfStatements: 495 | active: false 496 | DataClassContainsFunctions: 497 | active: false 498 | conversionFunctionPrefix: 'to' 499 | DataClassShouldBeImmutable: 500 | active: false 501 | EqualsNullCall: 502 | active: true 503 | EqualsOnSignatureLine: 504 | active: false 505 | ExplicitCollectionElementAccessMethod: 506 | active: false 507 | ExplicitItLambdaParameter: 508 | active: false 509 | ExpressionBodySyntax: 510 | active: false 511 | includeLineWrapping: false 512 | ForbiddenComment: 513 | active: true 514 | values: ['TODO:', 'FIXME:', 'STOPSHIP:'] 515 | allowedPatterns: '' 516 | ForbiddenImport: 517 | active: false 518 | imports: [] 519 | forbiddenPatterns: '' 520 | ForbiddenMethodCall: 521 | active: false 522 | methods: ['kotlin.io.println', 'kotlin.io.print'] 523 | ForbiddenPublicDataClass: 524 | active: false 525 | ignorePackages: ['*.internal', '*.internal.*'] 526 | ForbiddenVoid: 527 | active: false 528 | ignoreOverridden: false 529 | ignoreUsageInGenerics: false 530 | FunctionOnlyReturningConstant: 531 | active: true 532 | ignoreOverridableFunction: true 533 | excludedFunctions: 'describeContents' 534 | excludeAnnotatedFunction: ['dagger.Provides'] 535 | LibraryCodeMustSpecifyReturnType: 536 | active: true 537 | LibraryEntitiesShouldNotBePublic: 538 | active: false 539 | LoopWithTooManyJumpStatements: 540 | active: true 541 | maxJumpCount: 1 542 | MagicNumber: 543 | active: true 544 | ignoreNumbers: ['-1', '0', '1', '2'] 545 | ignoreHashCodeFunction: true 546 | ignorePropertyDeclaration: false 547 | ignoreLocalVariableDeclaration: false 548 | ignoreConstantDeclaration: true 549 | ignoreCompanionObjectPropertyDeclaration: true 550 | ignoreAnnotation: false 551 | ignoreNamedArgument: true 552 | ignoreEnums: false 553 | ignoreRanges: false 554 | MandatoryBracesIfStatements: 555 | active: false 556 | MandatoryBracesLoops: 557 | active: false 558 | MaxLineLength: 559 | active: true 560 | maxLineLength: 120 561 | excludePackageStatements: true 562 | excludeImportStatements: true 563 | excludeCommentStatements: false 564 | MayBeConst: 565 | active: true 566 | ModifierOrder: 567 | active: true 568 | NestedClassesVisibility: 569 | active: false 570 | NewLineAtEndOfFile: 571 | active: true 572 | NoTabs: 573 | active: false 574 | OptionalAbstractKeyword: 575 | active: true 576 | OptionalUnit: 577 | active: false 578 | OptionalWhenBraces: 579 | active: false 580 | PreferToOverPairSyntax: 581 | active: false 582 | ProtectedMemberInFinalClass: 583 | active: true 584 | RedundantExplicitType: 585 | active: false 586 | RedundantHigherOrderMapUsage: 587 | active: false 588 | RedundantVisibilityModifierRule: 589 | active: false 590 | ReturnCount: 591 | active: true 592 | max: 2 593 | excludedFunctions: 'equals' 594 | excludeLabeled: false 595 | excludeReturnFromLambda: true 596 | excludeGuardClauses: false 597 | SafeCast: 598 | active: true 599 | SerialVersionUIDInSerializableClass: 600 | active: false 601 | SpacingBetweenPackageAndImports: 602 | active: false 603 | ThrowsCount: 604 | active: true 605 | max: 2 606 | TrailingWhitespace: 607 | active: false 608 | UnderscoresInNumericLiterals: 609 | active: false 610 | acceptableDecimalLength: 5 611 | UnnecessaryAbstractClass: 612 | active: true 613 | UnnecessaryAnnotationUseSiteTarget: 614 | active: false 615 | UnnecessaryApply: 616 | active: false 617 | UnnecessaryInheritance: 618 | active: true 619 | UnnecessaryLet: 620 | active: false 621 | UnnecessaryParentheses: 622 | active: false 623 | UntilInsteadOfRangeTo: 624 | active: false 625 | UnusedImports: 626 | active: false 627 | UnusedPrivateClass: 628 | active: true 629 | UnusedPrivateMember: 630 | active: false 631 | allowedNames: '(_|ignored|expected|serialVersionUID)' 632 | UseArrayLiteralsInAnnotations: 633 | active: false 634 | UseCheckNotNull: 635 | active: false 636 | UseCheckOrError: 637 | active: false 638 | UseDataClass: 639 | active: true 640 | excludeAnnotatedClasses: [] 641 | allowVars: false 642 | UseEmptyCounterpart: 643 | active: false 644 | UseIfEmptyOrIfBlank: 645 | active: true 646 | UseIfInsteadOfWhen: 647 | active: true 648 | UseRequire: 649 | active: true 650 | UseRequireNotNull: 651 | active: false 652 | UselessCallOnNotNull: 653 | active: true 654 | UtilityClassWithPublicConstructor: 655 | active: true 656 | VarCouldBeVal: 657 | active: true 658 | WildcardImport: 659 | active: true --------------------------------------------------------------------------------