├── .DS_Store
├── .idea
├── encodings.xml
├── markdown-navigator
│ └── profiles_settings.xml
├── modules.xml
├── nodes-kotlin-template.iml
└── workspace.xml
├── README.md
├── art
└── usage.png
├── cookiecutter.json
└── {{cookiecutter.repo_name}}
├── .gitignore
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── {{ cookiecutter.package_dir }}
│ │ └── ExampleInstrumentedTest.kt
│ ├── debug
│ ├── AndroidManifest.xml
│ └── res
│ │ └── xml
│ │ └── network_security_config.xml
│ ├── main
│ ├── AndroidManifest.xml
│ ├── assets
│ │ └── all_translations.json
│ ├── java
│ │ └── {{ cookiecutter.package_dir }}
│ │ │ ├── App.kt
│ │ │ ├── inititializers
│ │ │ └── AppInitializer.kt
│ │ │ ├── injection
│ │ │ ├── components
│ │ │ │ └── AppComponent.kt
│ │ │ └── modules
│ │ │ │ ├── AppModule.kt
│ │ │ │ ├── ExecutorModule.kt
│ │ │ │ ├── InteractorModule.kt
│ │ │ │ ├── RestModule.kt
│ │ │ │ ├── RestRepositoryBinding.kt
│ │ │ │ └── StorageBindingModule.kt
│ │ │ └── storage
│ │ │ ├── PrefManagerImpl.kt
│ │ │ └── base
│ │ │ └── GsonFileStorageRepository.kt
│ └── res
│ │ ├── menu
│ │ └── main.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ └── values
│ │ ├── attrs.xml
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── ids.xml
│ │ ├── nstack_keys.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ └── test
│ └── java
│ └── {{ cookiecutter.package_dir }}
│ └── ExampleUnitTest.kt
├── build.gradle
├── data
├── .gitignore
├── build.gradle
└── src
│ └── main
│ └── java
│ └── {{ cookiecutter.package_dir }}
│ ├── models
│ ├── Photo.kt
│ └── Post.kt
│ ├── network
│ ├── Api.kt
│ ├── RestPostRepository.kt
│ └── util
│ │ ├── BufferedSourceConverterFactory.kt
│ │ ├── DateDeserializer.kt
│ │ ├── ItemTypeAdapterFactory.kt
│ │ └── RetrofitExtensions.kt
│ └── repositories
│ ├── PostRepository.kt
│ └── RepositoryException.kt
├── domain
├── .gitignore
├── build.gradle
└── src
│ └── main
│ └── java
│ └── {{ cookiecutter.package_dir }}
│ └── domain
│ ├── extensions
│ └── Extensions.kt
│ ├── interactors
│ ├── BaseAsyncInteractor.kt
│ ├── InteractorResult.kt
│ └── PostsInteractor.kt
│ └── managers
│ └── PrefManager.kt
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── presentation
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── {{ cookiecutter.package_dir }}
│ │ └── presentation
│ │ └── ExampleInstrumentedTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ ├── assets
│ │ └── all_translations.json
│ ├── java
│ │ └── {{ cookiecutter.package_dir }}
│ │ │ └── presentation
│ │ │ ├── extensions
│ │ │ ├── ContextExtensions.kt
│ │ │ ├── InteractorExtensions.kt
│ │ │ ├── LifecycleOwnerExtensions.kt
│ │ │ └── LiveDataExtensions.kt
│ │ │ ├── injection
│ │ │ ├── DaggerViewModelFactory.kt
│ │ │ ├── ViewModelBuilder.kt
│ │ │ └── ViewModelKey.kt
│ │ │ ├── nstack
│ │ │ └── Translation.java
│ │ │ ├── ui
│ │ │ ├── base
│ │ │ │ ├── BaseActivity.kt
│ │ │ │ ├── BaseFragment.kt
│ │ │ │ ├── BaseViewModel.kt
│ │ │ │ └── BaseViewModelExtensions.kt
│ │ │ └── main
│ │ │ │ ├── MainActivity+Hockey.kt
│ │ │ │ ├── MainActivity+NStack.kt
│ │ │ │ ├── MainActivity.kt
│ │ │ │ ├── MainActivityBuilder.kt
│ │ │ │ ├── MainActivityViewModel.kt
│ │ │ │ └── MainActivityViewState.kt
│ │ │ └── util
│ │ │ ├── SharedElementHelper.kt
│ │ │ └── SingleEvent.kt
│ └── res
│ │ ├── layout
│ │ └── activity_main.xml
│ │ └── values
│ │ ├── nstack_keys.xml
│ │ └── strings.xml
│ └── test
│ └── java
│ └── {{ cookiecutter.package_dir }}
│ └── presentation
│ └── ExampleUnitTest.java
└── settings.gradle
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dtunctuncer/kotlin-template-starter/16da3b46b0d88dac6af1181071bbef11af5390a2/.DS_Store
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/markdown-navigator/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/nodes-kotlin-template.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/workspace.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | kotlin-template
91 | kotlin-temp
92 | kotlin-template.
93 |
94 |
95 |
96 |
97 |
98 |
99 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # kotlin-template-starter
2 | Kotlin Clean Architecture Template Project. Uses dagger2 for dependency injection y'all. It is based on [nodes-kotlin-template](https://github.com/nodes-android/kotlin-template)
3 |
4 | You can change some common stuff
5 |
6 | 
7 |
8 | ### Install
9 | ```
10 | 1. Install python
11 | 2. Install pip
12 | 3. pip install cookiecutter
13 | ```
14 |
15 | Scaffold your project:
16 | ```
17 | cookiecutter https://github.com/dtunctuncer/kotlin-template-starter.git
18 | ```
19 | ### Project Tree After Scaffold
20 | ```bash
21 | ├── app
22 | │ ├── app.iml
23 | │ ├── build.gradle
24 | │ ├── proguard-rules.pro
25 | │ └── src
26 | │ ├── androidTest
27 | │ │ └── java
28 | │ │ └── com
29 | │ │ └── nodesagency
30 | │ │ └── test
31 | │ │ └── ExampleInstrumentedTest.kt
32 | │ ├── debug
33 | │ │ ├── AndroidManifest.xml
34 | │ │ └── res
35 | │ │ └── xml
36 | │ │ └── network_security_config.xml
37 | │ ├── main
38 | │ │ ├── AndroidManifest.xml
39 | │ │ ├── assets
40 | │ │ │ └── all_translations.json
41 | │ │ ├── java
42 | │ │ │ └── com
43 | │ │ │ └── nodesagency
44 | │ │ │ └── test
45 | │ │ │ ├── App.kt
46 | │ │ │ ├── inititializers
47 | │ │ │ │ └── AppInitializer.kt
48 | │ │ │ ├── injection
49 | │ │ │ │ ├── components
50 | │ │ │ │ │ └── AppComponent.kt
51 | │ │ │ │ └── modules
52 | │ │ │ │ ├── AppModule.kt
53 | │ │ │ │ ├── ExecutorModule.kt
54 | │ │ │ │ ├── InteractorModule.kt
55 | │ │ │ │ ├── RestModule.kt
56 | │ │ │ │ ├── RestRepositoryBinding.kt
57 | │ │ │ │ └── StorageBindingModule.kt
58 | │ │ │ └── storage
59 | │ │ │ ├── PrefManagerImpl.kt
60 | │ │ │ └── base
61 | │ │ │ └── GsonFileStorageRepository.kt
62 | │ │ └── res
63 | │ │ ├── menu
64 | │ │ │ └── main.xml
65 | │ │ ├── mipmap-hdpi
66 | │ │ │ ├── ic_launcher.png
67 | │ │ │ └── ic_launcher_round.png
68 | │ │ ├── mipmap-mdpi
69 | │ │ │ ├── ic_launcher.png
70 | │ │ │ └── ic_launcher_round.png
71 | │ │ ├── mipmap-xhdpi
72 | │ │ │ ├── ic_launcher.png
73 | │ │ │ └── ic_launcher_round.png
74 | │ │ ├── mipmap-xxhdpi
75 | │ │ │ ├── ic_launcher.png
76 | │ │ │ └── ic_launcher_round.png
77 | │ │ ├── mipmap-xxxhdpi
78 | │ │ │ ├── ic_launcher.png
79 | │ │ │ └── ic_launcher_round.png
80 | │ │ └── values
81 | │ │ ├── attrs.xml
82 | │ │ ├── colors.xml
83 | │ │ ├── dimens.xml
84 | │ │ ├── ids.xml
85 | │ │ ├── nstack_keys.xml
86 | │ │ ├── strings.xml
87 | │ │ └── styles.xml
88 | │ └── test
89 | │ └── java
90 | │ └── com
91 | │ └── nodesagency
92 | │ └── test
93 | │ └── ExampleUnitTest.kt
94 | ├── build.gradle
95 | ├── data
96 | │ ├── build.gradle
97 | │ ├── data.iml
98 | │ └── src
99 | │ └── main
100 | │ └── java
101 | │ └── com
102 | │ └── nodesagency
103 | │ └── test
104 | │ ├── models
105 | │ │ ├── Photo.kt
106 | │ │ └── Post.kt
107 | │ ├── network
108 | │ │ ├── Api.kt
109 | │ │ ├── RestPostRepository.kt
110 | │ │ └── util
111 | │ │ ├── BufferedSourceConverterFactory.kt
112 | │ │ ├── DateDeserializer.kt
113 | │ │ ├── ItemTypeAdapterFactory.kt
114 | │ │ └── RetrofitExtensions.kt
115 | │ └── repositories
116 | │ ├── PostRepository.kt
117 | │ └── RepositoryException.kt
118 | ├── domain
119 | │ ├── build.gradle
120 | │ ├── domain.iml
121 | │ └── src
122 | │ └── main
123 | │ └── java
124 | │ └── com
125 | │ └── nodesagency
126 | │ └── test
127 | │ └── domain
128 | │ ├── extensions
129 | │ │ └── Extensions.kt
130 | │ ├── interactors
131 | │ │ ├── BaseAsyncInteractor.kt
132 | │ │ ├── InteractorResult.kt
133 | │ │ └── PostsInteractor.kt
134 | │ └── managers
135 | │ └── PrefManager.kt
136 | ├── gradle
137 | │ └── wrapper
138 | │ ├── gradle-wrapper.jar
139 | │ └── gradle-wrapper.properties
140 | ├── gradle.properties
141 | ├── gradlew
142 | ├── gradlew.bat
143 | ├── local.properties
144 | ├── presentation
145 | │ ├── build.gradle
146 | │ ├── presentation.iml
147 | │ ├── proguard-rules.pro
148 | │ └── src
149 | │ ├── androidTest
150 | │ │ └── java
151 | │ │ └── com
152 | │ │ └── nodesagency
153 | │ │ └── test
154 | │ │ └── presentation
155 | │ │ └── ExampleInstrumentedTest.java
156 | │ ├── main
157 | │ │ ├── AndroidManifest.xml
158 | │ │ ├── assets
159 | │ │ │ └── all_translations.json
160 | │ │ ├── java
161 | │ │ │ └── com
162 | │ │ │ └── nodesagency
163 | │ │ │ └── test
164 | │ │ │ └── presentation
165 | │ │ │ ├── extensions
166 | │ │ │ │ ├── ContextExtensions.kt
167 | │ │ │ │ ├── InteractorExtensions.kt
168 | │ │ │ │ ├── LifecycleOwnerExtensions.kt
169 | │ │ │ │ └── LiveDataExtensions.kt
170 | │ │ │ ├── injection
171 | │ │ │ │ ├── DaggerViewModelFactory.kt
172 | │ │ │ │ ├── ViewModelBuilder.kt
173 | │ │ │ │ └── ViewModelKey.kt
174 | │ │ │ ├── nstack
175 | │ │ │ │ └── Translation.java
176 | │ │ │ ├── ui
177 | │ │ │ │ ├── base
178 | │ │ │ │ │ ├── BaseActivity.kt
179 | │ │ │ │ │ ├── BaseFragment.kt
180 | │ │ │ │ │ ├── BaseViewModel.kt
181 | │ │ │ │ │ └── BaseViewModelExtensions.kt
182 | │ │ │ │ └── main
183 | │ │ │ │ ├── MainActivity+Hockey.kt
184 | │ │ │ │ ├── MainActivity+NStack.kt
185 | │ │ │ │ ├── MainActivity.kt
186 | │ │ │ │ ├── MainActivityBuilder.kt
187 | │ │ │ │ ├── MainActivityViewModel.kt
188 | │ │ │ │ └── MainActivityViewState.kt
189 | │ │ │ └── util
190 | │ │ │ ├── SharedElementHelper.kt
191 | │ │ │ └── SingleEvent.kt
192 | │ │ └── res
193 | │ │ ├── layout
194 | │ │ │ └── activity_main.xml
195 | │ │ └── values
196 | │ │ ├── nstack_keys.xml
197 | │ │ └── strings.xml
198 | │ └── test
199 | │ └── java
200 | │ └── com
201 | │ └── nodesagency
202 | │ └── test
203 | │ └── presentation
204 | │ └── ExampleUnitTest.java
205 | └── settings.gradle
206 |
207 | 90 directories, 96 files
208 | ```
209 |
210 |
211 | ## Nodes Architecture Library
212 | The Template uses components from our Architecture library so be sure to read up on how that is used as well
213 |
214 | https://github.com/nodes-android/nodes-architecture-android
215 |
216 |
217 | __Below information might be slightly out of date__
218 | ### Layers
219 | This is a 4 layer onion architecture. Dependencies are only allowed to point inwards,
220 | meaning that the inner layer most not reference code in the outer layers directly.
221 | From inside out it consists of:
222 |
223 | #### Entities
224 | Models/POJOs implemented as data objects in kotlin.
225 |
226 | #### Business Logic / Use Cases
227 | Consist of interactors and repositories (the interfaces). The interactors encapsulates the business logic
228 | and perform operations on the entities. Interactors are scheduled to run in the
229 | background and return information to the outer layer through callbacks implemented in the outer layers.
230 |
231 | #### Interface Adapters
232 | ViewModel (as part of the MVVM pattern) are implemented in this layer. ViewModel holds information
233 | from the inner layers (business logic and entities) to the user interface etc. In other words they adapt
234 | the data for output to the outermost layer (Framework and Drivers)
235 |
236 | #### Frameworks and Drivers
237 | This is the outmost layer consisting of things such as the User interface (for android Activities, fragments etc), database libraries, retrofit,
238 | okhttp etc. This also contain specific implementations of the Repository interfaces the business logic layer needs to access data.
239 |
240 | ## Flow of control
241 | Example:
242 | 1. User clicks a button that loads a list of posts in a view.
243 | 3. OnClickListener executes a Interactor/UseCase asynchronously in the business logic layer.
244 | 4. The Interactor runs in the background accessing a post repository which fetches a list of posts
245 | 5. Presenter gets notified with the loaded posts or a message in case of error trough a callback
246 | 6. Presenter instructs the view (if attached) to update itself with the new data, or display an error to the user
247 |
248 | ## Patterns in use:
249 | - MVVM
250 | - Repository
251 | - Interactor (implemented with a pluggable executor)
252 | - Dependency Injection (and thus factory)
253 | - Inward dependency rule (all dependencies must point inwards)
254 |
255 | ## Stuff
256 | - kotlin data classes as entities
257 | - Retrofit2/OkHttp3
258 | - Android kotlin extensions (views are automatically made into properties on the activity)
259 | - Uses nstack-kotlin
260 | - Mockito and junit for testing
261 |
262 | ## Inspired/partially ripped off from the following sources:
263 | - [Clean Architecture by Uncle Bob](http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html)
264 | - [Some dudes android implementation](https://medium.com/@dmilicic/a-detailed-guide-on-developing-android-apps-using-the-clean-architecture-pattern-d38d71e94029)
265 | - [Some other dudes implementation](https://fernandocejas.com/2014/09/03/architecting-android-the-clean-way)
266 |
--------------------------------------------------------------------------------
/art/usage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dtunctuncer/kotlin-template-starter/16da3b46b0d88dac6af1181071bbef11af5390a2/art/usage.png
--------------------------------------------------------------------------------
/cookiecutter.json:
--------------------------------------------------------------------------------
1 | {
2 | "repo_name": "kotlin-template",
3 | "project_name": "{{ cookiecutter.repo_name|lower|replace('-', '') }}",
4 | "min_sdk": 19,
5 | "target_sdk": 28,
6 | "package_name": "com.nodesagency.{{ cookiecutter.project_name|lower|replace(' ', '')|replace('-', '') }}",
7 | "package_dir": "{{ cookiecutter.package_name | replace('.', '/') }}",
8 | "version": "1.0.0",
9 | "hockey_id_staging": "",
10 | "hockey_id_production": "",
11 | "_copy_without_render": [".gradle", "gradle/", "*gradlew", "*gradlew.bat"]
12 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.ap_
4 |
5 | # Files for the ART/Dalvik VM
6 | *.dex
7 |
8 | # Java class files
9 | *.class
10 |
11 | # Generated files
12 | bin/
13 | gen/
14 | out/
15 |
16 | # Gradle files
17 | .gradle/
18 | build/
19 |
20 | # Local configuration file (sdk path, etc)
21 | local.properties
22 |
23 | # Log Files
24 | *.log
25 |
26 | # Android Studio Navigation editor temp files
27 | .navigation/
28 |
29 | # Android Studio captures folder
30 | captures/
31 |
32 | # Intellij
33 | *.iml
34 | .idea/
35 | projectFilesBackup/
36 |
37 | # External native build folder generated in Android Studio 2.2 and later
38 | .externalNativeBuild
39 |
40 | # Windows thumbnail db
41 | Thumbs.db
42 |
43 | # OSX files
44 | .DS_Store
45 |
46 | #NDK
47 | obj/
48 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/README.md:
--------------------------------------------------------------------------------
1 | # kotlin-template
2 | Kotlin Clean Architecture Template Project. Uses dagger2 for dependency injection yall
3 |
4 | Projects using this architecture:
5 | - warningapp-kotlin-android
6 | - dfds-channel-android
7 |
8 | ## Live Templates
9 | The Kotlin template comes supported with a its own set of live templates which can be found at
10 |
11 | https://github.com/nodes-android/androidstudio-livetemplates
12 |
13 | It should make generating the boilerplate for activities and fragments easy.
14 |
15 | ## Nodes Architecture Library
16 | The Template uses components from our Architecture library so be sure to read up on how that is used as well
17 |
18 | https://github.com/nodes-android/nodes-architecture-android
19 |
20 |
21 |
22 | __Below information might be slightly out of date__
23 | ### Layers
24 | This is a 4 layer onion architecture. Dependencies are only allowed to point inwards,
25 | meaning that the inner layer most not reference code in the outer layers directly.
26 | From inside out it consists of:
27 |
28 | #### Entities
29 | Models/POJOs implemented as data objects in kotlin.
30 |
31 | #### Business Logic / Use Cases
32 | Consist of interactors and repositories (the interfaces). The interactors encapsulates the business logic
33 | and perform operations on the entities. Interactors are scheduled to run in the
34 | background and return information to the outer layer through callbacks implemented in the outer layers.
35 |
36 | #### Interface Adapters
37 | ViewModel (as part of the MVVM pattern) are implemented in this layer. ViewModel holds information
38 | from the inner layers (business logic and entities) to the user interface etc. In other words they adapt
39 | the data for output to the outermost layer (Framework and Drivers)
40 |
41 | #### Frameworks and Drivers
42 | This is the outmost layer consisting of things such as the User interface (for android Activities, fragments etc), database libraries, retrofit,
43 | okhttp etc. This also contain specific implementations of the Repository interfaces the business logic layer needs to access data.
44 |
45 | ## Flow of control
46 | Example:
47 | 1. User clicks a button that loads a list of posts in a view.
48 | 3. OnClickListener executes a Interactor/UseCase asynchronously in the business logic layer.
49 | 4. The Interactor runs in the background accessing a post repository which fetches a list of posts
50 | 5. Presenter gets notified with the loaded posts or a message in case of error trough a callback
51 | 6. Presenter instructs the view (if attached) to update itself with the new data, or display an error to the user
52 |
53 | ## Patterns in use:
54 | - MVVM
55 | - Repository
56 | - Interactor (implemented with a pluggable executor)
57 | - Dependency Injection (and thus factory)
58 | - Inward dependency rule (all dependencies must point inwards)
59 |
60 | ## Stuff
61 | - kotlin data classes as entities
62 | - Retrofit2/OkHttp3
63 | - Android kotlin extensions (views are automatically made into properties on the activity)
64 | - Uses nstack-kotlin
65 | - Mockito and junit for testing
66 |
67 | ## Inspired/partially ripped off from the following sources:
68 | - [Clean Architecture by Uncle Bob](http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html)
69 | - [Some dudes android implementation](https://medium.com/@dmilicic/a-detailed-guide-on-developing-android-apps-using-the-clean-architecture-pattern-d38d71e94029)
70 | - [Some other dudes implementation](https://fernandocejas.com/2014/09/03/architecting-android-the-clean-way)
71 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 | apply plugin: 'com.github.gfx.ribbonizer'
5 | apply plugin: 'dk.nodes.ci.bitrise'
6 | apply plugin: 'kotlin-kapt'
7 |
8 | android {
9 | compileSdkVersion sdks.compileSdkVersion
10 | buildToolsVersion sdks.buildToolsVersion
11 | flavorDimensions "default"
12 |
13 | defaultConfig {
14 | applicationId "{{ cookiecutter.package_name }}"
15 | minSdkVersion sdks.minSdkVersion
16 | targetSdkVersion sdks.targetSdkVersion
17 | multiDexEnabled true
18 | versionCode 2
19 | versionName "{{ cookiecutter.version }}"
20 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
21 |
22 | manifestPlaceholders = [
23 | appId : keys.appId,
24 | apiKey: keys.apiKey
25 | ]
26 |
27 | packagingOptions {
28 | pickFirst("META-INF/atomicfu.kotlin_module")
29 | }
30 | }
31 |
32 | buildTypes {
33 | debug {
34 | // uncomment the following line if you add a flavorbased signingConfig for debug builds
35 | //signingConfig null
36 | }
37 | release {
38 | buildConfigField "String", "API_URL", "${project.property("production.api.url")}" //""https://jsonplaceholder.typicode.com""
39 | minifyEnabled false
40 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
41 | }
42 | }
43 |
44 | productFlavors {
45 | staging {
46 | dimension "default"
47 | applicationIdSuffix ".staging"
48 | //signingConfig signingConfigs.staging
49 | deploy = "debug"
50 | hockeyAppId = "{{ cookiecutter.hockey_id_staging }}"
51 | manifestPlaceholders = [
52 | HOCKEYAPP_APP_ID: hockeyAppId,
53 | APP_NAME : "{{ cookiecutter.project_name }} - Staging"
54 | ]
55 | buildConfigField "String", "API_URL", "${project.property("staging.api.url")}" //""https://jsonplaceholder.typicode.com""
56 | }
57 | production {
58 | dimension "default"
59 | applicationIdSuffix ".production"
60 | //signingConfig signingConfigs.production
61 | deploy = "release"
62 | hockeyAppId = "{{ cookiecutter.hockey_id_production }}"
63 | manifestPlaceholders = [
64 | HOCKEYAPP_APP_ID: hockeyAppId,
65 | APP_NAME : "{{ cookiecutter.project_name }}"
66 | ]
67 | buildConfigField "String", "API_URL", "${project.property("production.api.url")}" //""https://jsonplaceholder.typicode.com""
68 | }
69 | }
70 |
71 | // Uncomment the following if you include signingConfigs
72 | /*
73 | signingConfigs {
74 | staging {
75 | keyAlias 'androiddebugkey'
76 | keyPassword 'android'
77 | storeFile file('debug.keystore')
78 | storePassword 'android'
79 | }
80 | production {
81 | keyAlias 'androiddebugkey'
82 | keyPassword 'android'
83 | storeFile file('debug.keystore')
84 | storePassword 'android'
85 | }
86 | }
87 | */
88 | }
89 |
90 | repositories {
91 | mavenLocal()
92 | }
93 |
94 | dependencies {
95 |
96 | fileTree(dir: "libs", include: ["*.jar"])
97 | androidTestImplementation("androidx.test.espresso:espresso-core:${versions.espresso_core}", {
98 | exclude group: "com.android.support", module: "support-annotations"
99 | })
100 | implementation project (':domain')
101 | implementation project (':presentation')
102 | implementation project (':data')
103 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${versions.kotlin}"
104 | implementation "org.jetbrains.kotlin:kotlin-reflect:${versions.kotlin}"
105 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.coroutines}"
106 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${versions.coroutines}"
107 |
108 | implementation 'androidx.appcompat:appcompat:1.0.2'
109 | implementation 'androidx.recyclerview:recyclerview:1.0.0'
110 | implementation 'com.google.android.material:material:1.1.0-alpha05'
111 | implementation 'androidx.cardview:cardview:1.0.0'
112 | implementation "androidx.constraintlayout:constraintlayout:${versions.constraint_layout}"
113 | implementation 'androidx.multidex:multidex:2.0.1'
114 |
115 | implementation "com.google.dagger:dagger-android:${versions.dagger}"
116 | implementation "com.google.dagger:dagger-android-support:${versions.dagger}"
117 | kapt "com.google.dagger:dagger-compiler:${versions.dagger}"
118 | kapt "com.google.dagger:dagger-android-processor:${versions.dagger}"
119 |
120 | implementation "net.hockeyapp.android:HockeySDK:${versions.hockey_sdk}"
121 | implementation "com.jakewharton.timber:timber:${versions.timber}"
122 |
123 | implementation("com.squareup.retrofit2:retrofit:${versions.retrofit}")
124 | implementation("com.squareup.retrofit2:converter-gson:${versions.retrofit}") {
125 | exclude module: "retrofit:${versions.retrofit}"
126 | }
127 | implementation "com.squareup.okhttp3:okhttp:${versions.okhttp}"
128 |
129 | implementation "dk.nodes.nstack:nstack-kotlin:${versions.nodes_nstack_kotlin}"
130 | implementation "dk.nodes.arch:base:${versions.nodes_arch}"
131 |
132 | implementation "com.squareup.retrofit2:adapter-rxjava2:${versions.retrofit}"
133 |
134 | implementation "io.reactivex.rxjava2:rxandroid:${versions.rx_android}"
135 | implementation "io.reactivex.rxjava2:rxjava:${versions.rx_java}"
136 | implementation "io.reactivex.rxjava2:rxkotlin:${versions.rx_kolin}"
137 |
138 | // Testing
139 | testImplementation "junit:junit:${versions.junit}"
140 | // required if you want to use Mockito for unit tests
141 | testImplementation "org.mockito:mockito-core:${versions.mockito}"
142 | // required if you want to use Mockito for Android tests
143 | androidTestImplementation "org.mockito:mockito-android:${versions.mockito}"
144 |
145 | //For Espresso UI testing
146 | androidTestImplementation "androidx.test.espresso:espresso-core:${versions.espresso_core}"
147 | androidTestImplementation "androidx.test.espresso:espresso-intents:${versions.espresso_core}"
148 | androidTestImplementation "androidx.test.espresso:espresso-contrib:${versions.espresso_core}"
149 |
150 | // The following section is only used to force the latest version to resolve conflicts
151 | implementation 'androidx.arch.core:core-common:2.0.1'
152 | implementation 'androidx.arch.core:core-runtime:2.1.0-alpha02'
153 | implementation "androidx.lifecycle:lifecycle-livedata-core:${versions.lifecycle}"
154 | implementation "androidx.lifecycle:lifecycle-runtime:${versions.lifecycle}"
155 | implementation "androidx.lifecycle:lifecycle-common:${versions.lifecycle}"
156 | implementation "androidx.lifecycle:lifecycle-extensions:${versions.lifecycle}"
157 | implementation "androidx.lifecycle:lifecycle-viewmodel:${versions.lifecycle}"
158 | implementation "androidx.lifecycle:lifecycle-livedata:${versions.lifecycle}"
159 | kapt "androidx.lifecycle:lifecycle-compiler:${versions.lifecycle}"
160 | implementation "androidx.core:core-ktx:${versions.ktx}"
161 | }
162 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in D:\programs\Android\sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | # Uncomment this to preserve the line number information for
20 | # debugging stack traces.
21 | #-keepattributes SourceFile,LineNumberTable
22 |
23 | # If you keep the line number information, uncomment this to
24 | # hide the original source file name.
25 | #-renamesourcefileattribute SourceFile
26 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/src/androidTest/java/{{ cookiecutter.package_dir }}/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}
2 |
3 | import androidx.test.InstrumentationRegistry
4 | import androidx.test.runner.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.assertEquals
10 |
11 | /**
12 | * Instrumentation 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 | @Throws(Exception::class)
20 | fun useAppContext() {
21 | // Context of the app under test.
22 | val appContext = InstrumentationRegistry.getTargetContext()
23 | assertEquals("dk.bison.wt", appContext.packageName)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/src/debug/res/xml/network_security_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
10 |
11 |
19 |
20 |
21 |
25 |
29 |
30 |
31 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/src/main/assets/all_translations.json:
--------------------------------------------------------------------------------
1 | {"da-DK":{"default":{"ok":"Ok","cancel":"Annuller","no":"Nej","yes":"Ja","retry":"Pr\u00f8v igen","edit":"Rediger","save":"Gem","back":"Tilbage","settings":"Indstillinger","later":"Senere","next":"N\u00e6ste","previous":"Forrige","skip":"Spring over"},"versionControl":{"updateHeader":"Ny version er ude","forceHeader":"Ny version er ude, du skal opdatere!","negativeBtn":"Annuller","positiveBtn":"Opdater","newInVersionHeader":"Nyt i denne version","okBtn":"Ok"},"rateReminder":{"title":"__title","body":"__body","yesBtn":"__yesBtn","laterBtn":"__laterBtn","noBtn":"__noBtn"},"error":{"authenticationError":"Login er udl\u00f8bet, login venligst ind igen.","connectionError":"Ingen eller d\u00e5rlig forbindelse, pr\u00f8v igen.","errorTitle":"Fejl","unknownError":"Ukendt fejl, pr\u00f8v igen."}},"en-GB":{"default":{"ok":"Ok","cancel":"__cancel","no":"__no","yes":"__yes","retry":"__retry","edit":"__edit","save":"__save","back":"__back","settings":"__settings","later":"__later","next":"__next","previous":"__previous","skip":"__skip"},"versionControl":{"updateHeader":"New version is out, please update","forceHeader":"New version is out, you have to update!","negativeBtn":"Cancel","positiveBtn":"Update","newInVersionHeader":"New in this version","okBtn":"Ok"},"rateReminder":{"title":"Rate the app","body":"We can see you like the application. Would you like to rate it?","yesBtn":"Yes","laterBtn":"Later","noBtn":"No"},"error":{"authenticationError":"__authenticationError","connectionError":"__connectionError","errorTitle":"__errorTitle","unknownError":"__unknownError"}},"es-ES":{"default":{"ok":"Ok","cancel":"__cancel","no":"__no","yes":"__yes","retry":"__retry","edit":"__edit","save":"__save","back":"__back","settings":"__settings","later":"__later","next":"__next","previous":"__previous","skip":"__skip"},"versionControl":{"updateHeader":"Nueva version disponible, por favor actualiza","forceHeader":"Nueva version disponible, tienes que actualizar!","negativeBtn":"Cancelar","positiveBtn":"Actualizar","newInVersionHeader":"Nuevo en esta version","okBtn":"Ok"},"rateReminder":{"title":"Valora la app","body":"Vemos que te gusta la aplicaci\u00f3n.\r\nQuieres valorarla?","yesBtn":"Si","laterBtn":"Despu\u00e9s","noBtn":"No"},"error":{"authenticationError":"__authenticationError","connectionError":"__connectionError","errorTitle":"__errorTitle","unknownError":"__unknownError"}}}
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/src/main/java/{{ cookiecutter.package_dir }}/App.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}
2 |
3 | import android.content.Context
4 | import androidx.multidex.MultiDex
5 | import dagger.android.AndroidInjector
6 | import dagger.android.DaggerApplication
7 | import {{ cookiecutter.package_name }}.inititializers.AppInitializer
8 | import {{ cookiecutter.package_name }}.injection.components.DaggerAppComponent
9 | import javax.inject.Inject
10 |
11 | class App : DaggerApplication() {
12 |
13 | @Inject lateinit var initializer: AppInitializer
14 | override fun onCreate() {
15 | super.onCreate()
16 | initializer.init(this)
17 | }
18 |
19 | override fun attachBaseContext(base: Context) {
20 | super.attachBaseContext(base)
21 | MultiDex.install(this)
22 | }
23 |
24 | override fun applicationInjector(): AndroidInjector {
25 | return DaggerAppComponent.factory().create(this)
26 | }
27 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/src/main/java/{{ cookiecutter.package_dir }}/inititializers/AppInitializer.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.inititializers
2 |
3 | import android.app.Application
4 | import dk.nodes.nstack.kotlin.NStack
5 | import {{ cookiecutter.package_name }}.BuildConfig
6 | import {{ cookiecutter.package_name }}.presentation.nstack.Translation
7 | import timber.log.Timber
8 | import javax.inject.Inject
9 |
10 | interface AppInitializer {
11 | fun init(app: Application)
12 | }
13 |
14 | class AppInitializerImpl @Inject constructor() : AppInitializer {
15 | override fun init(app: Application) {
16 | NStack.translationClass = Translation::class.java
17 | NStack.init(app)
18 | if (BuildConfig.DEBUG) {
19 | Timber.plant(Timber.DebugTree())
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/src/main/java/{{ cookiecutter.package_dir }}/injection/components/AppComponent.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.injection.components
2 |
3 | import dagger.Component
4 | import dagger.android.AndroidInjector
5 | import dagger.android.support.AndroidSupportInjectionModule
6 | import dk.nodes.arch.domain.injection.scopes.AppScope
7 | import {{ cookiecutter.package_name }}.App
8 | import {{ cookiecutter.package_name }}.injection.modules.AppModule
9 | import {{ cookiecutter.package_name }}.injection.modules.ExecutorModule
10 | import {{ cookiecutter.package_name }}.injection.modules.InteractorModule
11 | import {{ cookiecutter.package_name }}.injection.modules.RestModule
12 | import {{ cookiecutter.package_name }}.injection.modules.RestRepositoryBinding
13 | import {{ cookiecutter.package_name }}.injection.modules.StorageBindingModule
14 | import {{ cookiecutter.package_name }}.presentation.injection.ViewModelBuilder
15 |
16 | @Component(
17 | modules = [
18 | AndroidSupportInjectionModule::class,
19 | ViewModelBuilder::class,
20 | AppModule::class,
21 | ExecutorModule::class,
22 | InteractorModule::class,
23 | RestModule::class,
24 | RestRepositoryBinding::class,
25 | StorageBindingModule::class
26 | ]
27 | )
28 | @AppScope
29 | interface AppComponent : AndroidInjector {
30 | @Component.Factory
31 | abstract class Factory : AndroidInjector.Factory
32 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/src/main/java/{{ cookiecutter.package_dir }}/injection/modules/AppModule.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.injection.modules
2 |
3 | import android.content.Context
4 | import dagger.Binds
5 | import dagger.Module
6 | import dagger.Provides
7 | import {{ cookiecutter.package_name }}.App
8 | import {{ cookiecutter.package_name }}.inititializers.AppInitializer
9 | import {{ cookiecutter.package_name }}.inititializers.AppInitializerImpl
10 |
11 | @Module
12 | abstract class AppModule {
13 |
14 | @Binds
15 | abstract fun bindAppInitalizer(initializer: AppInitializerImpl): AppInitializer
16 |
17 | @Module
18 | companion object {
19 | @JvmStatic
20 | @Provides
21 | fun provideContext(application: App): Context = application.applicationContext
22 | }
23 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/src/main/java/{{ cookiecutter.package_dir }}/injection/modules/ExecutorModule.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.injection.modules
2 |
3 | import dagger.Module
4 | import dagger.Provides
5 | import dk.nodes.arch.domain.executor.Executor
6 | import dk.nodes.arch.domain.executor.ThreadExecutor
7 | import dk.nodes.arch.domain.injection.scopes.AppScope
8 |
9 | @Module
10 | class ExecutorModule {
11 | @Provides
12 | @AppScope
13 | fun provideExecutor(): Executor {
14 | return ThreadExecutor()
15 | }
16 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/src/main/java/{{ cookiecutter.package_dir }}/injection/modules/InteractorModule.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.injection.modules
2 |
3 | import dagger.Module
4 |
5 | @Module
6 | class InteractorModule
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/src/main/java/{{ cookiecutter.package_dir }}/injection/modules/RestModule.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.injection.modules
2 |
3 | import com.google.gson.Gson
4 | import com.google.gson.GsonBuilder
5 | import dagger.Module
6 | import dagger.Provides
7 | import dk.nodes.arch.domain.injection.scopes.AppScope
8 | import dk.nodes.nstack.kotlin.providers.NMetaInterceptor
9 | import {{ cookiecutter.package_name }}.BuildConfig
10 | import {{ cookiecutter.package_name }}.network.Api
11 | import {{ cookiecutter.package_name }}.network.util.BufferedSourceConverterFactory
12 | import {{ cookiecutter.package_name }}.network.util.DateDeserializer
13 | import {{ cookiecutter.package_name }}.network.util.ItemTypeAdapterFactory
14 | import okhttp3.OkHttpClient
15 | import retrofit2.Converter
16 | import retrofit2.Retrofit
17 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
18 | import retrofit2.converter.gson.GsonConverterFactory
19 | import java.util.Date
20 | import java.util.concurrent.TimeUnit
21 | import javax.inject.Named
22 |
23 | @Module
24 | class RestModule {
25 | @Provides
26 | fun provideTypeFactory(): ItemTypeAdapterFactory {
27 | return ItemTypeAdapterFactory()
28 | }
29 |
30 | @Provides
31 | fun provideDateDeserializer(): DateDeserializer {
32 | return DateDeserializer()
33 | }
34 |
35 | @Provides
36 | @AppScope
37 | fun provideGson(typeFactory: ItemTypeAdapterFactory, dateDeserializer: DateDeserializer): Gson {
38 | return GsonBuilder()
39 | .registerTypeAdapterFactory(typeFactory)
40 | .registerTypeAdapter(Date::class.java, dateDeserializer)
41 | .setDateFormat(DateDeserializer.DATE_FORMATS[0])
42 | .create()
43 | }
44 |
45 | @Provides
46 | @Named("NAME_BASE_URL")
47 | fun provideBaseUrlString(): String {
48 | return BuildConfig.API_URL
49 | }
50 |
51 | @Provides
52 | @AppScope
53 | fun provideGsonConverter(gson: Gson): Converter.Factory {
54 | return GsonConverterFactory.create(gson)
55 | }
56 |
57 | @Provides
58 | @AppScope
59 | fun provideHttpClient(): OkHttpClient {
60 | val clientBuilder = OkHttpClient.Builder()
61 | .connectTimeout(45, TimeUnit.SECONDS)
62 | .readTimeout(60, TimeUnit.SECONDS)
63 | .writeTimeout(60, TimeUnit.SECONDS)
64 | .addInterceptor(NMetaInterceptor(BuildConfig.BUILD_TYPE))
65 |
66 | if (BuildConfig.DEBUG) {
67 | val logging = okhttp3.logging.HttpLoggingInterceptor()
68 | logging.level = okhttp3.logging.HttpLoggingInterceptor.Level.BODY
69 | clientBuilder.addInterceptor(logging)
70 | }
71 |
72 | return clientBuilder.build()
73 | }
74 |
75 | @Provides
76 | @AppScope
77 | fun provideRetrofit(
78 | client: OkHttpClient,
79 | converter: Converter.Factory,
80 | @Named("NAME_BASE_URL") baseUrl: String
81 | ): Retrofit {
82 | return Retrofit.Builder()
83 | .client(client)
84 | .baseUrl(baseUrl)
85 | .addConverterFactory(BufferedSourceConverterFactory())
86 | .addConverterFactory(converter)
87 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
88 | .build()
89 | }
90 |
91 | @Provides
92 | @AppScope
93 | fun provideApi(retrofit: Retrofit): Api {
94 | return retrofit.create(Api::class.java)
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/src/main/java/{{ cookiecutter.package_dir }}/injection/modules/RestRepositoryBinding.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.injection.modules
2 |
3 | import dagger.Binds
4 | import dagger.Module
5 | import dk.nodes.arch.domain.injection.scopes.AppScope
6 | import {{ cookiecutter.package_name }}.network.RestPostRepository
7 | import {{ cookiecutter.package_name }}.repositories.PostRepository
8 |
9 | @Module
10 | abstract class RestRepositoryBinding {
11 | @Binds
12 | @AppScope
13 | abstract fun bindPostRepository(repository: RestPostRepository): PostRepository
14 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/src/main/java/{{ cookiecutter.package_dir }}/injection/modules/StorageBindingModule.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.injection.modules
2 |
3 | import dagger.Binds
4 | import dagger.Module
5 | import dk.nodes.arch.domain.injection.scopes.AppScope
6 | import {{ cookiecutter.package_name }}.domain.managers.PrefManager
7 | import {{ cookiecutter.package_name }}.storage.PrefManagerImpl
8 |
9 | @Module
10 | abstract class StorageBindingModule {
11 | @Binds
12 | @AppScope
13 | abstract fun bindPrefManager(manager: PrefManagerImpl): PrefManager
14 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/src/main/java/{{ cookiecutter.package_dir }}/storage/PrefManagerImpl.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.storage
2 |
3 | import android.content.Context
4 | import android.content.SharedPreferences
5 | import android.preference.PreferenceManager
6 | import androidx.core.content.edit
7 | import {{ cookiecutter.package_name }}.domain.managers.PrefManager
8 | import javax.inject.Inject
9 |
10 | class PrefManagerImpl @Inject constructor(context: Context) : PrefManager {
11 | private var sharedPrefs: SharedPreferences =
12 | PreferenceManager.getDefaultSharedPreferences(context)
13 |
14 | override fun getInt(key: String, defaultValue: Int): Int {
15 | return sharedPrefs.getInt(key, defaultValue)
16 | }
17 |
18 | override fun setInt(key: String, value: Int) {
19 | sharedPrefs.edit(commit = true) {
20 | putInt(key, value)
21 | }
22 | }
23 |
24 | override fun getLong(key: String, defaultValue: Long): Long =
25 | sharedPrefs.getLong(key, defaultValue)
26 |
27 | override fun setLong(key: String, value: Long) {
28 | sharedPrefs.edit(commit = true) {
29 | putLong(key, value)
30 | }
31 | }
32 |
33 | override fun getBoolean(key: String, defaultValue: Boolean): Boolean =
34 | sharedPrefs.getBoolean(key, defaultValue)
35 |
36 | override fun setBoolean(key: String, value: Boolean) {
37 | sharedPrefs.edit(commit = true) {
38 | putBoolean(key, value)
39 | }
40 | }
41 |
42 | override fun getFloat(key: String, defaultValue: Float): Float =
43 | sharedPrefs.getFloat(key, defaultValue)
44 |
45 | override fun setFloat(key: String, value: Float) {
46 | sharedPrefs.edit(commit = true) {
47 | putFloat(key, value)
48 | }
49 | }
50 |
51 | override fun getString(key: String, defaultValue: String?): String? =
52 | sharedPrefs.getString(key, defaultValue)
53 |
54 | override fun setString(key: String, value: String) {
55 | sharedPrefs.edit(commit = true) {
56 | putString(key, value)
57 | }
58 | }
59 |
60 | override fun remove(key: String) {
61 | sharedPrefs.edit(true) {
62 | remove(key)
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/src/main/java/{{ cookiecutter.package_dir }}/storage/base/GsonFileStorageRepository.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.storage.base
2 |
3 | import android.content.Context
4 | import com.google.gson.Gson
5 | import {{ cookiecutter.package_name }}.repositories.RepositoryException
6 | import timber.log.Timber
7 | import java.io.FileInputStream
8 | import java.io.FileOutputStream
9 | import java.io.InputStreamReader
10 | import java.lang.reflect.Type
11 |
12 | abstract class GsonFileStorageRepository(
13 | val context: Context,
14 | val gson: Gson,
15 | val filename: String
16 | ) {
17 |
18 | @Throws(RepositoryException::class)
19 | fun save(objects: T) {
20 | val outputStream: FileOutputStream
21 | try {
22 | val json = gson.toJson(objects)
23 | Timber.d("Saving json = $json")
24 | outputStream = context.openFileOutput(filename, Context.MODE_PRIVATE)
25 | outputStream.write(json.toByteArray())
26 | outputStream.close()
27 | } catch (e: Exception) {
28 | e.printStackTrace()
29 | throw(RepositoryException(
30 | -1,
31 | "file could not be written to internal storage"
32 | ))
33 | }
34 | }
35 |
36 | @Throws(RepositoryException::class)
37 | fun load(type: Type): T {
38 | val inputStream: FileInputStream
39 | try {
40 | inputStream = context.openFileInput(filename)
41 | val bufferSize = 1024
42 | val buffer = CharArray(bufferSize)
43 | val out = StringBuilder()
44 | val `in` = InputStreamReader(inputStream, "UTF-8")
45 | while (true) {
46 | val rsz = `in`.read(buffer, 0, buffer.size)
47 | if (rsz < 0)
48 | break
49 | out.append(buffer, 0, rsz)
50 | }
51 | val json = out.toString()
52 | Timber.d("Read json = $json")
53 | val objects: T = gson.fromJson(json, type)
54 | return objects
55 | } catch (e: Exception) {
56 | Timber.e("Catching file not found")
57 | throw(RepositoryException(
58 | -1,
59 | "File $filename could not be read from internal storage"
60 | ))
61 | }
62 | }
63 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/src/main/res/menu/main.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dtunctuncer/kotlin-template-starter/16da3b46b0d88dac6af1181071bbef11af5390a2/{{cookiecutter.repo_name}}/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dtunctuncer/kotlin-template-starter/16da3b46b0d88dac6af1181071bbef11af5390a2/{{cookiecutter.repo_name}}/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dtunctuncer/kotlin-template-starter/16da3b46b0d88dac6af1181071bbef11af5390a2/{{cookiecutter.repo_name}}/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dtunctuncer/kotlin-template-starter/16da3b46b0d88dac6af1181071bbef11af5390a2/{{cookiecutter.repo_name}}/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dtunctuncer/kotlin-template-starter/16da3b46b0d88dac6af1181071bbef11af5390a2/{{cookiecutter.repo_name}}/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dtunctuncer/kotlin-template-starter/16da3b46b0d88dac6af1181071bbef11af5390a2/{{cookiecutter.repo_name}}/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dtunctuncer/kotlin-template-starter/16da3b46b0d88dac6af1181071bbef11af5390a2/{{cookiecutter.repo_name}}/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dtunctuncer/kotlin-template-starter/16da3b46b0d88dac6af1181071bbef11af5390a2/{{cookiecutter.repo_name}}/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dtunctuncer/kotlin-template-starter/16da3b46b0d88dac6af1181071bbef11af5390a2/{{cookiecutter.repo_name}}/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dtunctuncer/kotlin-template-starter/16da3b46b0d88dac6af1181071bbef11af5390a2/{{cookiecutter.repo_name}}/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 16dp
5 | 16dp
6 | 8dp
7 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/src/main/res/values/ids.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/src/main/res/values/nstack_keys.xml:
--------------------------------------------------------------------------------
1 |
2 | {default_ok}
3 | {default_cancel}
4 | {default_no}
5 | {default_yes}
6 | {default_retry}
7 | {default_edit}
8 | {default_save}
9 | {default_back}
10 | {default_settings}
11 | {default_later}
12 | {default_next}
13 | {default_previous}
14 | {default_skip}
15 | {error_authenticationError}
16 | {error_connectionError}
17 | {error_errorTitle}
18 | {error_unknownError}
19 | {rateReminder_title}
20 | {rateReminder_body}
21 | {rateReminder_yesBtn}
22 | {rateReminder_laterBtn}
23 | {rateReminder_noBtn}
24 | {versionControl_updateHeader}
25 | {versionControl_forceHeader}
26 | {versionControl_negativeBtn}
27 | {versionControl_positiveBtn}
28 | {versionControl_newInVersionHeader}
29 | {versionControl_okBtn}
30 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Kotlin template
3 |
4 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/app/src/test/java/{{ cookiecutter.package_dir }}/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}
2 |
3 | import org.junit.Test
4 |
5 | /**
6 | * Example local unit test, which will execute on the development machine (host).
7 | *
8 | * @see [Testing documentation](http://d.android.com/tools/testing)
9 | */
10 | class ExampleUnitTest {
11 | @Test
12 | @Throws(Exception::class)
13 | fun addition_isCorrect() {
14 | org.junit.Assert.assertEquals(4, (2 + 2).toLong())
15 | }
16 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 |
5 | ext.versions = [
6 | kotlin : '1.3.31',
7 | coroutines : '1.2.1',
8 | constraint_layout : '2.0.0-alpha4',
9 | dagger : "2.22.1",
10 | lifecycle : "2.1.0-alpha04",
11 | hockey_sdk : "5.1.1",
12 | timber : "4.7.1",
13 | junit : '4.12',
14 | espresso_core : '3.1.1',
15 | mockito : '2.27.0',
16 | nodes_nstack_kotlin: '2.2.0',
17 | nodes_arch : '2.3.2',
18 | retrofit : "2.5.0",
19 | okhttp : "3.12.1",
20 | rx_android : '2.1.1',
21 | rx_java : "2.2.8",
22 | rx_kolin : "2.3.0",
23 | ktx : "1.0.1",
24 | rx_lint : '1.7.0'
25 | ]
26 |
27 | ext.keys = [
28 | appId : 'BmZHmoKuU99A5ZnOByOiRxMVSmAWC2yBz3OW',
29 | apiKey : 'yw9go00oCWt6zPhfbdjRYXiHRWmkQZQSuRke',
30 | acceptHeader: "da-DK"
31 | ]
32 |
33 | ext.sdks = [
34 | compileSdkVersion: {{ cookiecutter.target_sdk }},
35 | buildToolsVersion: '28.0.3',
36 | minSdkVersion : {{ cookiecutter.min_sdk }},
37 | targetSdkVersion : {{ cookiecutter.target_sdk }}
38 | ]
39 |
40 | repositories {
41 | mavenCentral()
42 | google()
43 | jcenter()
44 | }
45 | dependencies {
46 | classpath 'com.android.tools.build:gradle:3.4.0'
47 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}"
48 | classpath "dk.nodes.nstack:translation:2.3.0"
49 | classpath "com.github.gfx.ribbonizer:ribbonizer-plugin:2.1.0"
50 | classpath "dk.nodes.ci:bitrise:1.0"
51 | }
52 | }
53 |
54 | plugins {
55 | id "com.diffplug.gradle.spotless" version "3.22.0"
56 | id 'com.github.ben-manes.versions' version "0.21.0"
57 | }
58 |
59 | allprojects {
60 | repositories {
61 | mavenCentral()
62 | maven { url 'https://maven.google.com' }
63 | jcenter()
64 | mavenLocal()
65 | }
66 | }
67 |
68 |
69 | subprojects {
70 | apply plugin: 'com.diffplug.gradle.spotless'
71 | spotless {
72 | kotlin {
73 | target "**/*.kt"
74 | ktlint('0.31.0')
75 | }
76 | }
77 | }
78 |
79 | dependencyUpdates.resolutionStrategy {
80 | componentSelection { rules ->
81 | rules.all { ComponentSelection selection ->
82 | boolean rejected = ['alpha', 'beta', 'rc', 'cr', 'm', 'preview'].any { qualifier ->
83 | selection.candidate.version ==~ /(?i).*[.-]${qualifier}[.\d-]*/
84 | }
85 | if (rejected) {
86 | selection.reject('Release candidate')
87 | }
88 | }
89 | }
90 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/data/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/data/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'kotlin'
2 |
3 | dependencies {
4 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${versions.kotlin}"
5 | implementation("com.squareup.retrofit2:retrofit:${versions.retrofit}")
6 | implementation("com.squareup.retrofit2:converter-gson:${versions.retrofit}") {
7 | exclude module: "retrofit:${versions.retrofit}"
8 | }
9 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.coroutines}"
10 | implementation "com.google.dagger:dagger-android:${versions.dagger}"
11 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/data/src/main/java/{{ cookiecutter.package_dir }}/models/Photo.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.models
2 |
3 | data class Photo(
4 | var albumId: Int,
5 | var id: Int,
6 | var title: String,
7 | var url: String,
8 | var thumbnailUrl: String
9 | )
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/data/src/main/java/{{ cookiecutter.package_dir }}/models/Post.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.models
2 |
3 | data class Post(
4 | var userId: Int,
5 | var id: Int,
6 | var title: String,
7 | var body: String
8 | )
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/data/src/main/java/{{ cookiecutter.package_dir }}/network/Api.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.network
2 |
3 | import {{ cookiecutter.package_name }}.models.Photo
4 | import {{ cookiecutter.package_name }}.models.Post
5 | import retrofit2.Call
6 | import retrofit2.http.GET
7 |
8 | interface Api {
9 | @GET("posts")
10 | fun getPosts(): Call>
11 |
12 | @GET("photos")
13 | fun getPhotos(): Call>
14 | }
15 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/data/src/main/java/{{ cookiecutter.package_dir }}/network/RestPostRepository.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.network
2 |
3 | import {{ cookiecutter.package_name }}.models.Post
4 | import {{ cookiecutter.package_name }}.repositories.PostRepository
5 | import {{ cookiecutter.package_name }}.repositories.RepositoryException
6 | import javax.inject.Inject
7 |
8 | class RestPostRepository @Inject constructor(private val api: Api) : PostRepository {
9 | @Throws(RepositoryException::class)
10 | override suspend fun getPosts(cached: Boolean): List {
11 | val response = api.getPosts().execute()
12 | if (response.isSuccessful) {
13 | return response.body()
14 | ?: throw(RepositoryException(
15 | response.code(),
16 | response.message()
17 | ))
18 | }
19 | throw(RepositoryException(response.code(), response.message()))
20 | }
21 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/data/src/main/java/{{ cookiecutter.package_dir }}/network/util/BufferedSourceConverterFactory.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.network.util
2 |
3 | import okhttp3.ResponseBody
4 | import okio.BufferedSource
5 | import retrofit2.Converter
6 | import retrofit2.Retrofit
7 | import java.lang.reflect.Type
8 |
9 | /*
10 | This is necessary to get general purpose disk caching from Store library to work
11 | with retrofit. (Store expects okio BufferedSource)
12 | */
13 | class BufferedSourceConverterFactory : Converter.Factory() {
14 | override fun responseBodyConverter(
15 | type: Type?,
16 | annotations: Array?,
17 | retrofit: Retrofit?
18 | ): Converter? {
19 | return if (BufferedSource::class.java != type) {
20 | null
21 | } else Converter { value -> value.source() }
22 | }
23 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/data/src/main/java/{{ cookiecutter.package_dir }}/network/util/DateDeserializer.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.network.util
2 |
3 | import com.google.gson.JsonDeserializationContext
4 | import com.google.gson.JsonDeserializer
5 | import com.google.gson.JsonElement
6 | import com.google.gson.JsonParseException
7 | import java.lang.reflect.Type
8 | import java.text.ParseException
9 | import java.text.SimpleDateFormat
10 | import java.util.Arrays
11 | import java.util.Date
12 | import java.util.HashMap
13 | import java.util.Locale
14 |
15 | class DateDeserializer : JsonDeserializer {
16 | @Throws(JsonParseException::class)
17 | override fun deserialize(
18 | jsonElement: JsonElement,
19 | typeOF: Type,
20 | context: JsonDeserializationContext
21 | ): Date {
22 | for (format in DATE_FORMATS) {
23 | if (!formatters.containsKey(format)) {
24 | formatters[format] = SimpleDateFormat(format, Locale.getDefault())
25 | }
26 |
27 | try {
28 | return formatters[format]?.parse(jsonElement.asString) ?: Date()
29 | } catch (e: ParseException) {
30 | }
31 | }
32 | throw JsonParseException(
33 | "Unparseable date: ${jsonElement.asString} " +
34 | ". Supported formats: " + Arrays.toString(DATE_FORMATS)
35 | )
36 | }
37 |
38 | // replacement for a static member
39 | companion object {
40 | private val formatters = HashMap()
41 | val DATE_FORMATS = arrayOf(
42 | "yyyy-MM-dd'T'HH:mm:ss.SSSZ",
43 | "yyyy-MM-dd'T'HH:mm:ss'Z'",
44 | "yyyy-MM-dd'T'HH:mm:ssZ",
45 | "yyyy-MM-dd",
46 | "MMM dd, yyyy hh:mm:ss aaa"
47 | )
48 | }
49 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/data/src/main/java/{{ cookiecutter.package_dir }}/network/util/ItemTypeAdapterFactory.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.network.util
2 |
3 | import com.google.gson.Gson
4 | import com.google.gson.JsonElement
5 | import com.google.gson.TypeAdapter
6 | import com.google.gson.TypeAdapterFactory
7 | import com.google.gson.reflect.TypeToken
8 | import com.google.gson.stream.JsonReader
9 | import com.google.gson.stream.JsonWriter
10 | import java.io.IOException
11 |
12 | class ItemTypeAdapterFactory : TypeAdapterFactory {
13 | var rootContainerNames = listOf("data")
14 |
15 | override fun create(gson: Gson, type: TypeToken): TypeAdapter {
16 |
17 | val delegate = gson.getDelegateAdapter(this, type)
18 | val elementAdapter = gson.getAdapter(JsonElement::class.java)
19 |
20 | return object : TypeAdapter() {
21 |
22 | @Throws(IOException::class)
23 | override fun write(out: JsonWriter, value: T) {
24 | delegate.write(out, value)
25 | }
26 |
27 | @Throws(IOException::class)
28 | override fun read(`in`: JsonReader): T {
29 |
30 | var jsonElement = elementAdapter.read(`in`)
31 | if (jsonElement.isJsonObject) {
32 | // Log.e("debug", "parsing element " + jsonElement.toString())
33 | val jsonObject = jsonElement.asJsonObject
34 | val entry_set = jsonObject.entrySet()
35 | if (entry_set.size == 1) {
36 | val key: String = entry_set.iterator().next().key ?: ""
37 | val ele: JsonElement = entry_set.iterator().next().value
38 | if (rootContainerNames.contains(key)) {
39 | // Log.e("debug", "Doing deserialization workaround")
40 | return delegate.fromJsonTree(ele)
41 | }
42 | }
43 | }
44 | return delegate.fromJsonTree(jsonElement)
45 | }
46 | }.nullSafe()
47 | }
48 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/data/src/main/java/{{ cookiecutter.package_dir }}/network/util/RetrofitExtensions.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.network.util
2 |
3 | import kotlinx.coroutines.CompletableDeferred
4 | import kotlinx.coroutines.Deferred
5 | import retrofit2.Call
6 | import retrofit2.Callback
7 | import retrofit2.HttpException
8 | import retrofit2.Response
9 |
10 | fun Call.toDeferred(): Deferred {
11 | val deferred = CompletableDeferred()
12 | deferred.invokeOnCompletion {
13 | if (deferred.isCancelled) {
14 | cancel()
15 | }
16 | }
17 |
18 | enqueue(object : Callback {
19 | override fun onFailure(call: Call, t: Throwable) {
20 | deferred.completeExceptionally(t)
21 | }
22 |
23 | override fun onResponse(call: Call, response: Response) {
24 | if (response.isSuccessful) {
25 | deferred.complete(response.body()!!)
26 | } else {
27 | deferred.completeExceptionally(HttpException(response))
28 | }
29 | }
30 | })
31 | return deferred
32 | }
33 |
34 | fun Call.toDeferredResponse(): Deferred> {
35 | val deferred = CompletableDeferred>()
36 |
37 | deferred.invokeOnCompletion {
38 | if (deferred.isCancelled) {
39 | cancel()
40 | }
41 | }
42 |
43 | enqueue(object : Callback {
44 | override fun onFailure(call: Call, t: Throwable) {
45 | deferred.completeExceptionally(t)
46 | }
47 |
48 | override fun onResponse(call: Call, response: Response) {
49 | deferred.complete(response)
50 | }
51 | })
52 |
53 | return deferred
54 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/data/src/main/java/{{ cookiecutter.package_dir }}/repositories/PostRepository.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.repositories
2 |
3 | import {{ cookiecutter.package_name }}.models.Post
4 |
5 | interface PostRepository {
6 | suspend fun getPosts(cached: Boolean = false): List
7 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/data/src/main/java/{{ cookiecutter.package_dir }}/repositories/RepositoryException.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.repositories
2 |
3 | class RepositoryException(code: Int, msg: String) : RuntimeException(msg)
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/domain/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/domain/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'kotlin'
2 |
3 | dependencies {
4 | implementation project(':data')
5 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${versions.kotlin}"
6 | implementation "com.google.dagger:dagger-android:${versions.dagger}"
7 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.coroutines}"
8 | }
9 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/domain/src/main/java/{{ cookiecutter.package_dir }}/domain/extensions/Extensions.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.domain.extensions
2 |
3 | inline fun T.guard(block: T.() -> Unit): T {
4 | if (this == null) block(); return this
5 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/domain/src/main/java/{{ cookiecutter.package_dir }}/domain/interactors/BaseAsyncInteractor.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.domain.interactors
2 |
3 | interface BaseAsyncInteractor {
4 | suspend operator fun invoke(): O
5 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/domain/src/main/java/{{ cookiecutter.package_dir }}/domain/interactors/InteractorResult.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.domain.interactors
2 |
3 | sealed class InteractorResult {
4 |
5 | override fun toString(): String {
6 | return when (this) {
7 | is Success<*> -> "Success[data=$data]"
8 | is Fail -> "Fail[throwable=$throwable]"
9 | is Loading -> "Loading"
10 | Uninitialized -> "Uninitialized"
11 | }
12 | }
13 | }
14 |
15 | sealed class IncompleteResult : InteractorResult()
16 | sealed class CompleteResult : InteractorResult()
17 |
18 | data class Success(val data: T) : CompleteResult()
19 | data class Fail(val throwable: Throwable) : CompleteResult()
20 | object Uninitialized : IncompleteResult()
21 | class Loading : IncompleteResult()
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/domain/src/main/java/{{ cookiecutter.package_dir }}/domain/interactors/PostsInteractor.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.domain.interactors
2 |
3 | import {{ cookiecutter.package_name }}.models.Post
4 | import {{ cookiecutter.package_name }}.repositories.PostRepository
5 | import java.io.IOException
6 | import javax.inject.Inject
7 |
8 | class PostsInteractor @Inject constructor(
9 | private val postRepository: PostRepository
10 | ) : BaseAsyncInteractor> {
11 |
12 | override suspend fun invoke(): List {
13 | val posts = postRepository.getPosts(true)
14 | if (posts.isEmpty()) {
15 | throw IOException("Empty posts")
16 | }
17 | return posts
18 | }
19 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/domain/src/main/java/{{ cookiecutter.package_dir }}/domain/managers/PrefManager.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.domain.managers
2 |
3 | /**
4 | * Created by bison on 11/10/17.
5 | *
6 | * The clean version of JosoPrefs :D (inject into and use from interactors)
7 | */
8 | interface PrefManager {
9 | fun getInt(key: String, defaultValue: Int): Int
10 | fun setInt(key: String, value: Int)
11 | fun getLong(key: String, defaultValue: Long): Long
12 | fun setLong(key: String, value: Long)
13 | fun getBoolean(key: String, defaultValue: Boolean): Boolean
14 | fun setBoolean(key: String, value: Boolean)
15 | fun getFloat(key: String, defaultValue: Float): Float
16 | fun setFloat(key: String, value: Float)
17 | fun getString(key: String, defaultValue: String?): String?
18 | fun setString(key: String, value: String)
19 | fun remove(key: String)
20 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | org.gradle.jvmargs=-Xmx1536m
13 |
14 | # When configured, Gradle will run in incubating parallel mode.
15 | # This option should only be used with decoupled projects. More details, visit
16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
17 | # org.gradle.parallel=true
18 | kotlin.coroutines=enable
19 | android.useAndroidX=true
20 | android.enableJetifier=true
21 |
22 | kapt.incremental.apt=true
23 | kapt.use.worker.api=true
24 | kapt.include.compile.classpath=false
25 |
26 | staging.api.url="https://jsonplaceholder.typicode.com"
27 | production.api.url="https://jsonplaceholder.typicode.com"
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dtunctuncer/kotlin-template-starter/16da3b46b0d88dac6af1181071bbef11af5390a2/{{cookiecutter.repo_name}}/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Apr 29 10:06:29 CEST 2019
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
7 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/presentation/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/presentation/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-kapt'
4 | apply plugin: 'kotlin-android-extensions'
5 | apply plugin: 'dk.nstack.translation.plugin'
6 |
7 | translation {
8 | appId = keys.appId
9 | apiKey = keys.apiKey
10 | acceptHeader = keys.acceptHeader
11 | }
12 |
13 | android {
14 | compileSdkVersion sdks.compileSdkVersion
15 | defaultConfig {
16 | minSdkVersion sdks.minSdkVersion
17 | targetSdkVersion sdks.targetSdkVersion
18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
19 |
20 | }
21 |
22 | buildTypes {
23 | release {
24 | minifyEnabled false
25 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
26 | }
27 | }
28 |
29 | }
30 |
31 | dependencies {
32 | implementation project(':domain')
33 | implementation project(':data')
34 | implementation 'com.google.android.material:material:1.1.0-alpha05'
35 | implementation 'androidx.appcompat:appcompat:1.0.2'
36 | implementation 'androidx.arch.core:core-common:2.0.1'
37 | implementation 'androidx.arch.core:core-runtime:2.0.1'
38 | implementation "androidx.lifecycle:lifecycle-livedata-core:${versions.lifecycle}"
39 | implementation "androidx.lifecycle:lifecycle-runtime:${versions.lifecycle}"
40 | implementation "androidx.lifecycle:lifecycle-common:${versions.lifecycle}"
41 | implementation "androidx.lifecycle:lifecycle-extensions:${versions.lifecycle}"
42 | implementation "androidx.lifecycle:lifecycle-viewmodel:${versions.lifecycle}"
43 | implementation "androidx.lifecycle:lifecycle-livedata:${versions.lifecycle}"
44 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:${versions.lifecycle}"
45 | kapt "androidx.lifecycle:lifecycle-compiler:${versions.lifecycle}"
46 | implementation "androidx.core:core-ktx:${versions.ktx}"
47 | implementation "com.google.dagger:dagger-android:${versions.dagger}"
48 | implementation "com.google.dagger:dagger-android-support:${versions.dagger}"
49 | kapt "com.google.dagger:dagger-compiler:${versions.dagger}"
50 | kapt "com.google.dagger:dagger-android-processor:${versions.dagger}"
51 | implementation 'dk.nodes.nstack:nstack-kotlin:2.2.0'
52 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${versions.coroutines}"
53 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${versions.coroutines}"
54 | testImplementation 'junit:junit:4.12'
55 | implementation "io.reactivex.rxjava2:rxandroid:${versions.rx_android}"
56 | implementation "io.reactivex.rxjava2:rxjava:${versions.rx_java}"
57 | implementation "io.reactivex.rxjava2:rxkotlin:${versions.rx_kolin}"
58 | implementation "androidx.core:core-ktx:${versions.ktx}"
59 | implementation "net.hockeyapp.android:HockeySDK:${versions.hockey_sdk}"
60 | implementation "com.jakewharton.timber:timber:${versions.timber}"
61 | }
62 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/presentation/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/presentation/src/androidTest/java/{{ cookiecutter.package_dir }}/presentation/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.presentation;
2 |
3 | import android.content.Context;
4 | import android.support.test.InstrumentationRegistry;
5 | import android.support.test.runner.AndroidJUnit4;
6 |
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 |
10 | import static org.junit.Assert.*;
11 |
12 | /**
13 | * Instrumented test, which will execute on an Android device.
14 | *
15 | * @see Testing documentation
16 | */
17 | @RunWith(AndroidJUnit4.class)
18 | public class ExampleInstrumentedTest {
19 | @Test
20 | public void useAppContext() {
21 | // Context of the app under test.
22 | Context appContext = InstrumentationRegistry.getTargetContext();
23 |
24 | assertEquals("{{ cookiecutter.package_name }}.presentation.test", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/presentation/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/presentation/src/main/assets/all_translations.json:
--------------------------------------------------------------------------------
1 | {"da-DK":{"default":{"ok":"Ok","cancel":"Annuller","no":"Nej","yes":"Ja","retry":"Pr\u00f8v igen","edit":"Rediger","save":"Gem","back":"Tilbage","settings":"Indstillinger","later":"Senere","next":"N\u00e6ste","previous":"Forrige","skip":"Spring over"},"versionControl":{"updateHeader":"Ny version er ude","forceHeader":"Ny version er ude, du skal opdatere!","negativeBtn":"Annuller","positiveBtn":"Opdater","newInVersionHeader":"Nyt i denne version","okBtn":"Ok"},"rateReminder":{"title":"__title","body":"__body","yesBtn":"__yesBtn","laterBtn":"__laterBtn","noBtn":"__noBtn"},"error":{"authenticationError":"Login er udl\u00f8bet, login venligst ind igen.","connectionError":"Ingen eller d\u00e5rlig forbindelse, pr\u00f8v igen.","errorTitle":"Fejl","unknownError":"Ukendt fejl, pr\u00f8v igen."}},"en-GB":{"default":{"ok":"Ok","cancel":"__cancel","no":"__no","yes":"__yes","retry":"__retry","edit":"__edit","save":"__save","back":"__back","settings":"__settings","later":"__later","next":"__next","previous":"__previous","skip":"__skip"},"versionControl":{"updateHeader":"New version is out, please update","forceHeader":"New version is out, you have to update!","negativeBtn":"Cancel","positiveBtn":"Update","newInVersionHeader":"New in this version","okBtn":"Ok"},"rateReminder":{"title":"Rate the app","body":"We can see you like the application. Would you like to rate it?","yesBtn":"Yes","laterBtn":"Later","noBtn":"No"},"error":{"authenticationError":"__authenticationError","connectionError":"__connectionError","errorTitle":"__errorTitle","unknownError":"__unknownError"}},"es-ES":{"default":{"ok":"Ok","cancel":"__cancel","no":"__no","yes":"__yes","retry":"__retry","edit":"__edit","save":"__save","back":"__back","settings":"__settings","later":"__later","next":"__next","previous":"__previous","skip":"__skip"},"versionControl":{"updateHeader":"Nueva version disponible, por favor actualiza","forceHeader":"Nueva version disponible, tienes que actualizar!","negativeBtn":"Cancelar","positiveBtn":"Actualizar","newInVersionHeader":"Nuevo en esta version","okBtn":"Ok"},"rateReminder":{"title":"Valora la app","body":"Vemos que te gusta la aplicaci\u00f3n.\r\nQuieres valorarla?","yesBtn":"Si","laterBtn":"Despu\u00e9s","noBtn":"No"},"error":{"authenticationError":"__authenticationError","connectionError":"__connectionError","errorTitle":"__errorTitle","unknownError":"__unknownError"}}}
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/presentation/src/main/java/{{ cookiecutter.package_dir }}/presentation/extensions/ContextExtensions.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.presentation.extensions
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.util.DisplayMetrics
6 | import android.view.View
7 | import android.view.inputmethod.InputMethodManager
8 | import androidx.fragment.app.Fragment
9 |
10 | fun Fragment.hideKeyboard() {
11 | activity?.hideKeyboard()
12 | }
13 |
14 | fun Activity.hideKeyboard() {
15 | val view = currentFocus
16 | if (view != null) {
17 | val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
18 | imm?.hideSoftInputFromWindow(view.windowToken, 0)
19 | view.clearFocus()
20 | }
21 | }
22 |
23 | fun View.dpToPx(dp: Int): Int {
24 | return context.dpToPx(dp)
25 | }
26 |
27 | fun Fragment.dpToPx(dp: Int): Int {
28 | return requireContext().dpToPx(dp)
29 | }
30 |
31 | fun Context.dpToPx(dp: Int): Int {
32 | return (dp * (resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)).toInt()
33 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/presentation/src/main/java/{{ cookiecutter.package_dir }}/presentation/extensions/InteractorExtensions.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.presentation.extensions
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import {{ cookiecutter.package_name }}.domain.interactors.BaseAsyncInteractor
6 | import {{ cookiecutter.package_name }}.domain.interactors.CompleteResult
7 | import {{ cookiecutter.package_name }}.domain.interactors.Fail
8 | import {{ cookiecutter.package_name }}.domain.interactors.InteractorResult
9 | import {{ cookiecutter.package_name }}.domain.interactors.Loading
10 | import {{ cookiecutter.package_name }}.domain.interactors.Success
11 | import {{ cookiecutter.package_name }}.domain.interactors.Uninitialized
12 | import io.reactivex.BackpressureStrategy
13 | import io.reactivex.Flowable
14 | import io.reactivex.subjects.BehaviorSubject
15 | import kotlinx.coroutines.CoroutineScope
16 | import kotlinx.coroutines.Dispatchers
17 | import kotlinx.coroutines.ExperimentalCoroutinesApi
18 | import kotlinx.coroutines.channels.BroadcastChannel
19 | import kotlinx.coroutines.channels.Channel
20 | import kotlinx.coroutines.channels.ReceiveChannel
21 | import kotlinx.coroutines.flow.Flow
22 | import kotlinx.coroutines.flow.flow
23 | import kotlinx.coroutines.launch
24 | import kotlinx.coroutines.withContext
25 | import kotlin.coroutines.CoroutineContext
26 |
27 | interface LiveDataInteractor : BaseAsyncInteractor {
28 | val liveData: LiveData>
29 | }
30 |
31 | interface ResultInteractor : BaseAsyncInteractor>
32 |
33 | @ExperimentalCoroutinesApi
34 | interface ChannelInteractor : BaseAsyncInteractor {
35 | fun receive(): ReceiveChannel>
36 | }
37 |
38 | interface RxInteractor : BaseAsyncInteractor {
39 | fun observe(): Flowable>
40 | }
41 |
42 | interface FlowInteractor : BaseAsyncInteractor>>
43 |
44 | private class LiveDataInteractorImpl(private val interactor: BaseAsyncInteractor) :
45 | LiveDataInteractor {
46 | private val mutableLiveData = MutableLiveData>().apply {
47 | postValue(Uninitialized)
48 | }
49 |
50 | override val liveData = mutableLiveData
51 | override suspend operator fun invoke() {
52 | mutableLiveData.postValue(Loading())
53 | try {
54 | val result = interactor()
55 | mutableLiveData.postValue(Success(result))
56 | } catch (t: Throwable) {
57 | mutableLiveData.postValue(Fail(t))
58 | }
59 | }
60 | }
61 |
62 | @ExperimentalCoroutinesApi
63 | private class ChannelInteractorImpl(private val interactor: BaseAsyncInteractor) :
64 | ChannelInteractor {
65 |
66 | private val channel = BroadcastChannel>(Channel.CONFLATED).apply {
67 | offer(Uninitialized)
68 | }
69 |
70 | override fun receive() = channel.openSubscription()
71 |
72 | override suspend operator fun invoke() {
73 | channel.offer(Loading())
74 | try {
75 | channel.offer(Success(interactor()))
76 | } catch (t: Throwable) {
77 | channel.offer(Fail(t))
78 | }
79 | }
80 | }
81 |
82 | private class ResultInteractorImpl(private val interactor: BaseAsyncInteractor) :
83 | ResultInteractor {
84 | override suspend fun invoke(): CompleteResult {
85 | return try {
86 | Success(interactor())
87 | } catch (t: Throwable) {
88 | Fail(t)
89 | }
90 | }
91 | }
92 |
93 | private class RxInteractorImpl(private val interactor: BaseAsyncInteractor) :
94 | RxInteractor {
95 | override fun observe() = subject.toFlowable(BackpressureStrategy.LATEST)!!
96 | private val subject = BehaviorSubject.createDefault>(Uninitialized)
97 | override suspend operator fun invoke() {
98 | subject.onNext(Loading())
99 | try {
100 | subject.onNext(Success(interactor()))
101 | } catch (t: Throwable) {
102 | subject.onError(t)
103 | }
104 | }
105 | }
106 |
107 | private class FlowInteractorImpl(private val interactor: BaseAsyncInteractor) :
108 | FlowInteractor {
109 | override suspend fun invoke(): Flow> {
110 | return flow {
111 | emit(Loading())
112 | try {
113 | emit(Success(interactor()))
114 | } catch (t: Throwable) {
115 | emit(Fail(t))
116 | }
117 | }
118 | }
119 | }
120 |
121 | fun BaseAsyncInteractor.asResult(): ResultInteractor {
122 | return ResultInteractorImpl(this)
123 | }
124 |
125 | fun BaseAsyncInteractor.asLiveData(): LiveDataInteractor {
126 | return LiveDataInteractorImpl(this)
127 | }
128 |
129 | @ExperimentalCoroutinesApi
130 | fun BaseAsyncInteractor.asChannel(): ChannelInteractor {
131 | return ChannelInteractorImpl(this)
132 | }
133 |
134 | fun BaseAsyncInteractor.asRx(): RxInteractor {
135 | return RxInteractorImpl(this)
136 | }
137 |
138 | fun BaseAsyncInteractor.asFlow(): FlowInteractor {
139 | return FlowInteractorImpl(this)
140 | }
141 |
142 | fun CoroutineScope.launchInteractor(
143 | interactor: BaseAsyncInteractor,
144 | coroutineContext: CoroutineContext = Dispatchers.IO
145 | ) {
146 | launch(coroutineContext) { interactor() }
147 | }
148 |
149 | suspend fun runInteractor(
150 | interactor: BaseAsyncInteractor,
151 | coroutineContext: CoroutineContext = Dispatchers.IO
152 | ): T {
153 | return withContext(coroutineContext) { interactor() }
154 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/presentation/src/main/java/{{ cookiecutter.package_dir }}/presentation/extensions/LifecycleOwnerExtensions.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.presentation.extensions
2 |
3 | import androidx.fragment.app.Fragment
4 | import androidx.fragment.app.FragmentActivity
5 | import androidx.lifecycle.Lifecycle
6 | import androidx.lifecycle.LifecycleObserver
7 | import androidx.lifecycle.LifecycleOwner
8 | import androidx.lifecycle.OnLifecycleEvent
9 | import androidx.lifecycle.ViewModel
10 | import androidx.lifecycle.ViewModelProvider
11 | import androidx.lifecycle.ViewModelProviders
12 | import java.io.Serializable
13 |
14 | inline fun LifecycleOwner.getViewModel(factory: ViewModelProvider.Factory): VM {
15 | return when (this) {
16 | is Fragment -> ViewModelProviders.of(this, factory).get(VM::class.java)
17 | is FragmentActivity -> ViewModelProviders.of(this, factory).get(VM::class.java)
18 | else -> throw IllegalAccessError("Invalid LifecycleOwner")
19 | }
20 | }
21 |
22 | inline fun Fragment.getSharedViewModel(factory: ViewModelProvider.Factory): VM {
23 | return ViewModelProviders.of(requireActivity(), factory).get(VM::class.java)
24 | }
25 |
26 | private object UninitializedValue
27 |
28 | /**
29 | * This was copied from SynchronizedLazyImpl but modified to automatically initialize in ON_CREATE.
30 | */
31 | @Suppress("ClassName")
32 | class lifecycleAwareLazy(private val owner: LifecycleOwner, initializer: () -> T) : Lazy,
33 | Serializable {
34 | private var initializer: (() -> T)? = initializer
35 | @Volatile
36 | private var _value: Any? = UninitializedValue
37 | // final field is required to enable safe publication of constructed instance
38 | private val lock = this
39 |
40 | init {
41 | owner.lifecycle.addObserver(object : LifecycleObserver {
42 | @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
43 | fun onStart() {
44 | if (!isInitialized()) value
45 | owner.lifecycle.removeObserver(this)
46 | }
47 | })
48 | }
49 |
50 | @Suppress("LocalVariableName")
51 | override val value: T
52 | get() {
53 | val _v1 = _value
54 | if (_v1 !== UninitializedValue) {
55 | @Suppress("UNCHECKED_CAST")
56 | return _v1 as T
57 | }
58 |
59 | return synchronized(lock) {
60 | val _v2 = _value
61 | if (_v2 !== UninitializedValue) {
62 | @Suppress("UNCHECKED_CAST") (_v2 as T)
63 | } else {
64 | val typedValue = initializer!!()
65 | _value = typedValue
66 | initializer = null
67 | typedValue
68 | }
69 | }
70 | }
71 |
72 | override fun isInitialized(): Boolean = _value !== UninitializedValue
73 |
74 | override fun toString(): String =
75 | if (isInitialized()) value.toString() else "Lazy value not initialized yet."
76 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/presentation/src/main/java/{{ cookiecutter.package_dir }}/presentation/extensions/LiveDataExtensions.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.presentation.extensions
2 |
3 | import androidx.lifecycle.LifecycleOwner
4 | import androidx.lifecycle.LiveData
5 | import androidx.lifecycle.MediatorLiveData
6 | import androidx.lifecycle.Observer
7 | import {{ cookiecutter.package_name }}.presentation.util.EventObserver
8 | import {{ cookiecutter.package_name }}.presentation.util.SingleEvent
9 |
10 | inline fun LiveData.observe(
11 | lifecycleOwner: LifecycleOwner,
12 | crossinline observer: (T?) -> Unit
13 | ) {
14 | this.observe(lifecycleOwner, Observer {
15 | observer(it)
16 | })
17 | }
18 |
19 | inline fun LiveData.observeNonNull(
20 | lifecycleOwner: LifecycleOwner,
21 | crossinline observer: (T) -> Unit
22 | ) {
23 | this.observe(lifecycleOwner, Observer {
24 | it?.let(observer)
25 | })
26 | }
27 |
28 | inline fun > LiveData.observeEvent(
29 | lifecycleOwner: LifecycleOwner,
30 | crossinline observer: (E) -> Unit
31 | ) {
32 | this.observe(lifecycleOwner, EventObserver {
33 | observer(it)
34 | })
35 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/presentation/src/main/java/{{ cookiecutter.package_dir }}/presentation/injection/DaggerViewModelFactory.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.presentation.injection
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.ViewModelProvider
5 | import javax.inject.Inject
6 | import javax.inject.Provider
7 |
8 | internal class DaggerViewModelFactory @Inject constructor(
9 | private val creators: @JvmSuppressWildcards Map, Provider>
10 | ) : ViewModelProvider.Factory {
11 | override fun create(modelClass: Class): T {
12 | var creator: Provider? = creators[modelClass]
13 | if (creator == null) {
14 | for ((key, value) in creators) {
15 | if (modelClass.isAssignableFrom(key)) {
16 | creator = value
17 | break
18 | }
19 | }
20 | }
21 | if (creator == null) {
22 | throw IllegalArgumentException("Unknown model class: $modelClass")
23 | }
24 | try {
25 | @Suppress("UNCHECKED_CAST")
26 | return creator.get() as T
27 | } catch (e: Exception) {
28 | throw RuntimeException(e)
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/presentation/src/main/java/{{ cookiecutter.package_dir }}/presentation/injection/ViewModelBuilder.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.presentation.injection
2 |
3 | import androidx.lifecycle.ViewModelProvider
4 | import dagger.Binds
5 | import dagger.Module
6 | import {{ cookiecutter.package_name }}.presentation.ui.main.MainActivityBuilder
7 |
8 | @Module(
9 | includes = [
10 | MainActivityBuilder::class
11 | ]
12 | )
13 | abstract class ViewModelBuilder {
14 |
15 | @Binds
16 | internal abstract fun bindViewModelFactory(factory: DaggerViewModelFactory): ViewModelProvider.Factory
17 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/presentation/src/main/java/{{ cookiecutter.package_dir }}/presentation/injection/ViewModelKey.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.presentation.injection
2 |
3 | import androidx.lifecycle.ViewModel
4 | import dagger.MapKey
5 | import kotlin.reflect.KClass
6 |
7 | @Target(
8 | AnnotationTarget.FUNCTION,
9 | AnnotationTarget.PROPERTY_GETTER,
10 | AnnotationTarget.PROPERTY_SETTER
11 | )
12 | @Retention(AnnotationRetention.RUNTIME)
13 | @MapKey
14 | annotation class ViewModelKey(val value: KClass)
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/presentation/src/main/java/{{ cookiecutter.package_dir }}/presentation/nstack/Translation.java:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.presentation.nstack;
2 |
3 | /**
4 | * Created by nstack.io gradle translation plugin
5 | * Built from Accept Header: da-DK
6 | */
7 |
8 | public class Translation {
9 | public final static class defaultSection {
10 | public static String ok = "Ok";
11 | public static String cancel = "Annuller";
12 | public static String no = "Nej";
13 | public static String yes = "Ja";
14 | public static String retry = "Pr\u00F8v igen";
15 | public static String edit = "Rediger";
16 | public static String save = "Gem";
17 | public static String back = "Tilbage";
18 | public static String settings = "Indstillinger";
19 | public static String later = "Senere";
20 | public static String next = "N\u00E6ste";
21 | public static String previous = "Forrige";
22 | public static String skip = "Spring over";
23 | }
24 | public final static class error {
25 | public static String authenticationError = "Login er udl\u00F8bet, login venligst ind igen.";
26 | public static String connectionError = "Ingen eller d\u00E5rlig forbindelse, pr\u00F8v igen.";
27 | public static String errorTitle = "Fejl";
28 | public static String unknownError = "Ukendt fejl, pr\u00F8v igen.";
29 | }
30 | public final static class rateReminder {
31 | public static String title = "__title";
32 | public static String body = "__body";
33 | public static String yesBtn = "__yesBtn";
34 | public static String laterBtn = "__laterBtn";
35 | public static String noBtn = "__noBtn";
36 | }
37 | public final static class versionControl {
38 | public static String updateHeader = "Ny version er ude";
39 | public static String forceHeader = "Ny version er ude, du skal opdatere!";
40 | public static String negativeBtn = "Annuller";
41 | public static String positiveBtn = "Opdater";
42 | public static String newInVersionHeader = "Nyt i denne version";
43 | public static String okBtn = "Ok";
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/presentation/src/main/java/{{ cookiecutter.package_dir }}/presentation/ui/base/BaseActivity.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.presentation.ui.base
2 |
3 | import android.content.Context
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.ViewModelProvider
6 | import dagger.android.support.DaggerAppCompatActivity
7 | import dk.nodes.nstack.kotlin.inflater.NStackBaseContext
8 | import {{ cookiecutter.package_name }}.presentation.extensions.getViewModel
9 | import {{ cookiecutter.package_name }}.presentation.extensions.lifecycleAwareLazy
10 | import javax.inject.Inject
11 |
12 | abstract class BaseActivity : DaggerAppCompatActivity() {
13 |
14 | @Inject lateinit var viewModelFactory: ViewModelProvider.Factory
15 |
16 | override fun attachBaseContext(newBase: Context) {
17 | super.attachBaseContext(NStackBaseContext(newBase))
18 | }
19 |
20 | protected inline fun getViewModel(): VM =
21 | getViewModel(viewModelFactory)
22 |
23 | protected inline fun viewModel(): Lazy {
24 | return lifecycleAwareLazy(this) { getViewModel(viewModelFactory) }
25 | }
26 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/presentation/src/main/java/{{ cookiecutter.package_dir }}/presentation/ui/base/BaseFragment.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.presentation.ui.base
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.ViewModelProvider
5 | import dagger.android.support.DaggerFragment
6 | import {{ cookiecutter.package_name }}.presentation.extensions.getSharedViewModel
7 | import {{ cookiecutter.package_name }}.presentation.extensions.getViewModel
8 | import {{ cookiecutter.package_name }}.presentation.extensions.lifecycleAwareLazy
9 | import javax.inject.Inject
10 |
11 | abstract class BaseFragment : DaggerFragment() {
12 | @Inject lateinit var viewModelFactory: ViewModelProvider.Factory
13 |
14 | protected inline fun getViewModel(): VM =
15 | getViewModel(viewModelFactory)
16 |
17 | protected inline fun getSharedViewModel(): VM =
18 | getSharedViewModel(viewModelFactory)
19 |
20 | protected inline fun viewModel(): Lazy = lifecycleAwareLazy(this) {
21 | getViewModel()
22 | }
23 |
24 | protected inline fun sharedViewModel(): Lazy =
25 | lifecycleAwareLazy(this) {
26 | getSharedViewModel()
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/presentation/src/main/java/{{ cookiecutter.package_dir }}/presentation/ui/base/BaseViewModel.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.presentation.ui.base
2 |
3 | import androidx.lifecycle.ViewModel
4 | import io.reactivex.disposables.CompositeDisposable
5 | import java.io.Closeable
6 | import java.util.concurrent.ConcurrentHashMap
7 |
8 | open class BaseViewModel : ViewModel() {
9 |
10 | private val tagMap = ConcurrentHashMap()
11 |
12 | internal fun setTag(key: String, t: T): T {
13 | return getTag(key)
14 | ?: {
15 | tagMap[key] = t
16 | t
17 | }.invoke()
18 | }
19 |
20 | @Suppress("UNCHECKED_CAST")
21 | internal fun getTag(key: String) = tagMap[key] as? T
22 |
23 | protected val disposables = CompositeDisposable()
24 | override fun onCleared() {
25 | super.onCleared()
26 | tagMap.forEach { entry ->
27 | (entry.value as? Closeable)?.close()
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/presentation/src/main/java/{{ cookiecutter.package_dir }}/presentation/ui/base/BaseViewModelExtensions.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.presentation.ui.base
2 |
3 | import io.reactivex.disposables.CompositeDisposable
4 | import kotlinx.coroutines.CoroutineScope
5 | import kotlinx.coroutines.Dispatchers
6 | import kotlinx.coroutines.Job
7 | import kotlinx.coroutines.cancel
8 | import java.io.Closeable
9 | import kotlin.coroutines.CoroutineContext
10 |
11 | private const val jobKey = "{{ cookiecutter.package_name }}.presentation.base.JOB_KEY"
12 | private const val disposablesKey = "{{ cookiecutter.package_name }}.presentation.base.DISPOSABLES_KEY"
13 |
14 | val BaseViewModel.scope: CoroutineScope
15 | get() {
16 | return getTag(jobKey) ?: setTag(
17 | jobKey,
18 | CloseableCoroutineScope(Job() + Dispatchers.Main)
19 | )
20 | }
21 |
22 | val BaseViewModel.disposables: CompositeDisposable
23 | get() {
24 | return getTag(disposablesKey)?.disposables ?: setTag(
25 | disposablesKey, CloseableCompositeDisposable()
26 | ).disposables
27 | }
28 |
29 | private class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
30 | override val coroutineContext: CoroutineContext = context
31 | override fun close() {
32 | coroutineContext.cancel()
33 | }
34 | }
35 |
36 | private class CloseableCompositeDisposable : Closeable {
37 | val disposables = CompositeDisposable()
38 | override fun close() {
39 | disposables.dispose()
40 | }
41 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/presentation/src/main/java/{{ cookiecutter.package_dir }}/presentation/ui/main/MainActivity+Hockey.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.presentation.ui.main
2 |
3 | import {{ cookiecutter.package_name }}.presentation.BuildConfig
4 | import net.hockeyapp.android.CrashManager
5 | import net.hockeyapp.android.CrashManagerListener
6 | import net.hockeyapp.android.UpdateManager
7 |
8 | fun MainActivity.setupHockey() {
9 | if (BuildConfig.DEBUG) {
10 | // Auto-send crashes without asking user
11 | CrashManager.register(this, object : CrashManagerListener() {
12 | override fun shouldAutoUploadCrashes(): Boolean {
13 | return true
14 | }
15 | })
16 |
17 | // Check for updates from Hockey
18 | UpdateManager.register(this)
19 | }
20 |
21 | // GDPR / Google's Personal/Sensitive policy dictates that we should ask the user
22 | // in user facing builds
23 | else {
24 | CrashManager.register(this)
25 | }
26 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/presentation/src/main/java/{{ cookiecutter.package_dir }}/presentation/ui/main/MainActivity+NStack.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.presentation.ui.main
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import androidx.appcompat.app.AlertDialog
6 | import dk.nodes.nstack.kotlin.NStack
7 | import dk.nodes.nstack.kotlin.models.AppUpdate
8 | import dk.nodes.nstack.kotlin.models.AppUpdateState
9 | import {{ cookiecutter.package_name }}.presentation.nstack.Translation
10 | import timber.log.Timber
11 |
12 | fun MainActivity.setupNstack() {
13 | NStack.onAppUpdateListener = { appUpdate ->
14 | when (appUpdate.state) {
15 | AppUpdateState.NONE -> {
16 | }
17 | AppUpdateState.UPDATE -> {
18 | showUpdateDialog(appUpdate)
19 | }
20 | AppUpdateState.FORCE -> {
21 | showForceDialog(appUpdate)
22 | }
23 | AppUpdateState.CHANGELOG -> {
24 | showChangelogDialog(appUpdate)
25 | }
26 | }
27 | }
28 | NStack.appOpen { success ->
29 | Timber.e("appopen success = $success")
30 | }
31 | }
32 |
33 | fun MainActivity.showUpdateDialog(appUpdate: AppUpdate) {
34 | AlertDialog.Builder(this)
35 | .setTitle(appUpdate.title)
36 | .setMessage(appUpdate.message)
37 | .setPositiveButton(Translation.defaultSection.ok) { dialog, _ ->
38 | dialog.dismiss()
39 | }
40 | .show()
41 | }
42 |
43 | fun MainActivity.showChangelogDialog(appUpdate: AppUpdate) {
44 | AlertDialog.Builder(this)
45 | .setTitle(appUpdate.title)
46 | .setMessage(appUpdate.message)
47 | .setNegativeButton(appUpdate.negativeBtn) { dialog, _ ->
48 | dialog.dismiss()
49 | }
50 | .show()
51 | }
52 |
53 | fun MainActivity.startPlayStore() {
54 | try {
55 | startActivity(
56 | Intent(
57 | Intent.ACTION_VIEW,
58 | Uri.parse("market://details?id=$packageName")
59 | )
60 | )
61 | } catch (anfe: android.content.ActivityNotFoundException) {
62 | startActivity(
63 | Intent(
64 | Intent.ACTION_VIEW,
65 | Uri.parse("https://play.google.com/store/apps/details?id=$packageName")
66 | )
67 | )
68 | }
69 | }
70 |
71 | fun MainActivity.showForceDialog(appUpdate: AppUpdate) {
72 | val dialog = AlertDialog.Builder(this)
73 | .setTitle(appUpdate.title)
74 | .setMessage(appUpdate.message)
75 | .setCancelable(false)
76 | .setPositiveButton(Translation.defaultSection.ok, null)
77 | .create()
78 |
79 | dialog.setOnShowListener {
80 | val b = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
81 | b.setOnClickListener {
82 | startPlayStore()
83 | }
84 | }
85 |
86 | dialog.show()
87 | }
88 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/presentation/src/main/java/{{ cookiecutter.package_dir }}/presentation/ui/main/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.presentation.ui.main
2 |
3 | import android.os.Bundle
4 | import androidx.core.view.isVisible
5 | import com.google.android.material.snackbar.Snackbar
6 | import {{ cookiecutter.package_name }}.presentation.R
7 | import {{ cookiecutter.package_name }}.presentation.extensions.observeNonNull
8 | import {{ cookiecutter.package_name }}.presentation.nstack.Translation
9 | import {{ cookiecutter.package_name }}.presentation.ui.base.BaseActivity
10 | import kotlinx.android.synthetic.main.activity_main.*
11 | import net.hockeyapp.android.UpdateManager
12 |
13 | class MainActivity : BaseActivity() {
14 |
15 | private val viewModel by viewModel()
16 |
17 | override fun onCreate(savedInstanceState: Bundle?) {
18 | super.onCreate(savedInstanceState)
19 | setContentView(R.layout.activity_main)
20 | // setupNstack()
21 | // setupHockey()
22 | viewModel.viewState.observeNonNull(this) { state ->
23 | showLoading(state)
24 | showPosts(state)
25 | showErrorMessage(state)
26 | }
27 | viewModel.fetchPosts()
28 | }
29 |
30 | override fun onDestroy() {
31 | super.onDestroy()
32 | // If we checked for hockey updates, unregister
33 | UpdateManager.unregister()
34 | }
35 |
36 | private fun showPosts(state: MainActivityViewState) {
37 | postsTextView.text = state.posts.joinToString { it.title + System.lineSeparator() }
38 | }
39 |
40 | private fun showLoading(state: MainActivityViewState) {
41 | postsProgressBar.isVisible = state.isLoading
42 | }
43 |
44 | private fun showErrorMessage(state: MainActivityViewState) {
45 | state.errorMessage?.let {
46 | if (it.consumed) return@let
47 |
48 | Snackbar.make(
49 | postsTextView,
50 | it.consume() ?: Translation.error.unknownError,
51 | Snackbar.LENGTH_SHORT
52 | )
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/presentation/src/main/java/{{ cookiecutter.package_dir }}/presentation/ui/main/MainActivityBuilder.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.presentation.ui.main
2 |
3 | import androidx.lifecycle.ViewModel
4 | import dagger.Binds
5 | import dagger.Module
6 | import dagger.android.ContributesAndroidInjector
7 | import dagger.multibindings.IntoMap
8 | import {{ cookiecutter.package_name }}.presentation.injection.ViewModelKey
9 |
10 | @Module
11 | internal abstract class MainActivityBuilder {
12 |
13 | @Binds
14 | @IntoMap
15 | @ViewModelKey(MainActivityViewModel::class)
16 | abstract fun bindMainActivityViewMode(viewModel: MainActivityViewModel): ViewModel
17 |
18 | @ContributesAndroidInjector
19 | internal abstract fun mainActivity(): MainActivity
20 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/presentation/src/main/java/{{ cookiecutter.package_dir }}/presentation/ui/main/MainActivityViewModel.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.presentation.ui.main
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MediatorLiveData
5 | import {{ cookiecutter.package_name }}.domain.interactors.Fail
6 | import {{ cookiecutter.package_name }}.domain.interactors.InteractorResult
7 | import {{ cookiecutter.package_name }}.domain.interactors.Loading
8 | import {{ cookiecutter.package_name }}.domain.interactors.PostsInteractor
9 | import {{ cookiecutter.package_name }}.domain.interactors.Success
10 | import {{ cookiecutter.package_name }}.domain.interactors.Uninitialized
11 | import {{ cookiecutter.package_name }}.models.Post
12 | import {{ cookiecutter.package_name }}.presentation.extensions.asChannel
13 | import {{ cookiecutter.package_name }}.presentation.extensions.asFlow
14 | import {{ cookiecutter.package_name }}.presentation.extensions.asLiveData
15 | import {{ cookiecutter.package_name }}.presentation.extensions.asResult
16 | import {{ cookiecutter.package_name }}.presentation.extensions.asRx
17 | import {{ cookiecutter.package_name }}.presentation.extensions.runInteractor
18 | import {{ cookiecutter.package_name }}.presentation.nstack.Translation
19 | import {{ cookiecutter.package_name }}.presentation.ui.base.BaseViewModel
20 | import {{ cookiecutter.package_name }}.presentation.ui.base.scope
21 | import {{ cookiecutter.package_name }}.presentation.util.SingleEvent
22 | import kotlinx.coroutines.launch
23 | import javax.inject.Inject
24 |
25 | class MainActivityViewModel @Inject constructor(
26 | postsInteractor: PostsInteractor
27 | ) : BaseViewModel() {
28 |
29 | private val liveDataInteractor = postsInteractor.asLiveData()
30 | private val resultInteractor = postsInteractor.asResult()
31 | private val channelInteractor = postsInteractor.asChannel()
32 | private val rxInteractor = postsInteractor.asRx()
33 | private val flowInteractor = postsInteractor.asFlow()
34 | private val _viewState = MediatorLiveData()
35 | val viewState: LiveData = _viewState
36 |
37 | init {
38 | /** Uncomment below to test LiveDataInteractor */
39 | // _viewState.addSource(
40 | // Transformations.map(this.liveDataInteractor.liveData, ::mapResult),
41 | // _viewState::postValue
42 | // )
43 | //
44 | /** Uncomment below to test ChannelInteractor */
45 | // scope.launch {
46 | // channelInteractor.receive()
47 | // .map(Dispatchers.IO) { mapResult(it) }
48 | // .consumeEach(_viewState::postValue)
49 | // }
50 | //
51 | /** Uncomment below to test RxInteractor */
52 | // disposables += rxInteractor.observe()
53 | // .subscribeOn(Schedulers.io())
54 | // .observeOn(Schedulers.io())
55 | // .map(this::mapResult)
56 | // .onErrorReturn { mapResult(Fail(it)) }
57 | // .subscribe(_viewState::postValue, Timber::e)
58 | }
59 |
60 | fun fetchPosts() {
61 | /** Uncomment below to test RxInteractor */
62 | // scope.launchInteractor(rxInteractor)
63 | /** Uncomment below to test ChannelInteractor */
64 | // scope.launchInteractor(channelInteractor)
65 | /** Uncomment below to test LiveDataInteractor */
66 | // scope.launchInteractor(liveDataInteractor)
67 |
68 | /** Uncomment below to test ResultInteractor */
69 | scope.launch {
70 | _viewState.postValue(mapResult(Loading()))
71 | _viewState.postValue(mapResult(runInteractor(resultInteractor)))
72 | }
73 |
74 | /** Uncomment below to test FlowInteractor */
75 | // scope.launch(Dispatchers.IO) {
76 | // runInteractor(flowInteractor)
77 | // .map { mapResult(it) }
78 | // .collect { state ->
79 | // _viewState.postValue(state)
80 | // }
81 | // }
82 | }
83 |
84 | private fun mapResult(result: InteractorResult>): MainActivityViewState {
85 | return when (result) {
86 | is Success -> _viewState.value?.copy(posts = result.data, isLoading = false)
87 | ?: MainActivityViewState(posts = result.data)
88 | is Loading -> viewState.value?.copy(isLoading = true)
89 | ?: MainActivityViewState(isLoading = true)
90 | is Fail -> viewState.value?.copy(
91 | errorMessage = SingleEvent(Translation.error.unknownError),
92 | isLoading = false
93 | ) ?: MainActivityViewState(errorMessage = SingleEvent(Translation.error.unknownError))
94 | is Uninitialized -> MainActivityViewState()
95 | }
96 | }
97 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/presentation/src/main/java/{{ cookiecutter.package_dir }}/presentation/ui/main/MainActivityViewState.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.presentation.ui.main
2 |
3 | import {{ cookiecutter.package_name }}.models.Post
4 | import {{ cookiecutter.package_name }}.presentation.util.SingleEvent
5 |
6 | data class MainActivityViewState(
7 | val posts: List = emptyList(),
8 | val errorMessage: SingleEvent? = null,
9 | val isLoading: Boolean = false
10 | )
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/presentation/src/main/java/{{ cookiecutter.package_dir }}/presentation/util/SharedElementHelper.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.presentation.util
2 |
3 | import android.app.Activity
4 | import android.os.Build
5 | import android.os.Bundle
6 | import android.view.View
7 | import android.widget.ImageView
8 | import androidx.annotation.RequiresApi
9 | import androidx.core.app.ActivityOptionsCompat
10 | import androidx.core.util.Pair
11 | import androidx.fragment.app.FragmentTransaction
12 | import java.lang.ref.WeakReference
13 |
14 | class SharedElementHelper {
15 | private val sharedElementViews = mutableMapOf, String?>()
16 | private val transitionData = mutableMapOf()
17 |
18 | fun addSharedElementTransitionData(key: String, data: String) {
19 | transitionData[key] = data
20 | }
21 |
22 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
23 | fun addSharedElement(view: View) {
24 | sharedElementViews[WeakReference(view)] = view.transitionName
25 | }
26 |
27 | fun addSharedElement(view: View, name: String) {
28 | sharedElementViews[WeakReference(view)] = name
29 | }
30 |
31 | fun applyToTransaction(tx: FragmentTransaction) {
32 | for ((viewRef, customTransitionName) in sharedElementViews) {
33 | viewRef.get()?.apply {
34 | tx.addSharedElement(this, customTransitionName!!)
35 | }
36 | }
37 | }
38 |
39 | fun applyToIntent(activity: Activity): Bundle? {
40 | return ActivityOptionsCompat.makeSceneTransitionAnimation(
41 | activity,
42 | *sharedElementViews.map { Pair(it.key.get(), it.value) }.toList().toTypedArray()
43 | ).toBundle()
44 | }
45 |
46 | fun isEmpty(): Boolean = sharedElementViews.isEmpty()
47 |
48 | fun hasExternalImageViews(): Boolean = sharedElementViews.any { it is ImageView }
49 | }
50 |
51 | fun sharedElements(vararg elements: kotlin.Pair): SharedElementHelper {
52 | return SharedElementHelper().apply {
53 | elements.forEach {
54 | addSharedElement(it.first, it.second)
55 | }
56 | }
57 | }
58 |
59 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
60 | fun sharedElements(vararg elements: View): SharedElementHelper {
61 | return SharedElementHelper().apply {
62 | elements.forEach {
63 | addSharedElement(it)
64 | }
65 | }
66 | }
67 |
68 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
69 | fun sharedElements(elements: List): SharedElementHelper {
70 | return sharedElements(*elements.toTypedArray())
71 | }
72 |
73 | fun SharedElementHelper.withTransitionData(vararg transitionDataPairs: kotlin.Pair): SharedElementHelper {
74 | transitionDataPairs.forEach {
75 | addSharedElementTransitionData(it.first, it.second)
76 | }
77 | return this
78 | }
79 |
80 | fun SharedElementHelper.withTransitionData(data: Map): SharedElementHelper {
81 | return withTransitionData(*(data.entries.map { kotlin.Pair(it.key, it.value) }.toTypedArray()))
82 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/presentation/src/main/java/{{ cookiecutter.package_dir }}/presentation/util/SingleEvent.kt:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.presentation.util
2 |
3 | import androidx.lifecycle.Observer
4 |
5 | /**
6 | * Used as a wrapper for data that is exposed via a LiveData that represents an event.
7 | *
8 | * [Read more about this.](https://medium.com/google-developers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150)
9 | */
10 | open class SingleEvent(private val content: T) {
11 |
12 | var consumed = false
13 | private set // Allow external read but not write
14 |
15 | /**
16 | * Consumes the content if it's not been consumed yet.
17 | * @return The unconsumed content or `null` if it was consumed already.
18 | */
19 | fun consume(): T? {
20 | return if (consumed) {
21 | null
22 | } else {
23 | consumed = true
24 | content
25 | }
26 | }
27 |
28 | /**
29 | * @return The content whether it's been handled or not.
30 | */
31 | fun peek(): T = content
32 |
33 | override fun equals(other: Any?): Boolean {
34 | if (this === other) return true
35 | if (javaClass != other?.javaClass) return false
36 |
37 | other as SingleEvent<*>
38 |
39 | if (content != other.content) return false
40 | if (consumed != other.consumed) return false
41 |
42 | return true
43 | }
44 |
45 | override fun hashCode(): Int {
46 | var result = content?.hashCode() ?: 0
47 | result = 31 * result + consumed.hashCode()
48 | return result
49 | }
50 | }
51 |
52 | /**
53 | * An [Observer] for [SingleEvent]s, simplifying the pattern of checking if the [SingleEvent]'s content has
54 | * already been consumed.
55 | *
56 | * [onEventUnconsumedContent] is *only* called if the [SingleEvent]'s contents has not been consumed.
57 | */
58 |
59 | class EventObserver(private val onEventUnconsumedContent: (T) -> Unit) : Observer> {
60 | override fun onChanged(event: SingleEvent?) {
61 | event?.consume()?.run(onEventUnconsumedContent)
62 | }
63 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/presentation/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
14 |
15 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/presentation/src/main/res/values/nstack_keys.xml:
--------------------------------------------------------------------------------
1 |
2 | {default_ok}
3 | {default_cancel}
4 | {default_no}
5 | {default_yes}
6 | {default_retry}
7 | {default_edit}
8 | {default_save}
9 | {default_back}
10 | {default_settings}
11 | {default_later}
12 | {default_next}
13 | {default_previous}
14 | {default_skip}
15 | {error_authenticationError}
16 | {error_connectionError}
17 | {error_errorTitle}
18 | {error_unknownError}
19 | {rateReminder_title}
20 | {rateReminder_body}
21 | {rateReminder_yesBtn}
22 | {rateReminder_laterBtn}
23 | {rateReminder_noBtn}
24 | {versionControl_updateHeader}
25 | {versionControl_forceHeader}
26 | {versionControl_negativeBtn}
27 | {versionControl_positiveBtn}
28 | {versionControl_newInVersionHeader}
29 | {versionControl_okBtn}
30 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/presentation/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | presentation
3 |
4 |
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/presentation/src/test/java/{{ cookiecutter.package_dir }}/presentation/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package {{ cookiecutter.package_name }}.presentation;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/{{cookiecutter.repo_name}}/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 | include ':domain'
3 | include ':presentation'
4 | include ':data'
5 |
--------------------------------------------------------------------------------