├── .gitignore ├── LICENSE ├── README.md ├── apk └── app-dev-debug.apk ├── application ├── .gitignore ├── .idea │ ├── codeStyles │ │ ├── Project.xml │ │ └── codeStyleConfig.xml │ ├── gradle.xml │ ├── misc.xml │ ├── runConfigurations.xml │ └── vcs.xml ├── app │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── br │ │ │ └── com │ │ │ └── charleston │ │ │ └── github │ │ │ ├── TestAndroidApplication.kt │ │ │ ├── config │ │ │ └── Configuration.kt │ │ │ ├── mocks │ │ │ └── MockUrlApiModule.kt │ │ │ ├── runner │ │ │ └── MockTestRunner.kt │ │ │ ├── tests │ │ │ └── VoiceSearchScreenTest.kt │ │ │ └── viewactions │ │ │ └── RecyclerViewItemCountAssertion.kt │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── br │ │ │ └── com │ │ │ └── charleston │ │ │ └── github │ │ │ ├── AndroidApplication.kt │ │ │ ├── adapters │ │ │ └── DataBindingAdapter.kt │ │ │ ├── di │ │ │ ├── AppComponent.kt │ │ │ └── modules │ │ │ │ ├── ActivityModule.kt │ │ │ │ ├── FragmentModule.kt │ │ │ │ └── ViewModelModule.kt │ │ │ ├── extensions │ │ │ ├── LottieViewExt.kt │ │ │ └── TextViewExt.kt │ │ │ └── features │ │ │ ├── MainActivity.kt │ │ │ └── search │ │ │ ├── adapters │ │ │ ├── GithubListAdapter.kt │ │ │ └── ListListener.kt │ │ │ ├── screens │ │ │ ├── detail │ │ │ │ └── DetailFragment.kt │ │ │ ├── list │ │ │ │ └── ListFragment.kt │ │ │ └── voicesearch │ │ │ │ ├── VoiceSearchFragment.kt │ │ │ │ └── VoiceSearchHandler.kt │ │ │ ├── states │ │ │ └── SearchState.kt │ │ │ └── viewmodel │ │ │ └── VoiceSearchViewModel.kt │ │ └── res │ │ ├── anim │ │ ├── slide_in_left.xml │ │ ├── slide_in_right.xml │ │ ├── slide_out_left.xml │ │ └── slide_out_right.xml │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_code.xml │ │ ├── ic_code_fork_symbol.xml │ │ ├── ic_launcher_background.xml │ │ └── ic_star.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── fragment_detail.xml │ │ ├── fragment_list.xml │ │ ├── fragment_voice_search.xml │ │ └── item_list.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── 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 │ │ ├── navigation │ │ └── navigation_main.xml │ │ ├── raw │ │ └── data.json │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml ├── build.gradle ├── core │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── br │ │ │ └── com │ │ │ └── charleston │ │ │ └── core │ │ │ └── ExampleInstrumentedTest.java │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── br │ │ │ │ └── com │ │ │ │ └── charleston │ │ │ │ └── core │ │ │ │ ├── base │ │ │ │ ├── BaseActivity.kt │ │ │ │ ├── BaseFragment.kt │ │ │ │ ├── BaseViewModel.kt │ │ │ │ ├── Event.kt │ │ │ │ └── Injectable.kt │ │ │ │ ├── modules │ │ │ │ ├── AndroidModule.kt │ │ │ │ └── FactoryModule.kt │ │ │ │ └── viewmodel │ │ │ │ ├── ViewModelKey.kt │ │ │ │ └── ViewModelProviderFactory.kt │ │ └── res │ │ │ └── values │ │ │ └── strings.xml │ │ └── test │ │ └── java │ │ └── br │ │ └── com │ │ └── charleston │ │ └── core │ │ └── ExampleUnitTest.java ├── data │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── br │ │ │ └── com │ │ │ └── charleston │ │ │ └── data │ │ │ └── ExampleInstrumentedTest.java │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── br │ │ │ │ └── com │ │ │ │ └── charleston │ │ │ │ └── data │ │ │ │ ├── cloud │ │ │ │ ├── InterceptorModule.kt │ │ │ │ ├── NetworkModule.kt │ │ │ │ ├── RequestModule.kt │ │ │ │ ├── UrlApiModule.kt │ │ │ │ ├── constants.kt │ │ │ │ ├── requests │ │ │ │ │ └── GithubApi.kt │ │ │ │ └── responses │ │ │ │ │ ├── DataResponse.kt │ │ │ │ │ ├── GithubResponse.kt │ │ │ │ │ ├── UserResponse.kt │ │ │ │ │ └── mapper │ │ │ │ │ └── GithubCloudMapper.kt │ │ │ │ ├── di │ │ │ │ └── GithubModule.kt │ │ │ │ ├── extensions │ │ │ │ └── FileExt.kt │ │ │ │ └── repository │ │ │ │ ├── GithubDataRepository.kt │ │ │ │ ├── Mapper.kt │ │ │ │ └── cloud │ │ │ │ ├── GithubCloudDataStore.kt │ │ │ │ └── IGithubCloudDataStore.kt │ │ └── res │ │ │ └── values │ │ │ └── strings.xml │ │ └── test │ │ └── java │ │ └── br │ │ └── com │ │ └── charleston │ │ └── data │ │ └── ExampleUnitTest.java ├── dependencies.gradle ├── domain │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── br │ │ │ └── com │ │ │ └── charleston │ │ │ └── domain │ │ │ └── ExampleInstrumentedTest.java │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── br │ │ │ │ └── com │ │ │ │ └── charleston │ │ │ │ └── domain │ │ │ │ ├── DefaultObserver.kt │ │ │ │ ├── UseCase.kt │ │ │ │ ├── executor │ │ │ │ ├── IPostExecutionThread.kt │ │ │ │ ├── IThreadExecutor.kt │ │ │ │ ├── PostExecutionThread.kt │ │ │ │ └── ThreadExecutor.kt │ │ │ │ ├── interactor │ │ │ │ └── GetRepositories.kt │ │ │ │ ├── model │ │ │ │ ├── GithubModel.kt │ │ │ │ └── UserModel.kt │ │ │ │ └── repository │ │ │ │ └── IGithubRepository.kt │ │ └── res │ │ │ └── values │ │ │ └── strings.xml │ │ └── test │ │ └── java │ │ └── br │ │ └── com │ │ └── charleston │ │ └── domain │ │ └── ExampleUnitTest.java ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle └── assets ├── screens ├── detail.jpg ├── device-2018-12-27-173826.png ├── device-2018-12-27-174416.png ├── device-2018-12-27-174430.png ├── device-2018-12-27-174438.png ├── device-2018-12-27-174449.png └── navigation.png └── videos └── device-2018-12-27-174651.mp4 /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | # Android Studio Navigation editor temp files 30 | .navigation/ 31 | 32 | # Android Studio captures folder 33 | captures/ 34 | 35 | # IntelliJ 36 | *.iml 37 | .idea/workspace.xml 38 | .idea/tasks.xml 39 | .idea/gradle.xml 40 | .idea/assetWizardSettings.xml 41 | .idea/dictionaries 42 | .idea/libraries 43 | .idea/caches 44 | 45 | # Keystore files 46 | # Uncomment the following line if you do not want to check your keystore files in. 47 | #*.jks 48 | 49 | # External native build folder generated in Android Studio 2.2 and later 50 | .externalNativeBuild 51 | 52 | # Google Services (e.g. APIs or Firebase) 53 | google-services.json 54 | 55 | # Freeline 56 | freeline.py 57 | freeline/ 58 | freeline_project_description.json 59 | 60 | # fastlane 61 | fastlane/report.xml 62 | fastlane/Preview.html 63 | fastlane/screenshots 64 | fastlane/test_output 65 | fastlane/readme.md 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Github - Android 2 | 3 | The project follows the entire line of Google's standard development, based on Jetpack. 4 | 5 | This application only aims to show a list of items so that the user can see some data. 6 | 7 | And it also handles error handling if there is any failure to process the request made by the API. 8 | 9 | [APK](https://github.com/charleston10/github-android/blob/master/apk/app-dev-debug.apk?raw=true) || [VIDEO](https://github.com/charleston10/github-android/blob/master/assets/videos/device-2018-12-27-174651.mp4?raw=true) || [YOUTUBE](https://youtu.be/029L0QRRjWE) 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 | 36 | 37 | 38 | 39 | 40 | 41 |
BASEArchitectureIU
AppCompactDataBindingNavigation
Android KTXLifecyclesMaterial Components
KotlinLiveData
Android ArchViewModel
42 | 43 | 44 | **Screens** 45 | 46 | 47 | 48 | 49 | 50 | 51 | 54 | 57 | 60 | 61 |
InitialListeningSearching
52 | 53 | 55 | 56 | 58 | 59 |
62 | 63 | 64 | 65 | 66 | 67 | 68 | 71 | 74 | 77 | 78 |
FoundListDetail
69 | 70 | 72 | 73 | 75 | 76 |
79 | 80 | 81 | 82 | 83 | 84 | 87 | 88 |
Navigation
85 | 86 |
89 | 90 | ## CODE 91 | - **IDE - Android Studio 3.3 Beta 4** 92 | 93 | - **Gradle 3.2.1** 94 | 95 | - **Kotlin 1.3.0** 96 | 97 | - **AAC Android Architecture Components** *using guide Google JetPack* 98 | 99 | - **Clean Architecture** *for apply DRY, KISS, SOLID* 100 | 101 | - **DataBinding** *bind data model with view* 102 | 103 | - **ViewModel** *for interact view with business rules* 104 | 105 | - **RX Android** *for manipulate data and events in different layers of application* 106 | 107 | ## LIBRARIES 108 | 109 | - Retrofit 110 | - Dagger Android 111 | - Material Component 112 | - Timber 113 | - Lottie 114 | - RX Java / RX Android 115 | - Google Listen 116 | 117 | 118 | ## API 119 | 120 | Github Search Repositories Documentation: https://developer.github.com/v3/search/#search-repositories 121 | 122 | 123 | ## DESIGN 124 | 125 | **Material Components** 126 | 127 | https://github.com/material-components 128 | 129 | - Toolbar 130 | - RecyclerView 131 | - MaterialButton 132 | 133 | **Lottie** 134 | 135 | http://airbnb.io/lottie/ 136 | -------------------------------------------------------------------------------- /apk/app-dev-debug.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/charleston10/github-android/ca697d16bb0856c69dc2bda73cf9b5ce6f1610e7/apk/app-dev-debug.apk -------------------------------------------------------------------------------- /application/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | -------------------------------------------------------------------------------- /application/.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 10 | -------------------------------------------------------------------------------- /application/.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /application/.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 21 | -------------------------------------------------------------------------------- /application/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /application/.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /application/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /application/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /application/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-kapt' 4 | apply plugin: 'kotlin-android-extensions' 5 | apply plugin: "androidx.navigation.safeargs" 6 | 7 | android { 8 | def config = rootProject.ext 9 | 10 | compileSdkVersion config.compileSdkVersion 11 | buildToolsVersion config.buildToolsVersion 12 | 13 | defaultConfig { 14 | applicationId config.applicationId 15 | minSdkVersion config.minSdkVersion 16 | targetSdkVersion config.targetSdkVersion 17 | versionName config.versionName 18 | testApplicationId config.applicationTestId 19 | testInstrumentationRunner 'br.com.charleston.github.runner.MockTestRunner' 20 | } 21 | 22 | buildTypes { 23 | release { 24 | debuggable false 25 | shrinkResources true 26 | minifyEnabled true 27 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 28 | } 29 | 30 | debug { 31 | debuggable true 32 | minifyEnabled false 33 | } 34 | } 35 | 36 | flavorDimensions "default" 37 | productFlavors { 38 | dev { 39 | dimension "default" 40 | applicationIdSuffix ".dev" 41 | } 42 | prod { 43 | dimension "default" 44 | versionCode 1 45 | } 46 | } 47 | 48 | dataBinding.enabled = true 49 | 50 | compileOptions { 51 | sourceCompatibility JavaVersion.VERSION_1_8 52 | targetCompatibility JavaVersion.VERSION_1_8 53 | incremental = false 54 | } 55 | 56 | kotlinOptions { 57 | jvmTarget = "1.8" 58 | } 59 | 60 | packagingOptions { 61 | exclude 'LICENSE.txt' 62 | exclude 'META-INF/DEPENDENCIES' 63 | exclude 'META-INF/ASL2.0' 64 | exclude 'META-INF/NOTICE' 65 | exclude 'META-INF/LICENSE' 66 | exclude 'META-INF/rxjava.properties' 67 | } 68 | 69 | testBuildType "debug" 70 | testOptions { 71 | unitTests.returnDefaultValues = true 72 | unitTests.includeAndroidResources = true 73 | } 74 | } 75 | 76 | dependencies { 77 | implementation project(':core') 78 | implementation project(':domain') 79 | implementation project(':data') 80 | 81 | def libCore = rootProject.ext.coreDependencies 82 | implementation libCore.kotlin 83 | implementation libCore.billing 84 | implementation libCore.kodein 85 | implementation libCore.google.material 86 | 87 | implementation libCore.monitoring.timber 88 | 89 | implementation libCore.arch.archNavigationFragment 90 | implementation libCore.arch.archNavigationUi 91 | implementation libCore.arch.archRuntime 92 | implementation libCore.arch.archExtensions 93 | implementation libCore.arch.archConstraintLayout 94 | kapt libCore.arch.archCompiler 95 | 96 | implementation libCore.dagger.daggerAndroid 97 | implementation libCore.dagger.daggerSupport 98 | kapt libCore.dagger.daggerProcessor 99 | kapt libCore.dagger.daggerCompiler 100 | implementation libCore.dagger.dagger 101 | 102 | implementation libCore.rx.rxJava 103 | implementation libCore.rx.rxAndroid 104 | implementation(libCore.rx.rxPermission) { transitive = true } 105 | 106 | def lib = rootProject.ext.appDependencies 107 | implementation lib.external.glide 108 | implementation lib.external.lottie 109 | 110 | /* tests */ 111 | testImplementation lib.unitTest.kotlin 112 | testImplementation lib.unitTest.mockito 113 | testImplementation lib.unitTest.jUnit 114 | testImplementation lib.unitTest.spek 115 | testImplementation lib.unitTest.hamk 116 | 117 | androidTestImplementation lib.uiTest.espressoCore 118 | androidTestImplementation lib.uiTest.espressoIntent 119 | androidTestImplementation lib.uiTest.supportRules 120 | androidTestImplementation lib.uiTest.supportRunner 121 | androidTestImplementation lib.uiTest.mockServer 122 | } 123 | 124 | // prevent the "superClassName is empty" error for classes not annotated as tests 125 | tasks.withType(Test) { 126 | scanForTestClasses = false 127 | include "**/*Test.class" // whatever Ant pattern matches your test class files 128 | } -------------------------------------------------------------------------------- /application/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /application/app/src/androidTest/java/br/com/charleston/github/TestAndroidApplication.kt: -------------------------------------------------------------------------------- 1 | package br.com.charleston.github 2 | 3 | import br.com.charleston.github.di.AppComponent 4 | import br.com.charleston.github.di.DaggerAppComponent 5 | import br.com.charleston.github.mocks.MockUrlApiModule 6 | 7 | class TestAndroidApplication : AndroidApplication() { 8 | 9 | override fun createComponent(): AppComponent { 10 | return DaggerAppComponent 11 | .builder() 12 | .application(this) 13 | .moduleUrlApi(MockUrlApiModule()) 14 | .build() 15 | } 16 | } -------------------------------------------------------------------------------- /application/app/src/androidTest/java/br/com/charleston/github/config/Configuration.kt: -------------------------------------------------------------------------------- 1 | package br.com.charleston.github.config 2 | 3 | const val MOCK_SERVER_PORT = 36004 4 | const val MOCK_SERVER_DOMAIN = "localhost" 5 | const val MOCK_SERVER = "http://$MOCK_SERVER_DOMAIN:$MOCK_SERVER_PORT/" -------------------------------------------------------------------------------- /application/app/src/androidTest/java/br/com/charleston/github/mocks/MockUrlApiModule.kt: -------------------------------------------------------------------------------- 1 | package br.com.charleston.github.mocks 2 | 3 | import br.com.charleston.data.cloud.UrlApiModule 4 | import br.com.charleston.github.config.MOCK_SERVER 5 | 6 | class MockUrlApiModule : UrlApiModule() { 7 | override fun provideUrl(): String { 8 | return MOCK_SERVER 9 | } 10 | } -------------------------------------------------------------------------------- /application/app/src/androidTest/java/br/com/charleston/github/runner/MockTestRunner.kt: -------------------------------------------------------------------------------- 1 | package br.com.charleston.github.runner 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import androidx.test.runner.AndroidJUnitRunner 6 | import br.com.charleston.github.TestAndroidApplication 7 | 8 | class MockTestRunner : AndroidJUnitRunner() { 9 | 10 | override fun newApplication(cl: ClassLoader?, className: String?, context: Context?): Application { 11 | return super.newApplication(cl, TestAndroidApplication::class.java.name, context) 12 | } 13 | } -------------------------------------------------------------------------------- /application/app/src/androidTest/java/br/com/charleston/github/tests/VoiceSearchScreenTest.kt: -------------------------------------------------------------------------------- 1 | package br.com.charleston.github.tests 2 | 3 | import android.app.Activity 4 | import android.app.Instrumentation 5 | import android.content.Intent 6 | import android.speech.RecognizerIntent 7 | import androidx.annotation.StringRes 8 | import androidx.test.InstrumentationRegistry 9 | import androidx.test.espresso.Espresso.onView 10 | import androidx.test.espresso.action.ViewActions.click 11 | import androidx.test.espresso.assertion.ViewAssertions.matches 12 | import androidx.test.espresso.intent.Intents.intended 13 | import androidx.test.espresso.intent.Intents.intending 14 | import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction 15 | import androidx.test.espresso.intent.rule.IntentsTestRule 16 | import androidx.test.espresso.matcher.ViewMatchers.* 17 | import androidx.test.filters.LargeTest 18 | import androidx.test.rule.GrantPermissionRule 19 | import br.com.charleston.github.R 20 | import br.com.charleston.github.config.MOCK_SERVER_DOMAIN 21 | import br.com.charleston.github.config.MOCK_SERVER_PORT 22 | import br.com.charleston.github.features.MainActivity 23 | import okhttp3.mockwebserver.MockWebServer 24 | import org.junit.After 25 | import org.junit.Before 26 | import org.junit.Rule 27 | import org.junit.Test 28 | 29 | @LargeTest 30 | class VoiceSearchScreenTest { 31 | 32 | @get:Rule 33 | val activityRule = IntentsTestRule(MainActivity::class.java, false, false) 34 | 35 | @get:Rule 36 | val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(android.Manifest.permission.RECORD_AUDIO) 37 | 38 | private val context = InstrumentationRegistry.getInstrumentation().targetContext 39 | private val server = MockWebServer() 40 | 41 | private val searchBy = "Android" 42 | 43 | @Before 44 | fun setup() { 45 | server.apply { 46 | start(MOCK_SERVER_PORT) 47 | url(MOCK_SERVER_DOMAIN) 48 | } 49 | 50 | activityRule.launchActivity(null) 51 | } 52 | 53 | @After 54 | fun tearDown() { 55 | server.shutdown() 56 | } 57 | 58 | @Test 59 | fun shouldMessageAndButtonVisible() { 60 | onView(withId(R.id.message)) 61 | .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) 62 | 63 | onView(withId(R.id.voice)) 64 | .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) 65 | } 66 | 67 | @Test 68 | fun checkInitialMessage() { 69 | onView(withId(R.id.message)) 70 | .check(matches(withText(getStringToTest(R.string.voice_search_message_inital)))) 71 | } 72 | 73 | @Test 74 | fun checkMessageWhenPressSearch() { 75 | onView(withId(R.id.voice)) 76 | .perform(click()) 77 | 78 | onView(withId(R.id.message)) 79 | .check(matches(withText(getStringToTest(R.string.voice_search_message_listening)))) 80 | } 81 | 82 | @Test 83 | fun searchAndDisplayMessageResult() { 84 | intending(hasAction(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)) 85 | .respondWith( 86 | Instrumentation 87 | .ActivityResult( 88 | Activity.RESULT_OK, resultIntent() 89 | ) 90 | ) 91 | 92 | onView(withId(R.id.voice)) 93 | .perform(click()) 94 | 95 | 96 | intended( 97 | hasAction(RecognizerIntent.ACTION_RECOGNIZE_SPEECH) 98 | ) 99 | 100 | /*onView(withId(R.id.message)) 101 | .check( 102 | matches( 103 | withText( 104 | String.format(getStringToTest(R.string.voice_search_message_find_repository_by), search) 105 | ) 106 | ) 107 | )*/ 108 | } 109 | 110 | /** 111 | * Function for add 'space' in string with '\n' 112 | * because Android add when paint in screen 113 | * 114 | */ 115 | private fun getStringToTest(@StringRes resId: Int): String { 116 | val value = context.getString(resId) 117 | return value.replace("\n", "\n ") 118 | } 119 | 120 | private fun resultIntent(): Intent { 121 | return Intent().apply { 122 | putStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS, arrayListOf(searchBy)) 123 | } 124 | } 125 | } -------------------------------------------------------------------------------- /application/app/src/androidTest/java/br/com/charleston/github/viewactions/RecyclerViewItemCountAssertion.kt: -------------------------------------------------------------------------------- 1 | package br.com.charleston.github.viewactions 2 | 3 | import android.view.View 4 | import androidx.recyclerview.widget.RecyclerView 5 | import androidx.test.espresso.NoMatchingViewException 6 | import androidx.test.espresso.ViewAssertion 7 | import org.hamcrest.Matcher 8 | import org.hamcrest.MatcherAssert.assertThat 9 | import org.hamcrest.Matchers.`is` 10 | 11 | 12 | class RecyclerViewItemCountAssertion : ViewAssertion { 13 | 14 | private val matcher: Matcher 15 | 16 | constructor(expectedCount: Int) { 17 | this.matcher = `is`(expectedCount) 18 | } 19 | 20 | constructor(matcher: Matcher) { 21 | this.matcher = matcher 22 | } 23 | 24 | override fun check(view: View, noViewFoundException: NoMatchingViewException?) { 25 | if (noViewFoundException != null) { 26 | throw noViewFoundException 27 | } 28 | 29 | val recyclerView = view as RecyclerView 30 | val adapter = recyclerView.adapter 31 | 32 | adapter?.let { 33 | assertThat(it.itemCount, matcher) 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /application/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 14 | 15 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /application/app/src/main/java/br/com/charleston/github/AndroidApplication.kt: -------------------------------------------------------------------------------- 1 | package br.com.charleston.github 2 | 3 | import android.app.Activity 4 | import android.app.Application 5 | import android.os.Bundle 6 | import androidx.annotation.VisibleForTesting 7 | import androidx.fragment.app.Fragment 8 | import androidx.fragment.app.FragmentActivity 9 | import androidx.fragment.app.FragmentManager 10 | import br.com.charleston.core.base.Injectable 11 | import br.com.charleston.github.di.AppComponent 12 | import br.com.charleston.github.di.DaggerAppComponent 13 | import dagger.android.AndroidInjection 14 | import dagger.android.AndroidInjector 15 | import dagger.android.DispatchingAndroidInjector 16 | import dagger.android.HasActivityInjector 17 | import dagger.android.support.AndroidSupportInjection 18 | import dagger.android.support.HasSupportFragmentInjector 19 | import javax.inject.Inject 20 | 21 | open class AndroidApplication : Application(), 22 | HasActivityInjector, 23 | Application.ActivityLifecycleCallbacks { 24 | 25 | @Inject 26 | lateinit var dispatchingActivityAndroidInjector: DispatchingAndroidInjector 27 | 28 | @set:VisibleForTesting 29 | lateinit var appComponent: AppComponent 30 | 31 | init { 32 | registerActivityLifecycleCallbacks(this) 33 | } 34 | 35 | override fun onCreate() { 36 | super.onCreate() 37 | setupDagger() 38 | } 39 | 40 | override fun activityInjector(): AndroidInjector = dispatchingActivityAndroidInjector 41 | override fun onActivityCreated(activity: Activity?, p1: Bundle?) = handleActivity(activity) 42 | override fun onActivityPaused(p0: Activity?) {} 43 | override fun onActivityResumed(p0: Activity?) {} 44 | override fun onActivityStarted(p0: Activity?) {} 45 | override fun onActivityDestroyed(p0: Activity?) {} 46 | override fun onActivitySaveInstanceState(p0: Activity?, p1: Bundle?) {} 47 | override fun onActivityStopped(p0: Activity?) {} 48 | 49 | open fun createComponent(): AppComponent { 50 | val dagger = DaggerAppComponent 51 | .builder() 52 | .application(this) 53 | 54 | return dagger.build() 55 | } 56 | 57 | private fun setupDagger() { 58 | appComponent = createComponent().apply { 59 | inject(this@AndroidApplication) 60 | } 61 | } 62 | 63 | 64 | private fun handleActivity(activity: Activity?) { 65 | if (activity is HasSupportFragmentInjector) { 66 | AndroidInjection.inject(activity) 67 | } 68 | 69 | if (activity is FragmentActivity) { 70 | activity.supportFragmentManager 71 | .registerFragmentLifecycleCallbacks(object : FragmentManager.FragmentLifecycleCallbacks() { 72 | override fun onFragmentCreated(fm: FragmentManager, f: Fragment, savedInstanceState: Bundle?) { 73 | if (f is Injectable) { 74 | AndroidSupportInjection.inject(f) 75 | } 76 | } 77 | }, true) 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /application/app/src/main/java/br/com/charleston/github/adapters/DataBindingAdapter.kt: -------------------------------------------------------------------------------- 1 | package br.com.charleston.github.adapters 2 | 3 | import android.widget.ImageView 4 | import android.widget.TextView 5 | import androidx.databinding.BindingAdapter 6 | import com.bumptech.glide.Glide 7 | import com.bumptech.glide.request.RequestOptions 8 | 9 | class DataBindingAdapter { 10 | 11 | companion object { 12 | @JvmStatic 13 | @BindingAdapter(value = ["url"], requireAll = false) 14 | fun setImageUrl(view: ImageView, url: String?) { 15 | url?.let { 16 | Glide 17 | .with(view.context) 18 | .load(it) 19 | .apply(RequestOptions.circleCropTransform()) 20 | .into(view) 21 | } 22 | } 23 | 24 | @JvmStatic 25 | @BindingAdapter(value = ["bindInt"], requireAll = false) 26 | fun bindInt(view: TextView, value: Int?) { 27 | value?.let { 28 | view.text = value.toString() 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /application/app/src/main/java/br/com/charleston/github/di/AppComponent.kt: -------------------------------------------------------------------------------- 1 | package br.com.charleston.github.di 2 | 3 | import android.app.Application 4 | import br.com.charleston.core.modules.AndroidModule 5 | import br.com.charleston.core.modules.FactoryModule 6 | import br.com.charleston.data.cloud.InterceptorModule 7 | import br.com.charleston.data.cloud.NetworkModule 8 | import br.com.charleston.data.cloud.RequestModule 9 | import br.com.charleston.data.cloud.UrlApiModule 10 | import br.com.charleston.data.di.GithubModule 11 | import br.com.charleston.github.AndroidApplication 12 | import br.com.charleston.github.di.modules.ActivityModule 13 | import br.com.charleston.github.di.modules.FragmentModule 14 | import br.com.charleston.github.di.modules.ViewModelModule 15 | import dagger.BindsInstance 16 | import dagger.Component 17 | import dagger.android.support.AndroidSupportInjectionModule 18 | import javax.inject.Singleton 19 | 20 | @Singleton 21 | @Component( 22 | modules = [ 23 | AndroidSupportInjectionModule::class, 24 | FactoryModule::class, 25 | AndroidModule::class, 26 | ActivityModule::class, 27 | FragmentModule::class, 28 | ViewModelModule::class, 29 | InterceptorModule::class, 30 | NetworkModule::class, 31 | RequestModule::class, 32 | UrlApiModule::class, 33 | GithubModule::class 34 | ] 35 | ) 36 | interface AppComponent { 37 | 38 | @Component.Builder 39 | interface Builder { 40 | @BindsInstance 41 | fun application(application: Application): Builder 42 | 43 | fun moduleUrlApi(module: UrlApiModule): Builder 44 | 45 | fun build(): AppComponent 46 | } 47 | 48 | fun inject(app: AndroidApplication) 49 | } -------------------------------------------------------------------------------- /application/app/src/main/java/br/com/charleston/github/di/modules/ActivityModule.kt: -------------------------------------------------------------------------------- 1 | package br.com.charleston.github.di.modules 2 | 3 | import br.com.charleston.github.features.MainActivity 4 | import dagger.Module 5 | import dagger.android.ContributesAndroidInjector 6 | 7 | @Module 8 | abstract class ActivityModule { 9 | 10 | @ContributesAndroidInjector(modules = [FragmentModule::class]) 11 | abstract fun mainActivity(): MainActivity 12 | } -------------------------------------------------------------------------------- /application/app/src/main/java/br/com/charleston/github/di/modules/FragmentModule.kt: -------------------------------------------------------------------------------- 1 | package br.com.charleston.github.di.modules 2 | 3 | import br.com.charleston.github.features.search.screens.detail.DetailFragment 4 | import br.com.charleston.github.features.search.screens.list.ListFragment 5 | import br.com.charleston.github.features.search.screens.voicesearch.VoiceSearchFragment 6 | import dagger.Module 7 | import dagger.android.ContributesAndroidInjector 8 | 9 | @Module 10 | abstract class FragmentModule { 11 | 12 | @ContributesAndroidInjector 13 | abstract fun voiceSearchFragment(): VoiceSearchFragment 14 | 15 | @ContributesAndroidInjector 16 | abstract fun listFragment(): ListFragment 17 | 18 | @ContributesAndroidInjector 19 | abstract fun detailFragment() : DetailFragment 20 | } -------------------------------------------------------------------------------- /application/app/src/main/java/br/com/charleston/github/di/modules/ViewModelModule.kt: -------------------------------------------------------------------------------- 1 | package br.com.charleston.github.di.modules 2 | 3 | import androidx.lifecycle.ViewModel 4 | import br.com.charleston.core.viewmodel.ViewModelKey 5 | import br.com.charleston.github.features.search.viewmodel.VoiceSearchViewModel 6 | import dagger.Binds 7 | import dagger.Module 8 | import dagger.multibindings.IntoMap 9 | 10 | @Module 11 | abstract class ViewModelModule { 12 | 13 | @Binds 14 | @IntoMap 15 | @ViewModelKey(VoiceSearchViewModel::class) 16 | abstract fun githubViewModel(viewModel: VoiceSearchViewModel): ViewModel 17 | } -------------------------------------------------------------------------------- /application/app/src/main/java/br/com/charleston/github/extensions/LottieViewExt.kt: -------------------------------------------------------------------------------- 1 | package br.com.charleston.github.extensions 2 | 3 | import android.graphics.ColorFilter 4 | import android.graphics.PorterDuff 5 | import android.graphics.PorterDuffColorFilter 6 | import androidx.core.content.ContextCompat 7 | import br.com.charleston.github.R 8 | import com.airbnb.lottie.LottieAnimationView 9 | import com.airbnb.lottie.LottieProperty 10 | import com.airbnb.lottie.model.KeyPath 11 | import com.airbnb.lottie.value.LottieValueCallback 12 | 13 | fun LottieAnimationView.disable() { 14 | this.isEnabled = false 15 | this.addValueCallback( 16 | KeyPath("**"), 17 | LottieProperty.COLOR_FILTER, 18 | LottieValueCallback( 19 | PorterDuffColorFilter( 20 | ContextCompat.getColor(this.context, R.color.grey_100), 21 | PorterDuff.Mode.LIGHTEN 22 | ) 23 | ) 24 | ) 25 | } 26 | 27 | fun LottieAnimationView.enable() { 28 | this.isEnabled = true 29 | this.addValueCallback( 30 | KeyPath("**"), 31 | LottieProperty.COLOR_FILTER, 32 | LottieValueCallback(null) 33 | ) 34 | } 35 | 36 | -------------------------------------------------------------------------------- /application/app/src/main/java/br/com/charleston/github/extensions/TextViewExt.kt: -------------------------------------------------------------------------------- 1 | package br.com.charleston.github.extensions 2 | 3 | import android.os.Handler 4 | import android.widget.TextView 5 | 6 | fun TextView.typeWriter(text: String, delay: Long = 10) { 7 | var index = 0 8 | val handler = Handler() 9 | 10 | val characterAdder = object : Runnable { 11 | override fun run() { 12 | this@typeWriter.text = text.subSequence(0, index++) 13 | if (index <= text.length) { 14 | handler.postDelayed(this, delay) 15 | } 16 | } 17 | } 18 | handler.removeCallbacks(characterAdder) 19 | handler.postDelayed(characterAdder, delay) 20 | } -------------------------------------------------------------------------------- /application/app/src/main/java/br/com/charleston/github/features/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package br.com.charleston.github.features 2 | 3 | import android.view.MenuItem 4 | import androidx.lifecycle.ViewModelProviders 5 | import br.com.charleston.core.base.BaseActivity 6 | import br.com.charleston.core.base.BaseViewModel 7 | import br.com.charleston.github.R 8 | import br.com.charleston.github.databinding.ActivityMainBinding 9 | 10 | 11 | class MainActivity : BaseActivity() { 12 | 13 | override fun getLayoutId(): Int { 14 | return R.layout.activity_main 15 | } 16 | 17 | override fun getViewModel(): BaseViewModel { 18 | return ViewModelProviders 19 | .of(this, viewModelFactory) 20 | .get(BaseViewModel::class.java) 21 | } 22 | 23 | override fun onOptionsItemSelected(item: MenuItem?): Boolean { 24 | if (item?.itemId == android.R.id.home) { 25 | onBackPressed() 26 | } 27 | return super.onOptionsItemSelected(item) 28 | } 29 | } -------------------------------------------------------------------------------- /application/app/src/main/java/br/com/charleston/github/features/search/adapters/GithubListAdapter.kt: -------------------------------------------------------------------------------- 1 | package br.com.charleston.github.features.search.adapters 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.databinding.DataBindingUtil 6 | import androidx.databinding.ViewDataBinding 7 | import androidx.databinding.library.baseAdapters.BR 8 | import androidx.recyclerview.widget.RecyclerView 9 | import br.com.charleston.domain.model.GithubModel 10 | import br.com.charleston.github.R 11 | import br.com.charleston.github.databinding.ItemListBinding 12 | 13 | class GithubListAdapter( 14 | private val items: List, 15 | private val listener: ListListener 16 | ) : RecyclerView.Adapter() { 17 | 18 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { 19 | return ListViewHolder( 20 | DataBindingUtil.inflate( 21 | LayoutInflater.from(parent.context), 22 | R.layout.item_list, 23 | parent, 24 | false 25 | ) 26 | ) 27 | } 28 | 29 | override fun getItemCount(): Int { 30 | return items.size 31 | } 32 | 33 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { 34 | val item = items[position] 35 | val heroesHolder = (holder as ListViewHolder) 36 | heroesHolder.bind(item) 37 | } 38 | 39 | inner class ListViewHolder( 40 | private val viewDataBinding: ItemListBinding 41 | ) : RecyclerView.ViewHolder(viewDataBinding.root) { 42 | 43 | fun bind(data: GithubModel) { 44 | viewDataBinding.setVariable(BR.model, data) 45 | viewDataBinding.executePendingBindings() 46 | 47 | viewDataBinding.root.setOnClickListener { 48 | listener.onClickItem((data), viewDataBinding.imageView) 49 | } 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /application/app/src/main/java/br/com/charleston/github/features/search/adapters/ListListener.kt: -------------------------------------------------------------------------------- 1 | package br.com.charleston.github.features.search.adapters 2 | 3 | import android.widget.ImageView 4 | import br.com.charleston.domain.model.GithubModel 5 | 6 | interface ListListener { 7 | fun onClickItem(githubModel: GithubModel, imageView: ImageView) 8 | } -------------------------------------------------------------------------------- /application/app/src/main/java/br/com/charleston/github/features/search/screens/detail/DetailFragment.kt: -------------------------------------------------------------------------------- 1 | package br.com.charleston.github.features.search.screens.detail 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.lifecycle.ViewModelProviders 7 | import br.com.charleston.core.base.BaseFragment 8 | import br.com.charleston.core.base.BaseViewModel 9 | import br.com.charleston.github.R 10 | import br.com.charleston.github.databinding.FragmentDetailBinding 11 | 12 | 13 | class DetailFragment : BaseFragment() { 14 | 15 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 16 | super.onViewCreated(view, savedInstanceState) 17 | bindModel() 18 | bindToolbar() 19 | } 20 | 21 | override fun getLayoutId(): Int { 22 | return R.layout.fragment_detail 23 | } 24 | 25 | override fun getViewModel(): BaseViewModel { 26 | return ViewModelProviders 27 | .of(this, viewModelFactory) 28 | .get(BaseViewModel::class.java) 29 | } 30 | 31 | private fun bindModel() { 32 | arguments?.let { 33 | val safeArgs = DetailFragmentArgs.fromBundle(it) 34 | val model = safeArgs.model 35 | getViewDataBinding().model = model 36 | } 37 | } 38 | 39 | private fun bindToolbar() { 40 | getViewDataBinding().toolbar.apply { 41 | title = context.getString(R.string.detail_title) 42 | } 43 | 44 | (activity as AppCompatActivity).let { 45 | it.setSupportActionBar(getViewDataBinding().toolbar) 46 | it.supportActionBar?.setDisplayHomeAsUpEnabled(true) 47 | it.supportActionBar?.setDisplayShowHomeEnabled(true) 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /application/app/src/main/java/br/com/charleston/github/features/search/screens/list/ListFragment.kt: -------------------------------------------------------------------------------- 1 | package br.com.charleston.github.features.search.screens.list 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import android.widget.ImageView 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.lifecycle.ViewModelProviders 8 | import androidx.navigation.Navigation 9 | import androidx.recyclerview.widget.LinearLayoutManager 10 | import br.com.charleston.core.base.BaseFragment 11 | import br.com.charleston.core.base.BaseViewModel 12 | import br.com.charleston.domain.model.GithubModel 13 | import br.com.charleston.github.R 14 | import br.com.charleston.github.databinding.FragmentListBinding 15 | import br.com.charleston.github.features.search.adapters.GithubListAdapter 16 | import br.com.charleston.github.features.search.adapters.ListListener 17 | 18 | class ListFragment : BaseFragment(), 19 | ListListener { 20 | 21 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 22 | super.onViewCreated(view, savedInstanceState) 23 | bindToolbar() 24 | bindItems() 25 | } 26 | 27 | override fun getLayoutId(): Int { 28 | return R.layout.fragment_list 29 | } 30 | 31 | override fun getViewModel(): BaseViewModel { 32 | return ViewModelProviders 33 | .of(this, viewModelFactory) 34 | .get(BaseViewModel::class.java) 35 | } 36 | 37 | override fun onClickItem(githubModel: GithubModel, imageView: ImageView) { 38 | view?.let { 39 | val action = ListFragmentDirections.actionListFragmentToDetailFragment(githubModel) 40 | 41 | Navigation 42 | .findNavController(it) 43 | .navigate(action) 44 | } 45 | } 46 | 47 | private fun bindToolbar() { 48 | getViewDataBinding().toolbar.apply { 49 | title = context.getString(R.string.list_title) 50 | } 51 | 52 | (activity as AppCompatActivity) 53 | .setSupportActionBar(getViewDataBinding().toolbar) 54 | 55 | (activity as AppCompatActivity).supportActionBar?.setDisplayHomeAsUpEnabled(true) 56 | (activity as AppCompatActivity).supportActionBar?.setDisplayShowHomeEnabled(true) 57 | } 58 | 59 | private fun bindItems() { 60 | arguments?.let { 61 | val safeArgs = ListFragmentArgs.fromBundle(it) 62 | val items = safeArgs.items 63 | 64 | getViewDataBinding().list.apply { 65 | adapter = GithubListAdapter(items.toList(), this@ListFragment) 66 | layoutManager = LinearLayoutManager(context) 67 | } 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /application/app/src/main/java/br/com/charleston/github/features/search/screens/voicesearch/VoiceSearchFragment.kt: -------------------------------------------------------------------------------- 1 | package br.com.charleston.github.features.search.screens.voicesearch 2 | 3 | import android.Manifest 4 | import android.annotation.SuppressLint 5 | import android.content.Intent 6 | import android.os.Bundle 7 | import android.speech.RecognitionListener 8 | import android.speech.RecognizerIntent 9 | import android.speech.SpeechRecognizer 10 | import android.view.View 11 | import android.widget.Toast 12 | import androidx.lifecycle.Observer 13 | import androidx.lifecycle.ViewModelProviders 14 | import androidx.navigation.Navigation 15 | import br.com.charleston.core.base.BaseFragment 16 | import br.com.charleston.domain.model.GithubModel 17 | import br.com.charleston.github.R 18 | import br.com.charleston.github.databinding.FragmentVoiceSearchBinding 19 | import br.com.charleston.github.extensions.disable 20 | import br.com.charleston.github.extensions.enable 21 | import br.com.charleston.github.extensions.typeWriter 22 | import br.com.charleston.github.features.search.states.SearchState 23 | import br.com.charleston.github.features.search.viewmodel.VoiceSearchViewModel 24 | import com.tbruyelle.rxpermissions2.RxPermissions 25 | 26 | 27 | class VoiceSearchFragment 28 | : BaseFragment(), 29 | RecognitionListener, 30 | VoiceSearchHandler { 31 | 32 | private var items: List = arrayListOf() 33 | 34 | private lateinit var speech: SpeechRecognizer 35 | 36 | private val rxPermissions by lazy { RxPermissions(activity!!) } 37 | 38 | companion object { 39 | private const val MAX_RESULT = 1 40 | private const val LANGUAGE_PREFERENCE = "pt" 41 | } 42 | 43 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 44 | super.onViewCreated(view, savedInstanceState) 45 | observerViewModel() 46 | bindView() 47 | bindSpeech() 48 | } 49 | 50 | override fun onDestroy() { 51 | speech.destroy() 52 | super.onDestroy() 53 | } 54 | 55 | override fun onStop() { 56 | cancelSpeech() 57 | getViewDataBinding().showResult = false 58 | super.onStop() 59 | } 60 | 61 | override fun getLayoutId(): Int { 62 | return R.layout.fragment_voice_search 63 | } 64 | 65 | override fun getViewModel(): VoiceSearchViewModel { 66 | return ViewModelProviders 67 | .of(this, viewModelFactory) 68 | .get(VoiceSearchViewModel::class.java) 69 | } 70 | 71 | override fun onReadyForSpeech(params: Bundle?) {} 72 | override fun onRmsChanged(rmsdB: Float) {} 73 | override fun onBufferReceived(buffer: ByteArray?) {} 74 | override fun onPartialResults(partialResults: Bundle?) {} 75 | override fun onEvent(eventType: Int, params: Bundle?) {} 76 | override fun onBeginningOfSpeech() {} 77 | 78 | override fun onEndOfSpeech() { 79 | stopSpeech() 80 | } 81 | 82 | override fun onError(error: Int) { 83 | resetMessage() 84 | } 85 | 86 | override fun onResults(results: Bundle?) { 87 | val matches = results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION) 88 | matches?.get(0)?.let { 89 | search(it) 90 | } 91 | } 92 | 93 | @SuppressLint("CheckResult") 94 | override fun onSearchStart() { 95 | rxPermissions 96 | .request(Manifest.permission.RECORD_AUDIO) 97 | .subscribe { granted -> 98 | when { 99 | granted -> getViewModel().input.listen() 100 | else -> handlerPermissionError() 101 | } 102 | } 103 | } 104 | 105 | override fun onCancelResult() { 106 | getViewDataBinding().showResult = false 107 | resetMessage() 108 | } 109 | 110 | override fun onShowResult(view: View) { 111 | startListResult() 112 | } 113 | 114 | private fun bindView() { 115 | getViewDataBinding().run { 116 | handlers = this@VoiceSearchFragment 117 | showResult = false 118 | } 119 | } 120 | 121 | private fun bindSpeech() { 122 | speech = SpeechRecognizer.createSpeechRecognizer(context).apply { 123 | setRecognitionListener(this@VoiceSearchFragment) 124 | } 125 | } 126 | 127 | private fun observerViewModel() { 128 | getViewModel().output.run { 129 | search.observe(this@VoiceSearchFragment, 130 | Observer { 131 | renderSearchState(it) 132 | }) 133 | } 134 | } 135 | 136 | private fun renderSearchState(state: SearchState) { 137 | when (state) { 138 | is SearchState.Listening -> { 139 | playSpeech() 140 | } 141 | is SearchState.Success -> { 142 | setList(state.data) 143 | enableSearch() 144 | getViewDataBinding().showResult = true 145 | bindMessageResult(state.searchingByText) 146 | } 147 | is SearchState.Loading -> { 148 | disableSearch() 149 | bindMessageSearching(state.searchingByText) 150 | } 151 | is SearchState.NoResult -> { 152 | showErrorNoResult() 153 | enableSearch() 154 | } 155 | is SearchState.Error -> { 156 | showError(state.error) 157 | enableSearch() 158 | } 159 | } 160 | } 161 | 162 | private fun bindMessageSearching(value: String?) { 163 | bindMessage(value, getString(R.string.voice_search_message_find_repository_by)) 164 | } 165 | 166 | private fun bindMessageResult(value: String) { 167 | bindMessage(value, getString(R.string.voice_search_message_result_repository_by)) 168 | } 169 | 170 | private fun bindMessage(value: String?, message: String) { 171 | getViewDataBinding().message.typeWriter( 172 | String.format( 173 | message, 174 | value?.toUpperCase() 175 | ) 176 | ) 177 | } 178 | 179 | private fun search(text: String) { 180 | getViewModel().input.search(text) 181 | } 182 | 183 | private fun playSpeech() { 184 | getViewDataBinding().run { 185 | message.typeWriter(getString(R.string.voice_search_message_listening)) 186 | voice.playAnimation() 187 | } 188 | speech.startListening(speechIntent()) 189 | } 190 | 191 | private fun stopSpeech() { 192 | speech.stopListening() 193 | cancelAnimation() 194 | } 195 | 196 | private fun cancelSpeech() { 197 | speech.cancel() 198 | cancelAnimation() 199 | resetMessage() 200 | } 201 | 202 | private fun setList(items: List) { 203 | this.items = items 204 | } 205 | 206 | private fun startListResult() { 207 | view?.let { 208 | val action = VoiceSearchFragmentDirections.actionVoiceSearchFragmentToListFragment(items.toTypedArray()) 209 | 210 | Navigation.findNavController(it).navigate(action) 211 | } 212 | } 213 | 214 | private fun cancelAnimation() { 215 | getViewDataBinding().voice.run { 216 | frame = 0 217 | cancelAnimation() 218 | } 219 | } 220 | 221 | private fun speechIntent(): Intent { 222 | return Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply { 223 | putExtra( 224 | RecognizerIntent.EXTRA_LANGUAGE_PREFERENCE, 225 | LANGUAGE_PREFERENCE 226 | ) 227 | putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, activity?.packageName) 228 | putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH) 229 | putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, activity?.packageName) 230 | putExtra( 231 | RecognizerIntent.EXTRA_MAX_RESULTS, 232 | MAX_RESULT 233 | ) 234 | } 235 | } 236 | 237 | private fun handlerPermissionError() { 238 | getViewDataBinding().run { 239 | message.typeWriter(getString(R.string.voice_search_message_permission_error), 10) 240 | voice.visibility = View.GONE 241 | } 242 | } 243 | 244 | private fun resetMessage() { 245 | getViewDataBinding().message.text = getString(R.string.voice_search_message_inital) 246 | } 247 | 248 | private fun showErrorNoResult() { 249 | getViewDataBinding().message.text = getString(R.string.voice_search_message_no_result) 250 | } 251 | 252 | private fun showError(throwable: Throwable) { 253 | Toast.makeText(context, throwable.message, Toast.LENGTH_LONG).show() 254 | } 255 | 256 | private fun disableSearch() { 257 | getViewDataBinding().voice.disable() 258 | } 259 | 260 | private fun enableSearch() { 261 | getViewDataBinding().voice.enable() 262 | } 263 | } -------------------------------------------------------------------------------- /application/app/src/main/java/br/com/charleston/github/features/search/screens/voicesearch/VoiceSearchHandler.kt: -------------------------------------------------------------------------------- 1 | package br.com.charleston.github.features.search.screens.voicesearch 2 | 3 | import android.view.View 4 | 5 | interface VoiceSearchHandler { 6 | fun onSearchStart() 7 | fun onCancelResult() 8 | fun onShowResult(view: View) 9 | } -------------------------------------------------------------------------------- /application/app/src/main/java/br/com/charleston/github/features/search/states/SearchState.kt: -------------------------------------------------------------------------------- 1 | package br.com.charleston.github.features.search.states 2 | 3 | import br.com.charleston.domain.model.GithubModel 4 | 5 | sealed class SearchState { 6 | object Listening : SearchState() 7 | data class Loading(val searchingByText: String) : SearchState() 8 | data class Success(val searchingByText: String, val data: List) : SearchState() 9 | data class Error(val error: Throwable) : SearchState() 10 | object NoResult : SearchState() 11 | } -------------------------------------------------------------------------------- /application/app/src/main/java/br/com/charleston/github/features/search/viewmodel/VoiceSearchViewModel.kt: -------------------------------------------------------------------------------- 1 | package br.com.charleston.github.features.search.viewmodel 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import br.com.charleston.core.base.BaseViewModel 6 | import br.com.charleston.core.base.Event 7 | import br.com.charleston.domain.DefaultObserver 8 | import br.com.charleston.domain.interactor.GetRepositories 9 | import br.com.charleston.domain.model.GithubModel 10 | import br.com.charleston.github.features.search.states.SearchState 11 | import javax.inject.Inject 12 | 13 | interface InputVoiceSearchViewModel { 14 | fun listen() 15 | fun search(text: String) 16 | } 17 | 18 | interface OutputVoiceSearchViewModel { 19 | val search: LiveData 20 | } 21 | 22 | interface ContractVoiceSearchViewModel { 23 | val input: InputVoiceSearchViewModel 24 | val output: OutputVoiceSearchViewModel 25 | } 26 | 27 | class VoiceSearchViewModel @Inject constructor( 28 | private val getRepositories: GetRepositories 29 | ) : BaseViewModel(), 30 | ContractVoiceSearchViewModel, 31 | InputVoiceSearchViewModel, 32 | OutputVoiceSearchViewModel { 33 | 34 | override val input: InputVoiceSearchViewModel get() = this 35 | override val output: OutputVoiceSearchViewModel get() = this 36 | 37 | private val searchObservable = MutableLiveData() 38 | override val search: LiveData get() = searchObservable 39 | 40 | override fun listen() { 41 | searchObservable.postValue(SearchState.Listening) 42 | } 43 | 44 | override fun search(text: String) { 45 | getRepositories.execute(object : DefaultObserver>() { 46 | override fun onStart() { 47 | searchObservable.postValue(SearchState.Loading(text)) 48 | } 49 | 50 | override fun onNext(t: List) { 51 | handlerSuccess(text, t) 52 | } 53 | 54 | override fun onError(exception: Throwable) { 55 | super.onError(exception) 56 | searchObservable.postValue(SearchState.Error(exception)) 57 | } 58 | }, text) 59 | } 60 | 61 | private fun handlerSuccess(text: String, items: List) { 62 | if (items.isNotEmpty()) { 63 | searchObservable.postValue(SearchState.Success(text, items)) 64 | } else { 65 | searchObservable.postValue(SearchState.NoResult) 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /application/app/src/main/res/anim/slide_in_left.xml: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 25 | 29 | -------------------------------------------------------------------------------- /application/app/src/main/res/anim/slide_in_right.xml: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 25 | 29 | -------------------------------------------------------------------------------- /application/app/src/main/res/anim/slide_out_left.xml: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 25 | 29 | -------------------------------------------------------------------------------- /application/app/src/main/res/anim/slide_out_right.xml: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 25 | 29 | -------------------------------------------------------------------------------- /application/app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /application/app/src/main/res/drawable/ic_code.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /application/app/src/main/res/drawable/ic_code_fork_symbol.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /application/app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /application/app/src/main/res/drawable/ic_star.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /application/app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 15 | 16 | -------------------------------------------------------------------------------- /application/app/src/main/res/layout/fragment_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 24 | 25 | 35 | 42 | 51 | 58 | 63 | 73 | 84 | 91 | 97 | 103 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /application/app/src/main/res/layout/fragment_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 18 | 19 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /application/app/src/main/res/layout/fragment_voice_search.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 17 | 18 | 19 | 22 | 23 | 38 | 39 | 56 | 57 |