├── .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 | [](https://circleci.com/gh/MayconCardoso/KotlinLearning/tree/master)
9 | [](http://kotlinlang.org/)
10 | [](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 |
36 |
37 |
38 |
45 |
46 |
53 |
--------------------------------------------------------------------------------
/features/feature-login/src/main/res/layout/fragment_sign_up.xml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
17 |
18 |
26 |
27 |
37 |
38 |
48 |
49 |
56 |
57 |
64 |
--------------------------------------------------------------------------------
/features/feature-login/src/main/res/navigation/login_nav_graph.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
20 |
21 |
25 |
--------------------------------------------------------------------------------
/features/feature-login/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
--------------------------------------------------------------------------------
/features/feature-login/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Sign in
3 | Name
4 | Email
5 | Password
6 | Sign in
7 | Sign up
8 | "Welcome!"
9 |
10 | Não foi possível autenticar no momento!
11 | A senha deve possuir ao menos 5 caracteres
12 | Usuário não encontrado
13 | O formato do e-mail é invalido
14 | Usuário ou senha inválida
15 | Você deve informar seu usuário e senha
16 | Confirm password
17 | O endereço de e-mail informado já está cadastrado
18 | As senhas informadas são diferentes
19 |
20 |
--------------------------------------------------------------------------------
/features/feature-login/src/test/java/com/mctech/features/login/LoginViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.features.login
2 |
3 | import com.mctech.domain.errors.AuthException
4 | import com.mctech.domain.interaction.Result
5 | import com.mctech.domain.interaction.auth.AuthenticationUseCase
6 | import com.mctech.domain.interaction.auth.RegisterUserUseCase
7 | import com.mctech.domain.model.AuthRequest
8 | import com.mctech.domain.model.RegisterUser
9 | import com.mctech.domain.model.User
10 | import com.mctech.features.login.interaction.LoginUserInteraction
11 | import com.mctech.features.login.state.LoginState
12 | import com.mctech.test.arq.BaseViewModelTest
13 | import com.mctech.test.arq.extentions.*
14 | import com.nhaarman.mockitokotlin2.mock
15 | import com.nhaarman.mockitokotlin2.whenever
16 | import org.assertj.core.api.Assertions.assertThat
17 | import org.junit.Before
18 | import org.junit.Test
19 |
20 |
21 | /**
22 | * @author MAYCON CARDOSO on 2019-09-25.
23 | */
24 | class LoginViewModelTest : BaseViewModelTest() {
25 | private val authenticationUseCase = mock()
26 | private val registerUserUseCase = mock()
27 |
28 | private val authRequest = AuthRequest(email = "", password = "")
29 | private val expectedUser = User(name = "", email = "")
30 | private val registerRequest = RegisterUser(user = expectedUser, password = "", passwordConfirmation = "")
31 | private val expectedException = AuthException.WrongCredentialsException
32 |
33 | private lateinit var viewModel: LoginViewModel
34 |
35 | @Before
36 | fun `before each test`() {
37 | viewModel = LoginViewModel(
38 | authenticationUseCase,
39 | registerUserUseCase
40 | )
41 | }
42 |
43 | @Test
44 | fun `should init with unauthenticated state`() {
45 | viewModel.loginScreenState.collectValuesForTesting {
46 | it.assertCount(1)
47 | it.assertFirst().isEqualTo(
48 | LoginState.Unauthenticated
49 | )
50 | }
51 | }
52 |
53 | @Test
54 | fun `should change states when navigation to sign up screen`() {
55 | viewModel.lastAuthRequest.assertNoValue()
56 |
57 | viewModel.interact(LoginUserInteraction.NavigateToSignUn(authRequest))
58 |
59 | viewModel.loginScreenState.collectValuesForTesting {
60 | it.assertCount(1)
61 | it.assertFirst().isEqualTo(LoginState.Unauthenticated)
62 | }
63 |
64 | viewModel.lastAuthRequest.collectValuesForTesting {
65 | it.assertCount(1)
66 | it.assertFirst().isEqualTo(authRequest)
67 | }
68 | }
69 |
70 | @Test
71 | fun `should change state to authorized when sign in`() {
72 | assertPassOn(
73 | scenario = {
74 | authenticationUseCase.execute(authRequest)
75 | },
76 | interaction = LoginUserInteraction.TryLogin(authRequest)
77 | )
78 | }
79 |
80 | @Test
81 | fun `should change state to error when error while signing in`() {
82 | assertFailureOn(
83 | scenario = {
84 | authenticationUseCase.execute(authRequest)
85 | },
86 | interaction = LoginUserInteraction.TryLogin(authRequest)
87 | )
88 | }
89 |
90 | @Test
91 | fun `should change state to authorized when sign up`() {
92 | assertPassOn(
93 | scenario = {
94 | registerUserUseCase.execute(registerRequest)
95 | },
96 | interaction = LoginUserInteraction.TryRegisterUser(registerRequest)
97 | )
98 | }
99 |
100 | @Test
101 | fun `should change state to error when error while signing up`() {
102 | assertFailureOn(
103 | scenario = {
104 | registerUserUseCase.execute(registerRequest)
105 | },
106 | interaction = LoginUserInteraction.TryRegisterUser(registerRequest)
107 | )
108 | }
109 |
110 | private fun assertPassOn(interaction: LoginUserInteraction, scenario : suspend () -> Result) {
111 | viewModel.loginScreenState.test(
112 | scenario = {
113 | whenever(scenario.invoke()).thenReturn(Result.Success(expectedUser))
114 | },
115 | action = {
116 | viewModel.interact(interaction)
117 | },
118 | assertion = {
119 | it.assertCount(3)
120 | it.assertAtPosition(0).isExactlyInstanceOf(LoginState.Unauthenticated::class.java)
121 | it.assertAtPosition(1).isExactlyInstanceOf(LoginState.Loading::class.java)
122 | it.assertAtPosition(2).isExactlyInstanceOf(LoginState.Authenticated::class.java)
123 | }
124 | )
125 | }
126 |
127 | private fun assertFailureOn(interaction: LoginUserInteraction, scenario : suspend () -> Result) {
128 | viewModel.loginScreenState.test(
129 | scenario = {
130 | whenever(scenario.invoke()).thenReturn(Result.Failure(expectedException))
131 | },
132 | action = {
133 | viewModel.interact(interaction)
134 | },
135 | assertion = {
136 | it.assertCount(3)
137 | it.assertAtPosition(0).isExactlyInstanceOf(LoginState.Unauthenticated::class.java)
138 | it.assertAtPosition(1).isExactlyInstanceOf(LoginState.Loading::class.java)
139 | it.assertAtPosition(2).isExactlyInstanceOf(LoginState.Error::class.java)
140 | assertThat((it[2] as LoginState.Error).error).isEqualTo(expectedException)
141 | }
142 | )
143 | }
144 | }
--------------------------------------------------------------------------------
/features/feature-navigation/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 |
5 | dependencies {
6 | implementation globalDependencies.appCompact
7 |
8 | implementation globalDependencies.koin
9 |
10 | testImplementation globalTestDependencies.jUnit
11 | testImplementation globalTestDependencies.assertJ
12 | testImplementation globalTestDependencies.robolectric
13 | testImplementation globalTestDependencies.mockitoKotlin
14 | }
15 |
--------------------------------------------------------------------------------
/features/feature-navigation/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/features/feature-navigation/src/main/java/com/mctech/features/navigation/Navigator.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.features.navigation
2 |
3 | import android.content.Intent
4 | import androidx.fragment.app.FragmentActivity
5 |
6 | class Navigator(
7 | private val host: FragmentActivity,
8 | private val links: Map>
9 | ) {
10 | fun navigateTo(destination: Screen, finishHost : Boolean = false) {
11 | val next = Intent(host, find(destination))
12 | host.startActivity(next)
13 |
14 | if(finishHost) host.finish()
15 | }
16 |
17 | private fun find(target: Screen) =
18 | links[target]
19 | ?.let { it }
20 | ?: throw UnsupportedNavigation(target)
21 | }
--------------------------------------------------------------------------------
/features/feature-navigation/src/main/java/com/mctech/features/navigation/Screen.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.features.navigation
2 |
3 | sealed class Screen {
4 | object Splash : Screen()
5 | object Login : Screen()
6 | object Dashboard : Screen()
7 |
8 | override fun toString() = when (this) {
9 | Splash -> "Spash Screen"
10 | Login -> "Login Screen"
11 | Dashboard -> "Dashboard Screen"
12 | }
13 | }
--------------------------------------------------------------------------------
/features/feature-navigation/src/main/java/com/mctech/features/navigation/UnsupportedNavigation.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.features.navigation
2 |
3 | data class UnsupportedNavigation(val destination: Screen) : RuntimeException(
4 | "Cannot navigate to this destination -> $destination"
5 | )
--------------------------------------------------------------------------------
/features/feature-navigation/src/test/java/com/mctech/features/navigation/NavigatorTest.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.features.navigation
2 |
3 | import android.content.Intent
4 | import androidx.fragment.app.FragmentActivity
5 | import com.nhaarman.mockitokotlin2.argumentCaptor
6 | import com.nhaarman.mockitokotlin2.mock
7 | import com.nhaarman.mockitokotlin2.verify
8 | import org.assertj.core.api.Assertions.assertThat
9 | import org.assertj.core.api.Assertions.assertThatThrownBy
10 | import org.junit.Before
11 | import org.junit.Test
12 | import org.junit.runner.RunWith
13 | import org.robolectric.RobolectricTestRunner
14 |
15 | @RunWith(RobolectricTestRunner::class)
16 | class NavigatorTest{
17 | private lateinit var navigator: Navigator
18 | private val mockActivity = mock()
19 | private val links = mapOf>(
20 | Screen.Login to FragmentActivity::class.java
21 | )
22 |
23 | @Before
24 | fun `before each test`() {
25 | navigator = Navigator(mockActivity, links)
26 | }
27 |
28 | @Test
29 | fun `should navigate to supported screen`() {
30 | navigator.navigateTo(Screen.Login)
31 | argumentCaptor().apply {
32 | verify(mockActivity).startActivity(capture())
33 | assertThat(firstValue).isNotNull()
34 | }
35 | }
36 |
37 | @Test fun `should throw when navigating to unsupported screen`() {
38 | assertThatThrownBy { navigator.navigateTo(Screen.Splash) }
39 | .isEqualTo(
40 | UnsupportedNavigation(Screen.Splash)
41 | )
42 | }
43 | }
--------------------------------------------------------------------------------
/features/feature-onboarding/README.md:
--------------------------------------------------------------------------------
1 | ## Logger
2 |
3 | This is a library to manage the application OnBoarding.
4 |
--------------------------------------------------------------------------------
/features/feature-onboarding/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.appCompact
23 |
24 |
25 | testImplementation project(path: submodulesTest.sharedFeatureArq)
26 | }
27 |
--------------------------------------------------------------------------------
/features/feature-onboarding/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/features/feature-onboarding/src/main/java/com/mctech/features/onboarding/OnboardingActivity.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.features.onboarding
2 |
3 | import android.os.Bundle
4 | import com.mctech.feature.arq.BaseActivity
5 | import com.mctech.feature.arq.ComponentState
6 | import com.mctech.feature.arq.extentions.bindState
7 | import com.mctech.features.navigation.Screen
8 | import com.mctech.features.onboarding.state.OnBoardingNavigationState
9 | import org.koin.androidx.viewmodel.ext.android.viewModel
10 |
11 | /**
12 | * @author MAYCON CARDOSO on 2019-07-23.
13 | */
14 | class OnboardingActivity : BaseActivity() {
15 | private val viewModelAgent: OnboardingViewModel by viewModel()
16 |
17 | override fun onCreate(savedInstanceState: Bundle?) {
18 | super.onCreate(savedInstanceState)
19 | setContentView(R.layout.activity_on_boarding)
20 |
21 | bindState(viewModelAgent.userFlowState){
22 | when(it){
23 | is ComponentState.Success -> navigate(it.result)
24 | }
25 | }
26 | }
27 |
28 | private fun navigate(result: OnBoardingNavigationState) {
29 | when(result){
30 | is OnBoardingNavigationState.Unauthorized -> navigator.navigateTo(
31 | destination = Screen.Login,
32 | finishHost = true
33 | )
34 | is OnBoardingNavigationState.Authorized -> navigator.navigateTo(
35 | destination = Screen.Dashboard,
36 | finishHost = true
37 | )
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/features/feature-onboarding/src/main/java/com/mctech/features/onboarding/OnboardingViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.features.onboarding
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.liveData
5 | import com.mctech.domain.interaction.auth.CheckAuthSessionUseCase
6 | import com.mctech.feature.arq.BaseViewModel
7 | import com.mctech.feature.arq.ComponentState
8 | import com.mctech.features.onboarding.state.OnBoardingNavigationState
9 |
10 | class OnboardingViewModel(private val checkAuthSessionUseCase: CheckAuthSessionUseCase) : BaseViewModel() {
11 |
12 | val userFlowState: LiveData> = liveData {
13 | emit(ComponentState.Loading)
14 | val isUserLogged = checkAuthSessionUseCase.execute().result
15 | emit(
16 | ComponentState.Success(
17 | if (isUserLogged)
18 | OnBoardingNavigationState.Authorized
19 | else
20 | OnBoardingNavigationState.Unauthorized
21 | )
22 | )
23 | }
24 | }
--------------------------------------------------------------------------------
/features/feature-onboarding/src/main/java/com/mctech/features/onboarding/di/onboardingModule.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.features.onboarding.di
2 |
3 | import com.mctech.features.onboarding.OnboardingViewModel
4 | import org.koin.androidx.viewmodel.dsl.viewModel
5 | import org.koin.dsl.module
6 |
7 | val onboardingModule = module {
8 | viewModel { OnboardingViewModel(get()) }
9 | }
--------------------------------------------------------------------------------
/features/feature-onboarding/src/main/java/com/mctech/features/onboarding/state/OnBoardingNavigationState.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.features.onboarding.state
2 |
3 | sealed class OnBoardingNavigationState {
4 | object Authorized : OnBoardingNavigationState()
5 | object Unauthorized : OnBoardingNavigationState()
6 | }
--------------------------------------------------------------------------------
/features/feature-onboarding/src/main/res/layout/activity_on_boarding.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
17 |
18 |
22 |
--------------------------------------------------------------------------------
/features/feature-onboarding/src/test/java/com/mctech/features/onboarding/OnboardingViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.features.onboarding
2 |
3 | import com.mctech.domain.interaction.Result
4 | import com.mctech.domain.interaction.auth.CheckAuthSessionUseCase
5 | import com.mctech.feature.arq.ComponentState
6 | import com.mctech.features.onboarding.state.OnBoardingNavigationState
7 | import com.mctech.test.arq.BaseViewModelTest
8 | import com.mctech.test.arq.extentions.assertCount
9 | import com.mctech.test.arq.extentions.assertFirst
10 | import com.mctech.test.arq.extentions.assertLast
11 | import com.mctech.test.arq.extentions.collectValuesForTesting
12 | import com.nhaarman.mockitokotlin2.mock
13 | import com.nhaarman.mockitokotlin2.whenever
14 | import kotlinx.coroutines.runBlocking
15 | import org.assertj.core.api.Assertions.assertThat
16 | import org.junit.Before
17 | import org.junit.Test
18 |
19 | /**
20 | * @author MAYCON CARDOSO on 2019-09-22.
21 | */
22 | class OnboardingViewModelTest : BaseViewModelTest() {
23 | private val checkAuthSessionUseCase = mock()
24 | private lateinit var onboardingViewModel: OnboardingViewModel
25 |
26 | @Before
27 | fun `before each test`() {
28 | onboardingViewModel = OnboardingViewModel(checkAuthSessionUseCase)
29 | }
30 |
31 | @Test
32 | fun `should emit authorized state`() = onBoardTestStateHandler(
33 | isLogged = true,
34 | expectedState = OnBoardingNavigationState.Authorized
35 | )
36 |
37 | @Test
38 | fun `should emit unauthorized state`() = onBoardTestStateHandler(
39 | isLogged = false,
40 | expectedState = OnBoardingNavigationState.Unauthorized
41 | )
42 |
43 | private fun onBoardTestStateHandler(
44 | isLogged: Boolean,
45 | expectedState: OnBoardingNavigationState
46 | ) {
47 | runBlocking {
48 | whenever(checkAuthSessionUseCase.execute()).thenReturn(Result.Success(isLogged))
49 | onboardingViewModel.userFlowState.collectValuesForTesting {
50 | it.assertCount(2)
51 | it.assertFirst().isEqualTo(ComponentState.Loading)
52 | it.assertLast().isInstanceOf(ComponentState.Success::class.java)
53 |
54 | val result = (it[1] as ComponentState.Success).result
55 | assertThat(result).isEqualTo(expectedState)
56 | }
57 | }
58 | }
59 | }
--------------------------------------------------------------------------------
/features/feature-onboarding/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker:
--------------------------------------------------------------------------------
1 | mock-maker-inline
--------------------------------------------------------------------------------
/features/feature-quotation-filtering/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.appCompact
23 | implementation globalDependencies.constraintlayout
24 | implementation globalDependencies.materialDesign
25 |
26 |
27 | testImplementation project(path: submodulesTest.sharedFeatureArq)
28 | }
--------------------------------------------------------------------------------
/features/feature-quotation-filtering/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/features/feature-quotation-filtering/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | feature-joke-filtering
3 |
4 |
--------------------------------------------------------------------------------
/features/feature-quotation-list/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.appCompact
23 | implementation globalDependencies.constraintlayout
24 | implementation globalDependencies.materialDesign
25 |
26 |
27 | testImplementation project(path: submodulesTest.sharedFeatureArq)
28 | }
--------------------------------------------------------------------------------
/features/feature-quotation-list/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/features/feature-quotation-list/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | feature-joke-list
3 |
4 |
--------------------------------------------------------------------------------
/features/feature-quotation-random/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.appCompact
23 | implementation globalDependencies.constraintlayout
24 | implementation globalDependencies.materialDesign
25 |
26 |
27 | testImplementation project(path: submodulesTest.sharedFeatureArq)
28 | }
--------------------------------------------------------------------------------
/features/feature-quotation-random/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/features/feature-quotation-random/src/main/java/com/mctech/feature/random_joke/RandomQuotationActivity.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.feature.random_joke
2 |
3 | import android.os.Bundle
4 | import android.view.View
5 | import androidx.databinding.DataBindingUtil
6 | import com.mctech.domain.model.Quotation
7 | import com.mctech.feature.arq.BaseActivity
8 | import com.mctech.feature.arq.ComponentState
9 | import com.mctech.feature.arq.extentions.bindState
10 | import com.mctech.feature.random_joke.databinding.ActivityRandomJokeBinding
11 | import com.mctech.feature.random_joke.interaction.RandomQuotationInteraction
12 | import org.koin.androidx.viewmodel.ext.android.viewModel
13 |
14 | /**
15 | * @author MAYCON CARDOSO on 2019-09-30.
16 | */
17 | class RandomQuotationActivity : BaseActivity() {
18 | private val quotationViewModel : RandomQuotationViewModel by viewModel()
19 | private var binding: ActivityRandomJokeBinding? = null
20 |
21 | override fun onCreate(savedInstanceState: Bundle?) {
22 | super.onCreate(savedInstanceState)
23 | binding = DataBindingUtil.setContentView(this, R.layout.activity_random_joke)
24 |
25 | bindState(quotationViewModel.quotationState){
26 | renderUi(it)
27 | }
28 | }
29 |
30 | private fun renderUi(state: ComponentState) {
31 | when(state){
32 | is ComponentState.Initializing->{
33 | fetchQuotation()
34 | }
35 |
36 | is ComponentState.Loading -> {
37 | binding?.loading = true
38 | }
39 |
40 | is ComponentState.Error -> {
41 | binding?.loading = false
42 | }
43 |
44 | is ComponentState.Success -> {
45 | binding?.loading = false
46 | binding?.quotation = state.result
47 | }
48 | }
49 | }
50 |
51 | private fun fetchQuotation() {
52 | quotationViewModel.interact(RandomQuotationInteraction.LoadRandomQuotation)
53 | }
54 |
55 | fun onClickFetchQuotation(view: View) {
56 | fetchQuotation()
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/features/feature-quotation-random/src/main/java/com/mctech/feature/random_joke/RandomQuotationViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.feature.random_joke
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import com.mctech.domain.interaction.Result
6 | import com.mctech.domain.interaction.quotation.GetRandomQuotationCase
7 | import com.mctech.domain.model.Quotation
8 | import com.mctech.feature.arq.BaseViewModel
9 | import com.mctech.feature.arq.ComponentState
10 | import com.mctech.feature.arq.UserInteraction
11 | import com.mctech.feature.random_joke.interaction.RandomQuotationInteraction
12 |
13 | /**
14 | * @author MAYCON CARDOSO on 2019-09-30.
15 | */
16 | class RandomQuotationViewModel(
17 | private val getRandomCase: GetRandomQuotationCase
18 | ) : BaseViewModel(){
19 | private val _quotationState = MutableLiveData>(ComponentState.Initializing)
20 | val quotationState : LiveData> = _quotationState
21 |
22 | override suspend fun handleUserInteraction(interaction: UserInteraction) {
23 | when(interaction){
24 | is RandomQuotationInteraction.LoadRandomQuotation -> {
25 | fetchQuotation()
26 | }
27 | }
28 | }
29 |
30 | private suspend fun fetchQuotation() {
31 | _quotationState.value = ComponentState.Loading
32 |
33 | when(val result = getRandomCase.execute()){
34 | is Result.Failure -> {
35 | _quotationState.value = ComponentState.Error(result.throwable)
36 | }
37 | is Result.Success -> {
38 | _quotationState.value = ComponentState.Success(result.result)
39 | }
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/features/feature-quotation-random/src/main/java/com/mctech/feature/random_joke/di/randomQuotationModule.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.feature.random_joke.di
2 |
3 | import com.mctech.feature.random_joke.RandomQuotationViewModel
4 | import org.koin.androidx.viewmodel.dsl.viewModel
5 | import org.koin.dsl.module
6 |
7 | /**
8 | * @author MAYCON CARDOSO on 2019-09-30.
9 | */
10 | val randomQuotationModel = module {
11 | viewModel {
12 | RandomQuotationViewModel(
13 | getRandomCase = get()
14 | )
15 | }
16 | }
--------------------------------------------------------------------------------
/features/feature-quotation-random/src/main/java/com/mctech/feature/random_joke/interaction/RandomQuotationInteraction.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.feature.random_joke.interaction
2 |
3 | import com.mctech.feature.arq.UserInteraction
4 |
5 | /**
6 | * @author MAYCON CARDOSO on 2019-11-19.
7 | */
8 | sealed class RandomQuotationInteraction : UserInteraction {
9 | object LoadRandomQuotation : RandomQuotationInteraction()
10 | }
--------------------------------------------------------------------------------
/features/feature-quotation-random/src/main/res/layout/activity_random_joke.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
10 |
11 |
14 |
15 |
16 |
22 |
23 |
35 |
36 |
50 |
51 |
65 |
66 |
67 |
79 |
80 |
89 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/features/feature-quotation-random/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | feature-random-joke
3 | Mostrar outra
4 | Random Quotation
5 |
6 |
--------------------------------------------------------------------------------
/features/feature-quotation-random/src/test/java/com/mctech/feature/random_joke/RandomQuotationViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.feature.random_joke
2 |
3 | import com.mctech.domain.errors.QuotationException
4 | import com.mctech.domain.interaction.Result
5 | import com.mctech.domain.interaction.quotation.GetRandomQuotationCase
6 | import com.mctech.domain.model.Quotation
7 | import com.mctech.feature.arq.ComponentState
8 | import com.mctech.feature.random_joke.interaction.RandomQuotationInteraction
9 | import com.mctech.test.arq.BaseViewModelTest
10 | import com.mctech.test.arq.extentions.*
11 | import com.nhaarman.mockitokotlin2.mock
12 | import com.nhaarman.mockitokotlin2.whenever
13 | import org.assertj.core.api.Assertions
14 | import org.junit.Before
15 | import org.junit.Test
16 | import java.util.*
17 |
18 |
19 | /**
20 | * @author MAYCON CARDOSO on 2019-11-20.
21 | */
22 | class RandomQuotationViewModelTest : BaseViewModelTest() {
23 | private lateinit var viewModel: RandomQuotationViewModel
24 |
25 | private val expectationQuotation = createQuotation()
26 | private val getRandomQuotationCase = mock()
27 |
28 |
29 |
30 | @Before
31 | fun `before each test`() {
32 | viewModel = RandomQuotationViewModel(
33 | getRandomQuotationCase
34 | )
35 | }
36 |
37 | @Test
38 | fun `should init component`() {
39 | viewModel.quotationState.collectValuesForTesting {
40 | it.assertCount(1)
41 | it.assertFirst().isEqualTo(
42 | ComponentState.Initializing
43 | )
44 | }
45 | }
46 |
47 | @Test
48 | fun `should display quotation`() {
49 | viewModel.quotationState.test(
50 | scenario = {
51 | whenever(getRandomQuotationCase.execute()).thenReturn(Result.Success(expectationQuotation))
52 | },
53 | action = {
54 | viewModel.interact(RandomQuotationInteraction.LoadRandomQuotation)
55 | },
56 | assertion = {
57 | val successValue = it[2] as ComponentState.Success
58 |
59 | it.assertCount(3)
60 | it.assertFirst().isEqualTo(ComponentState.Initializing)
61 | it.assertAtPosition(2).isExactlyInstanceOf(ComponentState.Success::class.java)
62 |
63 | Assertions.assertThat(successValue.result).isEqualTo(expectationQuotation)
64 | }
65 | )
66 | }
67 |
68 | @Test
69 | fun `should display error`() {
70 | viewModel.quotationState.test(
71 | scenario = {
72 | whenever(getRandomQuotationCase.execute()).thenReturn(
73 | Result.Failure(
74 | QuotationException.UnknownQuotationException
75 | )
76 | )
77 | },
78 | action = {
79 | viewModel.interact(RandomQuotationInteraction.LoadRandomQuotation)
80 | },
81 | assertion = {
82 | val successValue = it[2] as ComponentState.Error
83 |
84 | it.assertCount(3)
85 | it.assertFirst().isEqualTo(ComponentState.Initializing)
86 | it.assertAtPosition(2).isExactlyInstanceOf(ComponentState.Error::class.java)
87 |
88 | Assertions.assertThat(successValue.reason).isEqualTo(
89 | QuotationException.UnknownQuotationException
90 | )
91 | }
92 | )
93 | }
94 |
95 | private fun createQuotation(
96 | id : String = "",
97 | tag : String = "",
98 | description: String = "",
99 | date : Date = Date(),
100 | author : String = "",
101 | twitterLink : String = ""
102 | ) = Quotation(
103 | id = id,
104 | description = description,
105 | date = date,
106 | author = author,
107 | twitterLink = twitterLink,
108 | tag = listOf(tag)
109 | )
110 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
22 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MayconCardoso/Modularized-Kotlin-Clean-Architecture-Showcase/7c2d04f56439c42b6bf8bc26807be25111d90dde/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Sep 11 22:03:41 BRT 2019
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/libraries/library-analytics/README.md:
--------------------------------------------------------------------------------
1 | ## Analytics
2 |
3 | This is a library to manage the application analytics; It has two implementation of it: Firebase and Muted
4 |
--------------------------------------------------------------------------------
/libraries/library-analytics/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 |
5 | dependencies {
6 | implementation globalDependencies.firebaseCore
7 | }
8 |
--------------------------------------------------------------------------------
/libraries/library-analytics/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/libraries/library-analytics/src/main/java/com/mctech/libraries/analytics/AnalyticsHelper.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.libraries.analytics
2 |
3 | import android.app.Activity
4 |
5 | /**
6 | * @author MAYCON CARDOSO on 2019-07-24.
7 | */
8 | interface AnalyticsHelper{
9 | fun sendScreenView(screenName: String, activity: Activity)
10 | fun logUiEvent(itemId: String, action: String)
11 | fun setUserSignedIn(isSignedIn: Boolean)
12 | fun setUserRegistered(isRegistered: Boolean)
13 | }
--------------------------------------------------------------------------------
/libraries/library-analytics/src/main/java/com/mctech/libraries/analytics/FirebaseAnalyticsHelper.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.libraries.analytics
2 |
3 | import android.app.Activity
4 | import android.app.Application
5 | import android.os.Bundle
6 | import com.google.firebase.analytics.FirebaseAnalytics
7 |
8 | /**
9 | * @author MAYCON CARDOSO on 2019-07-24.
10 | */
11 | class FirebaseAnalyticsHelper(
12 | application: Application
13 | ) : AnalyticsHelper {
14 |
15 | companion object{
16 | private const val UPROP_USER_SIGNED_IN = "user_signed_in"
17 | private const val UPROP_USER_REGISTERED = "user_registered"
18 | private const val CONTENT_TYPE_SCREEN_VIEW = "screen"
19 | private const val KEY_UI_ACTION = "ui_action"
20 | private const val CONTENT_TYPE_UI_EVENT = "ui event"
21 | }
22 |
23 | private var firebaseAnalytics: FirebaseAnalytics
24 |
25 | init {
26 | firebaseAnalytics = FirebaseAnalytics.getInstance(application)
27 | }
28 |
29 | override fun sendScreenView(screenName: String, activity: Activity) {
30 | val params = Bundle().apply {
31 | putString(FirebaseAnalytics.Param.ITEM_ID, screenName)
32 | putString(FirebaseAnalytics.Param.CONTENT_TYPE,
33 | CONTENT_TYPE_SCREEN_VIEW
34 | )
35 | }
36 | firebaseAnalytics.run {
37 | setCurrentScreen(activity, screenName, null)
38 | logEvent(FirebaseAnalytics.Event.SELECT_CONTENT, params)
39 | }
40 | }
41 |
42 | override fun logUiEvent(itemId: String, action: String) {
43 | val params = Bundle().apply {
44 | putString(FirebaseAnalytics.Param.ITEM_ID, itemId)
45 | putString(FirebaseAnalytics.Param.CONTENT_TYPE,
46 | CONTENT_TYPE_UI_EVENT
47 | )
48 | putString(KEY_UI_ACTION, action)
49 | }
50 | firebaseAnalytics.logEvent(FirebaseAnalytics.Event.SELECT_CONTENT, params)
51 | }
52 |
53 | override fun setUserSignedIn(isSignedIn: Boolean) {
54 | firebaseAnalytics.setUserProperty(UPROP_USER_SIGNED_IN, isSignedIn.toString())
55 | }
56 |
57 | override fun setUserRegistered(isRegistered: Boolean) {
58 | firebaseAnalytics.setUserProperty(UPROP_USER_REGISTERED, isRegistered.toString())
59 | }
60 | }
--------------------------------------------------------------------------------
/libraries/library-analytics/src/main/java/com/mctech/libraries/analytics/MutedAnalyticsHelper.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.libraries.analytics
2 |
3 | import android.app.Activity
4 |
5 | /**
6 | * @author MAYCON CARDOSO on 2019-07-24.
7 | */
8 | class MutedAnalyticsHelper : AnalyticsHelper {
9 | override fun sendScreenView(screenName: String, activity: Activity) {}
10 | override fun logUiEvent(itemId: String, action: String) {}
11 | override fun setUserSignedIn(isSignedIn: Boolean) {}
12 | override fun setUserRegistered(isRegistered: Boolean) {}
13 | }
--------------------------------------------------------------------------------
/libraries/library-app-theme/README.md:
--------------------------------------------------------------------------------
1 | ## App Theme
2 |
3 | This is a library to manage the application theme and style.
4 |
--------------------------------------------------------------------------------
/libraries/library-app-theme/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 |
5 | dependencies {
6 | implementation globalDependencies.appCompact
7 | implementation globalDependencies.materialDesign
8 | }
--------------------------------------------------------------------------------
/libraries/library-app-theme/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/libraries/library-app-theme/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #008577
4 | #00574B
5 | #D81B60
6 | #FBFBFB
7 |
8 |
--------------------------------------------------------------------------------
/libraries/library-app-theme/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 12dp
4 |
--------------------------------------------------------------------------------
/libraries/library-app-theme/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
14 |
15 |
23 |
24 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/libraries/library-logger/README.md:
--------------------------------------------------------------------------------
1 | ## Logger
2 |
3 | This is a library to manage the application logging.
4 |
--------------------------------------------------------------------------------
/libraries/library-logger/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'kotlin'
2 |
3 | dependencies {
4 | implementation globalDependencies.kotlinStdLib
5 | }
--------------------------------------------------------------------------------
/libraries/library-logger/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/libraries/library-logger/src/main/java/com/mctech/libraries/logger/Logger.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.libraries.logger
2 |
3 | /**
4 | * @author MAYCON CARDOSO on 2019-07-22.
5 | */
6 | interface Logger {
7 | fun v(message: String)
8 | fun d(message: String)
9 | fun i(message: String)
10 | fun w(message: String)
11 | fun e(message: String)
12 | fun e(e: Throwable)
13 | }
--------------------------------------------------------------------------------
/libraries/library-logger/src/main/java/com/mctech/libraries/logger/MutedLogger.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.libraries.logger
2 |
3 |
4 | /**
5 | * @author MAYCON CARDOSO on 2019-07-22.
6 | */
7 | class MutedLogger : Logger {
8 | override fun v(message: String) = Unit
9 | override fun d(message: String) = Unit
10 | override fun i(message: String) = Unit
11 | override fun w(message: String) = Unit
12 | override fun e(message: String) = Unit
13 | override fun e(e: Throwable) = Unit
14 | }
--------------------------------------------------------------------------------
/libraries/library-networking/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'kotlin'
2 |
3 | dependencies {
4 | implementation globalDependencies.kotlinStdLib
5 | implementation globalDependencies.kotlinCoroutinesCore
6 |
7 | implementation globalDependencies.okHttp
8 | implementation globalDependencies.retrofit
9 | implementation globalDependencies.retrofitGsonConverter
10 |
11 | testImplementation globalTestDependencies.jUnit
12 | testImplementation globalTestDependencies.assertJ
13 | }
--------------------------------------------------------------------------------
/libraries/library-networking/src/main/java/com/mctech/library/networking/NetworkError.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.library.networking
2 |
3 | /**
4 | * @author MAYCON CARDOSO on 2019-09-28.
5 | */
6 | sealed class NetworkError : Throwable() {
7 | object ClientException : NetworkError()
8 | object RemoteException : NetworkError()
9 |
10 | object HostUnreachable : NetworkError()
11 | object OperationTimeout : NetworkError()
12 | object ConnectionSpike : NetworkError()
13 | object UnknownNetworkingError : NetworkError()
14 |
15 | override fun toString() =
16 | when (this) {
17 | ClientException -> "Issue originated from client"
18 | RemoteException -> "Issue incoming from server"
19 |
20 | HostUnreachable -> "Cannot reach remote host"
21 | OperationTimeout -> "Networking operation timed out"
22 | ConnectionSpike -> "In-flight networking operation interrupted"
23 | UnknownNetworkingError -> "Fatal networking exception"
24 | }
25 | }
--------------------------------------------------------------------------------
/libraries/library-networking/src/main/java/com/mctech/library/networking/NetworkErrorTransformer.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.library.networking
2 |
3 | import retrofit2.HttpException
4 | import java.io.IOException
5 | import java.net.ConnectException
6 | import java.net.NoRouteToHostException
7 | import java.net.SocketTimeoutException
8 | import java.net.UnknownHostException
9 |
10 | /**
11 | * @author MAYCON CARDOSO on 2019-09-28.
12 | *
13 | * Transform any exception into a NetworkError in order to avoid any crash on the app.
14 | */
15 | object NetworkErrorTransformer {
16 | fun transform(incoming: Throwable) = when (incoming) {
17 | is HttpException -> {
18 | translateHttpExceptionUsingStatusCode(incoming.code())
19 | }
20 | is SocketTimeoutException -> {
21 | NetworkError.OperationTimeout
22 | }
23 | is UnknownHostException,
24 | is ConnectException,
25 | is NoRouteToHostException -> {
26 | NetworkError.HostUnreachable
27 | }
28 | else -> {
29 | resolveOtherException(incoming)
30 | }
31 | }
32 |
33 | private fun resolveOtherException(incoming: Throwable) = if(isRequestCanceled(incoming)){
34 | NetworkError.ConnectionSpike
35 | } else{
36 | NetworkError.UnknownNetworkingError
37 | }
38 |
39 | private fun isRequestCanceled(throwable: Throwable) =
40 | throwable is IOException &&
41 | throwable.message?.contentEquals("Canceled") ?: false
42 |
43 | private fun translateHttpExceptionUsingStatusCode(code: Int) =
44 | when (code) {
45 | in 400..499 -> NetworkError.ClientException
46 | else -> NetworkError.RemoteException
47 | }
48 | }
--------------------------------------------------------------------------------
/libraries/library-networking/src/main/java/com/mctech/library/networking/RetrofitBuilder.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.library.networking
2 |
3 | import com.google.gson.GsonBuilder
4 | import okhttp3.OkHttpClient
5 | import retrofit2.Retrofit
6 | import retrofit2.converter.gson.GsonConverterFactory
7 |
8 | /**
9 | * @author MAYCON CARDOSO on 2019-09-28.
10 | */
11 | object RetrofitBuilder {
12 |
13 | operator fun invoke(apiURL: String, httpClient: OkHttpClient) =
14 | with(Retrofit.Builder()) {
15 | val gson = GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss").create()
16 |
17 | baseUrl(apiURL)
18 | client(httpClient)
19 | addConverterFactory(GsonConverterFactory.create(gson))
20 | build()
21 | }
22 | }
--------------------------------------------------------------------------------
/libraries/library-networking/src/main/java/com/mctech/library/networking/secureRequest.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.library.networking
2 |
3 | import kotlinx.coroutines.Dispatchers
4 | import kotlinx.coroutines.withContext
5 |
6 | /**
7 | * This method helps the application to avoid unexpected crashes during some network request.
8 | * Basically, if there is any issue on the request, we can transform the error into
9 | * another one that the app know about.
10 | */
11 | suspend fun secureRequest(target: suspend () -> T): T = withContext(Dispatchers.IO){
12 | try {
13 | target.invoke()
14 | } catch (incoming: Throwable) {
15 | throw NetworkErrorTransformer.transform(incoming)
16 | }
17 | }
--------------------------------------------------------------------------------
/libraries/library-networking/src/test/java/com/mctech/library/networking/NetworkErrorTransformerTest.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.library.networking
2 |
3 | import okhttp3.MediaType.Companion.toMediaTypeOrNull
4 | import okhttp3.ResponseBody.Companion.toResponseBody
5 | import org.assertj.core.api.Assertions
6 | import org.junit.Test
7 | import retrofit2.HttpException
8 | import retrofit2.Response
9 | import java.io.IOException
10 | import java.net.ConnectException
11 | import java.net.NoRouteToHostException
12 | import java.net.SocketTimeoutException
13 | import java.net.UnknownHostException
14 |
15 |
16 | /**
17 | * @author MAYCON CARDOSO on 2019-09-28.
18 | */
19 |
20 | class NetworkErrorTransformerTest {
21 |
22 | @Test
23 | fun `should return a client exception`() {
24 | internalAssertion(
25 | exception = createHttpException(400, "Bad format request"),
26 | expectedValue = NetworkError.ClientException::class.java
27 | )
28 |
29 | internalAssertion(
30 | exception = createHttpException(404, "Not found"),
31 | expectedValue = NetworkError.ClientException::class.java
32 | )
33 | }
34 |
35 | @Test
36 | fun `should return a server exception`() {
37 | internalAssertion(
38 | exception = createHttpException(500, "Internal server error"),
39 | expectedValue = NetworkError.RemoteException::class.java
40 | )
41 | }
42 |
43 | @Test
44 | fun `should return a operation timeout`() {
45 | internalAssertion(
46 | exception = SocketTimeoutException(),
47 | expectedValue = NetworkError.OperationTimeout::class.java
48 | )
49 | }
50 |
51 | @Test
52 | fun `should return a host unreachable`() {
53 | internalAssertion(
54 | exception = UnknownHostException(),
55 | expectedValue = NetworkError.HostUnreachable::class.java
56 | )
57 | internalAssertion(
58 | exception = ConnectException(),
59 | expectedValue = NetworkError.HostUnreachable::class.java
60 | )
61 | internalAssertion(
62 | exception = NoRouteToHostException(),
63 | expectedValue = NetworkError.HostUnreachable::class.java
64 | )
65 | }
66 |
67 | @Test
68 | fun `should return a connection spike`() {
69 | internalAssertion(
70 | exception = IOException("Canceled"),
71 | expectedValue = NetworkError.ConnectionSpike::class.java
72 | )
73 | }
74 |
75 | @Test
76 | fun `should return a default exception`() {
77 | internalAssertion(
78 | exception = Throwable(),
79 | expectedValue = NetworkError.UnknownNetworkingError::class.java
80 | )
81 | }
82 |
83 | private fun internalAssertion(exception : Throwable, expectedValue : Class<*>){
84 | val result = NetworkErrorTransformer.transform(exception)
85 | Assertions.assertThat(result)
86 | .isExactlyInstanceOf(
87 | expectedValue
88 | )
89 | }
90 |
91 | private fun createHttpException(code: Int, error: String): HttpException {
92 | val format = "application/json".toMediaTypeOrNull()
93 | val responseBody = error.toResponseBody(format)
94 | return HttpException(Response.error(code, responseBody))
95 | }
96 | }
--------------------------------------------------------------------------------
/libraries/library-networking/src/test/java/com/mctech/library/networking/SecureRequestKtTest.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.library.networking
2 |
3 | import kotlinx.coroutines.runBlocking
4 | import org.assertj.core.api.Assertions.assertThat
5 | import org.junit.Test
6 |
7 | /**
8 | * @author MAYCON CARDOSO on 2019-09-28.
9 | */
10 | class SecureRequestKtTest {
11 |
12 | @Test
13 | fun `should return a known network error`() {
14 | runBlocking {
15 |
16 | val result = runCatching {
17 | secureRequest(suspend {
18 | throw Throwable()
19 | })
20 | }.exceptionOrNull()
21 |
22 | assertThat(result)
23 | .isExactlyInstanceOf(
24 | NetworkError.UnknownNetworkingError::class.java
25 | )
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/libraries/library-shared-feature-arq-testing/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: submodulesLibraries.logger)
7 | implementation globalDependencies.lifeCycleLiveData
8 |
9 | api globalTestDependencies.jUnit
10 | api globalTestDependencies.assertJ
11 | api globalTestDependencies.koinTest
12 | api globalTestDependencies.testArqCor
13 | api globalTestDependencies.coroutines
14 | api globalTestDependencies.mockitoKotlin
15 | }
--------------------------------------------------------------------------------
/libraries/library-shared-feature-arq-testing/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/libraries/library-shared-feature-arq-testing/src/main/java/com/mctech/test/arq/BaseViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.test.arq
2 |
3 | import com.mctech.test.arq.rules.CoroutinesMainTestRule
4 | import com.mctech.test.arq.rules.KoinModuleTestRule
5 | import kotlinx.coroutines.ExperimentalCoroutinesApi
6 | import org.junit.Rule
7 | import org.koin.test.AutoCloseKoinTest
8 |
9 | /**
10 | * @author MAYCON CARDOSO on 2019-09-25.
11 | */
12 | @ExperimentalCoroutinesApi
13 | abstract class BaseViewModelTest : AutoCloseKoinTest() {
14 | @get:Rule
15 | val koinRule = KoinModuleTestRule()
16 | @get:Rule
17 | val coroutinesTestRule = CoroutinesMainTestRule()
18 | }
--------------------------------------------------------------------------------
/libraries/library-shared-feature-arq-testing/src/main/java/com/mctech/test/arq/extentions/listAssertionExtention.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.test.arq.extentions
2 |
3 | import org.assertj.core.api.Assertions
4 |
5 | /**
6 | * @author MAYCON CARDOSO on 2019-09-25.
7 | */
8 | fun List.assertEmpty() = assertCount(0)
9 | fun List.assertCount(count : Int) = Assertions.assertThat(size).isEqualTo(count)
10 | fun List.assertAtPosition(position : Int) = Assertions.assertThat(get(position))
11 | fun List.assertFirst() = assertAtPosition(0)
12 | fun List.assertLast() = assertAtPosition(size - 1)
13 |
--------------------------------------------------------------------------------
/libraries/library-shared-feature-arq-testing/src/main/java/com/mctech/test/arq/extentions/livedataTestExtentions.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.test.arq.extentions
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.Observer
5 | import kotlinx.coroutines.runBlocking
6 |
7 | fun LiveData.collectValuesForTesting(assertion: (List) -> Unit) {
8 | test { assertion(it) }
9 | }
10 |
11 | fun LiveData.test(
12 | scenario: suspend () -> Unit = {},
13 | action: () -> Unit = {},
14 | assertion: (List) -> Unit
15 | ) {
16 | val emittedValues = mutableListOf()
17 | val observer = Observer {
18 | emittedValues.add(it)
19 | }
20 |
21 | try {
22 | runBlocking {
23 | scenario()
24 | observeForever(observer)
25 | action()
26 | assertion(emittedValues)
27 | }
28 | } finally {
29 | removeObserver(observer)
30 | }
31 | }
32 |
33 | fun LiveData.assertNoValue() {
34 | collectValuesForTesting { it.assertEmpty() }
35 | }
--------------------------------------------------------------------------------
/libraries/library-shared-feature-arq-testing/src/main/java/com/mctech/test/arq/rules/CoroutinesMainTestRule.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.test.arq.rules
2 |
3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule
4 | import kotlinx.coroutines.Dispatchers
5 | import kotlinx.coroutines.ExperimentalCoroutinesApi
6 | import kotlinx.coroutines.test.TestCoroutineDispatcher
7 | import kotlinx.coroutines.test.resetMain
8 | import kotlinx.coroutines.test.setMain
9 | import org.junit.runner.Description
10 |
11 | @ExperimentalCoroutinesApi
12 | class CoroutinesMainTestRule(
13 | val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
14 | ) : InstantTaskExecutorRule() {
15 |
16 | override fun starting(description: Description?) {
17 | super.starting(description)
18 | Dispatchers.setMain(testDispatcher)
19 | }
20 |
21 | override fun finished(description: Description?) {
22 | super.finished(description)
23 | Dispatchers.resetMain()
24 | testDispatcher.cleanupTestCoroutines()
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/libraries/library-shared-feature-arq-testing/src/main/java/com/mctech/test/arq/rules/KoinModuleTestRule.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.test.arq.rules
2 |
3 | import com.mctech.libraries.logger.Logger
4 | import com.mctech.libraries.logger.MutedLogger
5 | import org.junit.rules.TestWatcher
6 | import org.junit.runner.Description
7 | import org.koin.core.context.startKoin
8 | import org.koin.dsl.module
9 |
10 | class KoinModuleTestRule : TestWatcher() {
11 | override fun starting(description: Description?) {
12 | super.starting(description)
13 | startKoin {
14 | modules(module {
15 | single { MutedLogger() as Logger }
16 | })
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/libraries/library-shared-feature-arq/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-kapt'
3 | apply plugin: 'kotlin-android'
4 | apply plugin: 'kotlin-android-extensions'
5 |
6 | dependencies {
7 | implementation project(path: submodulesFeatures.navigation)
8 | implementation project(path: submodulesLibraries.logger)
9 | implementation globalDependencies.appCompact
10 |
11 | implementation globalDependencies.lifeCycleViewModel
12 | implementation globalDependencies.lifeCycleLiveRuntime
13 | implementation globalDependencies.lifeCycleLiveData
14 | implementation globalDependencies.koin
15 | implementation globalDependencies.koinScope
16 |
17 |
18 | testImplementation project(path: submodulesTest.sharedFeatureArq)
19 | }
--------------------------------------------------------------------------------
/libraries/library-shared-feature-arq/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/libraries/library-shared-feature-arq/src/main/java/com/mctech/feature/arq/BaseActivity.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.feature.arq
2 |
3 | import androidx.appcompat.app.AppCompatActivity
4 | import com.mctech.features.navigation.Navigator
5 | import org.koin.android.ext.android.inject
6 | import org.koin.core.parameter.parametersOf
7 |
8 | /**
9 | * @author MAYCON CARDOSO on 2019-09-05.
10 | */
11 | abstract class BaseActivity : AppCompatActivity() {
12 | val navigator: Navigator by inject { parametersOf(this) }
13 | }
--------------------------------------------------------------------------------
/libraries/library-shared-feature-arq/src/main/java/com/mctech/feature/arq/BaseFragment.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.feature.arq
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.annotation.LayoutRes
8 | import androidx.fragment.app.Fragment
9 | import androidx.lifecycle.ViewModel
10 |
11 | /**
12 | * @author MAYCON CARDOSO on 2019-09-05.
13 | */
14 | abstract class BaseFragment : Fragment() {
15 | @LayoutRes
16 | abstract fun getLayoutId(): Int
17 |
18 | override fun onCreateView(
19 | inflater: LayoutInflater, container: ViewGroup?,
20 | savedInstanceState: Bundle?
21 | ) : View = inflater.inflate(getLayoutId(), container, false)
22 | }
23 |
--------------------------------------------------------------------------------
/libraries/library-shared-feature-arq/src/main/java/com/mctech/feature/arq/BaseViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.feature.arq
2 |
3 | import androidx.annotation.VisibleForTesting
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import com.mctech.libraries.logger.Logger
7 | import kotlinx.coroutines.launch
8 | import org.koin.core.context.GlobalContext.get
9 | import java.lang.reflect.Modifier.PROTECTED
10 |
11 | /**
12 | * @author MAYCON CARDOSO on 2019-09-05.
13 | */
14 | open class BaseViewModel : ViewModel() {
15 | val logger: Logger by get().koin.inject()
16 |
17 | private var userFlowInteraction = mutableListOf()
18 |
19 | fun interact(userInteraction: UserInteraction) {
20 | viewModelScope.launch {
21 | suspendedInteraction(userInteraction)
22 | }
23 | }
24 |
25 | suspend fun suspendedInteraction(userInteraction: UserInteraction) {
26 | userFlowInteraction.add(userInteraction)
27 | handleUserInteraction(userInteraction)
28 | }
29 |
30 | @VisibleForTesting(otherwise = PROTECTED)
31 | open suspend fun handleUserInteraction(interaction: UserInteraction) = Unit
32 |
33 | fun reprocessLastInteraction() {
34 | viewModelScope.launch {
35 | handleUserInteraction(userFlowInteraction.last())
36 | }
37 | }
38 |
39 | override fun onCleared() {
40 | userFlowInteraction.clear()
41 | super.onCleared()
42 | }
43 | }
--------------------------------------------------------------------------------
/libraries/library-shared-feature-arq/src/main/java/com/mctech/feature/arq/ComponentState.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.feature.arq
2 |
3 | sealed class ComponentState {
4 | object Initializing : ComponentState()
5 | object Loading : ComponentState()
6 | data class Error(val reason: Throwable) : ComponentState()
7 | data class Success(val result: T) : ComponentState()
8 | }
--------------------------------------------------------------------------------
/libraries/library-shared-feature-arq/src/main/java/com/mctech/feature/arq/UserInteraction.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.feature.arq
2 |
3 | interface UserInteraction
--------------------------------------------------------------------------------
/libraries/library-shared-feature-arq/src/main/java/com/mctech/feature/arq/components/TextViewAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.feature.arq.components
2 |
3 | import android.widget.TextView
4 | import androidx.databinding.BindingAdapter
5 | import java.text.SimpleDateFormat
6 | import java.util.*
7 |
8 | @BindingAdapter("android:text")
9 | fun formatDate(view: TextView, date: Date?) {
10 | if(date == null) return
11 |
12 | view.text = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(date)
13 | }
14 |
--------------------------------------------------------------------------------
/libraries/library-shared-feature-arq/src/main/java/com/mctech/feature/arq/components/ViewAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.feature.arq.components
2 |
3 | import android.view.View
4 | import android.view.View.GONE
5 | import android.view.View.VISIBLE
6 | import androidx.databinding.BindingAdapter
7 |
8 | @set:BindingAdapter("gone")
9 | var View.gone
10 | get() = visibility == GONE
11 | set(value) {
12 | visibility = if (value) GONE else VISIBLE
13 | }
14 |
15 | @set:BindingAdapter("visible")
16 | var View.visible
17 | get() = visibility == VISIBLE
18 | set(value) {
19 | visibility = if (value) VISIBLE else GONE
20 | }
--------------------------------------------------------------------------------
/libraries/library-shared-feature-arq/src/main/java/com/mctech/feature/arq/extentions/baseActivityExtention.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.feature.arq.extentions
2 |
3 | import android.widget.Toast
4 | import androidx.appcompat.app.AppCompatActivity
5 | import androidx.fragment.app.Fragment
6 | import androidx.lifecycle.LiveData
7 | import androidx.lifecycle.Observer
8 | import androidx.lifecycle.lifecycleScope
9 | import com.mctech.feature.arq.BaseActivity
10 | import com.mctech.feature.arq.BaseFragment
11 | import com.mctech.feature.arq.ComponentState
12 | import kotlinx.coroutines.launch
13 |
14 |
15 | fun BaseActivity<*>.bindData(observable : LiveData, block : (result : T) -> Unit) {
16 | lifecycleScope.launch {
17 | observable.observe(this@bindData, Observer {
18 | block(it)
19 | })
20 | }
21 | }
22 |
23 | fun BaseFragment<*>.bindData(observable : LiveData, block : (result : T) -> Unit) {
24 | lifecycleScope.launch {
25 | observable.observe(this@bindData, Observer {
26 | block(it)
27 | })
28 | }
29 | }
30 |
31 | fun BaseActivity<*>.bindState(observable : LiveData>, block : (result : ComponentState) -> Unit) {
32 | lifecycleScope.launch {
33 | observable.observe(this@bindState, Observer {
34 | block(it)
35 | })
36 | }
37 | }
38 |
39 | fun BaseFragment<*>.bindState(observable : LiveData>, block : (result : ComponentState) -> Unit) {
40 | lifecycleScope.launch {
41 | observable.observe(this@bindState, Observer {
42 | block(it)
43 | })
44 | }
45 | }
46 |
47 | fun AppCompatActivity.toast(message: Int) =
48 | Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
49 |
50 | fun Fragment.toast(message: Int) = activity?.let {
51 | Toast.makeText(activity, message, Toast.LENGTH_SHORT).show()
52 | }
53 |
--------------------------------------------------------------------------------
/libraries/library-shared-feature-arq/src/main/java/com/mctech/feature/arq/extentions/editTextExtentions.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.feature.arq.extentions
2 |
3 | import android.widget.EditText
4 |
5 | fun EditText.getValue() = this.text.toString().trim()
6 |
--------------------------------------------------------------------------------
/libraries/library-shared-feature-arq/src/main/java/com/mctech/feature/arq/extentions/viewExtentions.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.feature.arq.extentions
2 |
3 | import android.view.View
4 |
5 |
6 | fun View.show() {
7 | this.visibility = View.VISIBLE
8 | }
9 |
10 | fun View.hide() {
11 | this.visibility = View.GONE
12 | }
13 |
14 | fun View.setVisibilityByState(visible: Boolean) {
15 | if (visible) show()
16 | else hide()
17 | }
18 |
19 | fun View.enable() {
20 | this.isEnabled = true
21 | }
22 |
23 | fun View.disable() {
24 | this.isEnabled = false
25 | }
26 |
27 | fun View.enableByState(enabled: Boolean) {
28 | if (enabled) enable()
29 | else disable()
30 | }
--------------------------------------------------------------------------------
/libraries/library-shared-feature-arq/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | library-shared-feature-arq
3 |
4 |
--------------------------------------------------------------------------------
/libraries/library-shared-feature-arq/src/test/java/com/mctech/feature/arq/BaseViewModelArqTest.kt:
--------------------------------------------------------------------------------
1 | package com.mctech.feature.arq
2 |
3 | import com.mctech.test.arq.BaseViewModelTest
4 | import com.nhaarman.mockitokotlin2.spy
5 | import com.nhaarman.mockitokotlin2.times
6 | import com.nhaarman.mockitokotlin2.verify
7 | import kotlinx.coroutines.runBlocking
8 | import org.junit.Before
9 | import org.junit.Test
10 |
11 | /**
12 | * @author MAYCON CARDOSO on 2019-09-26.
13 | */
14 | open class BaseViewModelArqTest : BaseViewModelTest() {
15 | lateinit var baseViewModel: BaseViewModel
16 |
17 | @Before
18 | fun `before each test`() {
19 | baseViewModel = spy(BaseViewModel())
20 | }
21 |
22 |
23 | @Test
24 | fun `should add interaction on stack`() {
25 | runBlocking {
26 | baseViewModel.interact(MockInteraction.NoParamInteraction)
27 | verify(baseViewModel).suspendedInteraction(MockInteraction.NoParamInteraction)
28 | }
29 | }
30 |
31 | @Test
32 | fun `should retry last interaction`() {
33 | runBlocking {
34 | val expected = MockInteraction.ParamInteraction("")
35 |
36 | baseViewModel.interact(MockInteraction.NoParamInteraction)
37 | baseViewModel.interact(expected)
38 | baseViewModel.reprocessLastInteraction()
39 | verify(baseViewModel, times(2)).handleUserInteraction(expected)
40 |
41 |
42 | baseViewModel.interact(MockInteraction.NoParamInteraction)
43 | baseViewModel.reprocessLastInteraction()
44 | verify(baseViewModel, times(3)).handleUserInteraction(MockInteraction.NoParamInteraction)
45 | }
46 | }
47 |
48 | }
49 |
50 | sealed class MockInteraction : UserInteraction {
51 | object NoParamInteraction : MockInteraction()
52 | data class ParamInteraction(val foo: String) : MockInteraction()
53 | }
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 | include ':data'
3 | include ':domain'
4 |
5 | include ':libraries:library-logger'
6 | include ':libraries:library-analytics'
7 | include ':libraries:library-app-theme'
8 | include ':libraries:library-networking'
9 | include ':libraries:library-shared-feature-arq'
10 | include ':libraries:library-shared-feature-arq-testing'
11 |
12 | include ':features:feature-onboarding'
13 | include ':features:feature-login'
14 | include ':features:feature-quotation-filtering'
15 | include ':features:feature-quotation-random'
16 | include ':features:feature-quotation-list'
17 | include ':features:feature-navigation'
--------------------------------------------------------------------------------