├── .gitignore
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── hms
│ │ └── archdemo
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── hms
│ │ │ └── archdemo
│ │ │ ├── ArchitectureDemoApp.kt
│ │ │ ├── common
│ │ │ ├── Resource.kt
│ │ │ └── extensions
│ │ │ │ ├── ResultExt.kt
│ │ │ │ └── StringExtensions.kt
│ │ │ ├── data
│ │ │ ├── data_source
│ │ │ │ ├── BaseRemoteDataSource.kt
│ │ │ │ └── UsersRemoteDataSource.kt
│ │ │ ├── model
│ │ │ │ ├── NameRemoteModel.kt
│ │ │ │ ├── PictureRemoteModel.kt
│ │ │ │ ├── UserRemoteModel.kt
│ │ │ │ └── UsersResponse.kt
│ │ │ ├── remote
│ │ │ │ └── service
│ │ │ │ │ └── UserService.kt
│ │ │ └── repository
│ │ │ │ └── UsersRepositoryImpl.kt
│ │ │ ├── di
│ │ │ ├── NetworkModule.kt
│ │ │ ├── RepositoryModule.kt
│ │ │ └── UseCaseModule.kt
│ │ │ ├── domain
│ │ │ ├── decider
│ │ │ │ └── UserDecider.kt
│ │ │ ├── mapper
│ │ │ │ └── UserMapper.kt
│ │ │ ├── model
│ │ │ │ ├── Gender.kt
│ │ │ │ └── User.kt
│ │ │ ├── repository
│ │ │ │ └── UsersRepository.kt
│ │ │ └── use_case
│ │ │ │ ├── FetchUsersUseCase.kt
│ │ │ │ └── FetchUsersUseCaseImpl.kt
│ │ │ └── ui
│ │ │ ├── MainActivity.kt
│ │ │ ├── theme
│ │ │ ├── Color.kt
│ │ │ ├── Shape.kt
│ │ │ ├── Theme.kt
│ │ │ └── Type.kt
│ │ │ └── users
│ │ │ ├── UserItemUiState.kt
│ │ │ ├── UserListScreen.kt
│ │ │ ├── UserListUiState.kt
│ │ │ ├── UserViewModel.kt
│ │ │ └── components
│ │ │ └── UserListItem.kt
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ └── ic_launcher_background.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ │ └── xml
│ │ ├── backup_rules.xml
│ │ └── data_extraction_rules.xml
│ └── test
│ └── java
│ └── com
│ └── hms
│ └── archdemo
│ ├── data
│ └── repository
│ │ └── FakeUsersRepository.kt
│ ├── domain
│ ├── decider
│ │ └── UserDeciderTest.kt
│ ├── mapper
│ │ ├── UserFactory.kt
│ │ └── UserMapperTest.kt
│ └── use_case
│ │ ├── FakeFetchUsersUseCase.kt
│ │ ├── FetchUsersUseCaseTest.kt
│ │ ├── FetchUsersUseCaseTestWithTurbine.kt
│ │ └── TestModels.kt
│ ├── ui
│ └── users
│ │ ├── ParameterizedUserItemUiStateTest.kt
│ │ ├── UserItemUiStateTest.kt
│ │ └── UserViewModelTest.kt
│ └── util
│ ├── CoroutineRule.kt
│ └── extensions
│ └── TruthExtensions.kt
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 |
17 | # Created by https://www.toptal.com/developers/gitignore/api/androidstudio,android,jetbrains,intellij,kotlin,java
18 | # Edit at https://www.toptal.com/developers/gitignore?templates=androidstudio,android,jetbrains,intellij,kotlin,java
19 |
20 | ### Android ###
21 | # Gradle files
22 | .gradle/
23 | build/
24 |
25 | # Log/OS Files
26 | *.log
27 |
28 | # Android Studio generated files and folders
29 | captures/
30 | .externalNativeBuild/
31 | .cxx/
32 | *.apk
33 | output.json
34 |
35 | # IntelliJ
36 | .idea/
37 | misc.xml
38 | deploymentTargetDropDown.xml
39 | render.experimental.xml
40 |
41 | # Keystore files
42 | *.jks
43 | *.keystore
44 |
45 | # Google Services (e.g. APIs or Firebase)
46 | google-services.json
47 |
48 | # Android Profiling
49 | *.hprof
50 |
51 | ### Android Patch ###
52 | gen-external-apklibs
53 |
54 | # Replacement of .externalNativeBuild directories introduced
55 | # with Android Studio 3.5.
56 |
57 | ### Intellij ###
58 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
59 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
60 |
61 | # User-specific stuff
62 | .idea/**/workspace.xml
63 | .idea/**/tasks.xml
64 | .idea/**/usage.statistics.xml
65 | .idea/**/dictionaries
66 | .idea/**/shelf
67 |
68 | # AWS User-specific
69 | .idea/**/aws.xml
70 |
71 | # Generated files
72 | .idea/**/contentModel.xml
73 |
74 | # Sensitive or high-churn files
75 | .idea/**/dataSources/
76 | .idea/**/dataSources.ids
77 | .idea/**/dataSources.local.xml
78 | .idea/**/sqlDataSources.xml
79 | .idea/**/dynamic.xml
80 | .idea/**/uiDesigner.xml
81 | .idea/**/dbnavigator.xml
82 |
83 | # Gradle
84 | .idea/**/gradle.xml
85 | .idea/**/libraries
86 |
87 | # Gradle and Maven with auto-import
88 | # When using Gradle or Maven with auto-import, you should exclude module files,
89 | # since they will be recreated, and may cause churn. Uncomment if using
90 | # auto-import.
91 | # .idea/artifacts
92 | # .idea/compiler.xml
93 | # .idea/jarRepositories.xml
94 | # .idea/modules.xml
95 | # .idea/*.iml
96 | # .idea/modules
97 | # *.iml
98 | # *.ipr
99 |
100 | # CMake
101 | cmake-build-*/
102 |
103 | # Mongo Explorer plugin
104 | .idea/**/mongoSettings.xml
105 |
106 | # File-based project format
107 | *.iws
108 |
109 | # IntelliJ
110 | out/
111 |
112 | # mpeltonen/sbt-idea plugin
113 | .idea_modules/
114 |
115 | # JIRA plugin
116 | atlassian-ide-plugin.xml
117 |
118 | # Cursive Clojure plugin
119 | .idea/replstate.xml
120 |
121 | # SonarLint plugin
122 | .idea/sonarlint/
123 |
124 | # Crashlytics plugin (for Android Studio and IntelliJ)
125 | com_crashlytics_export_strings.xml
126 | crashlytics.properties
127 | crashlytics-build.properties
128 | fabric.properties
129 |
130 | # Editor-based Rest Client
131 | .idea/httpRequests
132 |
133 | # Android studio 3.1+ serialized cache file
134 | .idea/caches/build_file_checksums.ser
135 |
136 | ### Intellij Patch ###
137 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
138 |
139 | # *.iml
140 | # modules.xml
141 | # .idea/misc.xml
142 | # *.ipr
143 |
144 | # Sonarlint plugin
145 | # https://plugins.jetbrains.com/plugin/7973-sonarlint
146 | .idea/**/sonarlint/
147 |
148 | # SonarQube Plugin
149 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
150 | .idea/**/sonarIssues.xml
151 |
152 | # Markdown Navigator plugin
153 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
154 | .idea/**/markdown-navigator.xml
155 | .idea/**/markdown-navigator-enh.xml
156 | .idea/**/markdown-navigator/
157 |
158 | # Cache file creation bug
159 | # See https://youtrack.jetbrains.com/issue/JBR-2257
160 | .idea/$CACHE_FILE$
161 |
162 | # CodeStream plugin
163 | # https://plugins.jetbrains.com/plugin/12206-codestream
164 | .idea/codestream.xml
165 |
166 | ### Java ###
167 | # Compiled class file
168 | *.class
169 |
170 | # Log file
171 |
172 | # BlueJ files
173 | *.ctxt
174 |
175 | # Mobile Tools for Java (J2ME)
176 | .mtj.tmp/
177 |
178 | # Package Files #
179 | *.jar
180 | *.war
181 | *.nar
182 | *.ear
183 | *.zip
184 | *.tar.gz
185 | *.rar
186 |
187 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
188 | hs_err_pid*
189 | replay_pid*
190 |
191 | ### JetBrains ###
192 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
193 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
194 |
195 | # User-specific stuff
196 |
197 | # AWS User-specific
198 |
199 | # Generated files
200 |
201 | # Sensitive or high-churn files
202 |
203 | # Gradle
204 |
205 | # Gradle and Maven with auto-import
206 | # When using Gradle or Maven with auto-import, you should exclude module files,
207 | # since they will be recreated, and may cause churn. Uncomment if using
208 | # auto-import.
209 | # .idea/artifacts
210 | # .idea/compiler.xml
211 | # .idea/jarRepositories.xml
212 | # .idea/modules.xml
213 | # .idea/*.iml
214 | # .idea/modules
215 | # *.iml
216 | # *.ipr
217 |
218 | # CMake
219 |
220 | # Mongo Explorer plugin
221 |
222 | # File-based project format
223 |
224 | # IntelliJ
225 |
226 | # mpeltonen/sbt-idea plugin
227 |
228 | # JIRA plugin
229 |
230 | # Cursive Clojure plugin
231 |
232 | # SonarLint plugin
233 |
234 | # Crashlytics plugin (for Android Studio and IntelliJ)
235 |
236 | # Editor-based Rest Client
237 |
238 | # Android studio 3.1+ serialized cache file
239 |
240 | ### JetBrains Patch ###
241 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
242 |
243 | # *.iml
244 | # modules.xml
245 | # .idea/misc.xml
246 | # *.ipr
247 |
248 | # Sonarlint plugin
249 | # https://plugins.jetbrains.com/plugin/7973-sonarlint
250 |
251 | # SonarQube Plugin
252 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
253 |
254 | # Markdown Navigator plugin
255 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
256 |
257 | # Cache file creation bug
258 | # See https://youtrack.jetbrains.com/issue/JBR-2257
259 |
260 | # CodeStream plugin
261 | # https://plugins.jetbrains.com/plugin/12206-codestream
262 |
263 | ### Kotlin ###
264 | # Compiled class file
265 |
266 | # Log file
267 |
268 | # BlueJ files
269 |
270 | # Mobile Tools for Java (J2ME)
271 |
272 | # Package Files #
273 |
274 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
275 |
276 | ### AndroidStudio ###
277 | # Covers files to be ignored for android development using Android Studio.
278 |
279 | # Built application files
280 | *.ap_
281 | *.aab
282 |
283 | # Files for the ART/Dalvik VM
284 | *.dex
285 |
286 | # Java class files
287 |
288 | # Generated files
289 | bin/
290 | gen/
291 |
292 |
293 | # Signing files
294 | .signing/
295 |
296 | # Local configuration file (sdk path, etc)
297 |
298 | # Proguard folder generated by Eclipse
299 | proguard/
300 |
301 | # Log Files
302 |
303 | # Android Studio
304 | /*/build/
305 | /*/local.properties
306 | /*/out
307 | /*/*/build
308 | /*/*/production
309 | .navigation/
310 | *.ipr
311 | *~
312 | *.swp
313 |
314 | # Keystore files
315 |
316 | # Google Services (e.g. APIs or Firebase)
317 | # google-services.json
318 |
319 | # Android Patch
320 |
321 | # NDK
322 | obj/
323 |
324 | # IntelliJ IDEA
325 | /out/
326 |
327 | # User-specific configurations
328 | .idea/caches/
329 | .idea/libraries/
330 | .idea/shelf/
331 | .idea/workspace.xml
332 | .idea/tasks.xml
333 | .idea/.name
334 | .idea/compiler.xml
335 | .idea/copyright/profiles_settings.xml
336 | .idea/encodings.xml
337 | .idea/misc.xml
338 | .idea/modules.xml
339 | .idea/scopes/scope_settings.xml
340 | .idea/dictionaries
341 | .idea/vcs.xml
342 | .idea/jsLibraryMappings.xml
343 | .idea/datasources.xml
344 | .idea/dataSources.ids
345 | .idea/sqlDataSources.xml
346 | .idea/dynamic.xml
347 | .idea/uiDesigner.xml
348 | .idea/assetWizardSettings.xml
349 | .idea/gradle.xml
350 | .idea/jarRepositories.xml
351 | .idea/navEditor.xml
352 |
353 | # Legacy Eclipse project files
354 | .classpath
355 | .project
356 | .cproject
357 | .settings/
358 |
359 | # Mobile Tools for Java (J2ME)
360 |
361 | # Package Files #
362 |
363 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml)
364 |
365 | ## Plugin-specific files:
366 |
367 | # mpeltonen/sbt-idea plugin
368 |
369 | # JIRA plugin
370 |
371 | # Mongo Explorer plugin
372 | .idea/mongoSettings.xml
373 |
374 | # Crashlytics plugin (for Android Studio and IntelliJ)
375 |
376 | ### AndroidStudio Patch ###
377 |
378 | !/gradle/wrapper/gradle-wrapper.jar
379 |
380 | # End of https://www.toptal.com/developers/gitignore/api/androidstudio,android,jetbrains,intellij,kotlin,java
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Android Architecture Training Demo
2 |
3 | A Simple app to demonstrate [Latest Android App Architecture](https://developer.android.com/jetpack/guide) with cutting edge techs such as
4 |
5 | ✅ MVVM, Clean Architecture \
6 | ✅ Jetpack Compose \
7 | ✅ [Hilt (DI)](https://developer.android.com/training/dependency-injection/hilt-android) \
8 | ✅ [Coroutines](https://developer.android.com/kotlin/coroutines)
9 |
10 |
11 |
12 | Used API : https://randomuser.me/api/?results=100
13 |
14 |
15 |
16 |
17 |
18 | https://user-images.githubusercontent.com/28221219/175256297-e0bd15e4-689d-4ad9-bed4-fe1a45a4f1ff.mp4
19 |
20 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 |
17 | # Created by https://www.toptal.com/developers/gitignore/api/androidstudio,android,jetbrains,intellij,kotlin,java
18 | # Edit at https://www.toptal.com/developers/gitignore?templates=androidstudio,android,jetbrains,intellij,kotlin,java
19 |
20 | ### Android ###
21 | # Gradle files
22 | .gradle/
23 | build/
24 |
25 | # Log/OS Files
26 | *.log
27 |
28 | # Android Studio generated files and folders
29 | captures/
30 | .externalNativeBuild/
31 | .cxx/
32 | *.apk
33 | output.json
34 |
35 | # IntelliJ
36 | .idea/
37 | misc.xml
38 | deploymentTargetDropDown.xml
39 | render.experimental.xml
40 |
41 | # Keystore files
42 | *.jks
43 | *.keystore
44 |
45 | # Google Services (e.g. APIs or Firebase)
46 | google-services.json
47 |
48 | # Android Profiling
49 | *.hprof
50 |
51 | ### Android Patch ###
52 | gen-external-apklibs
53 |
54 | # Replacement of .externalNativeBuild directories introduced
55 | # with Android Studio 3.5.
56 |
57 | ### Intellij ###
58 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
59 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
60 |
61 | # User-specific stuff
62 | .idea/**/workspace.xml
63 | .idea/**/tasks.xml
64 | .idea/**/usage.statistics.xml
65 | .idea/**/dictionaries
66 | .idea/**/shelf
67 |
68 | # AWS User-specific
69 | .idea/**/aws.xml
70 |
71 | # Generated files
72 | .idea/**/contentModel.xml
73 |
74 | # Sensitive or high-churn files
75 | .idea/**/dataSources/
76 | .idea/**/dataSources.ids
77 | .idea/**/dataSources.local.xml
78 | .idea/**/sqlDataSources.xml
79 | .idea/**/dynamic.xml
80 | .idea/**/uiDesigner.xml
81 | .idea/**/dbnavigator.xml
82 |
83 | # Gradle
84 | .idea/**/gradle.xml
85 | .idea/**/libraries
86 |
87 | # Gradle and Maven with auto-import
88 | # When using Gradle or Maven with auto-import, you should exclude module files,
89 | # since they will be recreated, and may cause churn. Uncomment if using
90 | # auto-import.
91 | # .idea/artifacts
92 | # .idea/compiler.xml
93 | # .idea/jarRepositories.xml
94 | # .idea/modules.xml
95 | # .idea/*.iml
96 | # .idea/modules
97 | # *.iml
98 | # *.ipr
99 |
100 | # CMake
101 | cmake-build-*/
102 |
103 | # Mongo Explorer plugin
104 | .idea/**/mongoSettings.xml
105 |
106 | # File-based project format
107 | *.iws
108 |
109 | # IntelliJ
110 | out/
111 |
112 | # mpeltonen/sbt-idea plugin
113 | .idea_modules/
114 |
115 | # JIRA plugin
116 | atlassian-ide-plugin.xml
117 |
118 | # Cursive Clojure plugin
119 | .idea/replstate.xml
120 |
121 | # SonarLint plugin
122 | .idea/sonarlint/
123 |
124 | # Crashlytics plugin (for Android Studio and IntelliJ)
125 | com_crashlytics_export_strings.xml
126 | crashlytics.properties
127 | crashlytics-build.properties
128 | fabric.properties
129 |
130 | # Editor-based Rest Client
131 | .idea/httpRequests
132 |
133 | # Android studio 3.1+ serialized cache file
134 | .idea/caches/build_file_checksums.ser
135 |
136 | ### Intellij Patch ###
137 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
138 |
139 | # *.iml
140 | # modules.xml
141 | # .idea/misc.xml
142 | # *.ipr
143 |
144 | # Sonarlint plugin
145 | # https://plugins.jetbrains.com/plugin/7973-sonarlint
146 | .idea/**/sonarlint/
147 |
148 | # SonarQube Plugin
149 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
150 | .idea/**/sonarIssues.xml
151 |
152 | # Markdown Navigator plugin
153 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
154 | .idea/**/markdown-navigator.xml
155 | .idea/**/markdown-navigator-enh.xml
156 | .idea/**/markdown-navigator/
157 |
158 | # Cache file creation bug
159 | # See https://youtrack.jetbrains.com/issue/JBR-2257
160 | .idea/$CACHE_FILE$
161 |
162 | # CodeStream plugin
163 | # https://plugins.jetbrains.com/plugin/12206-codestream
164 | .idea/codestream.xml
165 |
166 | ### Java ###
167 | # Compiled class file
168 | *.class
169 |
170 | # Log file
171 |
172 | # BlueJ files
173 | *.ctxt
174 |
175 | # Mobile Tools for Java (J2ME)
176 | .mtj.tmp/
177 |
178 | # Package Files #
179 | *.jar
180 | *.war
181 | *.nar
182 | *.ear
183 | *.zip
184 | *.tar.gz
185 | *.rar
186 |
187 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
188 | hs_err_pid*
189 | replay_pid*
190 |
191 | ### JetBrains ###
192 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
193 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
194 |
195 | # User-specific stuff
196 |
197 | # AWS User-specific
198 |
199 | # Generated files
200 |
201 | # Sensitive or high-churn files
202 |
203 | # Gradle
204 |
205 | # Gradle and Maven with auto-import
206 | # When using Gradle or Maven with auto-import, you should exclude module files,
207 | # since they will be recreated, and may cause churn. Uncomment if using
208 | # auto-import.
209 | # .idea/artifacts
210 | # .idea/compiler.xml
211 | # .idea/jarRepositories.xml
212 | # .idea/modules.xml
213 | # .idea/*.iml
214 | # .idea/modules
215 | # *.iml
216 | # *.ipr
217 |
218 | # CMake
219 |
220 | # Mongo Explorer plugin
221 |
222 | # File-based project format
223 |
224 | # IntelliJ
225 |
226 | # mpeltonen/sbt-idea plugin
227 |
228 | # JIRA plugin
229 |
230 | # Cursive Clojure plugin
231 |
232 | # SonarLint plugin
233 |
234 | # Crashlytics plugin (for Android Studio and IntelliJ)
235 |
236 | # Editor-based Rest Client
237 |
238 | # Android studio 3.1+ serialized cache file
239 |
240 | ### JetBrains Patch ###
241 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
242 |
243 | # *.iml
244 | # modules.xml
245 | # .idea/misc.xml
246 | # *.ipr
247 |
248 | # Sonarlint plugin
249 | # https://plugins.jetbrains.com/plugin/7973-sonarlint
250 |
251 | # SonarQube Plugin
252 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
253 |
254 | # Markdown Navigator plugin
255 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
256 |
257 | # Cache file creation bug
258 | # See https://youtrack.jetbrains.com/issue/JBR-2257
259 |
260 | # CodeStream plugin
261 | # https://plugins.jetbrains.com/plugin/12206-codestream
262 |
263 | ### Kotlin ###
264 | # Compiled class file
265 |
266 | # Log file
267 |
268 | # BlueJ files
269 |
270 | # Mobile Tools for Java (J2ME)
271 |
272 | # Package Files #
273 |
274 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
275 |
276 | ### AndroidStudio ###
277 | # Covers files to be ignored for android development using Android Studio.
278 |
279 | # Built application files
280 | *.ap_
281 | *.aab
282 |
283 | # Files for the ART/Dalvik VM
284 | *.dex
285 |
286 | # Java class files
287 |
288 | # Generated files
289 | bin/
290 | gen/
291 |
292 |
293 | # Signing files
294 | .signing/
295 |
296 | # Local configuration file (sdk path, etc)
297 |
298 | # Proguard folder generated by Eclipse
299 | proguard/
300 |
301 | # Log Files
302 |
303 | # Android Studio
304 | /*/build/
305 | /*/local.properties
306 | /*/out
307 | /*/*/build
308 | /*/*/production
309 | .navigation/
310 | *.ipr
311 | *~
312 | *.swp
313 |
314 | # Keystore files
315 |
316 | # Google Services (e.g. APIs or Firebase)
317 | # google-services.json
318 |
319 | # Android Patch
320 |
321 | # NDK
322 | obj/
323 |
324 | # IntelliJ IDEA
325 | /out/
326 |
327 | # User-specific configurations
328 | .idea/caches/
329 | .idea/libraries/
330 | .idea/shelf/
331 | .idea/workspace.xml
332 | .idea/tasks.xml
333 | .idea/.name
334 | .idea/compiler.xml
335 | .idea/copyright/profiles_settings.xml
336 | .idea/encodings.xml
337 | .idea/misc.xml
338 | .idea/modules.xml
339 | .idea/scopes/scope_settings.xml
340 | .idea/dictionaries
341 | .idea/vcs.xml
342 | .idea/jsLibraryMappings.xml
343 | .idea/datasources.xml
344 | .idea/dataSources.ids
345 | .idea/sqlDataSources.xml
346 | .idea/dynamic.xml
347 | .idea/uiDesigner.xml
348 | .idea/assetWizardSettings.xml
349 | .idea/gradle.xml
350 | .idea/jarRepositories.xml
351 | .idea/navEditor.xml
352 |
353 | # Legacy Eclipse project files
354 | .classpath
355 | .project
356 | .cproject
357 | .settings/
358 |
359 | # Mobile Tools for Java (J2ME)
360 |
361 | # Package Files #
362 |
363 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml)
364 |
365 | ## Plugin-specific files:
366 |
367 | # mpeltonen/sbt-idea plugin
368 |
369 | # JIRA plugin
370 |
371 | # Mongo Explorer plugin
372 | .idea/mongoSettings.xml
373 |
374 | # Crashlytics plugin (for Android Studio and IntelliJ)
375 |
376 | ### AndroidStudio Patch ###
377 |
378 | !/gradle/wrapper/gradle-wrapper.jar
379 |
380 | # End of https://www.toptal.com/developers/gitignore/api/androidstudio,android,jetbrains,intellij,kotlin,java
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'org.jetbrains.kotlin.android'
4 | id 'kotlin-kapt'
5 | id 'dagger.hilt.android.plugin'
6 | }
7 |
8 | android {
9 | compileSdk 32
10 |
11 | defaultConfig {
12 | applicationId "com.hms.archdemo"
13 | minSdk 21
14 | targetSdk 32
15 | versionCode 1
16 | versionName "1.0"
17 |
18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
19 | vectorDrawables {
20 | useSupportLibrary true
21 | }
22 | }
23 |
24 | buildTypes {
25 | release {
26 | minifyEnabled false
27 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
28 | }
29 | }
30 | compileOptions {
31 | sourceCompatibility JavaVersion.VERSION_1_8
32 | targetCompatibility JavaVersion.VERSION_1_8
33 | }
34 | kotlinOptions {
35 | jvmTarget = '1.8'
36 | }
37 | buildFeatures {
38 | compose true
39 | }
40 | composeOptions {
41 | kotlinCompilerExtensionVersion compose_version
42 | }
43 | packagingOptions {
44 | resources {
45 | excludes += '/META-INF/{AL2.0,LGPL2.1}'
46 | }
47 | }
48 | }
49 |
50 | dependencies {
51 |
52 | implementation 'androidx.core:core-ktx:1.8.0'
53 | implementation "androidx.compose.ui:ui:$compose_version"
54 | implementation "androidx.compose.material:material:$compose_version"
55 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
56 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
57 | implementation 'androidx.activity:activity-compose:1.4.0'
58 | testImplementation 'junit:junit:4.13.2'
59 | testImplementation "com.google.truth:truth:1.1.3"
60 | testImplementation "io.mockk:mockk:1.12.4"
61 | testImplementation 'org.robolectric:robolectric:4.8'
62 | testImplementation 'app.cash.turbine:turbine:0.9.0'
63 | androidTestImplementation 'androidx.test.ext:junit:1.1.3'
64 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
65 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
66 | debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
67 | debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
68 |
69 | //Dagger - Hilt
70 | implementation "com.google.dagger:hilt-android:2.41"
71 | kapt "com.google.dagger:hilt-android-compiler:2.41"
72 | kapt "androidx.hilt:hilt-compiler:1.0.0"
73 | implementation 'androidx.hilt:hilt-navigation-compose:1.0.0'
74 |
75 | // Coroutines
76 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'
77 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
78 | testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
79 |
80 | // Coroutine Lifecycle Scopes
81 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1"
82 | implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.1"
83 |
84 | //Image Loading
85 | implementation "com.github.skydoves:landscapist-glide:1.5.2"
86 |
87 | //Retrofit
88 | implementation "com.squareup.retrofit2:retrofit:2.9.0"
89 | implementation "com.squareup.retrofit2:converter-gson:2.9.0"
90 |
91 | //OkHttp
92 | implementation "com.squareup.okhttp3:okhttp:4.9.3"
93 | implementation "com.squareup.okhttp3:logging-interceptor:4.9.3"
94 | }
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/hms/archdemo/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.hms.archdemo", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
19 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hms/archdemo/ArchitectureDemoApp.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo
2 |
3 | import android.app.Application
4 | import dagger.hilt.android.HiltAndroidApp
5 |
6 | @HiltAndroidApp
7 | class ArchitectureDemoApp : Application()
--------------------------------------------------------------------------------
/app/src/main/java/com/hms/archdemo/common/Resource.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.common
2 |
3 |
4 | sealed interface Resource {
5 | data class Success(val data: T) : Resource
6 | data class Error(val exception: Throwable? = null) : Resource
7 | object Loading : Resource
8 | }
9 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hms/archdemo/common/extensions/ResultExt.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.common.extensions
2 |
3 | import com.hms.archdemo.common.Resource
4 |
5 | inline fun Resource.map(transform: (T) -> R): Resource {
6 | return when (this) {
7 | is Resource.Success -> Resource.Success(
8 | transform(data)
9 | )
10 | is Resource.Error -> Resource.Error(exception)
11 | is Resource.Loading -> Resource.Loading
12 | }
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/hms/archdemo/common/extensions/StringExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.common.extensions
2 |
3 | import java.util.*
4 |
5 | fun String.capitalizeFirstChar(): String {
6 | return replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
7 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/hms/archdemo/data/data_source/BaseRemoteDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.data.data_source
2 |
3 | import com.hms.archdemo.common.Resource
4 | import com.hms.archdemo.common.Resource.*
5 | import kotlinx.coroutines.Dispatchers
6 | import kotlinx.coroutines.flow.catch
7 | import kotlinx.coroutines.flow.flow
8 | import kotlinx.coroutines.flow.flowOn
9 | import kotlinx.coroutines.flow.onStart
10 |
11 |
12 | abstract class BaseRemoteDataSource {
13 |
14 | protected suspend fun safeApiCall(apiCall: suspend () -> T) = flow> {
15 | val response = apiCall.invoke()
16 | emit(Success(response))
17 | }.onStart {
18 | emit(Loading)
19 | }.catch {
20 | emit(Error(exception = it))
21 | }.flowOn(Dispatchers.IO)
22 |
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/hms/archdemo/data/data_source/UsersRemoteDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.data.data_source
2 |
3 | import com.hms.archdemo.data.remote.service.UserService
4 | import javax.inject.Inject
5 |
6 | class UsersRemoteDataSource @Inject constructor(
7 | private val userService: UserService
8 | ) : BaseRemoteDataSource() {
9 |
10 | suspend fun fetchUsers() = safeApiCall { userService.fetchUsers() }
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/hms/archdemo/data/model/NameRemoteModel.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.data.model
2 |
3 | data class NameRemoteModel(
4 | val title: String,
5 | val first: String,
6 | val last: String
7 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/hms/archdemo/data/model/PictureRemoteModel.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.data.model
2 |
3 | data class PictureRemoteModel(
4 | val large: String,
5 | val medium: String,
6 | val thumbnail: String
7 | )
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hms/archdemo/data/model/UserRemoteModel.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.data.model
2 |
3 | data class UserRemoteModel(
4 | val gender: String,
5 | val name: NameRemoteModel,
6 | val phone: String,
7 | val email: String,
8 | val picture: PictureRemoteModel
9 | )
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hms/archdemo/data/model/UsersResponse.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.data.model
2 |
3 | data class UsersResponse(
4 | val results: List
5 | )
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hms/archdemo/data/remote/service/UserService.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.data.remote.service
2 |
3 | import com.hms.archdemo.data.model.UsersResponse
4 | import retrofit2.http.GET
5 | import retrofit2.http.Query
6 |
7 | interface UserService {
8 |
9 | @GET(".")
10 | suspend fun fetchUsers(
11 | @Query("results") results: Int = 500
12 | ): UsersResponse
13 |
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/hms/archdemo/data/repository/UsersRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.data.repository
2 |
3 | import com.hms.archdemo.common.Resource
4 | import com.hms.archdemo.common.extensions.map
5 | import com.hms.archdemo.data.data_source.UsersRemoteDataSource
6 | import com.hms.archdemo.data.model.UserRemoteModel
7 | import com.hms.archdemo.data.model.UsersResponse
8 | import com.hms.archdemo.domain.repository.UsersRepository
9 | import kotlinx.coroutines.flow.Flow
10 | import kotlinx.coroutines.flow.map
11 | import javax.inject.Inject
12 |
13 | class UsersRepositoryImpl @Inject constructor(
14 | private val usersRemoteDataSource: UsersRemoteDataSource,
15 | // you can use multiple data sources in repository classes
16 | // you can use other repository classes, too
17 | ) : UsersRepository {
18 |
19 | override suspend fun fetchUsers(): Flow>> {
20 | return usersRemoteDataSource.fetchUsers().map {
21 | it.map { usersResponse ->
22 | mapOnUsersResponse(usersResponse = usersResponse)
23 | }
24 | }
25 | }
26 |
27 |
28 | private fun mapOnUsersResponse(usersResponse: UsersResponse): List =
29 | usersResponse.results
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/hms/archdemo/di/NetworkModule.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.di
2 |
3 | import com.hms.archdemo.BuildConfig
4 | import com.hms.archdemo.data.remote.service.UserService
5 | import dagger.Module
6 | import dagger.Provides
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.components.SingletonComponent
9 | import okhttp3.Interceptor
10 | import okhttp3.OkHttpClient
11 | import okhttp3.logging.HttpLoggingInterceptor
12 | import retrofit2.Retrofit
13 | import retrofit2.converter.gson.GsonConverterFactory
14 | import java.util.concurrent.TimeUnit
15 | import javax.inject.Named
16 | import javax.inject.Singleton
17 |
18 |
19 | @Module
20 | @InstallIn(SingletonComponent::class)
21 | object NetworkModule {
22 |
23 | private const val CONNECT_TIMEOUT = 20L
24 | private const val READ_TIMEOUT = 60L
25 | private const val WRITE_TIMEOUT = 120L
26 |
27 |
28 | @Provides
29 | @Named("BASE_URL")
30 | fun provideBaseUrl() = "https://randomuser.me/api/"
31 |
32 | @Provides
33 | @Singleton
34 | fun provideRetrofit(okHttpClient: OkHttpClient, @Named("BASE_URL") baseUrl: String): Retrofit {
35 | return Retrofit.Builder()
36 | .baseUrl(baseUrl)
37 | .client(okHttpClient)
38 | .addConverterFactory(GsonConverterFactory.create())
39 | .build()
40 | }
41 |
42 | @Provides
43 | @Singleton
44 | fun provideHttpLoggingInterceptor(): Interceptor {
45 | return HttpLoggingInterceptor().apply {
46 | level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY
47 | else HttpLoggingInterceptor.Level.NONE
48 | }
49 | }
50 |
51 | @Provides
52 | @Singleton
53 | fun provideOkHttpClient(
54 | httpLoggingInterceptor: Interceptor,
55 | ): OkHttpClient {
56 | return OkHttpClient.Builder()
57 | .addInterceptor(httpLoggingInterceptor)
58 | .connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS)
59 | .readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)
60 | .writeTimeout(WRITE_TIMEOUT, TimeUnit.SECONDS)
61 | .build()
62 | }
63 |
64 | @Provides
65 | @Singleton
66 | fun provideUserService(
67 | retrofit: Retrofit
68 | ): UserService {
69 | return retrofit.create(UserService::class.java)
70 | }
71 |
72 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/hms/archdemo/di/RepositoryModule.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.di
2 |
3 | import com.hms.archdemo.data.repository.UsersRepositoryImpl
4 | import com.hms.archdemo.domain.repository.UsersRepository
5 | import dagger.Binds
6 | import dagger.Module
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.components.SingletonComponent
9 |
10 |
11 | @Module
12 | @InstallIn(SingletonComponent::class)
13 | abstract class RepositoryModule {
14 |
15 | @Binds
16 | abstract fun provideUsersRepository(
17 | usersRepositoryImpl: UsersRepositoryImpl
18 | ): UsersRepository
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/hms/archdemo/di/UseCaseModule.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.di
2 |
3 | import com.hms.archdemo.domain.use_case.FetchUsersUseCase
4 | import com.hms.archdemo.domain.use_case.FetchUsersUseCaseImpl
5 | import dagger.Binds
6 | import dagger.Module
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.components.SingletonComponent
9 |
10 |
11 | @Module
12 | @InstallIn(SingletonComponent::class)
13 | abstract class UseCaseModule {
14 |
15 | @Binds
16 | abstract fun provideFetchUsersUseCase(
17 | fetchUsersUseCaseImpl: FetchUsersUseCaseImpl
18 | ): FetchUsersUseCase
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hms/archdemo/domain/decider/UserDecider.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.domain.decider
2 |
3 | import com.hms.archdemo.common.extensions.capitalizeFirstChar
4 | import com.hms.archdemo.data.model.NameRemoteModel
5 | import com.hms.archdemo.data.model.PictureRemoteModel
6 | import com.hms.archdemo.domain.model.Gender
7 | import javax.inject.Inject
8 |
9 | class UserDecider @Inject constructor() {
10 |
11 | fun decideFullName(nameModel: NameRemoteModel): String {
12 | return nameModel.first.capitalizeFirstChar() + " " + nameModel.last.uppercase()
13 | }
14 |
15 | fun decideImageUrl(image: PictureRemoteModel): String {
16 | return image.large
17 | }
18 |
19 | fun decideGender(gender: String): Gender {
20 | return Gender.from(value = gender)
21 | }
22 |
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/hms/archdemo/domain/mapper/UserMapper.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.domain.mapper
2 |
3 | import com.hms.archdemo.data.model.UserRemoteModel
4 | import com.hms.archdemo.domain.decider.UserDecider
5 | import com.hms.archdemo.domain.model.User
6 | import javax.inject.Inject
7 |
8 | class UserMapper @Inject constructor(
9 | private val decider: UserDecider
10 | ) {
11 |
12 | fun mapFrom(userRemoteModel: UserRemoteModel): User = with(userRemoteModel) {
13 | User(
14 | fullName = decider.decideFullName(name),
15 | email = email,
16 | phone = phone,
17 | gender = decider.decideGender(gender),
18 | imageUrl = decider.decideImageUrl(userRemoteModel.picture)
19 | )
20 | }
21 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/hms/archdemo/domain/model/Gender.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.domain.model
2 |
3 | enum class Gender(val value: String) {
4 | MALE("male"),
5 | FEMALE("female"),
6 | OTHER("other");
7 |
8 | companion object {
9 | fun from(value: String): Gender = values().find { it.value == value } ?: OTHER
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hms/archdemo/domain/model/User.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.domain.model
2 |
3 | data class User(
4 | val fullName: String,
5 | val email: String,
6 | val phone: String,
7 | val gender: Gender,
8 | val imageUrl: String
9 | )
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hms/archdemo/domain/repository/UsersRepository.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.domain.repository
2 |
3 | import com.hms.archdemo.common.Resource
4 | import com.hms.archdemo.data.model.UserRemoteModel
5 | import kotlinx.coroutines.flow.Flow
6 |
7 | interface UsersRepository {
8 | suspend fun fetchUsers(): Flow>>
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/hms/archdemo/domain/use_case/FetchUsersUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.domain.use_case
2 |
3 | import com.hms.archdemo.common.Resource
4 | import com.hms.archdemo.domain.model.User
5 | import kotlinx.coroutines.flow.Flow
6 |
7 | interface FetchUsersUseCase {
8 | suspend fun fetchUsers(): Flow>>
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/hms/archdemo/domain/use_case/FetchUsersUseCaseImpl.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.domain.use_case
2 |
3 | import com.hms.archdemo.common.Resource
4 | import com.hms.archdemo.common.extensions.map
5 | import com.hms.archdemo.domain.mapper.UserMapper
6 | import com.hms.archdemo.domain.model.User
7 | import com.hms.archdemo.domain.repository.UsersRepository
8 | import kotlinx.coroutines.flow.Flow
9 | import kotlinx.coroutines.flow.map
10 | import javax.inject.Inject
11 |
12 | class FetchUsersUseCaseImpl @Inject constructor(
13 | private val usersRepository: UsersRepository,
14 | private val userMapper: UserMapper,
15 | // you can use multiple repositories in UseCase classes
16 | // you can use other UseCase classes, too
17 | ) : FetchUsersUseCase {
18 |
19 | override suspend fun fetchUsers(): Flow>> {
20 | return usersRepository.fetchUsers().map {
21 | it.map { userRemoteModelList ->
22 | userRemoteModelList.map(userMapper::mapFrom)
23 | }
24 | }
25 | }
26 |
27 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/hms/archdemo/ui/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.ui
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.material.MaterialTheme
8 | import androidx.compose.material.Surface
9 | import androidx.compose.material.Text
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.tooling.preview.Preview
13 | import com.hms.archdemo.ui.theme.ArchitectureDemoTheme
14 | import com.hms.archdemo.ui.users.UserListScreen
15 | import dagger.hilt.android.AndroidEntryPoint
16 |
17 | @AndroidEntryPoint
18 | class MainActivity : ComponentActivity() {
19 | override fun onCreate(savedInstanceState: Bundle?) {
20 | super.onCreate(savedInstanceState)
21 | setContent {
22 | ArchitectureDemoTheme {
23 | // A surface container using the 'background' color from the theme
24 | Surface(
25 | modifier = Modifier.fillMaxSize(),
26 | color = MaterialTheme.colors.background
27 | ) {
28 |
29 | UserListScreen()
30 |
31 | /* val navController = rememberNavController()
32 | NavHost(
33 | navController = navController,
34 | startDestination = "user_list"
35 | ) {
36 | composable(route = "user_list") {
37 | UserListScreen()
38 | }
39 | }*/
40 |
41 | }
42 | }
43 | }
44 | }
45 | }
46 |
47 | @Composable
48 | fun Greeting(name: String) {
49 | Text(text = "Hello $name!")
50 | }
51 |
52 | @Preview(showBackground = true)
53 | @Composable
54 | fun DefaultPreview() {
55 | ArchitectureDemoTheme {
56 | Greeting("Android")
57 | }
58 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/hms/archdemo/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple200 = Color(0xFFBB86FC)
6 | val Purple500 = Color(0xFF6200EE)
7 | val Purple700 = Color(0xFF3700B3)
8 | val Teal200 = Color(0xFF03DAC5)
--------------------------------------------------------------------------------
/app/src/main/java/com/hms/archdemo/ui/theme/Shape.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.ui.theme
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.material.Shapes
5 | import androidx.compose.ui.unit.dp
6 |
7 | val Shapes = Shapes(
8 | small = RoundedCornerShape(4.dp),
9 | medium = RoundedCornerShape(4.dp),
10 | large = RoundedCornerShape(0.dp)
11 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/hms/archdemo/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.ui.theme
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.material.MaterialTheme
5 | import androidx.compose.material.darkColors
6 | import androidx.compose.material.lightColors
7 | import androidx.compose.runtime.Composable
8 |
9 | private val DarkColorPalette = darkColors(
10 | primary = Purple200,
11 | primaryVariant = Purple700,
12 | secondary = Teal200
13 | )
14 |
15 | private val LightColorPalette = lightColors(
16 | primary = Purple500,
17 | primaryVariant = Purple700,
18 | secondary = Teal200
19 |
20 | /* Other default colors to override
21 | background = Color.White,
22 | surface = Color.White,
23 | onPrimary = Color.White,
24 | onSecondary = Color.Black,
25 | onBackground = Color.Black,
26 | onSurface = Color.Black,
27 | */
28 | )
29 |
30 | @Composable
31 | fun ArchitectureDemoTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
32 | val colors = if (darkTheme) {
33 | DarkColorPalette
34 | } else {
35 | LightColorPalette
36 | }
37 |
38 | MaterialTheme(
39 | colors = colors,
40 | typography = Typography,
41 | shapes = Shapes,
42 | content = content
43 | )
44 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/hms/archdemo/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.ui.theme
2 |
3 | import androidx.compose.material.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | body1 = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp
15 | )
16 | /* Other default text styles to override
17 | button = TextStyle(
18 | fontFamily = FontFamily.Default,
19 | fontWeight = FontWeight.W500,
20 | fontSize = 14.sp
21 | ),
22 | caption = TextStyle(
23 | fontFamily = FontFamily.Default,
24 | fontWeight = FontWeight.Normal,
25 | fontSize = 12.sp
26 | )
27 | */
28 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/hms/archdemo/ui/users/UserItemUiState.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.ui.users
2 |
3 | import androidx.compose.ui.graphics.Color
4 | import com.hms.archdemo.domain.model.Gender
5 | import com.hms.archdemo.domain.model.User
6 |
7 | data class UserItemUiState(
8 | private val user: User
9 | ) {
10 | fun getName() = user.fullName
11 |
12 | fun getEmail() = user.email
13 |
14 | fun getPhone() = user.phone
15 |
16 | fun getGenderColor() = if (user.gender == Gender.MALE) Color.Blue else Color.Magenta
17 |
18 | fun getImageUrl() = user.imageUrl
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hms/archdemo/ui/users/UserListScreen.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.ui.users
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.foundation.lazy.LazyColumn
5 | import androidx.compose.foundation.lazy.items
6 | import androidx.compose.material.CircularProgressIndicator
7 | import androidx.compose.material.MaterialTheme
8 | import androidx.compose.material.Text
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Alignment
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.graphics.Color
13 | import androidx.compose.ui.text.style.TextAlign
14 | import androidx.compose.ui.unit.dp
15 | import androidx.hilt.navigation.compose.hiltViewModel
16 | import com.hms.archdemo.ui.users.components.UserListItem
17 |
18 | @Composable
19 | fun UserListScreen(
20 | viewModel: UserViewModel = hiltViewModel()
21 | ) {
22 | val uiState = viewModel.uiState
23 |
24 | Column {
25 |
26 | Text(
27 | "Users",
28 | style = MaterialTheme.typography.h4,
29 | modifier = Modifier.padding(start = 16.dp)
30 | )
31 |
32 | Box(modifier = Modifier.fillMaxSize()) {
33 | if (uiState.usersItemUiStates.isNotEmpty()) {
34 | LazyColumn(modifier = Modifier.fillMaxSize()) {
35 | items(uiState.usersItemUiStates) { userItemUiState ->
36 | UserListItem(userItemUiState = userItemUiState)
37 | }
38 | }
39 | }
40 |
41 | if (uiState.error.isNotEmpty()) {
42 | Text(
43 | text = uiState.error,
44 | color = MaterialTheme.colors.error,
45 | textAlign = TextAlign.Center,
46 | modifier = Modifier
47 | .fillMaxWidth()
48 | .padding(horizontal = 20.dp)
49 | .align(Alignment.Center)
50 | )
51 | }
52 |
53 | if (uiState.isLoading) {
54 | CircularProgressIndicator(
55 | modifier = Modifier
56 | .align(Alignment.Center)
57 | .size(200.dp),
58 | color = Color.Green
59 | )
60 | }
61 | }
62 |
63 | }
64 |
65 |
66 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/hms/archdemo/ui/users/UserListUiState.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.ui.users
2 |
3 | data class UserListUiState(
4 | val isLoading: Boolean,
5 | val usersItemUiStates: List,
6 | val error: String
7 | ) {
8 |
9 | companion object {
10 | fun initial() = UserListUiState(
11 | isLoading = true,
12 | usersItemUiStates = emptyList(),
13 | error = ""
14 | )
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/app/src/main/java/com/hms/archdemo/ui/users/UserViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.ui.users
2 |
3 | import androidx.compose.runtime.getValue
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.compose.runtime.setValue
6 | import androidx.lifecycle.ViewModel
7 | import androidx.lifecycle.viewModelScope
8 | import com.hms.archdemo.common.Resource.*
9 | import com.hms.archdemo.domain.model.User
10 | import com.hms.archdemo.domain.use_case.FetchUsersUseCase
11 | import dagger.hilt.android.lifecycle.HiltViewModel
12 | import kotlinx.coroutines.flow.collect
13 | import kotlinx.coroutines.flow.onEach
14 | import kotlinx.coroutines.launch
15 | import javax.inject.Inject
16 |
17 | @HiltViewModel
18 | class UserViewModel @Inject constructor(
19 | private val fetchUsersUseCase: FetchUsersUseCase
20 | ) : ViewModel() {
21 |
22 | var uiState by mutableStateOf(UserListUiState.initial())
23 | private set
24 |
25 | init {
26 | getUsers()
27 | }
28 |
29 | private fun getUsers() {
30 | viewModelScope.launch {
31 | fetchUsersUseCase.fetchUsers().onEach { result ->
32 | when (result) {
33 | is Error -> anErrorOccurred(result.exception?.message.orEmpty())
34 | Loading -> setLoading()
35 | is Success -> setUsers(result.data)
36 | }
37 | }.collect()
38 | }
39 | }
40 |
41 | private fun anErrorOccurred(errorMessage: String) {
42 | uiState = uiState.copy(
43 | isLoading = false,
44 | error = errorMessage,
45 | usersItemUiStates = emptyList()
46 | )
47 | }
48 |
49 | private fun setUsers(users: List) {
50 | uiState = uiState.copy(
51 | isLoading = false,
52 | usersItemUiStates = users.map { user -> UserItemUiState(user = user) },
53 | error = ""
54 | )
55 | }
56 |
57 | private fun setLoading() {
58 | uiState = uiState.copy(
59 | isLoading = true,
60 | usersItemUiStates = emptyList(),
61 | error = ""
62 | )
63 | }
64 |
65 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/hms/archdemo/ui/users/components/UserListItem.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.ui.users.components
2 |
3 | import android.content.res.Configuration
4 | import androidx.compose.foundation.layout.*
5 | import androidx.compose.material.Card
6 | import androidx.compose.material.MaterialTheme
7 | import androidx.compose.material.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.draw.clip
11 | import androidx.compose.ui.layout.ContentScale
12 | import androidx.compose.ui.platform.LocalConfiguration
13 | import androidx.compose.ui.unit.dp
14 | import com.hms.archdemo.ui.users.UserItemUiState
15 | import com.skydoves.landscapist.glide.GlideImage
16 |
17 | @Composable
18 | fun UserListItem(
19 | userItemUiState: UserItemUiState
20 | ) {
21 | Card(
22 | elevation = 8.dp,
23 | modifier = Modifier
24 | .fillMaxWidth()
25 | .padding(top = 16.dp, start = 16.dp, end = 16.dp)
26 | .wrapContentHeight(),
27 | backgroundColor = userItemUiState.getGenderColor()
28 |
29 | ) {
30 |
31 | Row(
32 | modifier = Modifier.fillMaxWidth()
33 | ) {
34 | GlideImage(
35 | modifier = Modifier
36 | .fillMaxWidth(CalculateImageWidthFractionAccordingOrientation())
37 | .aspectRatio(0.6f)
38 | .clip(MaterialTheme.shapes.medium),
39 | contentScale = ContentScale.Crop,
40 | imageModel = userItemUiState.getImageUrl(),
41 | )
42 |
43 | Column(modifier = Modifier.padding(start = 16.dp, top = 4.dp)) {
44 | Text(userItemUiState.getName(), style = MaterialTheme.typography.h5)
45 | Spacer(modifier = Modifier.height(8.dp))
46 |
47 | Text(userItemUiState.getEmail())
48 | Spacer(modifier = Modifier.height(8.dp))
49 |
50 | Text(userItemUiState.getPhone())
51 | Spacer(modifier = Modifier.height(8.dp))
52 | }
53 | }
54 | }
55 |
56 | }
57 |
58 | @Composable
59 | private fun CalculateImageWidthFractionAccordingOrientation(): Float {
60 | val orientation = LocalConfiguration.current.orientation
61 | return if (orientation == Configuration.ORIENTATION_LANDSCAPE)
62 | 0.15f
63 | else 0.4f
64 | }
65 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cengiztoru/CleanArch-with-Unit-Test/ac25e65816a64329bfe4b9164ab5cb88f21b80e7/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cengiztoru/CleanArch-with-Unit-Test/ac25e65816a64329bfe4b9164ab5cb88f21b80e7/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cengiztoru/CleanArch-with-Unit-Test/ac25e65816a64329bfe4b9164ab5cb88f21b80e7/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cengiztoru/CleanArch-with-Unit-Test/ac25e65816a64329bfe4b9164ab5cb88f21b80e7/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cengiztoru/CleanArch-with-Unit-Test/ac25e65816a64329bfe4b9164ab5cb88f21b80e7/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cengiztoru/CleanArch-with-Unit-Test/ac25e65816a64329bfe4b9164ab5cb88f21b80e7/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cengiztoru/CleanArch-with-Unit-Test/ac25e65816a64329bfe4b9164ab5cb88f21b80e7/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cengiztoru/CleanArch-with-Unit-Test/ac25e65816a64329bfe4b9164ab5cb88f21b80e7/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cengiztoru/CleanArch-with-Unit-Test/ac25e65816a64329bfe4b9164ab5cb88f21b80e7/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cengiztoru/CleanArch-with-Unit-Test/ac25e65816a64329bfe4b9164ab5cb88f21b80e7/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Architecture Demo
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/test/java/com/hms/archdemo/data/repository/FakeUsersRepository.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.data.repository
2 |
3 | import com.hms.archdemo.common.Resource
4 | import com.hms.archdemo.data.model.UserRemoteModel
5 | import com.hms.archdemo.domain.repository.UsersRepository
6 | import com.hms.archdemo.domain.use_case.dummyThrowable
7 | import kotlinx.coroutines.flow.Flow
8 | import kotlinx.coroutines.flow.MutableStateFlow
9 | import kotlinx.coroutines.flow.update
10 |
11 | class FakeUsersRepository : UsersRepository {
12 |
13 | private val usersFlow = MutableStateFlow>>(Resource.Loading)
14 |
15 | override suspend fun fetchUsers(): Flow>> = usersFlow
16 |
17 | fun sendRemoteUsers(users: List) {
18 | usersFlow.update {
19 | Resource.Success(users)
20 | }
21 | }
22 |
23 | fun removeUsers() {
24 | usersFlow.update {
25 | Resource.Error(dummyThrowable())
26 | }
27 | }
28 |
29 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/hms/archdemo/domain/decider/UserDeciderTest.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.domain.decider
2 |
3 | import com.hms.archdemo.data.model.NameRemoteModel
4 | import com.hms.archdemo.data.model.PictureRemoteModel
5 | import com.hms.archdemo.domain.model.Gender
6 | import com.hms.archdemo.util.extensions.shouldEqual
7 | import org.junit.Before
8 | import org.junit.Test
9 |
10 | class UserDeciderTest {
11 |
12 | private lateinit var decider: UserDecider
13 |
14 | @Before
15 | fun setUp() {
16 | decider = UserDecider()
17 | }
18 |
19 | @Test
20 | fun `given remote name model, when decideFullName called, then should capitalize first name, uppercase last name and finally merging them`() {
21 |
22 | //given
23 | val nameRemoteModel = NameRemoteModel("Mr", "cengiz", "toru")
24 |
25 | val expectedUserFullName = "Cengiz TORU"
26 |
27 | //when
28 | val actualUserFullName = decider.decideFullName(nameRemoteModel)
29 |
30 | //then
31 | actualUserFullName shouldEqual expectedUserFullName
32 | }
33 |
34 | @Test
35 | fun `given male, when decideGender called, then should return gender as MALE `() {
36 |
37 | //given
38 | val gender = "male"
39 |
40 | val expectedGender = Gender.MALE
41 |
42 | //when
43 | val actualGender = decider.decideGender(gender)
44 |
45 | //then
46 | actualGender shouldEqual expectedGender
47 | }
48 |
49 | @Test
50 | fun `given female, when decideGender called, then should return gender as FEMALE `() {
51 |
52 | //given
53 | val gender = "female"
54 |
55 | val expectedGender = Gender.FEMALE
56 |
57 | //when
58 | val actualGender = decider.decideGender(gender)
59 |
60 | //then
61 | actualGender shouldEqual expectedGender
62 |
63 | }
64 |
65 | @Test
66 | fun `given unknown gender, when decideGender called, then should return gender as OHTER `() {
67 |
68 | //given
69 | val gender = "somethings"
70 |
71 | val expectedGender = Gender.OTHER
72 |
73 | //when
74 | val actualGender = decider.decideGender(gender)
75 |
76 | //then
77 | actualGender shouldEqual expectedGender
78 |
79 | }
80 |
81 | @Test
82 | fun `given PictureRemoteModel, when decideImageUrl called, then should return large image url `() {
83 |
84 | //given
85 | val pictureRemoteModel =
86 | PictureRemoteModel("largeImageUrl", "mediumImageUrl", "thumbnailImageUrl")
87 |
88 | val expectedImageUrl = pictureRemoteModel.large
89 |
90 | //when
91 | val actualImageUrl = decider.decideImageUrl(pictureRemoteModel)
92 |
93 | //then
94 | actualImageUrl shouldEqual expectedImageUrl
95 |
96 | }
97 |
98 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/hms/archdemo/domain/mapper/UserFactory.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.domain.mapper
2 |
3 | import com.hms.archdemo.data.model.NameRemoteModel
4 | import com.hms.archdemo.data.model.PictureRemoteModel
5 | import com.hms.archdemo.data.model.UserRemoteModel
6 | import com.hms.archdemo.domain.model.Gender
7 | import com.hms.archdemo.domain.model.User
8 |
9 | object UserFactory {
10 | private val FAKE_USER_NAME = NameRemoteModel("mr", "cengiz", "toru")
11 | private const val FAKE_USER_EMAIL = "ct@ct.com"
12 | private const val FAKE_USER_PHONE = "+905554443322"
13 | private val FAKE_PICTURE_MODEL =
14 | PictureRemoteModel("largeImageUrl", "mediumImageUrl", "thumbnailImageUrl")
15 |
16 | fun createUserRemoteModel(gender: String): UserRemoteModel {
17 | return UserRemoteModel(
18 | name = FAKE_USER_NAME,
19 | gender = gender,
20 | picture = FAKE_PICTURE_MODEL,
21 | email = FAKE_USER_EMAIL,
22 | phone = FAKE_USER_PHONE
23 | )
24 | }
25 |
26 | fun createUser(gender: Gender): User {
27 | return User(
28 | fullName = "Cengiz TORU",
29 | gender = gender,
30 | imageUrl = FAKE_PICTURE_MODEL.large,
31 | email = FAKE_USER_EMAIL,
32 | phone = FAKE_USER_PHONE
33 | )
34 | }
35 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/hms/archdemo/domain/mapper/UserMapperTest.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.domain.mapper
2 |
3 | import android.os.Build
4 | import com.hms.archdemo.data.model.UserRemoteModel
5 | import com.hms.archdemo.domain.decider.UserDecider
6 | import com.hms.archdemo.domain.model.Gender
7 | import com.hms.archdemo.domain.model.User
8 | import com.hms.archdemo.util.extensions.shouldEqual
9 | import org.junit.Test
10 | import org.junit.runner.RunWith
11 | import org.robolectric.ParameterizedRobolectricTestRunner
12 | import org.robolectric.annotation.Config
13 |
14 | @RunWith(ParameterizedRobolectricTestRunner::class)
15 | @Config(sdk = [Build.VERSION_CODES.O_MR1], manifest = Config.NONE)
16 | class UserMapperTest constructor(
17 | private val userRemoteModel: UserRemoteModel,
18 | private val expectedUser: User
19 | ) {
20 |
21 |
22 | @Test
23 | fun `Given userRemoteModel, When mapFrom method called, Then should return expectedUser model`() {
24 |
25 | //Given
26 | val userDecider = UserDecider()
27 | val userMapper = UserMapper(userDecider)
28 |
29 | //When
30 | val actualUser = userMapper.mapFrom(userRemoteModel)
31 |
32 | //Then
33 | actualUser shouldEqual expectedUser
34 | }
35 |
36 | companion object {
37 | @JvmStatic
38 | @ParameterizedRobolectricTestRunner.Parameters(name = "given userRemoteModel {0}, expectedUser {1}")
39 | fun provideParameters(): List> {
40 | return listOf(
41 | getUserCase1(),
42 | getUserCase2(),
43 | getUserCase3(),
44 | )
45 | }
46 |
47 | private fun getUserCase1() = arrayOf(
48 | //Male User
49 | UserFactory.createUserRemoteModel(gender = "male"),
50 | UserFactory.createUser(gender = Gender.MALE)
51 | )
52 |
53 | private fun getUserCase2() = arrayOf(
54 | //Female User
55 | UserFactory.createUserRemoteModel(gender = "female"),
56 | UserFactory.createUser(gender = Gender.FEMALE)
57 | )
58 |
59 | private fun getUserCase3() = arrayOf(
60 | //UnDefined Gender
61 | UserFactory.createUserRemoteModel(gender = "somethings"),
62 | UserFactory.createUser(gender = Gender.OTHER)
63 | )
64 | }
65 |
66 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/hms/archdemo/domain/use_case/FakeFetchUsersUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.domain.use_case
2 |
3 | import com.hms.archdemo.common.Resource
4 | import com.hms.archdemo.domain.model.User
5 | import kotlinx.coroutines.flow.Flow
6 | import kotlinx.coroutines.flow.flow
7 |
8 | class FakeFetchUsersUseCase(
9 | private val isSuccessful: Boolean = true
10 | ) : FetchUsersUseCase {
11 |
12 | override suspend fun fetchUsers(): Flow>> {
13 | return flow {
14 | if (isSuccessful) {
15 | emit(
16 | Resource.Success(dummyUserList())
17 | )
18 | } else {
19 | emit(Resource.Error(dummyErrorException()))
20 | }
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/hms/archdemo/domain/use_case/FetchUsersUseCaseTest.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.domain.use_case
2 |
3 | import com.hms.archdemo.common.Resource
4 | import com.hms.archdemo.data.repository.FakeUsersRepository
5 | import com.hms.archdemo.domain.decider.UserDecider
6 | import com.hms.archdemo.domain.mapper.UserMapper
7 | import com.hms.archdemo.util.CoroutineRule
8 | import com.hms.archdemo.util.extensions.shouldEqual
9 | import kotlinx.coroutines.ExperimentalCoroutinesApi
10 | import kotlinx.coroutines.flow.collect
11 | import kotlinx.coroutines.flow.first
12 | import kotlinx.coroutines.launch
13 | import kotlinx.coroutines.test.runTest
14 | import org.junit.Before
15 | import org.junit.Rule
16 | import org.junit.Test
17 |
18 |
19 | @OptIn(ExperimentalCoroutinesApi::class)
20 | class FetchUsersUseCaseTest {
21 |
22 | @get:Rule
23 | val coroutineRule = CoroutineRule()
24 |
25 | private val usersRepository = FakeUsersRepository()
26 | private val userDecider = UserDecider()
27 | private val userMapper = UserMapper(userDecider)
28 |
29 | private lateinit var fetchUsersUseCase: FetchUsersUseCase
30 |
31 | @Before
32 | fun setup() {
33 | fetchUsersUseCase = FetchUsersUseCaseImpl(usersRepository, userMapper)
34 | }
35 |
36 | @Test
37 | fun `When starting to fetch users then repository should expose Loading initially`() = runTest {
38 | Resource.Loading shouldEqual fetchUsersUseCase.fetchUsers().first()
39 | }
40 |
41 | @Test
42 | fun `Given remote models when fetching users from remote then use case should return remote models as ui models`() =
43 | runTest {
44 | val collectJob =
45 | launch(coroutineRule.testDispatcher) { fetchUsersUseCase.fetchUsers().collect() }
46 | //Given
47 | usersRepository.sendRemoteUsers(dummyRemoteModelUserList())
48 |
49 | //When
50 | val fetchUsersResult = fetchUsersUseCase.fetchUsers().first()
51 |
52 | //Then
53 | assert(fetchUsersResult is Resource.Success)
54 |
55 | val actualData = (fetchUsersUseCase.fetchUsers()
56 | .first() as Resource.Success).data
57 |
58 | val expectedUsers = dummyUserList()
59 |
60 | actualData shouldEqual expectedUsers
61 |
62 | collectJob.cancel()
63 | }
64 |
65 | @Test
66 | fun `Given error when fetching users from remote then use case should expose failure`() =
67 | runTest {
68 | val collectJob =
69 | launch(coroutineRule.testDispatcher) { fetchUsersUseCase.fetchUsers().collect() }
70 |
71 | //Given
72 | usersRepository.removeUsers()
73 |
74 | //When
75 | val fetchUsersResult = fetchUsersUseCase.fetchUsers().first()
76 |
77 | //Then
78 | assert(fetchUsersResult is Resource.Error)
79 |
80 | val actualException = (fetchUsersUseCase.fetchUsers()
81 | .first() as Resource.Error).exception
82 |
83 | assert(actualException != null)
84 |
85 | val expectedException = dummyThrowable()
86 |
87 | actualException!!.message shouldEqual expectedException.message
88 |
89 | collectJob.cancel()
90 | }
91 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/hms/archdemo/domain/use_case/FetchUsersUseCaseTestWithTurbine.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.domain.use_case
2 |
3 | import app.cash.turbine.test
4 | import com.hms.archdemo.common.Resource
5 | import com.hms.archdemo.data.repository.FakeUsersRepository
6 | import com.hms.archdemo.domain.decider.UserDecider
7 | import com.hms.archdemo.domain.mapper.UserMapper
8 | import com.hms.archdemo.util.CoroutineRule
9 | import com.hms.archdemo.util.extensions.shouldEqual
10 | import kotlinx.coroutines.ExperimentalCoroutinesApi
11 | import kotlinx.coroutines.test.runTest
12 | import org.junit.Before
13 | import org.junit.Rule
14 | import org.junit.Test
15 |
16 |
17 | @OptIn(ExperimentalCoroutinesApi::class)
18 | class FetchUsersUseCaseTestWithTurbine {
19 |
20 | @get:Rule
21 | val coroutineRule = CoroutineRule()
22 |
23 | private val usersRepository = FakeUsersRepository()
24 | private val userDecider = UserDecider()
25 | private val userMapper = UserMapper(userDecider)
26 |
27 | private lateinit var fetchUsersUseCase: FetchUsersUseCase
28 |
29 | @Before
30 | fun setup() {
31 | fetchUsersUseCase = FetchUsersUseCaseImpl(usersRepository, userMapper)
32 | }
33 |
34 | @Test
35 | fun `When starting to fetch users then repository should expose Loading initially`() = runTest {
36 | fetchUsersUseCase.fetchUsers().test {
37 | awaitItem() shouldEqual Resource.Loading
38 | }
39 | }
40 |
41 | @Test
42 | fun `Given remote models when fetching users from remote then use case should expose remote models as ui models`() =
43 | runTest {
44 | //Given
45 | usersRepository.sendRemoteUsers(dummyRemoteModelUserList())
46 |
47 | fetchUsersUseCase.fetchUsers().test {
48 |
49 | //When
50 | val result = awaitItem()
51 |
52 | //Then
53 | assert(result is Resource.Success)
54 |
55 | val actualUsers = (result as Resource.Success).data
56 | val expectedUsers = dummyUserList()
57 |
58 | actualUsers shouldEqual expectedUsers
59 | }
60 | }
61 |
62 | @Test
63 | fun `Given error when fetching users from remote then use case should expose error message`() =
64 | runTest {
65 | //Given
66 | usersRepository.removeUsers()
67 |
68 | fetchUsersUseCase.fetchUsers().test {
69 |
70 | //When
71 | val fetchUsersResult = awaitItem()
72 |
73 | //Then
74 | assert(fetchUsersResult is Resource.Error)
75 |
76 | val actualException =
77 | (fetchUsersResult as Resource.Error).exception
78 |
79 | assert(actualException != null)
80 |
81 | val expectedException = dummyThrowable()
82 |
83 | actualException!!.message shouldEqual expectedException.message
84 |
85 | }
86 |
87 | }
88 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/hms/archdemo/domain/use_case/TestModels.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.domain.use_case
2 |
3 | import com.hms.archdemo.domain.mapper.UserFactory
4 | import com.hms.archdemo.domain.model.Gender
5 | import com.hms.archdemo.ui.users.UserItemUiState
6 | import java.io.IOException
7 |
8 | /** REMOTE MODEL */
9 | fun maleRemoteUser() = UserFactory.createUserRemoteModel("male")
10 | fun femaleRemoteUser() = UserFactory.createUserRemoteModel("female")
11 | fun undefinedRemoteUser() = UserFactory.createUserRemoteModel("somethings")
12 | fun dummyRemoteModelUserList() = listOf(
13 | maleRemoteUser(),
14 | femaleRemoteUser(),
15 | undefinedRemoteUser()
16 | )
17 |
18 |
19 | /** USER UI MODEL */
20 | fun maleUser() = UserFactory.createUser(Gender.MALE)
21 | fun femaleUser() = UserFactory.createUser(Gender.FEMALE)
22 | fun undefinedUser() = UserFactory.createUser(Gender.OTHER)
23 | fun dummyUserList() = listOf(
24 | maleUser(),
25 | femaleUser(),
26 | undefinedUser()
27 | )
28 |
29 | fun fakeUserListItemsWithUiState() = dummyUserList().map { user -> UserItemUiState(user) }
30 |
31 | fun dummyErrorException() = IOException("An Error Occurred")
32 |
33 | fun dummyThrowable() = Throwable("A dummy throwable")
--------------------------------------------------------------------------------
/app/src/test/java/com/hms/archdemo/ui/users/ParameterizedUserItemUiStateTest.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.ui.users
2 |
3 | import android.os.Build
4 | import androidx.compose.ui.graphics.Color
5 | import com.hms.archdemo.domain.model.Gender
6 | import com.hms.archdemo.domain.model.User
7 | import com.hms.archdemo.util.extensions.shouldEqual
8 | import org.junit.Test
9 | import org.junit.runner.RunWith
10 | import org.robolectric.ParameterizedRobolectricTestRunner
11 | import org.robolectric.annotation.Config
12 |
13 | data class ColorWrapper(val color: Color)
14 |
15 | @RunWith(ParameterizedRobolectricTestRunner::class)
16 | @Config(sdk = [Build.VERSION_CODES.O_MR1], manifest = Config.NONE)
17 | class ParameterizedUserItemUiStateTest constructor(
18 | val userGender: Gender,
19 | val expectedColorWrapper: ColorWrapper
20 | ) {
21 | @Test
22 | fun `Given userGender as parameter, When getGenderColor method called, Then should return expectedColor`() {
23 |
24 | //Given
25 | val expectedColor = expectedColorWrapper.color
26 | val user = User("", "", "", userGender, "")
27 | val userItemUiSate = UserItemUiState(user)
28 |
29 | //When
30 | val actualColor = userItemUiSate.getGenderColor()
31 |
32 | //Then
33 | actualColor shouldEqual expectedColor
34 | }
35 |
36 | companion object {
37 | @JvmStatic
38 | @ParameterizedRobolectricTestRunner.Parameters(name = "given userGender {0}, expectedColor {1}")
39 | fun provideParameters(): List> {
40 | return listOf(
41 | arrayOf(Gender.MALE, ColorWrapper(Color.Blue)),
42 | arrayOf(Gender.FEMALE, ColorWrapper(Color.Magenta)),
43 | arrayOf(Gender.OTHER, ColorWrapper(Color.Magenta)),
44 | )
45 | }
46 | }
47 |
48 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/hms/archdemo/ui/users/UserItemUiStateTest.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.ui.users
2 |
3 | import androidx.compose.ui.graphics.Color
4 | import com.hms.archdemo.domain.model.Gender
5 | import com.hms.archdemo.domain.model.User
6 | import com.hms.archdemo.util.extensions.shouldEqual
7 | import org.junit.Test
8 |
9 | class UserItemUiStateTest {
10 |
11 | @Test
12 | fun `Given MALE user as parameter, When getGenderColor method called, Then should return Blue Color`() {
13 |
14 | //Given
15 | val expectedColor = Color.Blue
16 | val user = User("", "", "", Gender.MALE, "")
17 | val userItemUiSate = UserItemUiState(user)
18 |
19 | //When
20 | val actualColor = userItemUiSate.getGenderColor()
21 |
22 | //Then
23 | actualColor shouldEqual expectedColor
24 |
25 | }
26 |
27 | @Test
28 | fun `Given FEMALE user as parameter, When getGenderColor method called, Then should return Magenta Color`() {
29 |
30 | //Given
31 | val expectedColor = Color.Magenta
32 | val user = User("", "", "", Gender.FEMALE, "")
33 | val userItemUiSate = UserItemUiState(user)
34 |
35 | //When
36 | val actualColor = userItemUiSate.getGenderColor()
37 |
38 | //Then
39 | actualColor shouldEqual expectedColor
40 |
41 | }
42 |
43 | @Test
44 | fun `Given OTHER gender user as parameter, When getGenderColor method called, Then should return Magenta Color`() {
45 |
46 | //Given
47 | val expectedColor = Color.Magenta
48 | val user = User("", "", "", Gender.OTHER, "")
49 | val userItemUiSate = UserItemUiState(user)
50 |
51 | //When
52 | val actualColor = userItemUiSate.getGenderColor()
53 |
54 | //Then
55 | actualColor shouldEqual expectedColor
56 |
57 | }
58 |
59 |
60 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/hms/archdemo/ui/users/UserViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.ui.users
2 |
3 | import com.hms.archdemo.domain.use_case.FakeFetchUsersUseCase
4 | import com.hms.archdemo.domain.use_case.dummyErrorException
5 | import com.hms.archdemo.domain.use_case.fakeUserListItemsWithUiState
6 | import com.hms.archdemo.util.CoroutineRule
7 | import com.hms.archdemo.util.extensions.shouldEqual
8 | import kotlinx.coroutines.ExperimentalCoroutinesApi
9 | import org.junit.Assert.assertTrue
10 | import org.junit.Rule
11 | import org.junit.Test
12 |
13 | @OptIn(ExperimentalCoroutinesApi::class)
14 | class UserViewModelTest {
15 |
16 | @get:Rule
17 | val coroutineRule = CoroutineRule()
18 |
19 | @Test
20 | fun `Given creating UserViewModel, Then should exposes loading ui state initially`() {
21 |
22 | //Given
23 | val fetchUsersUseCase = FakeFetchUsersUseCase()
24 | val viewModel = UserViewModel(fetchUsersUseCase)
25 |
26 | //Then
27 | assertTrue(viewModel.uiState.isLoading)
28 | }
29 |
30 | @Test
31 | fun `Given fetching users successfully, When getUsers called, Then uiState should update as success`() {
32 |
33 | //Given
34 | val fetchUsersUseCase = FakeFetchUsersUseCase(isSuccessful = true)
35 | val viewModel = UserViewModel(fetchUsersUseCase)
36 |
37 | val expectedUiState = UserListUiState(
38 | isLoading = false,
39 | fakeUserListItemsWithUiState(),
40 | error = ""
41 | )
42 |
43 | //When
44 | coroutineRule.testDispatcher.scheduler.runCurrent()
45 | val actualUiState = viewModel.uiState
46 |
47 | //Then
48 | actualUiState shouldEqual expectedUiState
49 |
50 | }
51 |
52 | @Test
53 | fun `Given fetching users failed, When getUsers called, Then uiState should update as error`() {
54 |
55 | //Given
56 | val fetchUsersUseCase = FakeFetchUsersUseCase(isSuccessful = false)
57 | val viewModel = UserViewModel(fetchUsersUseCase)
58 |
59 | val expectedUiState = UserListUiState(
60 | isLoading = false,
61 | usersItemUiStates = emptyList(),
62 | error = dummyErrorException().message.orEmpty()
63 | )
64 |
65 | //When
66 | coroutineRule.testDispatcher.scheduler.runCurrent()
67 | val actualUiState = viewModel.uiState
68 |
69 | //Then
70 | actualUiState shouldEqual expectedUiState
71 |
72 | }
73 |
74 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/hms/archdemo/util/CoroutineRule.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.util
2 |
3 | import kotlinx.coroutines.Dispatchers
4 | import kotlinx.coroutines.ExperimentalCoroutinesApi
5 | import kotlinx.coroutines.test.StandardTestDispatcher
6 | import kotlinx.coroutines.test.TestDispatcher
7 | import kotlinx.coroutines.test.resetMain
8 | import kotlinx.coroutines.test.setMain
9 | import org.junit.rules.TestWatcher
10 | import org.junit.runner.Description
11 |
12 | @ExperimentalCoroutinesApi
13 | class CoroutineRule(
14 | val testDispatcher: TestDispatcher = StandardTestDispatcher()
15 | ) : TestWatcher() {
16 |
17 | override fun starting(description: Description) {
18 | Dispatchers.setMain(testDispatcher)
19 | }
20 |
21 | override fun finished(description: Description) {
22 | Dispatchers.resetMain()
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/hms/archdemo/util/extensions/TruthExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.hms.archdemo.util.extensions
2 |
3 | import com.google.common.truth.Truth.assertThat
4 |
5 | infix fun Any?.shouldEqual(expected: Any?) {
6 | assertThat(this).isEqualTo(expected)
7 | }
8 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext {
3 | compose_version = '1.1.1'
4 | }
5 | }// Top-level build file where you can add configuration options common to all sub-projects/modules.
6 | plugins {
7 | id 'com.android.application' version '7.1.0' apply false
8 | id 'com.android.library' version '7.1.0' apply false
9 | id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
10 | id 'com.google.dagger.hilt.android' version '2.41' apply false
11 | }
12 |
13 | task clean(type: Delete) {
14 | delete rootProject.buildDir
15 | }
--------------------------------------------------------------------------------
/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=-Xmx2048m -Dfile.encoding=UTF-8
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 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cengiztoru/CleanArch-with-Unit-Test/ac25e65816a64329bfe4b9164ab5cb88f21b80e7/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Jun 21 12:23:26 TRT 2022
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 | rootProject.name = "Architecture Demo"
16 | include ':app'
17 |
--------------------------------------------------------------------------------