├── .circleci └── config.yml ├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── mctech │ │ └── kotlinlearning │ │ ├── App.kt │ │ ├── di │ │ └── modules │ │ │ ├── analyticsModule.kt │ │ │ ├── loggingModule.kt │ │ │ ├── navigatorModule.kt │ │ │ ├── networkingModule.kt │ │ │ └── useCaseModules.kt │ │ └── platform │ │ └── logger │ │ └── LogcatLogger.kt │ └── res │ ├── mipmap-hdpi │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ └── ic_launcher_round.png │ └── values │ └── strings.xml ├── build-dependencies.gradle ├── build-modularization.gradle ├── build.gradle ├── data ├── build.gradle └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── mctech │ │ │ └── data │ │ │ ├── auth │ │ │ └── AuthRepository.kt │ │ │ ├── di │ │ │ └── dataModule.kt │ │ │ ├── networkRequestHandler.kt │ │ │ └── quotation │ │ │ ├── api │ │ │ └── QuotationAPI.kt │ │ │ ├── datasource │ │ │ ├── QuotationCacheDataSource.kt │ │ │ ├── QuotationCacheDataSourceImpl.kt │ │ │ ├── QuotationDataSource.kt │ │ │ └── QuotationRemoteDataSourceImpl.kt │ │ │ ├── model │ │ │ ├── QuotationAuthorResponse.kt │ │ │ ├── QuotationSourceResponse.kt │ │ │ ├── RandomQuotationEmbeddedResponse.kt │ │ │ └── RandomQuotationResponse.kt │ │ │ └── repository │ │ │ └── QuotationRepository.kt │ └── res │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── mctech │ └── data │ └── quotation │ ├── QuotationCacheDataSourceImplTest.kt │ ├── QuotationRemoteDataSourceImplTest.kt │ └── QuotationRepositoryTest.kt ├── domain ├── build.gradle └── src │ ├── main │ └── java │ │ └── com │ │ └── mctech │ │ └── domain │ │ ├── errors │ │ ├── AuthException.kt │ │ ├── NetworkException.kt │ │ └── QuotationException.kt │ │ ├── interaction │ │ ├── Result.kt │ │ ├── auth │ │ │ ├── AuthenticationUseCase.kt │ │ │ ├── CheckAuthSessionUseCase.kt │ │ │ └── RegisterUserUseCase.kt │ │ └── quotation │ │ │ └── GetRandomQuotationCase.kt │ │ ├── model │ │ ├── AuthRequest.kt │ │ ├── Quotation.kt │ │ ├── RegisterUser.kt │ │ └── User.kt │ │ ├── services │ │ ├── AuthService.kt │ │ └── QuotationService.kt │ │ └── validation │ │ ├── EmailValidator.kt │ │ └── PasswordlValidator.kt │ └── test │ ├── kotlin │ └── com │ │ └── mctech │ │ └── domain │ │ ├── TestDataFactory.kt │ │ ├── interaction │ │ ├── auth │ │ │ ├── AuthenticationUseCaseTest.kt │ │ │ ├── CheckAuthSessionUseCaseTest.kt │ │ │ └── RegisterUserUseCaseTest.kt │ │ └── quotation │ │ │ └── GetRandomQuotationCaseTest.kt │ │ ├── model │ │ ├── AuthRequestTest.kt │ │ └── RegisterUserTest.kt │ │ └── validation │ │ ├── EmailValidatorTest.kt │ │ └── PasswordlValidatorTest.kt │ └── resources │ └── mockito-extensions │ └── org.mockito.plugins.MockMaker ├── features ├── feature-login │ ├── README.md │ ├── build.gradle │ └── src │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── mctech │ │ │ │ └── features │ │ │ │ └── login │ │ │ │ ├── LoginActivity.kt │ │ │ │ ├── LoginSignInFragment.kt │ │ │ │ ├── LoginSignUpFragment.kt │ │ │ │ ├── LoginViewModel.kt │ │ │ │ ├── di │ │ │ │ └── loginModule.kt │ │ │ │ ├── interaction │ │ │ │ └── LoginUserInteraction.kt │ │ │ │ └── state │ │ │ │ ├── LoginErrorStateResources.kt │ │ │ │ └── LoginState.kt │ │ └── res │ │ │ ├── layout │ │ │ ├── activity_login.xml │ │ │ ├── fragment_sign_in.xml │ │ │ └── fragment_sign_up.xml │ │ │ ├── navigation │ │ │ └── login_nav_graph.xml │ │ │ └── values │ │ │ ├── dimens.xml │ │ │ └── strings.xml │ │ └── test │ │ └── java │ │ └── com │ │ └── mctech │ │ └── features │ │ └── login │ │ └── LoginViewModelTest.kt ├── feature-navigation │ ├── build.gradle │ └── src │ │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── mctech │ │ │ └── features │ │ │ └── navigation │ │ │ ├── Navigator.kt │ │ │ ├── Screen.kt │ │ │ └── UnsupportedNavigation.kt │ │ └── test │ │ └── java │ │ └── com │ │ └── mctech │ │ └── features │ │ └── navigation │ │ └── NavigatorTest.kt ├── feature-onboarding │ ├── README.md │ ├── build.gradle │ └── src │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── mctech │ │ │ │ └── features │ │ │ │ └── onboarding │ │ │ │ ├── OnboardingActivity.kt │ │ │ │ ├── OnboardingViewModel.kt │ │ │ │ ├── di │ │ │ │ └── onboardingModule.kt │ │ │ │ └── state │ │ │ │ └── OnBoardingNavigationState.kt │ │ └── res │ │ │ └── layout │ │ │ └── activity_on_boarding.xml │ │ └── test │ │ ├── java │ │ └── com │ │ │ └── mctech │ │ │ └── features │ │ │ └── onboarding │ │ │ └── OnboardingViewModelTest.kt │ │ └── resources │ │ └── mockito-extensions │ │ └── org.mockito.plugins.MockMaker ├── feature-quotation-filtering │ ├── build.gradle │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ └── res │ │ └── values │ │ └── strings.xml ├── feature-quotation-list │ ├── build.gradle │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ └── res │ │ └── values │ │ └── strings.xml └── feature-quotation-random │ ├── build.gradle │ └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── mctech │ │ │ └── feature │ │ │ └── random_joke │ │ │ ├── RandomQuotationActivity.kt │ │ │ ├── RandomQuotationViewModel.kt │ │ │ ├── di │ │ │ └── randomQuotationModule.kt │ │ │ └── interaction │ │ │ └── RandomQuotationInteraction.kt │ └── res │ │ ├── layout │ │ └── activity_random_joke.xml │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── mctech │ └── feature │ └── random_joke │ └── RandomQuotationViewModelTest.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── libraries ├── library-analytics │ ├── README.md │ ├── build.gradle │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── mctech │ │ └── libraries │ │ └── analytics │ │ ├── AnalyticsHelper.kt │ │ ├── FirebaseAnalyticsHelper.kt │ │ └── MutedAnalyticsHelper.kt ├── library-app-theme │ ├── README.md │ ├── build.gradle │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ └── res │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ └── styles.xml ├── library-logger │ ├── README.md │ ├── build.gradle │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── mctech │ │ └── libraries │ │ └── logger │ │ ├── Logger.kt │ │ └── MutedLogger.kt ├── library-networking │ ├── build.gradle │ └── src │ │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── mctech │ │ │ └── library │ │ │ └── networking │ │ │ ├── NetworkError.kt │ │ │ ├── NetworkErrorTransformer.kt │ │ │ ├── RetrofitBuilder.kt │ │ │ └── secureRequest.kt │ │ └── test │ │ └── java │ │ └── com │ │ └── mctech │ │ └── library │ │ └── networking │ │ ├── NetworkErrorTransformerTest.kt │ │ └── SecureRequestKtTest.kt ├── library-shared-feature-arq-testing │ ├── build.gradle │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── mctech │ │ └── test │ │ └── arq │ │ ├── BaseViewModelTest.kt │ │ ├── extentions │ │ ├── listAssertionExtention.kt │ │ └── livedataTestExtentions.kt │ │ └── rules │ │ ├── CoroutinesMainTestRule.kt │ │ └── KoinModuleTestRule.kt └── library-shared-feature-arq │ ├── build.gradle │ └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── mctech │ │ │ └── feature │ │ │ └── arq │ │ │ ├── BaseActivity.kt │ │ │ ├── BaseFragment.kt │ │ │ ├── BaseViewModel.kt │ │ │ ├── ComponentState.kt │ │ │ ├── UserInteraction.kt │ │ │ ├── components │ │ │ ├── TextViewAdapter.kt │ │ │ └── ViewAdapter.kt │ │ │ └── extentions │ │ │ ├── baseActivityExtention.kt │ │ │ ├── editTextExtentions.kt │ │ │ └── viewExtentions.kt │ └── res │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── mctech │ └── feature │ └── arq │ └── BaseViewModelArqTest.kt └── settings.gradle /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: ~/code 5 | docker: 6 | - image: circleci/android:api-28 7 | environment: 8 | JVM_OPTS: -Xmx3200m 9 | steps: 10 | - checkout 11 | - restore_cache: 12 | key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }} 13 | 14 | - run: 15 | name: Store Google Service File 16 | command: echo $GOOGLE_SERVICES > app/google-services.json 17 | 18 | - run: 19 | name: Store Security API Key 20 | command: echo $SECURITY_API_KEY > securityKeys.properties 21 | 22 | - run: 23 | name: Download Dependencies 24 | command: ./gradlew androidDependencies 25 | 26 | - save_cache: 27 | paths: 28 | - ~/.gradle 29 | key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }} 30 | 31 | - run: 32 | name: Run Tests 33 | command: ./gradlew test 34 | 35 | - store_artifacts: 36 | path: app/build/reports 37 | destination: reports 38 | 39 | - store_test_results: 40 | path: app/build/test-results -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: mayconcardoso 2 | -------------------------------------------------------------------------------- /.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 | .idea/* 45 | 46 | # Keystore files 47 | # Uncomment the following line if you do not want to check your keystore files in. 48 | #*.jks 49 | 50 | # External native build folder generated in Android Studio 2.2 and later 51 | .externalNativeBuild 52 | 53 | # Google Services (e.g. APIs or Firebase) 54 | google-services.json 55 | 56 | # Freeline 57 | freeline.py 58 | freeline/ 59 | freeline_project_description.json 60 | 61 | # fastlane 62 | fastlane/report.xml 63 | fastlane/Preview.html 64 | fastlane/screenshots 65 | fastlane/test_output 66 | fastlane/readme.md 67 | 68 | # Security 69 | securityKeys.properties 70 | -------------------------------------------------------------------------------- /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 | ## Deprecated 2 | This project is deprecated, you could check [this one](https://github.com/MayconCardoso/StockTradeTracking) instead. 3 | 4 | 5 | Android Kotlin Clean Architecture Learning Playground 6 | = 7 | 8 | [![CircleCI](https://circleci.com/gh/MayconCardoso/Modularized-Kotlin-Clean-Architecture-Showcase/tree/master.svg?style=svg)](https://circleci.com/gh/MayconCardoso/KotlinLearning/tree/master) 9 | [![Kotlin Version](https://img.shields.io/badge/kotlin-1.3.50-blue.svg)](http://kotlinlang.org/) 10 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) 11 | 12 | This project has been created just to practice a little bit of Kotlin and it's libraries; It has been structured in a multi-module fashion, with semantics guided by Clean Architecture; this means that high-level modules don't know anything about low-level ones. 13 | 14 | I gotta say that almost all developers have their favorite libraries to handle many different things. For example, many developers, including me, would use Dagger to inject their dependencies. But, the point in this project is: instead of using all the libraries that I am used to, I am going to use a different one to improve my skills. So in the Dagger case, I am going to use Koin instead and so on. 15 | 16 | Below we have a mapper of the libraries I am used to using versus the ones that I will apply in this project. 17 | 18 | > * Java -> Kotlin 19 | > * Android Support Library -> AndroidX 20 | > * RxJava -> Kotlin Coroutines 21 | > * Dagger 2 -> Koin 22 | > * TODO the others. 23 | 24 | Beside of that, I'll try using all the Android Architecture Components plus Android security rules all covered of unit tests. 25 | 26 | Again, it's just a playground project to study. 27 | 28 | License 29 | - 30 | 31 | Copyright 2019 Maycon dos Santos Cardoso 32 | 33 | Licensed under the Apache License, Version 2.0 (the "License"); 34 | you may not use this file except in compliance with the License. 35 | You may obtain a copy of the License at 36 | 37 | http://www.apache.org/licenses/LICENSE-2.0 38 | 39 | Unless required by applicable law or agreed to in writing, software 40 | distributed under the License is distributed on an "AS IS" BASIS, 41 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 42 | See the License for the specific language governing permissions and 43 | limitations under the License. 44 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | apply plugin: 'com.google.gms.google-services' 5 | 6 | android { 7 | defaultConfig { 8 | applicationId "com.mctech.kotlinlearning" 9 | versionCode 1 10 | versionName "1.0" 11 | 12 | multiDexEnabled true 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | implementation project(path: submodulesPlatform.domain) 24 | implementation project(path: submodulesPlatform.data) 25 | 26 | implementation project(path: submodulesLibraries.sharedFeatureArq) 27 | implementation project(path: submodulesLibraries.networking) 28 | implementation project(path: submodulesLibraries.analytics) 29 | implementation project(path: submodulesLibraries.appTheme) 30 | implementation project(path: submodulesLibraries.logger) 31 | 32 | implementation project(path: submodulesFeatures.navigation) 33 | implementation project(path: submodulesFeatures.login) 34 | implementation project(path: submodulesFeatures.onBoarding) 35 | implementation project(path: submodulesFeatures.quotationList) 36 | implementation project(path: submodulesFeatures.quotationRandom) 37 | implementation project(path: submodulesFeatures.quotationFiltering) 38 | 39 | 40 | implementation globalDependencies.multidex 41 | 42 | implementation globalDependencies.kotlinStdLib 43 | implementation globalDependencies.kotlinCoreKTX 44 | 45 | implementation globalDependencies.koin 46 | 47 | implementation globalDependencies.okHttp 48 | implementation globalDependencies.okHttpLoggin 49 | implementation globalDependencies.retrofit 50 | 51 | implementation globalDependencies.appCompact 52 | implementation globalDependencies.constraintlayout 53 | 54 | testImplementation globalTestDependencies.jUnit 55 | androidTestImplementation globalTestDependencies.testRunner 56 | androidTestImplementation globalTestDependencies.espresso 57 | } 58 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/mctech/kotlinlearning/App.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.kotlinlearning 2 | 3 | import androidx.multidex.MultiDexApplication 4 | import com.mctech.data.di.dataModule 5 | import com.mctech.feature.random_joke.di.randomQuotationModel 6 | import com.mctech.features.login.di.loginModule 7 | import com.mctech.features.onboarding.di.onboardingModule 8 | import com.mctech.kotlinlearning.di.modules.* 9 | import org.koin.android.ext.koin.androidContext 10 | import org.koin.android.ext.koin.androidLogger 11 | import org.koin.core.context.startKoin 12 | 13 | class App : MultiDexApplication(){ 14 | override fun onCreate() { 15 | super.onCreate() 16 | initDependencyInjection() 17 | } 18 | 19 | private fun initDependencyInjection() { 20 | startKoin{ 21 | androidLogger() 22 | androidContext(this@App) 23 | modules(listOf( 24 | // Platform 25 | dataModule, 26 | useCaseModules, 27 | 28 | // Libraries 29 | loggingModule, 30 | analyticsModule, 31 | networkingModule, 32 | 33 | // Features 34 | navigatorModule, 35 | onboardingModule, 36 | loginModule, 37 | randomQuotationModel 38 | )) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/mctech/kotlinlearning/di/modules/analyticsModule.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.kotlinlearning.di.modules 2 | 3 | import com.mctech.libraries.analytics.AnalyticsHelper 4 | import com.mctech.libraries.analytics.FirebaseAnalyticsHelper 5 | import org.koin.dsl.module 6 | 7 | /** 8 | * @author MAYCON CARDOSO on 2019-09-05. 9 | */ 10 | val analyticsModule = module { 11 | single { 12 | FirebaseAnalyticsHelper( 13 | get() 14 | ) as AnalyticsHelper 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mctech/kotlinlearning/di/modules/loggingModule.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.kotlinlearning.di.modules 2 | 3 | import com.mctech.kotlinlearning.platform.logger.LogcatLogger 4 | import com.mctech.libraries.logger.Logger 5 | import org.koin.dsl.module 6 | 7 | /** 8 | * @author MAYCON CARDOSO on 2019-09-05. 9 | */ 10 | val loggingModule = module{ 11 | single{LogcatLogger as Logger} 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mctech/kotlinlearning/di/modules/navigatorModule.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.kotlinlearning.di.modules 2 | 3 | import androidx.fragment.app.FragmentActivity 4 | import com.mctech.feature.random_joke.RandomQuotationActivity 5 | import com.mctech.features.login.LoginActivity 6 | import com.mctech.features.navigation.Navigator 7 | import com.mctech.features.navigation.Screen 8 | import org.koin.dsl.module 9 | 10 | 11 | val navigatorModule = module { 12 | single { 13 | mapOf>( 14 | Screen.Login to LoginActivity::class.java, 15 | Screen.Dashboard to RandomQuotationActivity::class.java 16 | ) 17 | } 18 | 19 | factory { (view: FragmentActivity) -> Navigator(view, get()) } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mctech/kotlinlearning/di/modules/networkingModule.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.kotlinlearning.di.modules 2 | 3 | import com.mctech.library.networking.RetrofitBuilder 4 | import okhttp3.OkHttpClient 5 | import okhttp3.logging.HttpLoggingInterceptor 6 | import org.koin.dsl.module 7 | import retrofit2.Retrofit 8 | 9 | /** 10 | * @author MAYCON CARDOSO on 2019-09-05. 11 | */ 12 | val networkingModule = module { 13 | single { 14 | val logger = HttpLoggingInterceptor().apply { 15 | level = HttpLoggingInterceptor.Level.BODY 16 | } 17 | 18 | OkHttpClient.Builder() 19 | .addInterceptor(logger) 20 | .build() 21 | } 22 | 23 | single { 24 | RetrofitBuilder( 25 | apiURL = "https://matchilling-tronald-dump-v1.p.rapidapi.com", 26 | httpClient = get() 27 | ) as Retrofit 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mctech/kotlinlearning/di/modules/useCaseModules.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.kotlinlearning.di.modules 2 | 3 | import com.mctech.domain.interaction.auth.AuthenticationUseCase 4 | import com.mctech.domain.interaction.auth.CheckAuthSessionUseCase 5 | import com.mctech.domain.interaction.auth.RegisterUserUseCase 6 | import com.mctech.domain.interaction.quotation.GetRandomQuotationCase 7 | import org.koin.dsl.module 8 | 9 | val useCaseModules = module { 10 | // Auth 11 | factory { CheckAuthSessionUseCase(get()) } 12 | factory { RegisterUserUseCase(get()) } 13 | factory { AuthenticationUseCase(get()) } 14 | 15 | // Quotation 16 | factory { GetRandomQuotationCase(get()) } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mctech/kotlinlearning/platform/logger/LogcatLogger.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.kotlinlearning.platform.logger 2 | 3 | import android.util.Log 4 | import com.mctech.libraries.logger.Logger 5 | 6 | /** 7 | * @author MAYCON CARDOSO on 2019-07-22. 8 | */ 9 | internal object LogcatLogger : Logger { 10 | const val TAG = "com.mctech.logger" 11 | 12 | override fun v(message: String) { 13 | Log.v(TAG, message) 14 | } 15 | 16 | override fun d(message: String) { 17 | Log.d(TAG, message) 18 | } 19 | 20 | override fun i(message: String) { 21 | Log.i(TAG, message) 22 | } 23 | 24 | override fun w(message: String) { 25 | Log.w(TAG, message) 26 | } 27 | 28 | override fun e(message: String) { 29 | Log.e(TAG, message) 30 | } 31 | 32 | override fun e(e: Throwable) { 33 | Log.e(TAG, e.message, e) 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MayconCardoso/Modularized-Kotlin-Clean-Architecture-Showcase/7c2d04f56439c42b6bf8bc26807be25111d90dde/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MayconCardoso/Modularized-Kotlin-Clean-Architecture-Showcase/7c2d04f56439c42b6bf8bc26807be25111d90dde/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MayconCardoso/Modularized-Kotlin-Clean-Architecture-Showcase/7c2d04f56439c42b6bf8bc26807be25111d90dde/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MayconCardoso/Modularized-Kotlin-Clean-Architecture-Showcase/7c2d04f56439c42b6bf8bc26807be25111d90dde/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MayconCardoso/Modularized-Kotlin-Clean-Architecture-Showcase/7c2d04f56439c42b6bf8bc26807be25111d90dde/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Kotlin Learning 3 | 4 | -------------------------------------------------------------------------------- /build-dependencies.gradle: -------------------------------------------------------------------------------- 1 | ext { 2 | //============================================================================================== 3 | // Versions - Application 4 | //============================================================================================== 5 | MULTIDEX = '1.0.3' 6 | 7 | KOTLIN_VERSION = '1.3.41' 8 | KOTLIN_CORE_KTX = '1.0.2' 9 | KOTLIN_COROUTINES_CORE = '1.3.0-RC2' 10 | KOTLIN_COROUTINES_ANDROID = '1.2.1' 11 | 12 | APP_COMPAT = '1.0.2' 13 | CONSTRAINT_LAYOUT = '1.1.3' 14 | MATERIAL_DESIGN = '1.0.0-rc01' 15 | NAV_VERSION = "2.1.0" 16 | 17 | FIREBASE_CORE = '16.0.4' 18 | FIREBASE_AUTH = '18.1.0' 19 | 20 | AC_LIFECYCLE = '2.2.0-alpha01' 21 | 22 | KOIN = '2.0.1' 23 | 24 | //============================================================================================== 25 | // Versions - Tests 26 | //============================================================================================== 27 | JUNIT = '4.12' 28 | TEST_RUNNER = '1.2.0' 29 | EXPRESSO_CORE = '3.2.0' 30 | ROBOLETRIC = '4.3' 31 | MOCKITO_KOTLIN = '2.1.0' 32 | ASSERTJ = '3.11.1' 33 | KOTLIN_COROUTINES_TEST = '1.3.1' 34 | ARQ_CORE_TEST = '2.0.0' 35 | RETROFIT = '2.6.2' 36 | OKHTTP = '4.2.0' 37 | //============================================================================================== 38 | // 39 | // 40 | // 41 | // 42 | //============================================================================================== 43 | // SUBMODULES 44 | //============================================================================================== 45 | submodulesPlatform = [ 46 | domain : ':domain', 47 | data : ':data', 48 | ] 49 | 50 | submodulesLibraries = [ 51 | analytics : ':libraries:library-analytics', 52 | networking : ':libraries:library-networking', 53 | appTheme : ':libraries:library-app-theme', 54 | logger : ':libraries:library-logger', 55 | sharedFeatureArq : ':libraries:library-shared-feature-arq', 56 | ] 57 | 58 | submodulesFeatures = [ 59 | login : ':features:feature-login', 60 | onBoarding : ':features:feature-onboarding', 61 | navigation : ':features:feature-navigation', 62 | quotationList : ':features:feature-quotation-list', 63 | quotationRandom : ':features:feature-quotation-random', 64 | quotationFiltering : ':features:feature-quotation-filtering', 65 | ] 66 | 67 | submodulesTest = [ 68 | sharedFeatureArq : ':libraries:library-shared-feature-arq-testing', 69 | ] 70 | //============================================================================================== 71 | // 72 | // 73 | // 74 | // 75 | //============================================================================================== 76 | // Compiles - Application 77 | //============================================================================================== 78 | globalDependencies = [ 79 | multidex : "com.android.support:multidex:$MULTIDEX", 80 | 81 | // KOTLIN 82 | kotlinStdLib : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION", 83 | kotlinCoreKTX : "androidx.core:core-ktx:$KOTLIN_CORE_KTX", 84 | kotlinCoroutinesCore : "org.jetbrains.kotlinx:kotlinx-coroutines-core:$KOTLIN_COROUTINES_CORE", 85 | kotlinCoroutinesAndroid : "org.jetbrains.kotlinx:kotlinx-coroutines-android:$KOTLIN_COROUTINES_ANDROID", 86 | kotlinCoroutinesPlayService : "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:$KOTLIN_COROUTINES_ANDROID", 87 | 88 | // VIEW 89 | appCompact : "androidx.appcompat:appcompat:$APP_COMPAT", 90 | constraintlayout : "androidx.constraintlayout:constraintlayout:$CONSTRAINT_LAYOUT", 91 | materialDesign : "com.google.android.material:material:$MATERIAL_DESIGN", 92 | 93 | // FIREBASE 94 | firebaseCore : "com.google.firebase:firebase-core:$FIREBASE_CORE", 95 | firebaseAuth : "com.google.firebase:firebase-auth:$FIREBASE_AUTH", 96 | 97 | // ANDROID ARQ 98 | lifeCycleLiveRuntime : "androidx.lifecycle:lifecycle-runtime-ktx:$AC_LIFECYCLE", 99 | lifeCycleLiveExtentions : "androidx.lifecycle:lifecycle-extensions:$AC_LIFECYCLE", 100 | lifeCycleViewModel : "androidx.lifecycle:lifecycle-viewmodel-ktx:$AC_LIFECYCLE", 101 | lifeCycleLiveData : "androidx.lifecycle:lifecycle-livedata-ktx:$AC_LIFECYCLE", 102 | navigationFragment : "androidx.navigation:navigation-fragment-ktx:$NAV_VERSION", 103 | navigationFragmentUi : "androidx.navigation:navigation-ui-ktx:$NAV_VERSION", 104 | 105 | // Koin 106 | koin : "org.koin:koin-android:$KOIN", 107 | koinViewModel : "org.koin:koin-androidx-viewmodel:$KOIN", 108 | koinScope : "org.koin:koin-androidx-scope:$KOIN", 109 | 110 | // NETWORKING 111 | retrofit : "com.squareup.retrofit2:retrofit:$RETROFIT", 112 | retrofitGsonConverter : "com.squareup.retrofit2:converter-gson:$RETROFIT", 113 | okHttp : "com.squareup.okhttp3:okhttp:$OKHTTP", 114 | okHttpLoggin : "com.squareup.okhttp3:logging-interceptor:$OKHTTP" 115 | ] 116 | //============================================================================================== 117 | // 118 | // 119 | // 120 | // 121 | //============================================================================================== 122 | // Compiles - Tests 123 | //============================================================================================== 124 | globalTestDependencies = [ 125 | jUnit : "junit:junit:${JUNIT}", 126 | mockitoKotlin : "com.nhaarman.mockitokotlin2:mockito-kotlin:${MOCKITO_KOTLIN}", 127 | testRunner : "androidx.test:runner:${TEST_RUNNER}", 128 | espresso : "androidx.test.espresso:espresso-core:${EXPRESSO_CORE}", 129 | robolectric : "org.robolectric:robolectric:${ROBOLETRIC}" , 130 | assertJ : "org.assertj:assertj-core:${ASSERTJ}", 131 | coroutines : "org.jetbrains.kotlinx:kotlinx-coroutines-test:${KOTLIN_COROUTINES_TEST}", 132 | testArqCor : "androidx.arch.core:core-testing:${ARQ_CORE_TEST}", 133 | koinTest : "org.koin:koin-test:${KOIN}" 134 | 135 | ] 136 | } -------------------------------------------------------------------------------- /build-modularization.gradle: -------------------------------------------------------------------------------- 1 | ext { 2 | 3 | // Sdk and tools 4 | minSdk = 21 5 | targetSdk = 28 6 | compileSdk = 28 7 | buildTools = '28.0.3' 8 | 9 | // App dependencies 10 | javaVersion = JavaVersion.VERSION_1_8 11 | 12 | // Test 13 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 14 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | apply from: 'build-modularization.gradle' 2 | apply from: 'build-dependencies.gradle' 3 | 4 | buildscript { 5 | ext.kotlin_version = '1.3.50' 6 | 7 | repositories { 8 | google() 9 | jcenter() 10 | 11 | } 12 | dependencies { 13 | classpath 'com.android.tools.build:gradle:3.5.0' 14 | classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.50' 15 | classpath 'com.google.gms:google-services:4.3.2' 16 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 17 | } 18 | } 19 | 20 | allprojects { 21 | repositories { 22 | google() 23 | jcenter() 24 | 25 | } 26 | } 27 | 28 | subprojects { 29 | afterEvaluate { project -> 30 | if (project.hasProperty('android')) { 31 | android { 32 | buildToolsVersion buildTools 33 | compileSdkVersion compileSdk 34 | 35 | defaultConfig { 36 | minSdkVersion minSdk 37 | targetSdkVersion targetSdk 38 | testInstrumentationRunner testInstrumentationRunner 39 | } 40 | 41 | compileOptions { 42 | sourceCompatibility javaVersion 43 | targetCompatibility javaVersion 44 | } 45 | 46 | dataBinding { enabled = true } 47 | 48 | lintOptions { 49 | abortOnError false 50 | } 51 | } 52 | } 53 | } 54 | } 55 | 56 | task clean(type: Delete) { 57 | delete rootProject.buildDir 58 | } 59 | -------------------------------------------------------------------------------- /data/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | def props = new Properties() 7 | file("../securityKeys.properties").withInputStream { props.load(it) } 8 | 9 | buildTypes { 10 | debug{ 11 | buildConfigField "String", "ApiKey", props.getProperty("apiKey") 12 | } 13 | release { 14 | buildConfigField "String", "ApiKey", props.getProperty("apiKey") 15 | } 16 | } 17 | } 18 | 19 | dependencies { 20 | implementation project(path: submodulesPlatform.domain) 21 | implementation project(path: submodulesLibraries.networking) 22 | 23 | implementation globalDependencies.kotlinStdLib 24 | implementation globalDependencies.kotlinCoroutinesCore 25 | implementation globalDependencies.kotlinCoroutinesAndroid 26 | implementation globalDependencies.kotlinCoroutinesPlayService 27 | 28 | implementation globalDependencies.koin 29 | 30 | implementation globalDependencies.firebaseAuth 31 | implementation globalDependencies.retrofit 32 | implementation globalDependencies.retrofitGsonConverter 33 | 34 | 35 | testImplementation globalTestDependencies.jUnit 36 | testImplementation globalTestDependencies.assertJ 37 | } -------------------------------------------------------------------------------- /data/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /data/src/main/java/com/mctech/data/auth/AuthRepository.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.data.auth 2 | 3 | import com.google.firebase.auth.* 4 | import com.mctech.domain.errors.AuthException 5 | import com.mctech.domain.model.AuthRequest 6 | import com.mctech.domain.model.RegisterUser 7 | import com.mctech.domain.model.User 8 | import com.mctech.domain.services.AuthService 9 | import kotlinx.coroutines.Dispatchers 10 | import kotlinx.coroutines.tasks.await 11 | import kotlinx.coroutines.withContext 12 | 13 | /** 14 | * @author MAYCON CARDOSO on 2019-08-31. 15 | */ 16 | class AuthRepository(val firebaseAuth: FirebaseAuth) : AuthService { 17 | override suspend fun fetchLoggedUser() = firebaseAuth.currentUser?.let { 18 | User( 19 | it.uid, 20 | it.displayName.orEmpty(), 21 | it.email 22 | ) 23 | } 24 | 25 | override suspend fun registerUser(registerUser: RegisterUser) = withContext(Dispatchers.IO){ 26 | try{ 27 | // Create user 28 | firebaseAuth.createUserWithEmailAndPassword( 29 | registerUser.user.email!!, registerUser.password 30 | ).await() 31 | 32 | // Get user 33 | val currentUser = firebaseAuth.currentUser 34 | 35 | // Update informations 36 | currentUser?.apply { 37 | val profileChangeRequest = UserProfileChangeRequest 38 | .Builder() 39 | .setDisplayName(registerUser.user.name) 40 | .build() 41 | 42 | updateProfile(profileChangeRequest).await() 43 | } 44 | 45 | currentUser != null 46 | } 47 | catch (e : Exception){ 48 | when(e){ 49 | is FirebaseAuthWeakPasswordException -> throw AuthException.PasswordUnderSixCharactersException 50 | else -> throw AuthException.UnknownAuthException 51 | } 52 | 53 | } 54 | } 55 | 56 | override suspend fun login(user: AuthRequest) = withContext(Dispatchers.IO){ 57 | try{ 58 | val authResult = firebaseAuth.signInWithEmailAndPassword(user.email, user.password).await() 59 | authResult?.user != null 60 | } 61 | catch (e : Exception){ 62 | when(e){ 63 | is IllegalArgumentException -> throw AuthException.EmptyFormValueException 64 | is FirebaseAuthInvalidCredentialsException -> throw AuthException.InvalidEmailFormatException 65 | is FirebaseAuthInvalidUserException -> throw AuthException.UserNotFoundException 66 | else -> throw AuthException.UnknownAuthException 67 | } 68 | } 69 | } 70 | 71 | override suspend fun logout() = firebaseAuth.signOut() 72 | } -------------------------------------------------------------------------------- /data/src/main/java/com/mctech/data/di/dataModule.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.data.di 2 | 3 | import com.google.firebase.auth.FirebaseAuth 4 | import com.mctech.data.auth.AuthRepository 5 | import com.mctech.data.quotation.api.QuotationAPI 6 | import com.mctech.data.quotation.datasource.QuotationCacheDataSource 7 | import com.mctech.data.quotation.datasource.QuotationCacheDataSourceImpl 8 | import com.mctech.data.quotation.datasource.QuotationDataSource 9 | import com.mctech.data.quotation.datasource.QuotationRemoteDataSourceImpl 10 | import com.mctech.data.quotation.repository.QuotationRepository 11 | import com.mctech.domain.services.AuthService 12 | import com.mctech.domain.services.QuotationService 13 | import org.koin.dsl.module 14 | import retrofit2.Retrofit 15 | 16 | val dataModule = module { 17 | // Auth 18 | single { FirebaseAuth.getInstance() } 19 | single { 20 | AuthRepository( 21 | firebaseAuth = get() 22 | ) 23 | } 24 | 25 | // Quotation 26 | single { QuotationCacheDataSourceImpl() } 27 | single { 28 | val retrofit = get() 29 | val api = retrofit.create(QuotationAPI::class.java) 30 | 31 | QuotationRemoteDataSourceImpl(api) 32 | } 33 | single { 34 | QuotationRepository( 35 | cacheDataSource = get(), 36 | remoteDataSource = get() 37 | ) 38 | } 39 | } -------------------------------------------------------------------------------- /data/src/main/java/com/mctech/data/networkRequestHandler.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.data 2 | 3 | import com.mctech.domain.errors.NetworkException 4 | import com.mctech.library.networking.secureRequest 5 | 6 | 7 | // Called just to handle a secure request returning the response or throwing an error known by the app. 8 | suspend fun networkRequestHandler(target: suspend () -> T) = try { 9 | // Call method who map all networking exception when it happen 10 | secureRequest(target) 11 | } catch (error: Exception) { 12 | throw NetworkException() 13 | } 14 | 15 | suspend fun networkRequestSilentErrorHandler(target: suspend () -> T) = try { 16 | // Call method who map all networking exception when it happen 17 | secureRequest(target) 18 | } catch (error: Exception) { 19 | error.printStackTrace() 20 | null 21 | } -------------------------------------------------------------------------------- /data/src/main/java/com/mctech/data/quotation/api/QuotationAPI.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.data.quotation.api 2 | 3 | import com.mctech.data.BuildConfig 4 | import com.mctech.data.quotation.model.RandomQuotationResponse 5 | import retrofit2.http.GET 6 | import retrofit2.http.Headers 7 | 8 | /** 9 | * @author MAYCON CARDOSO on 2019-11-06. 10 | */ 11 | interface QuotationAPI { 12 | companion object { 13 | const val API_KEY = BuildConfig.ApiKey 14 | } 15 | 16 | @Headers( 17 | "x-rapidapi-host:matchilling-tronald-dump-v1.p.rapidapi.com", 18 | "accept:application/hal+json", 19 | "x-rapidapi-key:$API_KEY" 20 | ) 21 | @GET("random/quote") 22 | suspend fun getRandom(): RandomQuotationResponse 23 | } -------------------------------------------------------------------------------- /data/src/main/java/com/mctech/data/quotation/datasource/QuotationCacheDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.data.quotation.datasource 2 | 3 | import com.mctech.domain.model.Quotation 4 | 5 | interface QuotationCacheDataSource { 6 | suspend fun saveByTag(tag: String, page: Int?) 7 | suspend fun getRandom(): Quotation? 8 | suspend fun getByTag(tag: String, page: Int?): List? 9 | } -------------------------------------------------------------------------------- /data/src/main/java/com/mctech/data/quotation/datasource/QuotationCacheDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.data.quotation.datasource 2 | 3 | import com.mctech.domain.model.Quotation 4 | 5 | /** 6 | * @author MAYCON CARDOSO on 2019-09-30. 7 | */ 8 | class QuotationCacheDataSourceImpl : QuotationCacheDataSource { 9 | override suspend fun saveByTag(tag: String, page: Int?) { 10 | } 11 | 12 | override suspend fun getRandom(): Quotation? = null 13 | 14 | override suspend fun getByTag(tag: String, page: Int?): List? = null 15 | 16 | } -------------------------------------------------------------------------------- /data/src/main/java/com/mctech/data/quotation/datasource/QuotationDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.data.quotation.datasource 2 | 3 | import com.mctech.domain.services.QuotationService 4 | 5 | /** 6 | * @author MAYCON CARDOSO on 2019-09-30. 7 | */ 8 | interface QuotationDataSource : QuotationService -------------------------------------------------------------------------------- /data/src/main/java/com/mctech/data/quotation/datasource/QuotationRemoteDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.data.quotation.datasource 2 | 3 | import com.mctech.data.quotation.api.QuotationAPI 4 | import com.mctech.domain.model.Quotation 5 | 6 | /** 7 | * @author MAYCON CARDOSO on 2019-09-30. 8 | */ 9 | class QuotationRemoteDataSourceImpl(private val api: QuotationAPI) : 10 | QuotationDataSource { 11 | override suspend fun getRandom() = api.getRandom().let { 12 | Quotation( 13 | it.id, 14 | it.description, 15 | it.date, 16 | it.tags, 17 | it.embedded.author[0].name, 18 | it.embedded.source[0].url 19 | ) 20 | } 21 | 22 | override suspend fun getByTag(tag: String, page: Int?): List { 23 | TODO() 24 | } 25 | } -------------------------------------------------------------------------------- /data/src/main/java/com/mctech/data/quotation/model/QuotationAuthorResponse.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.data.quotation.model 2 | 3 | /** 4 | * @author MAYCON CARDOSO on 2019-11-19. 5 | */ 6 | data class QuotationAuthorResponse( 7 | val id : String, 8 | val name : String 9 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/mctech/data/quotation/model/QuotationSourceResponse.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.data.quotation.model 2 | 3 | /** 4 | * @author MAYCON CARDOSO on 2019-11-19. 5 | */ 6 | data class QuotationSourceResponse( 7 | val url : String 8 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/mctech/data/quotation/model/RandomQuotationEmbeddedResponse.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.data.quotation.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | /** 6 | * @author MAYCON CARDOSO on 2019-11-06. 7 | */ 8 | class RandomQuotationEmbeddedResponse( 9 | @SerializedName("author") 10 | val author: List, 11 | 12 | @SerializedName("source") 13 | val source: List 14 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/mctech/data/quotation/model/RandomQuotationResponse.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.data.quotation.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | import java.util.* 5 | 6 | /** 7 | * @author MAYCON CARDOSO on 2019-11-06. 8 | */ 9 | class RandomQuotationResponse( 10 | 11 | @SerializedName("quote_id") 12 | val id: String, 13 | 14 | @SerializedName("appeared_at") 15 | val date: Date, 16 | 17 | @SerializedName("tags") 18 | val tags: List, 19 | 20 | @SerializedName("value") 21 | val description: String, 22 | 23 | @SerializedName("_embedded") 24 | val embedded: RandomQuotationEmbeddedResponse 25 | 26 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/mctech/data/quotation/repository/QuotationRepository.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.data.quotation.repository 2 | 3 | import com.mctech.data.quotation.datasource.QuotationCacheDataSource 4 | import com.mctech.data.quotation.datasource.QuotationDataSource 5 | import com.mctech.domain.errors.QuotationException 6 | import com.mctech.domain.model.Quotation 7 | import com.mctech.domain.services.QuotationService 8 | import com.mctech.library.networking.NetworkError 9 | 10 | /** 11 | * @author MAYCON CARDOSO on 2019-09-30. 12 | */ 13 | class QuotationRepository( 14 | private val cacheDataSource: QuotationCacheDataSource, 15 | private val remoteDataSource: QuotationDataSource 16 | ) : QuotationService { 17 | 18 | override suspend fun getRandom(): Quotation { 19 | return try { 20 | remoteDataSource.getRandom() 21 | } catch (exception: Exception) { 22 | throw QuotationException.UnknownQuotationException 23 | } 24 | } 25 | 26 | override suspend fun getByTag(tag: String, page: Int?): List { 27 | return try { 28 | remoteDataSource.getByTag(tag, page).apply { 29 | cacheDataSource.saveByTag(tag, page) 30 | } 31 | } catch (exception: Exception) { 32 | if (exception is NetworkError) { 33 | return cacheDataSource.getByTag(tag, page) 34 | ?: throw QuotationException.UnknownQuotationException 35 | } 36 | throw QuotationException.UnknownQuotationException 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /data/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | data 3 | 4 | -------------------------------------------------------------------------------- /data/src/test/java/com/mctech/data/quotation/QuotationCacheDataSourceImplTest.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.data.quotation 2 | 3 | import org.junit.Test 4 | 5 | /** 6 | * @author MAYCON CARDOSO on 2019-09-30. 7 | */ 8 | class QuotationCacheDataSourceImplTest { 9 | 10 | @Test 11 | fun saveByTag() { 12 | } 13 | 14 | @Test 15 | fun getRandom() { 16 | } 17 | 18 | @Test 19 | fun getByTag() { 20 | } 21 | } -------------------------------------------------------------------------------- /data/src/test/java/com/mctech/data/quotation/QuotationRemoteDataSourceImplTest.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.data.quotation 2 | 3 | import org.junit.Test 4 | 5 | /** 6 | * @author MAYCON CARDOSO on 2019-09-30. 7 | */ 8 | class QuotationRemoteDataSourceImplTest { 9 | 10 | @Test 11 | fun getRandom() { 12 | } 13 | 14 | @Test 15 | fun getByTag() { 16 | } 17 | } -------------------------------------------------------------------------------- /data/src/test/java/com/mctech/data/quotation/QuotationRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.data.quotation 2 | 3 | import org.junit.Test 4 | 5 | /** 6 | * @author MAYCON CARDOSO on 2019-09-30. 7 | */ 8 | class QuotationRepositoryTest { 9 | 10 | @Test 11 | fun getRandom() { 12 | } 13 | 14 | @Test 15 | fun getByTag() { 16 | } 17 | } -------------------------------------------------------------------------------- /domain/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'kotlin' 2 | 3 | dependencies { 4 | implementation globalDependencies.kotlinStdLib 5 | implementation globalDependencies.kotlinCoroutinesCore 6 | 7 | testImplementation globalTestDependencies.jUnit 8 | testImplementation globalTestDependencies.assertJ 9 | 10 | testImplementation globalTestDependencies.jUnit 11 | testImplementation globalTestDependencies.assertJ 12 | testImplementation globalTestDependencies.coroutines 13 | testImplementation globalTestDependencies.mockitoKotlin 14 | testImplementation globalTestDependencies.testArqCor 15 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/mctech/domain/errors/AuthException.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.domain.errors 2 | 3 | sealed class AuthException : RuntimeException(){ 4 | object UserNotFoundException : AuthException() 5 | object EmptyFormValueException : AuthException() 6 | object WrongCredentialsException : AuthException() 7 | object NoAuthSessionFoundException : AuthException() 8 | object PasswordUnderSixCharactersException : AuthException() 9 | object PasswordsDoNotMatchException : AuthException() 10 | object UnknownAuthException : AuthException() 11 | 12 | object AlreadyRegisteredUserException : AuthException() 13 | object InvalidEmailFormatException : AuthException() 14 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/mctech/domain/errors/NetworkException.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.domain.errors 2 | 3 | class NetworkException : Exception("Algo inesperado aconteceu enquanto tentava conectar no servidor. Tente novamente em alguns segundos.") -------------------------------------------------------------------------------- /domain/src/main/java/com/mctech/domain/errors/QuotationException.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.domain.errors 2 | 3 | /** 4 | * @author MAYCON CARDOSO on 2019-09-30. 5 | */ 6 | sealed class QuotationException : RuntimeException() { 7 | object UnknownQuotationException : QuotationException() 8 | object ConnectionIssueException : QuotationException() 9 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/mctech/domain/interaction/Result.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.domain.interaction 2 | 3 | sealed class Result { 4 | data class Success(val result: T) : Result() 5 | data class Failure(val throwable: Throwable) : Result() 6 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/mctech/domain/interaction/auth/AuthenticationUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.domain.interaction.auth 2 | 3 | import com.mctech.domain.errors.AuthException 4 | import com.mctech.domain.interaction.Result 5 | import com.mctech.domain.model.AuthRequest 6 | import com.mctech.domain.model.User 7 | import com.mctech.domain.services.AuthService 8 | 9 | class AuthenticationUseCase(private val authService: AuthService) { 10 | suspend fun execute(authRequest: AuthRequest): Result { 11 | try { 12 | authRequest.validateOrThrow() 13 | 14 | if (authService.login(authRequest)) { 15 | return Result.Success(authService.fetchLoggedUser()!!) 16 | } 17 | } catch (exception: Throwable) { 18 | return Result.Failure( 19 | if (exception is AuthException) exception 20 | else AuthException.UnknownAuthException 21 | ) 22 | } 23 | 24 | return Result.Failure(AuthException.UnknownAuthException) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /domain/src/main/java/com/mctech/domain/interaction/auth/CheckAuthSessionUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.domain.interaction.auth 2 | 3 | import com.mctech.domain.interaction.Result 4 | import com.mctech.domain.services.AuthService 5 | 6 | class CheckAuthSessionUseCase(private val authService: AuthService) { 7 | suspend fun execute() = Result.Success(authService.fetchLoggedUser() != null) 8 | } 9 | -------------------------------------------------------------------------------- /domain/src/main/java/com/mctech/domain/interaction/auth/RegisterUserUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.domain.interaction.auth 2 | 3 | import com.mctech.domain.errors.AuthException 4 | import com.mctech.domain.interaction.Result 5 | import com.mctech.domain.model.RegisterUser 6 | import com.mctech.domain.model.User 7 | import com.mctech.domain.services.AuthService 8 | 9 | class RegisterUserUseCase(private val authService: AuthService) { 10 | suspend fun execute(registerUser: RegisterUser): Result { 11 | try { 12 | registerUser.validateOrThrow() 13 | 14 | // Register user 15 | if (authService.registerUser(registerUser)) { 16 | return Result.Success(authService.fetchLoggedUser()!!) 17 | } 18 | } catch (exception: Throwable) { 19 | return Result.Failure( 20 | if (exception is AuthException) exception 21 | else AuthException.UnknownAuthException 22 | ) 23 | } 24 | 25 | return Result.Failure(AuthException.UnknownAuthException) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /domain/src/main/java/com/mctech/domain/interaction/quotation/GetRandomQuotationCase.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.domain.interaction.quotation 2 | 3 | import com.mctech.domain.errors.QuotationException 4 | import com.mctech.domain.interaction.Result 5 | import com.mctech.domain.model.Quotation 6 | import com.mctech.domain.services.QuotationService 7 | 8 | /** 9 | * @author MAYCON CARDOSO on 2019-09-30. 10 | */ 11 | class GetRandomQuotationCase(private val quotationService: QuotationService) { 12 | suspend fun execute(): Result { 13 | return try { 14 | Result.Success(quotationService.getRandom()) 15 | } catch (exception: Exception) { 16 | Result.Failure( 17 | if (exception is QuotationException) exception 18 | else QuotationException.UnknownQuotationException 19 | ) 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/mctech/domain/model/AuthRequest.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.domain.model 2 | 3 | import com.mctech.domain.errors.AuthException 4 | import com.mctech.domain.validation.EmailValidator 5 | 6 | /** 7 | * @author MAYCON CARDOSO on 2019-07-25. 8 | */ 9 | data class AuthRequest( 10 | val type: AuthRequestType = AuthRequestType.EMAIL, 11 | val email: String, 12 | var password: String 13 | ) { 14 | fun validateOrThrow() { 15 | if (!EmailValidator(email)) 16 | throw AuthException.InvalidEmailFormatException 17 | } 18 | } 19 | 20 | enum class AuthRequestType { 21 | EMAIL 22 | } 23 | 24 | -------------------------------------------------------------------------------- /domain/src/main/java/com/mctech/domain/model/Quotation.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.domain.model 2 | 3 | import java.util.* 4 | 5 | /** 6 | * @author MAYCON CARDOSO on 2019-09-30. 7 | */ 8 | data class Quotation( 9 | val id : String, 10 | val description: String, 11 | val date: Date, 12 | val tag: List, 13 | val author: String, 14 | val twitterLink: String 15 | ) -------------------------------------------------------------------------------- /domain/src/main/java/com/mctech/domain/model/RegisterUser.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.domain.model 2 | 3 | import com.mctech.domain.errors.AuthException 4 | import com.mctech.domain.validation.EmailValidator 5 | import com.mctech.domain.validation.PasswordlValidator 6 | 7 | /** 8 | * @author MAYCON CARDOSO on 2019-07-25. 9 | */ 10 | data class RegisterUser( 11 | val user: User, 12 | val password: String, 13 | val passwordConfirmation : String 14 | ) { 15 | fun validateOrThrow() { 16 | if (user.name.isEmpty()) 17 | throw AuthException.EmptyFormValueException 18 | 19 | if (!EmailValidator(user.email)) 20 | throw AuthException.InvalidEmailFormatException 21 | 22 | if (!PasswordlValidator(password)) 23 | throw AuthException.PasswordUnderSixCharactersException 24 | 25 | if (password != passwordConfirmation) 26 | throw AuthException.PasswordsDoNotMatchException 27 | } 28 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/mctech/domain/model/User.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.domain.model 2 | 3 | /** 4 | * @author MAYCON CARDOSO on 2019-07-25. 5 | */ 6 | data class User( 7 | var id: String? = "", 8 | val name: String, 9 | val email: String?, 10 | var profilePicture: String? = "" 11 | ) -------------------------------------------------------------------------------- /domain/src/main/java/com/mctech/domain/services/AuthService.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.domain.services 2 | 3 | import com.mctech.domain.model.AuthRequest 4 | import com.mctech.domain.model.RegisterUser 5 | import com.mctech.domain.model.User 6 | 7 | /** 8 | * @author MAYCON CARDOSO on 2019-07-25. 9 | */ 10 | interface AuthService { 11 | suspend fun fetchLoggedUser(): User? 12 | suspend fun registerUser(registerUser: RegisterUser) : Boolean 13 | suspend fun login(user: AuthRequest) : Boolean 14 | suspend fun logout() 15 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/mctech/domain/services/QuotationService.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.domain.services 2 | 3 | import com.mctech.domain.model.Quotation 4 | 5 | /** 6 | * @author MAYCON CARDOSO on 2019-09-30. 7 | */ 8 | interface QuotationService { 9 | suspend fun getRandom(): Quotation 10 | suspend fun getByTag(tag: String, page : Int?): List 11 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/mctech/domain/validation/EmailValidator.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.domain.validation 2 | 3 | import java.util.regex.Pattern 4 | 5 | 6 | object EmailValidator{ 7 | private val pattern = Pattern.compile( 8 | "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$" 9 | ) 10 | 11 | operator fun invoke(email : String?) = email?.let { 12 | return pattern.matcher(email).matches() 13 | } ?: false 14 | 15 | 16 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/mctech/domain/validation/PasswordlValidator.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.domain.validation 2 | 3 | 4 | object PasswordlValidator{ 5 | operator fun invoke(password : String?) = password?.let { 6 | password.length >= 6 7 | } ?: false 8 | } -------------------------------------------------------------------------------- /domain/src/test/kotlin/com/mctech/domain/TestDataFactory.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.domain 2 | 3 | import com.mctech.domain.model.AuthRequest 4 | import com.mctech.domain.model.Quotation 5 | import com.mctech.domain.model.RegisterUser 6 | import com.mctech.domain.model.User 7 | import java.util.* 8 | 9 | object TestDataFactory{ 10 | fun createAuthRequest(email: String) = AuthRequest( 11 | email = email, 12 | password = "" 13 | ) 14 | 15 | fun createRegisterUserRequest( 16 | email: String = "", 17 | name : String = "", 18 | password : String = "", 19 | passwordConfirmation : String = "" 20 | ) = RegisterUser( 21 | user = User( 22 | name = name, 23 | email = email 24 | ), 25 | password = password, 26 | passwordConfirmation = passwordConfirmation 27 | ) 28 | 29 | fun createQuotation( 30 | id : String = "", 31 | tag : String = "", 32 | description: String = "", 33 | date : Date = Date(), 34 | author : String = "", 35 | twitterLink : String = "" 36 | ) = Quotation( 37 | id = id, 38 | description = description, 39 | date = date, 40 | author = author, 41 | twitterLink = twitterLink, 42 | tag = listOf(tag) 43 | ) 44 | } -------------------------------------------------------------------------------- /domain/src/test/kotlin/com/mctech/domain/interaction/auth/AuthenticationUseCaseTest.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.domain.interaction.auth 2 | 3 | import com.mctech.domain.errors.AuthException 4 | import com.mctech.domain.interaction.Result 5 | import com.mctech.domain.model.AuthRequest 6 | import com.mctech.domain.model.User 7 | import com.mctech.domain.services.AuthService 8 | import com.nhaarman.mockitokotlin2.any 9 | import com.nhaarman.mockitokotlin2.mock 10 | import com.nhaarman.mockitokotlin2.verify 11 | import com.nhaarman.mockitokotlin2.whenever 12 | import kotlinx.coroutines.ExperimentalCoroutinesApi 13 | import kotlinx.coroutines.test.runBlockingTest 14 | import org.assertj.core.api.Assertions.assertThat 15 | import org.junit.Before 16 | import org.junit.Test 17 | 18 | /** 19 | * @author MAYCON CARDOSO on 2019-09-22. 20 | */ 21 | @ExperimentalCoroutinesApi 22 | class AuthenticationUseCaseTest { 23 | private val authService = mock() 24 | private val authRequest = mock() 25 | private val expectedUser = User(name = "", email = "") 26 | 27 | private lateinit var authSessionUseCase: AuthenticationUseCase 28 | 29 | @Before 30 | fun `before each test`() { 31 | authSessionUseCase = AuthenticationUseCase(authService) 32 | } 33 | 34 | @Test 35 | fun `should call request validation`() = runBlockingTest { 36 | authSessionUseCase.execute(authRequest) 37 | verify(authRequest).validateOrThrow() 38 | } 39 | 40 | @Test 41 | fun `should sign in`() = runBlockingTest { 42 | whenever(authService.login(authRequest)).thenReturn(true) 43 | whenever(authService.fetchLoggedUser()).thenReturn(expectedUser) 44 | 45 | val result = authSessionUseCase.execute(authRequest) 46 | val resultUser = (result as Result.Success).result 47 | 48 | assertThat(result).isInstanceOf(Result.Success::class.java) 49 | assertThat(resultUser).isEqualTo(expectedUser) 50 | } 51 | 52 | @Test 53 | fun `should fail throwing auth exception`() = runBlockingTest { 54 | whenever(authService.login(any())).thenThrow(AuthException.WrongCredentialsException) 55 | 56 | val result = authSessionUseCase.execute(authRequest) 57 | val resultException = (result as Result.Failure).throwable 58 | 59 | assertThat(result).isInstanceOf(Result.Failure::class.java) 60 | assertThat(resultException).isEqualTo(AuthException.WrongCredentialsException) 61 | } 62 | 63 | @Test 64 | fun `should fail with unknown exception when any unknown problem happen`() = runBlockingTest { 65 | whenever(authService.login(any())).thenReturn(false) 66 | whenever(authService.fetchLoggedUser()).thenReturn(expectedUser) 67 | 68 | val result = authSessionUseCase.execute(authRequest) 69 | val resultException = (result as Result.Failure).throwable 70 | 71 | assertThat(result).isInstanceOf(Result.Failure::class.java) 72 | assertThat(resultException).isEqualTo(AuthException.UnknownAuthException) 73 | } 74 | 75 | @Test 76 | fun `should fail mapping unknown exception`() = runBlockingTest { 77 | whenever(authService.login(any())).thenThrow(IllegalArgumentException()) 78 | 79 | val result = authSessionUseCase.execute(authRequest) 80 | val resultException = (result as Result.Failure).throwable 81 | 82 | assertThat(result).isInstanceOf(Result.Failure::class.java) 83 | assertThat(resultException).isEqualTo(AuthException.UnknownAuthException) 84 | } 85 | } -------------------------------------------------------------------------------- /domain/src/test/kotlin/com/mctech/domain/interaction/auth/CheckAuthSessionUseCaseTest.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.domain.interaction.auth 2 | 3 | import com.mctech.domain.interaction.Result 4 | import com.mctech.domain.model.User 5 | import com.mctech.domain.services.AuthService 6 | import com.nhaarman.mockitokotlin2.mock 7 | import com.nhaarman.mockitokotlin2.whenever 8 | import kotlinx.coroutines.test.runBlockingTest 9 | import org.assertj.core.api.Assertions.assertThat 10 | import org.junit.Before 11 | import org.junit.Test 12 | 13 | /** 14 | * @author MAYCON CARDOSO on 2019-09-22. 15 | */ 16 | class CheckAuthSessionUseCaseTest { 17 | private val authService = mock() 18 | private lateinit var checkAuthSessionUseCase: CheckAuthSessionUseCase 19 | 20 | @Before 21 | fun `before each test`() { 22 | checkAuthSessionUseCase = CheckAuthSessionUseCase(authService) 23 | } 24 | 25 | @Test 26 | fun `should return true`() = runBlockingTest { 27 | whenever(authService.fetchLoggedUser()).thenReturn( 28 | User( 29 | name = "Teste", 30 | email = "Teste" 31 | ) 32 | ) 33 | performAssertion( 34 | result = checkAuthSessionUseCase.execute(), 35 | expectedValue = true 36 | ) 37 | } 38 | 39 | @Test 40 | fun `should return false`() = runBlockingTest { 41 | whenever(authService.fetchLoggedUser()).thenReturn(null) 42 | performAssertion( 43 | result = checkAuthSessionUseCase.execute(), 44 | expectedValue = false 45 | ) 46 | } 47 | 48 | private fun performAssertion(result: Result, expectedValue: Boolean) { 49 | assertThat(result) 50 | .isExactlyInstanceOf(Result.Success::class.java) 51 | .isEqualTo(Result.Success(expectedValue)) 52 | } 53 | } -------------------------------------------------------------------------------- /domain/src/test/kotlin/com/mctech/domain/interaction/auth/RegisterUserUseCaseTest.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.domain.interaction.auth 2 | 3 | import com.mctech.domain.errors.AuthException 4 | import com.mctech.domain.interaction.Result 5 | import com.mctech.domain.model.RegisterUser 6 | import com.mctech.domain.model.User 7 | import com.mctech.domain.services.AuthService 8 | import com.nhaarman.mockitokotlin2.mock 9 | import com.nhaarman.mockitokotlin2.verify 10 | import com.nhaarman.mockitokotlin2.whenever 11 | import kotlinx.coroutines.test.runBlockingTest 12 | import org.assertj.core.api.Assertions 13 | import org.junit.Before 14 | import org.junit.Test 15 | 16 | /** 17 | * @author MAYCON CARDOSO on 2019-09-24. 18 | */ 19 | class RegisterUserUseCaseTest { 20 | private val authService = mock() 21 | private val registerUserRequest = mock() 22 | private val expectedUser = User(name = "", email = "") 23 | 24 | private lateinit var useCase: RegisterUserUseCase 25 | 26 | @Before 27 | fun `before each test`() { 28 | useCase = RegisterUserUseCase(authService) 29 | } 30 | 31 | @Test 32 | fun `should call request validation`() = runBlockingTest { 33 | useCase.execute(registerUserRequest) 34 | verify(registerUserRequest).validateOrThrow() 35 | } 36 | 37 | @Test 38 | fun `should sign up`() = runBlockingTest { 39 | whenever(authService.registerUser(registerUserRequest)).thenReturn(true) 40 | whenever(authService.fetchLoggedUser()).thenReturn(expectedUser) 41 | 42 | val result = useCase.execute(registerUserRequest) 43 | val resultUser = (result as Result.Success).result 44 | 45 | Assertions.assertThat(result).isInstanceOf(Result.Success::class.java) 46 | Assertions.assertThat(resultUser).isEqualTo(expectedUser) 47 | } 48 | 49 | 50 | @Test 51 | fun `should fail throwing auth exception`() = runBlockingTest { 52 | whenever(authService.registerUser(registerUserRequest)).thenThrow(AuthException.WrongCredentialsException) 53 | 54 | val result = useCase.execute(registerUserRequest) 55 | val resultException = (result as Result.Failure).throwable 56 | 57 | Assertions.assertThat(result).isInstanceOf(Result.Failure::class.java) 58 | Assertions.assertThat(resultException).isEqualTo(AuthException.WrongCredentialsException) 59 | } 60 | 61 | @Test 62 | fun `should fail with unknown exception when any unknown problem happen`() = runBlockingTest { 63 | whenever(authService.registerUser(registerUserRequest)).thenReturn(false) 64 | whenever(authService.fetchLoggedUser()).thenReturn(expectedUser) 65 | 66 | val result = useCase.execute(registerUserRequest) 67 | val resultException = (result as Result.Failure).throwable 68 | 69 | Assertions.assertThat(result).isInstanceOf(Result.Failure::class.java) 70 | Assertions.assertThat(resultException).isEqualTo(AuthException.UnknownAuthException) 71 | } 72 | 73 | @Test 74 | fun `should fail mapping unknown exception`() = runBlockingTest { 75 | whenever(authService.registerUser(registerUserRequest)).thenThrow(IllegalArgumentException()) 76 | 77 | val result = useCase.execute(registerUserRequest) 78 | val resultException = (result as Result.Failure).throwable 79 | 80 | Assertions.assertThat(result).isInstanceOf(Result.Failure::class.java) 81 | Assertions.assertThat(resultException).isEqualTo(AuthException.UnknownAuthException) 82 | } 83 | 84 | } -------------------------------------------------------------------------------- /domain/src/test/kotlin/com/mctech/domain/interaction/quotation/GetRandomQuotationCaseTest.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.domain.interaction.quotation 2 | 3 | import com.mctech.domain.TestDataFactory 4 | import com.mctech.domain.errors.QuotationException 5 | import com.mctech.domain.interaction.Result 6 | import com.mctech.domain.services.QuotationService 7 | import com.nhaarman.mockitokotlin2.mock 8 | import com.nhaarman.mockitokotlin2.whenever 9 | import kotlinx.coroutines.test.runBlockingTest 10 | import org.assertj.core.api.Assertions.assertThat 11 | import org.junit.Before 12 | import org.junit.Test 13 | 14 | /** 15 | * @author MAYCON CARDOSO on 2019-09-30. 16 | */ 17 | class GetRandomQuotationCaseTest { 18 | private val service = mock() 19 | private lateinit var getRandomCase: GetRandomQuotationCase 20 | 21 | @Before 22 | fun `before each test`() { 23 | getRandomCase = GetRandomQuotationCase(service) 24 | } 25 | 26 | @Test 27 | fun `should return quotation`() = runBlockingTest { 28 | val expectedValue = TestDataFactory.createQuotation() 29 | 30 | whenever(service.getRandom()).thenReturn(expectedValue) 31 | 32 | val result = getRandomCase.execute() 33 | 34 | assertThat(result) 35 | .isExactlyInstanceOf(Result.Success::class.java) 36 | .isEqualTo(Result.Success(expectedValue)) 37 | } 38 | 39 | @Test 40 | fun `should return known exception`() = failureAssertion( 41 | exception = QuotationException.ConnectionIssueException, 42 | expectedException = QuotationException.ConnectionIssueException 43 | ) 44 | 45 | @Test 46 | fun `should return unknown exception`() = failureAssertion( 47 | exception = RuntimeException(), 48 | expectedException = QuotationException.UnknownQuotationException 49 | ) 50 | 51 | private fun failureAssertion(exception: Exception, expectedException: Exception) = runBlockingTest { 52 | whenever(service.getRandom()).thenThrow(exception) 53 | 54 | val result = getRandomCase.execute() 55 | val resultException = (result as Result.Failure).throwable 56 | 57 | assertThat(result).isInstanceOf(Result.Failure::class.java) 58 | assertThat(resultException).isEqualTo(expectedException) 59 | } 60 | } -------------------------------------------------------------------------------- /domain/src/test/kotlin/com/mctech/domain/model/AuthRequestTest.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.domain.model 2 | 3 | import com.mctech.domain.TestDataFactory 4 | import com.mctech.domain.errors.AuthException 5 | import org.assertj.core.api.Assertions 6 | import org.junit.Test 7 | 8 | /** 9 | * @author MAYCON CARDOSO on 2019-09-21. 10 | */ 11 | class AuthRequestTest { 12 | @Test 13 | fun `should throw when email fail`() { 14 | val request = TestDataFactory.createAuthRequest("") 15 | Assertions.assertThatThrownBy { request.validateOrThrow() } 16 | .isEqualTo( 17 | AuthException.InvalidEmailFormatException 18 | ) 19 | } 20 | 21 | @Test 22 | fun `should validate`() { 23 | val request = TestDataFactory.createAuthRequest("maycon.santos.cardoso@gmail.com") 24 | request.validateOrThrow() 25 | } 26 | } -------------------------------------------------------------------------------- /domain/src/test/kotlin/com/mctech/domain/model/RegisterUserTest.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.domain.model 2 | 3 | import com.mctech.domain.TestDataFactory 4 | import com.mctech.domain.errors.AuthException 5 | import org.assertj.core.api.Assertions.assertThatThrownBy 6 | import org.junit.Test 7 | 8 | /** 9 | * @author MAYCON CARDOSO on 2019-09-21. 10 | */ 11 | class RegisterUserTest{ 12 | companion object{ 13 | const val EMAIL = "maycon.santos.cardoso@gmail.com" 14 | const val NAME = "Maycon dos Santos Cardoso" 15 | const val PASSWORD = "123456789" 16 | } 17 | 18 | @Test 19 | fun `should throw when name is empty`() { 20 | val request = TestDataFactory.createRegisterUserRequest( 21 | email = EMAIL, 22 | password = PASSWORD, 23 | passwordConfirmation = PASSWORD 24 | ) 25 | 26 | assertThatThrownBy { request.validateOrThrow() } 27 | .isEqualTo( 28 | AuthException.EmptyFormValueException 29 | ) 30 | } 31 | 32 | @Test 33 | fun `should throw when email fail`() { 34 | val request = TestDataFactory.createRegisterUserRequest( 35 | name = NAME, 36 | password = PASSWORD, 37 | passwordConfirmation = PASSWORD 38 | ) 39 | 40 | assertThatThrownBy { request.validateOrThrow() } 41 | .isEqualTo( 42 | AuthException.InvalidEmailFormatException 43 | ) 44 | } 45 | 46 | @Test 47 | fun `should throw when password fail`() { 48 | val request = TestDataFactory.createRegisterUserRequest( 49 | name = NAME, 50 | email = EMAIL 51 | ) 52 | 53 | assertThatThrownBy { request.validateOrThrow() } 54 | .isEqualTo( 55 | AuthException.PasswordUnderSixCharactersException 56 | ) 57 | } 58 | 59 | @Test 60 | fun `should throw when passwords do not match`() { 61 | val request = TestDataFactory.createRegisterUserRequest( 62 | name = NAME, 63 | password = "123456", 64 | passwordConfirmation = "654321", 65 | email = EMAIL 66 | ) 67 | 68 | assertThatThrownBy { request.validateOrThrow() } 69 | .isEqualTo( 70 | AuthException.PasswordsDoNotMatchException 71 | ) 72 | } 73 | 74 | @Test 75 | fun `should validate`() { 76 | val request = TestDataFactory.createRegisterUserRequest( 77 | email = EMAIL, 78 | name = NAME, 79 | password = PASSWORD, 80 | passwordConfirmation = PASSWORD 81 | ) 82 | request.validateOrThrow() 83 | } 84 | } -------------------------------------------------------------------------------- /domain/src/test/kotlin/com/mctech/domain/validation/EmailValidatorTest.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.domain.validation 2 | 3 | import org.junit.Assert.assertFalse 4 | import org.junit.Assert.assertTrue 5 | import org.junit.Test 6 | 7 | /** 8 | * @author MAYCON CARDOSO on 2019-09-21. 9 | */ 10 | class EmailValidatorTest { 11 | 12 | @Test 13 | fun `should validate`() { 14 | assertTrue(EmailValidator("maycon.santos.cardoso@gmail.com")) 15 | assertTrue(EmailValidator("maycon.santos.cardoso@gmail.com.br")) 16 | assertTrue(EmailValidator("maycon.1994_teste@gmail.com.br")) 17 | } 18 | 19 | @Test 20 | fun `should fail when email empty`() { 21 | assertFalse(EmailValidator("")) 22 | } 23 | 24 | @Test 25 | fun `should fail when email null`() { 26 | assertFalse(EmailValidator(null)) 27 | } 28 | 29 | @Test 30 | fun `should fail when email without at`() { 31 | assertFalse(EmailValidator("teste.teste.com.br")) 32 | assertFalse(EmailValidator("testetesteteste")) 33 | } 34 | 35 | @Test 36 | fun `should fail when email double at`() { 37 | assertFalse(EmailValidator("teste@@teste.com")) 38 | assertFalse(EmailValidator("teste@teste@teste.com")) 39 | } 40 | 41 | @Test 42 | fun `should fail when email double dot`() { 43 | assertFalse(EmailValidator("teste@teste..com")) 44 | } 45 | @Test 46 | fun `should fail when email dot in the end`() { 47 | assertFalse(EmailValidator("teste@teste.com.")) 48 | } 49 | } -------------------------------------------------------------------------------- /domain/src/test/kotlin/com/mctech/domain/validation/PasswordlValidatorTest.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.domain.validation 2 | 3 | import org.junit.Assert.assertFalse 4 | import org.junit.Assert.assertTrue 5 | import org.junit.Test 6 | 7 | /** 8 | * @author MAYCON CARDOSO on 2019-09-21. 9 | */ 10 | class PasswordlValidatorTest { 11 | 12 | @Test 13 | fun `should validate`() { 14 | assertTrue(PasswordlValidator("dsa@asio@90190")) 15 | assertTrue(PasswordlValidator("123456")) 16 | assertTrue(PasswordlValidator("1234567")) 17 | } 18 | 19 | @Test 20 | fun `should fail when password under 5 characters`() { 21 | assertFalse(PasswordlValidator("1")) 22 | assertFalse(PasswordlValidator("12")) 23 | assertFalse(PasswordlValidator("123")) 24 | assertFalse(PasswordlValidator("1234")) 25 | } 26 | } -------------------------------------------------------------------------------- /domain/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker: -------------------------------------------------------------------------------- 1 | mock-maker-inline -------------------------------------------------------------------------------- /features/feature-login/README.md: -------------------------------------------------------------------------------- 1 | ## Login 2 | 3 | This is a library to manage the application authentication overall. 4 | -------------------------------------------------------------------------------- /features/feature-login/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | dependencies { 6 | implementation project(path: submodulesPlatform.domain) 7 | implementation project(path: submodulesFeatures.navigation) 8 | implementation project(path: submodulesLibraries.sharedFeatureArq) 9 | implementation project(path: submodulesLibraries.appTheme) 10 | 11 | implementation globalDependencies.koin 12 | implementation globalDependencies.koinViewModel 13 | 14 | implementation globalDependencies.kotlinCoroutinesCore 15 | implementation globalDependencies.kotlinCoroutinesAndroid 16 | 17 | implementation globalDependencies.lifeCycleLiveRuntime 18 | implementation globalDependencies.lifeCycleLiveExtentions 19 | implementation globalDependencies.lifeCycleViewModel 20 | implementation globalDependencies.lifeCycleLiveData 21 | 22 | implementation globalDependencies.navigationFragment 23 | implementation globalDependencies.navigationFragmentUi 24 | 25 | implementation globalDependencies.appCompact 26 | implementation globalDependencies.constraintlayout 27 | implementation globalDependencies.materialDesign 28 | 29 | 30 | testImplementation project(path: submodulesTest.sharedFeatureArq) 31 | } -------------------------------------------------------------------------------- /features/feature-login/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /features/feature-login/src/main/java/com/mctech/features/login/LoginActivity.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.features.login 2 | 3 | import android.os.Bundle 4 | import com.mctech.feature.arq.BaseActivity 5 | import com.mctech.feature.arq.extentions.bindData 6 | import com.mctech.features.login.state.LoginState 7 | import com.mctech.features.navigation.Screen 8 | import org.koin.androidx.viewmodel.ext.android.viewModel 9 | 10 | class LoginActivity : BaseActivity() { 11 | private val loginViewModel: LoginViewModel by viewModel() 12 | 13 | override fun onCreate(savedInstanceState: Bundle?) { 14 | super.onCreate(savedInstanceState) 15 | setContentView(R.layout.activity_login) 16 | 17 | bindData(loginViewModel.loginScreenState) { 18 | when (it) { 19 | is LoginState.Authenticated -> navigator.navigateTo( 20 | destination = Screen.Dashboard, 21 | finishHost = true 22 | ) 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /features/feature-login/src/main/java/com/mctech/features/login/LoginSignInFragment.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.features.login 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.lifecycle.lifecycleScope 6 | import androidx.navigation.fragment.findNavController 7 | import com.mctech.domain.model.AuthRequest 8 | import com.mctech.feature.arq.BaseFragment 9 | import com.mctech.feature.arq.extentions.* 10 | import com.mctech.features.login.interaction.LoginUserInteraction 11 | import com.mctech.features.login.state.LoginState 12 | import com.mctech.features.login.state.toStateResource 13 | import kotlinx.android.synthetic.main.fragment_sign_in.* 14 | import kotlinx.coroutines.launch 15 | import org.koin.androidx.viewmodel.ext.android.sharedViewModel 16 | 17 | class LoginSignInFragment : BaseFragment() { 18 | private val loginViewModel: LoginViewModel by sharedViewModel() 19 | 20 | override fun getLayoutId() = R.layout.fragment_sign_in 21 | 22 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 23 | super.onViewCreated(view, savedInstanceState) 24 | 25 | bindData(loginViewModel.loginScreenState) { renderUi(it) } 26 | 27 | btSignIn?.setOnClickListener { tryLogin() } 28 | btSignUp?.setOnClickListener { navigateToSignUp() } 29 | } 30 | 31 | private fun tryLogin() { 32 | lifecycleScope.launch { 33 | loginViewModel.suspendedInteraction( 34 | LoginUserInteraction.TryLogin(createAuthRequest()) 35 | ) 36 | } 37 | } 38 | 39 | private fun navigateToSignUp() { 40 | loginViewModel.interact( 41 | LoginUserInteraction.NavigateToSignUn(createAuthRequest()) 42 | ) 43 | findNavController().navigate(R.id.action_loginFormFragment_to_loginSignUpFragment) 44 | } 45 | 46 | private fun createAuthRequest() = AuthRequest( 47 | email = etUsername.getValue(), 48 | password = etPassword.getValue() 49 | ) 50 | 51 | private fun renderUi(loginState: LoginState) { 52 | when (loginState) { 53 | is LoginState.Loading -> { 54 | switchState(isLoading = true) 55 | } 56 | is LoginState.Unauthenticated -> { 57 | switchState(isLoading = false) 58 | } 59 | is LoginState.Error -> { 60 | switchState(isLoading = false) 61 | toast(loginState.toStateResource().message) 62 | } 63 | } 64 | } 65 | 66 | private fun setFormState(enabled: Boolean) { 67 | etUsername.enableByState(enabled) 68 | etPassword.enableByState(enabled) 69 | btSignIn.setVisibilityByState(enabled) 70 | btSignUp.setVisibilityByState(enabled) 71 | } 72 | 73 | private fun setProgressState(showing: Boolean) { 74 | loadingProgress.setVisibilityByState(showing) 75 | } 76 | 77 | private fun switchState(isLoading: Boolean) { 78 | setProgressState(isLoading) 79 | setFormState(!isLoading) 80 | } 81 | } -------------------------------------------------------------------------------- /features/feature-login/src/main/java/com/mctech/features/login/LoginSignUpFragment.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.features.login 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.lifecycle.lifecycleScope 6 | import com.mctech.domain.model.AuthRequest 7 | import com.mctech.domain.model.RegisterUser 8 | import com.mctech.domain.model.User 9 | import com.mctech.feature.arq.BaseFragment 10 | import com.mctech.feature.arq.extentions.* 11 | import com.mctech.features.login.interaction.LoginUserInteraction 12 | import com.mctech.features.login.state.LoginState 13 | import com.mctech.features.login.state.toStateResource 14 | import kotlinx.android.synthetic.main.fragment_sign_up.* 15 | import kotlinx.coroutines.launch 16 | import org.koin.androidx.viewmodel.ext.android.sharedViewModel 17 | 18 | class LoginSignUpFragment : BaseFragment() { 19 | private val loginViewModel: LoginViewModel by sharedViewModel() 20 | 21 | override fun getLayoutId() = R.layout.fragment_sign_up 22 | 23 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 24 | super.onViewCreated(view, savedInstanceState) 25 | 26 | bindData(loginViewModel.loginScreenState) { renderUi(it) } 27 | bindData(loginViewModel.lastAuthRequest) { autoFillUpForm(it) } 28 | btSignUp?.setOnClickListener { tryRegisterUser() } 29 | } 30 | 31 | private fun renderUi(loginState: LoginState) { 32 | when (loginState) { 33 | is LoginState.Unauthenticated -> { 34 | switchState(isLoading = false) 35 | } 36 | is LoginState.Loading -> { 37 | switchState(isLoading = true) 38 | } 39 | is LoginState.Error -> { 40 | switchState(isLoading = false) 41 | toast(loginState.toStateResource().message) 42 | } 43 | } 44 | } 45 | 46 | private fun tryRegisterUser() { 47 | lifecycleScope.launch { 48 | loginViewModel.suspendedInteraction( 49 | LoginUserInteraction.TryRegisterUser( 50 | RegisterUser( 51 | user = User( 52 | name = etName.getValue(), 53 | email = etUsername.getValue() 54 | ), 55 | password = etPassword.getValue(), 56 | passwordConfirmation = etConfirmPassword.getValue() 57 | ) 58 | ) 59 | ) 60 | } 61 | } 62 | 63 | private fun autoFillUpForm(request: AuthRequest) { 64 | etUsername.setText(request.email) 65 | etPassword.setText(request.password) 66 | etConfirmPassword.setText(request.password) 67 | } 68 | 69 | private fun setFormState(enabled: Boolean) { 70 | etName.enableByState(enabled) 71 | etUsername.enableByState(enabled) 72 | etPassword.enableByState(enabled) 73 | etConfirmPassword.enableByState(enabled) 74 | btSignUp.setVisibilityByState(enabled) 75 | } 76 | 77 | private fun setProgressState(showing: Boolean) { 78 | loadingProgress.setVisibilityByState(showing) 79 | } 80 | 81 | private fun switchState(isLoading: Boolean) { 82 | setProgressState(isLoading) 83 | setFormState(!isLoading) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /features/feature-login/src/main/java/com/mctech/features/login/LoginViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.features.login 2 | 3 | import androidx.annotation.MainThread 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.MutableLiveData 6 | import com.mctech.domain.errors.AuthException 7 | import com.mctech.domain.interaction.Result 8 | import com.mctech.domain.interaction.auth.AuthenticationUseCase 9 | import com.mctech.domain.interaction.auth.RegisterUserUseCase 10 | import com.mctech.domain.model.AuthRequest 11 | import com.mctech.domain.model.RegisterUser 12 | import com.mctech.domain.model.User 13 | import com.mctech.feature.arq.BaseViewModel 14 | import com.mctech.feature.arq.UserInteraction 15 | import com.mctech.features.login.interaction.LoginUserInteraction 16 | import com.mctech.features.login.state.LoginState 17 | 18 | class LoginViewModel( 19 | private val authenticationUseCase: AuthenticationUseCase, 20 | private val registerUserUseCase: RegisterUserUseCase 21 | ) : BaseViewModel() { 22 | private val _loginScreenState = MutableLiveData(LoginState.Unauthenticated) 23 | val loginScreenState: LiveData get() = _loginScreenState 24 | 25 | private val _lastAuthRequest = MutableLiveData() 26 | val lastAuthRequest: LiveData get() = _lastAuthRequest 27 | 28 | override suspend fun handleUserInteraction(interaction: UserInteraction) { 29 | when(interaction){ 30 | is LoginUserInteraction.NavigateToSignUn -> navigationToSignUp(interaction.authRequest) 31 | is LoginUserInteraction.TryLogin -> doLogin(interaction.authRequest) 32 | is LoginUserInteraction.TryRegisterUser -> registerUser(interaction.registerUser) 33 | } 34 | } 35 | 36 | 37 | @MainThread 38 | private suspend fun registerUser(registerUser: RegisterUser) = executeAuthInteraction { 39 | registerUserUseCase.execute(registerUser) 40 | } 41 | 42 | @MainThread 43 | private suspend fun doLogin(authRequest: AuthRequest) = executeAuthInteraction { 44 | authenticationUseCase.execute(authRequest) 45 | } 46 | 47 | @MainThread 48 | private fun navigationToSignUp(authRequest: AuthRequest) { 49 | _lastAuthRequest.value = authRequest 50 | _loginScreenState.value = LoginState.Unauthenticated 51 | } 52 | 53 | @MainThread 54 | private suspend fun executeAuthInteraction(block: suspend () -> Result) { 55 | _loginScreenState.value = LoginState.Loading 56 | 57 | when (val authResult = block.invoke()) { 58 | is Result.Success<*> -> _loginScreenState.value = LoginState.Authenticated 59 | is Result.Failure -> _loginScreenState.value = LoginState.Error( 60 | error = authResult.throwable as AuthException 61 | ) 62 | } 63 | 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /features/feature-login/src/main/java/com/mctech/features/login/di/loginModule.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.features.login.di 2 | 3 | import com.mctech.features.login.LoginViewModel 4 | import org.koin.androidx.viewmodel.dsl.viewModel 5 | import org.koin.dsl.module 6 | 7 | val loginModule = module { 8 | viewModel { 9 | LoginViewModel( 10 | get(), 11 | get() 12 | ) 13 | } 14 | } -------------------------------------------------------------------------------- /features/feature-login/src/main/java/com/mctech/features/login/interaction/LoginUserInteraction.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.features.login.interaction 2 | 3 | import com.mctech.domain.model.AuthRequest 4 | import com.mctech.domain.model.RegisterUser 5 | import com.mctech.feature.arq.UserInteraction 6 | 7 | sealed class LoginUserInteraction : UserInteraction { 8 | data class NavigateToSignUn(val authRequest: AuthRequest) : LoginUserInteraction() 9 | data class TryLogin(val authRequest: AuthRequest) : LoginUserInteraction() 10 | data class TryRegisterUser(val registerUser: RegisterUser) : LoginUserInteraction() 11 | } -------------------------------------------------------------------------------- /features/feature-login/src/main/java/com/mctech/features/login/state/LoginErrorStateResources.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.features.login.state 2 | 3 | import com.mctech.domain.errors.AuthException 4 | import com.mctech.features.login.R 5 | 6 | data class LoginErrorStateResources(val message: Int) { 7 | companion object { 8 | operator fun invoke(error: AuthException) = 9 | when (error) { 10 | is AuthException.EmptyFormValueException -> LoginErrorStateResources( 11 | R.string.auth_form_empty 12 | ) 13 | is AuthException.UserNotFoundException -> LoginErrorStateResources( 14 | R.string.auth_user_not_found 15 | ) 16 | is AuthException.InvalidEmailFormatException -> LoginErrorStateResources( 17 | R.string.auth_email_bad_format 18 | ) 19 | is AuthException.PasswordUnderSixCharactersException -> LoginErrorStateResources( 20 | R.string.auth_invalid_password 21 | ) 22 | is AuthException.WrongCredentialsException -> LoginErrorStateResources( 23 | R.string.auth_wrong_credentials 24 | ) 25 | is AuthException.AlreadyRegisteredUserException -> LoginErrorStateResources( 26 | R.string.auth_user_already_registered 27 | ) 28 | is AuthException.PasswordsDoNotMatchException -> LoginErrorStateResources( 29 | R.string.auth_password_dont_match 30 | ) 31 | else -> LoginErrorStateResources( 32 | R.string.auth_unknown_error 33 | ) 34 | } 35 | } 36 | } 37 | 38 | fun LoginState.Error.toStateResource() = LoginErrorStateResources(error) -------------------------------------------------------------------------------- /features/feature-login/src/main/java/com/mctech/features/login/state/LoginState.kt: -------------------------------------------------------------------------------- 1 | package com.mctech.features.login.state 2 | 3 | import com.mctech.domain.errors.AuthException 4 | 5 | sealed class LoginState { 6 | object Loading : LoginState() 7 | object Unauthenticated : LoginState() 8 | object Authenticated : LoginState() 9 | data class Error(val error : AuthException) : LoginState() 10 | } -------------------------------------------------------------------------------- /features/feature-login/src/main/res/layout/activity_login.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 16 | 17 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /features/feature-login/src/main/res/layout/fragment_sign_in.xml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | 18 | 28 | 29 |