├── 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 |
4 |
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 | [](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 |
10 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | xmlns:android
20 |
21 | ^$
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | xmlns:.*
31 |
32 | ^$
33 |
34 |
35 | BY_NAME
36 |
37 |
38 |
39 |
40 |
41 |
42 | .*:id
43 |
44 | http://schemas.android.com/apk/res/android
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | .*:name
54 |
55 | http://schemas.android.com/apk/res/android
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | name
65 |
66 | ^$
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | style
76 |
77 | ^$
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | .*
87 |
88 | ^$
89 |
90 |
91 | BY_NAME
92 |
93 |
94 |
95 |
96 |
97 |
98 | .*
99 |
100 | http://schemas.android.com/apk/res/android
101 |
102 |
103 | ANDROID_ATTRIBUTE_ORDER
104 |
105 |
106 |
107 |
108 |
109 |
110 | .*
111 |
112 | .*
113 |
114 |
115 | BY_NAME
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
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
--------------------------------------------------------------------------------