├── .gitignore
├── .idea
├── codeStyles
│ └── Project.xml
├── encodings.xml
├── misc.xml
├── modules.xml
├── runConfigurations.xml
└── vcs.xml
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── sysdata
│ │ │ └── kt
│ │ │ └── ktandroidarchitecture
│ │ │ ├── MainApplication.kt
│ │ │ ├── di
│ │ │ └── AppModule.kt
│ │ │ ├── repository
│ │ │ ├── AuthRepositoryImpl.kt
│ │ │ ├── GitHubRepo.kt
│ │ │ └── model
│ │ │ │ ├── UIUserLogged.kt
│ │ │ │ └── UserLogged.kt
│ │ │ ├── ui
│ │ │ ├── GitHubActivity.kt
│ │ │ └── LoginActivity.kt
│ │ │ ├── usecase
│ │ │ ├── GitHubUseCase.kt
│ │ │ ├── LoginUseCase.kt
│ │ │ └── Params.kt
│ │ │ └── viewmodel
│ │ │ ├── GitHubViewModel.kt
│ │ │ └── LoginViewModel.kt
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ └── ic_launcher_background.xml
│ │ ├── layout
│ │ ├── activity_git_hub.xml
│ │ └── activity_login.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ └── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ └── test
│ └── java
│ └── com
│ └── sysdata
│ └── kt
│ └── ktandroidarchitecture
│ ├── AuthRepositoryTest.kt
│ ├── CheckModulesTest.kt
│ ├── GitHubRepositoryTest.kt
│ ├── LiveDataTestUtil.kt
│ ├── LoginUseCaseTest.kt
│ └── LoginViewModelTest.kt
├── build.gradle
├── docs
├── Action.md
├── ActionFlowDiagram-thumbnail.png
├── ActionFlowDiagram.png
├── DI-Dagger.md
├── InstrumentedTests.md
├── Repository.md
├── UI.md
├── UI_to_VM.png
├── UiModel.md
├── UnitTests.md
├── UseCase.md
├── ViewModel.md
├── actionQueue.png
├── actionSingleUseCase.png
├── action_execute.png
├── action_queue_execute.png
├── create_usecase.gif
├── queue_action.gif
├── repository.png
├── settings.zip
├── single_action.gif
├── test
│ ├── test_apk_location.png
│ ├── test_apk_request_firebase.png
│ ├── test_device_results_firebase.png
│ ├── test_result.png
│ ├── test_run_instrumented_firebase.png
│ ├── test_run_test.png
│ ├── test_select_devices_firebase.png
│ └── test_single_device_result_firebase.png
├── uimodel.png
└── usecase.png
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── ktandroidarchitecturecore
├── .gitignore
├── build.gradle
├── proguard-rules.pro
├── publishBintray.gradle
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── it
│ │ └── sysdata
│ │ └── ktandroidarchitecturecore
│ │ ├── BaseConfig.kt
│ │ ├── BaseRepository.kt
│ │ ├── exception
│ │ └── Failure.kt
│ │ ├── functional
│ │ └── Either.kt
│ │ ├── interactor
│ │ ├── Action.kt
│ │ ├── ActionParams.kt
│ │ ├── ActionQueue.kt
│ │ ├── SingleAction.kt
│ │ └── UseCase.kt
│ │ └── platform
│ │ ├── BaseViewModel.kt
│ │ ├── KParcelable.kt
│ │ ├── SafeExecute.kt
│ │ ├── SafeExecuteInterface.kt
│ │ └── SingleLiveEvent.kt
│ └── res
│ └── values
│ └── strings.xml
├── networkmodule
├── .gitignore
├── build.gradle
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── example
│ │ └── networkmodule
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── example
│ │ │ └── networkmodule
│ │ │ ├── api
│ │ │ ├── api
│ │ │ │ ├── GitHubService.kt
│ │ │ │ └── GitHubServiceAPI.kt
│ │ │ └── model
│ │ │ │ └── Repo.kt
│ │ │ └── di
│ │ │ └── NetworkModule.kt
│ └── res
│ │ └── values
│ │ └── strings.xml
│ └── test
│ └── java
│ └── com
│ └── example
│ └── networkmodule
│ └── ExampleUnitTest.kt
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.ap_
4 |
5 | # Files for the ART/Dalvik VM
6 | *.dex
7 |
8 | # Java class files
9 | *.class
10 |
11 | # Generated files
12 | bin/
13 | gen/
14 | out/
15 |
16 | # Gradle files
17 | .gradle/
18 | build/
19 |
20 | # Local configuration file (sdk path, etc)
21 | local.properties
22 |
23 | # Proguard folder generated by Eclipse
24 | proguard/
25 |
26 | # Log Files
27 | *.log
28 |
29 | # Android Studio Navigation editor temp files
30 | .navigation/
31 |
32 | # Android Studio captures folder
33 | captures/
34 |
35 | # IntelliJ
36 | *.iml
37 | .idea/
38 |
39 | # Keystore files
40 | # Uncomment the following line if you do not want to check your keystore files in.
41 | #*.jks
42 |
43 | # External native build folder generated in Android Studio 2.2 and later
44 | .externalNativeBuild
45 |
46 | # Google Services (e.g. APIs or Firebase)
47 | google-services.json
48 |
49 | # Freeline
50 | freeline.py
51 | freeline/
52 | freeline_project_description.json
53 |
54 | # fastlane
55 | fastlane/report.xml
56 | fastlane/Preview.html
57 | fastlane/screenshots
58 | fastlane/test_output
59 | fastlane/readme.md
60 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | xmlns:android
14 |
15 | ^$
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | xmlns:.*
25 |
26 | ^$
27 |
28 |
29 | BY_NAME
30 |
31 |
32 |
33 |
34 |
35 |
36 | .*:id
37 |
38 | http://schemas.android.com/apk/res/android
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | .*:name
48 |
49 | http://schemas.android.com/apk/res/android
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | name
59 |
60 | ^$
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | style
70 |
71 | ^$
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 | .*
81 |
82 | ^$
83 |
84 |
85 | BY_NAME
86 |
87 |
88 |
89 |
90 |
91 |
92 | .*
93 |
94 | http://schemas.android.com/apk/res/android
95 |
96 |
97 | ANDROID_ATTRIBUTE_ORDER
98 |
99 |
100 |
101 |
102 |
103 |
104 | .*
105 |
106 | .*
107 |
108 |
109 | BY_NAME
110 |
111 |
112 |
113 |
114 |
115 |
116 |
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
24 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # KTAndroidArchitecture
2 | A Kotlin android architecture with Google Architecture Components
3 | ## 1. A Brief Introduciton
4 | The app is a sample project that shows how to implement the KTAndroidArchitecture into your Android app.
5 |
6 | ### 1.1 What is KTAndroidArchitecture?
7 | It is a layer-based architecture that allows a real decoupling of the UI components from the business logic.
8 |
9 | 
10 |
11 | The main components are:
12 |
13 | * [UIModel](docs/UiModel.md)
14 | * [UseCase](docs/UseCase.md)
15 | * [Repository](docs/Repository.md)
16 | * [Action](docs/Action.md)
17 |
18 | Here you can find a list of guides depending on your implementations :
19 | * [DI - Use with Dagger](docs/DI-Dagger.md)
20 | * [Instrumented Tests with Espresso](docs/InstrumentedTests.md)
21 | * [Unit Tests with Koin](docs/UnitTests.md)
22 |
23 | ## 2. How to use it?
24 |
25 | ### 2.1 Import dependency
26 |
27 | #### 2.1.1 in **Project level `build.gradle`** add this repository
28 | ```gradle
29 | maven { url 'https://dl.bintray.com/sysdata/maven' }
30 | ```
31 | #### 2.1.2 in your **App level `build.gradle`** add this dependecy
32 | ```gradle
33 | implementation 'it.sysdata.mobile:ktandroidarchitecturecore:1.0.3'
34 | ```
35 |
36 | #### 2.1.3 import the settings for use live template and file template
37 |
38 | * [Settings.zip](docs/settings.zip)
39 |
40 |
41 | ### 2.2 Create a Repository
42 | A repository just needs to extend **BaseRepository** in this way
43 | ```kotlin
44 | class AuthRepository:BaseRepository()
45 | ```
46 |
47 | ### 2.3 Create a UseCase
48 | A usecase has to extend **UseCase** and implement a the "run" method:
49 | ```kotlin
50 | class LoginUseCase: UseCase() {
51 | override suspend fun run(params: LoginActionParams): Either {
52 | do something
53 | return result
54 | }
55 | }
56 | ```
57 | or you can just create a new usecase by going to File Template New->Kotlin Use Case
58 |
59 | 
60 |
61 |
62 | The "run" function defined inside the UseCase can return a **Failure object** or a **Model object**.
63 |
64 | The input params are defined in a Param object which is a data class defined like this
65 | ```kotlin
66 | data class LoginActionParams(val email: String, val password: String) : ActionParams()
67 | ```
68 |
69 | ### 2.4 Create a ViewModel for your Activity/Fragment
70 |
71 | A ViewModel needs to extend the abstract class BaseViewModel
72 | ```kotlin
73 | class LoginViewModel: BaseViewModel()
74 | ```
75 |
76 | #### 2.4.1 Define an Action inside the ViewModel
77 |
78 | An **Action** can be created by using a Builder like this
79 | ```kotlin
80 | val actionLogin = Action.Builder()
81 | .useCase(LoginUseCase::class.java)
82 | .buildWithUiModel { UiModel(it) }
83 | ```
84 | or you can just use the live template **ac** to create a usecase straight from the class!
85 |
86 | 
87 |
88 | The flow is composed by the following steps:
89 |
90 | 1. The execution of an Action performed by the method execute(...) of Action class.
91 | 2. The first logical step is the post of an object inside an internal livedata called LoadingLiveData indicating that loading has started. The UI can observe this LiveData using the method observeLoadingStatus(...).
92 | 3. The next step is the execution of a usecase which uses repositories to retrieve some data.
93 | 4. The result of repositories' call is returned to the usecase.
94 | 5. The post of an object inside an internal livedata called LoadingLivedata indicating that loading has finished. The UI can observe this LiveData using the method observeLoadingStatus(...)
95 | 6. the post of the usecase result in two internal livedatas based on the success or the failure; the UI can observe these two LiveDatas by using observe(...) and observeFailure(...)
96 |
97 | 
98 |
99 | ### 2.5 Call the Action from the Activity/Fragment
100 |
101 | 
102 |
103 | An action has several methods like:
104 | - ``` action.observe(...) ```, this method observes the success of the operation defined inside the usecase;
105 | - ``` action.observeFailure(...) ```, this method observes the failure of the operation;
106 | - ``` action.observeLoadingStatus(...) ```, this method observes the loading state of the operation;
107 | - ``` action.execute(...) ```, this method calls the "run" function inside the usecase and executes the operation;
108 | - ``` action.safeExecute(...) ```, this method calls the "run" function inside the usecase and executes the operation into in a SafeExecuteInterface;
109 |
110 | ### 2.6 Custom safe executor
111 | The `safeExecute()` method call is meant to be used if you plan a different behaviour based on the error of your response.
112 | If you use the simple `execute()` call, whenever there's an error, the method will throw an exception and you will have to
113 | handle it by try-catching it and defining a behaviour for that UseCase.
114 | If you use the `safeExecute()` method call, whenever there's an error, it will fallback to an `InternalError` failure in case of
115 | an exception, saving you from an application crash.
116 | Moreover, if you'd like to customize the behaviour in a general way, you can define a class that extends from the `SafeExecuteInterface`
117 | and inside the overridden `safeExecute()` call you can define the custom behaviour you want for your application.
118 | Finally, you'll have to assign the custom `SafeExecutor` as the `BaseConfig.safeExecutor` variable.
119 |
120 | ```kotlin
121 | BaseConfig.safeExecutor = MySafeExecutor()
122 | ```
123 |
124 | It is suggested that you assign this variable as soon as possible (i.e. in the `Application`'s class `onCreate()` method).
125 |
126 |
127 | To call an action you have to write this:
128 | ```kotlin
129 | viewModel.action.observe(this, ::onActionSuccess)
130 | viewModel.action.observeFailure(this, ::onActionFailed)
131 | viewModel.action.execute(Params)
132 | ```
133 |
134 | ## 3 KTAndroidArchitecture main components
135 |
136 | ### 3.1 UI
137 |
138 | The UI layer of the architecture includes Activities, Fragments and Views.
139 |
140 | ### 3.2 UIModel
141 |
142 | A **UIModel** is an object that contains all UI-related datas of a view, fragment or activity.
143 |
144 | [Read More](docs/UiModel.md)
145 |
146 | ### 3.3 ViewModels with Livedata
147 |
148 | Each activity or fragment could have a **ViewModel** which is an object designed to store and manage UI-related data in a lifecycle aware way by defining some **Actions** to call one or more **UseCases**
149 |
150 | ### 3.4 UseCase
151 | A **UseCase** is a wrapper for a small business logic operation. A **UseCase** can use one or more **Repository** to retrieve or to set data, then it returns the response event.
152 |
153 | [Read More](docs/UseCase.md)
154 |
155 | ### 3.5 Repository
156 | A **Repository** handles the process of saving or retrieving data from a datasource, it is managed by one or more **UseCase**.
157 |
158 | [Read More](docs/Repository.md)
159 |
160 | ### 3.6 Action
161 | An **Action** handles the process of calling a **UseCase** and map the response. Usually, an action uses only one **UseCase**, but it is possible to define an **ActionQueue** in order to call multiple **UseCases** sequentially.
162 | Into an **ActionQueue** each **UseCase**, except the first, takes the result of the previous as parameters and gives the output to the next one.
163 |
164 | **Action**
165 |
166 | 
167 |
168 | ```kotlin
169 | val actionLogin = Action.Builder()
170 | .useCase(LoginUseCase::class.java)
171 | .buildWithUiModel { UiModel(it) }
172 | ```
173 |
174 | **ActionQueue**
175 |
176 | 
177 |
178 | ```kotlin
179 | val actionQueue = ActionQueue.Builder()
180 | .setFirstUseCase(FirstUseCase::class.java)
181 | .addUseCase(...)
182 | .setLastUseCase(...)
183 | ```
184 | or just **acq** command live template
185 |
186 | 
187 |
188 | Optionally, you can add a CoroutineScope to the action.
189 |
190 | ```kotlin
191 | actionLogin.execute(LoginActionParams(username, password), viewModelScope)
192 | ```
193 |
194 | For more informations about the CoroutineScope,
195 | please refer to this articles :
196 | - https://medium.com/androiddevelopers/easy-coroutines-in-android-viewmodelscope-25bffb605471
197 | - https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/
198 |
199 | ### 3.7 Testing
200 |
201 | #### 3.7.1 InstrumentedTests
202 | It is possible to perform instrumented tests on the architecture.
203 | To learn more about instrumented tests, please, refer to the following section: [Instrumented Tests](docs/InstrumentedTests.md)
204 |
205 | #### 3.7.2 Unit test
206 | It is possible to perform unit tests on the architecture.
207 | To learn more about unit tests, please, refer to the following section: [Unit tests](docs/UnitTests.md)
208 |
209 | ## CHANGELOG ##
210 |
211 | **1.0.3**
212 | - added safeExecute.
213 |
214 | **1.0.2**
215 | - Added Scope to actions.
216 |
217 | **1.0.1**
218 | - fixed abnormal behavior on ActionQueue.
219 | - Added the possibility to add the action instance instead of the class. This is useful for dependency injection (i.e. Dagger).
220 | - Added ActionSingle.
221 | - fixed the behavior of the error post in the observeFailure function.
222 | - Updated Gradle plugin version.
223 |
224 | # License
225 |
226 | Copyright (C) 2020 Sysdata S.p.A.
227 |
228 | Licensed under the Apache License, Version 2.0 (the "License");
229 | you may not use this file except in compliance with the License.
230 | You may obtain a copy of the License at
231 |
232 | http://www.apache.org/licenses/LICENSE-2.0
233 |
234 | Unless required by applicable law or agreed to in writing, software
235 | distributed under the License is distributed on an "AS IS" BASIS,
236 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
237 | See the License for the specific language governing permissions and
238 | limitations under the License.
239 |
240 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 | apply plugin: 'kotlin-kapt'
5 |
6 | android {
7 | compileSdkVersion 28
8 | defaultConfig {
9 | applicationId "com.sysdata.kt.ktandroidarchitecture"
10 | minSdkVersion 19
11 | targetSdkVersion 28
12 | versionCode 1
13 | versionName "1.0"
14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
15 | }
16 | buildTypes {
17 | release {
18 | minifyEnabled false
19 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
20 | }
21 | }
22 |
23 | packagingOptions {
24 | exclude 'META-INF/main.kotlin_module'
25 | }
26 | }
27 |
28 | dependencies {
29 | def ktx_version = "2.0.0"
30 | implementation fileTree(dir: 'libs', include: ['*.jar'])
31 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
32 | implementation 'androidx.appcompat:appcompat:1.1.0'
33 | implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-experimental-adapter:1.0.0'
34 | implementation "com.google.android.material:material:1.2.0-alpha03"
35 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
36 | kapt "androidx.databinding:databinding-compiler:3.5.3"
37 | implementation "androidx.lifecycle:lifecycle-extensions:2.1.0"
38 | implementation "androidx.lifecycle:lifecycle-viewmodel:2.1.0"
39 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$ktx_version"
40 | implementation "androidx.fragment:fragment-ktx:1.2.0-rc04"
41 | implementation project(':networkmodule')
42 |
43 | implementation project(':ktandroidarchitecturecore')
44 | testImplementation 'junit:junit:4.13'
45 | androidTestImplementation 'androidx.test:runner:1.2.0'
46 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
47 | testImplementation 'org.mockito:mockito-core:3.1.0'
48 | testImplementation 'android.arch.core:core-testing:1.1.1'
49 | testImplementation 'com.jraska.livedata:testing-ktx:1.1.0'
50 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2"
51 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2"
52 | testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.0"
53 | // Koin for Unit tests
54 | testImplementation "org.koin:koin-test:$koin_version"
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sysdata/kt/ktandroidarchitecture/MainApplication.kt:
--------------------------------------------------------------------------------
1 | package com.sysdata.kt.ktandroidarchitecture
2 |
3 | import android.app.Application
4 | import com.example.networkmodule.di.networkModule
5 | import com.sysdata.kt.ktandroidarchitecture.di.appModule
6 | import org.koin.android.ext.koin.androidContext
7 | import org.koin.android.ext.koin.androidLogger
8 | import org.koin.core.context.startKoin
9 | import org.koin.core.logger.Level
10 |
11 |
12 | class MainApplication : Application() {
13 |
14 |
15 | override fun onCreate() {
16 | super.onCreate()
17 |
18 | startKoin {
19 | androidLogger(Level.DEBUG)
20 | androidContext(this@MainApplication)
21 | modules(listOf(appModule, networkModule))
22 | }
23 | }
24 |
25 |
26 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sysdata/kt/ktandroidarchitecture/di/AppModule.kt:
--------------------------------------------------------------------------------
1 | package com.sysdata.kt.ktandroidarchitecture.di
2 |
3 | import com.sysdata.kt.ktandroidarchitecture.repository.AuthRepository
4 | import com.sysdata.kt.ktandroidarchitecture.repository.AuthRepositoryImpl
5 | import com.sysdata.kt.ktandroidarchitecture.repository.GitHubRepo
6 | import com.sysdata.kt.ktandroidarchitecture.repository.GitHubRepoImpl
7 | import com.sysdata.kt.ktandroidarchitecture.usecase.GitHubUseCase
8 | import com.sysdata.kt.ktandroidarchitecture.usecase.LoginUseCase
9 | import com.sysdata.kt.ktandroidarchitecture.viewmodel.GitHubViewModel
10 | import com.sysdata.kt.ktandroidarchitecture.viewmodel.LoginViewModel
11 | import org.koin.androidx.viewmodel.dsl.viewModel
12 | import org.koin.dsl.module
13 |
14 | val appModule = module{
15 | // single instance of AuthRepository
16 | single{AuthRepositoryImpl()}
17 | single{GitHubRepoImpl(get())}
18 |
19 | // simple UseCase factory
20 | factory { LoginUseCase(get()) }
21 | factory { GitHubUseCase(get()) }
22 |
23 | // LoginViewModel ViewModel
24 | viewModel { LoginViewModel(get())}
25 | viewModel { GitHubViewModel(get()) }
26 |
27 |
28 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sysdata/kt/ktandroidarchitecture/repository/AuthRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.sysdata.kt.ktandroidarchitecture.repository
2 |
3 | import com.sysdata.kt.ktandroidarchitecture.repository.model.UserLogged
4 | import it.sysdata.ktandroidarchitecturecore.BaseRepository
5 |
6 |
7 | interface AuthRepository {
8 | fun login(email: String, password: String): UserLogged
9 |
10 | }
11 |
12 | class AuthRepositoryImpl : BaseRepository(),AuthRepository {
13 |
14 |
15 |
16 | override fun login(email: String, password: String): UserLogged {
17 | if (email.isEmpty() || password.isEmpty())
18 | throw RuntimeException()
19 |
20 | return UserLogged(email)
21 | }
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sysdata/kt/ktandroidarchitecture/repository/GitHubRepo.kt:
--------------------------------------------------------------------------------
1 | package com.sysdata.kt.ktandroidarchitecture.repository
2 |
3 | import com.example.networkmodule.api.api.GitHubService
4 | import com.example.networkmodule.api.model.Repo
5 |
6 | interface GitHubRepo {
7 | suspend fun getRepositoryByUser(user: String): List
8 |
9 | }
10 |
11 | class GitHubRepoImpl(private val api: GitHubService) : GitHubRepo {
12 |
13 | override suspend fun getRepositoryByUser(user: String): List {
14 | return api.listRepos(user)
15 | }
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sysdata/kt/ktandroidarchitecture/repository/model/UIUserLogged.kt:
--------------------------------------------------------------------------------
1 | package com.sysdata.kt.ktandroidarchitecture.repository.model
2 |
3 |
4 | data class UIUserLogged(var email: String = "")
5 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sysdata/kt/ktandroidarchitecture/repository/model/UserLogged.kt:
--------------------------------------------------------------------------------
1 | package com.sysdata.kt.ktandroidarchitecture.repository.model
2 |
3 |
4 | data class UserLogged(var username: String = "")
5 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sysdata/kt/ktandroidarchitecture/ui/GitHubActivity.kt:
--------------------------------------------------------------------------------
1 | package com.sysdata.kt.ktandroidarchitecture.ui
2 |
3 | import android.os.Bundle
4 | import android.util.Log
5 | import androidx.appcompat.app.AppCompatActivity
6 | import androidx.lifecycle.viewModelScope
7 | import com.sysdata.kt.ktandroidarchitecture.R
8 | import com.sysdata.kt.ktandroidarchitecture.usecase.GitHubActionParams
9 | import com.sysdata.kt.ktandroidarchitecture.viewmodel.GitHubViewModel
10 | import org.koin.androidx.viewmodel.ext.android.viewModel
11 |
12 | class GitHubActivity : AppCompatActivity() {
13 | private val viewModel by viewModel()
14 |
15 | override fun onCreate(savedInstanceState: Bundle?) {
16 | super.onCreate(savedInstanceState)
17 | setContentView(R.layout.activity_git_hub)
18 | viewModel.gitHubAction.observe(this) {
19 | Log.e("Repo", it.toString())
20 | }
21 | viewModel.gitHubAction.execute(GitHubActionParams("SysdataSpA"), viewModel.viewModelScope)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/src/main/java/com/sysdata/kt/ktandroidarchitecture/ui/LoginActivity.kt:
--------------------------------------------------------------------------------
1 | package com.sysdata.kt.ktandroidarchitecture.ui
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import android.text.Editable
6 | import android.text.TextWatcher
7 | import android.view.View
8 | import android.widget.Toast
9 | import androidx.fragment.app.FragmentActivity
10 | import com.sysdata.kt.ktandroidarchitecture.R
11 | import com.sysdata.kt.ktandroidarchitecture.repository.model.UIUserLogged
12 | import com.sysdata.kt.ktandroidarchitecture.viewmodel.LoginViewModel
13 | import it.sysdata.ktandroidarchitecturecore.exception.Failure
14 | import kotlinx.android.synthetic.main.activity_login.*
15 | import org.koin.androidx.viewmodel.ext.android.viewModel
16 |
17 | class LoginActivity : FragmentActivity(), View.OnClickListener, TextWatcher {
18 |
19 | private val viewModel by viewModel()
20 |
21 | override fun onCreate(savedInstanceState: Bundle?) {
22 | super.onCreate(savedInstanceState)
23 | viewModel.actionLogin.observe(this, ::onUserLoggged)
24 | viewModel.actionLogin.observeFailure(this, ::onLoginFailed)
25 |
26 | setContentView(R.layout.activity_login)
27 | loginBtn.setOnClickListener(this)
28 | loginBtn.isEnabled = validateForm()
29 |
30 | usernameValue.addTextChangedListener(this)
31 | passwordValue.addTextChangedListener(this)
32 | }
33 |
34 |
35 | private fun onLoginFailed(failure: Failure?) {
36 | Toast.makeText(this, "failure : ${failure.toString()}", Toast.LENGTH_SHORT).show()
37 | }
38 |
39 | private fun onUserLoggged(userLogged: UIUserLogged?) {
40 | startActivity(Intent(this, GitHubActivity::class.java))
41 | Toast.makeText(this, "user : ${userLogged?.email}", Toast.LENGTH_SHORT).show()
42 | }
43 |
44 |
45 | override fun onClick(p0: View?) {
46 | viewModel.login(usernameValue.text.toString(), passwordValue.text.toString())
47 | }
48 |
49 | fun validateForm(): Boolean {
50 | return usernameValue.text.isNotEmpty() && passwordValue.text.isNotEmpty() && usernameValue.text.isNotBlank() && passwordValue.text.isNotBlank()
51 | }
52 |
53 | override fun afterTextChanged(p0: Editable?) {
54 | loginBtn.isEnabled = validateForm()
55 | }
56 |
57 | override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
58 |
59 | override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
60 |
61 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sysdata/kt/ktandroidarchitecture/usecase/GitHubUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.sysdata.kt.ktandroidarchitecture.usecase
2 |
3 | import com.example.networkmodule.api.model.Repo
4 | import com.sysdata.kt.ktandroidarchitecture.repository.GitHubRepo
5 | import com.sysdata.kt.ktandroidarchitecture.repository.model.UserLogged
6 | import it.sysdata.ktandroidarchitecturecore.exception.Failure
7 | import it.sysdata.ktandroidarchitecturecore.functional.Either
8 | import it.sysdata.ktandroidarchitecturecore.interactor.UseCase
9 |
10 | class GitHubUseCase(private val repo: GitHubRepo) : UseCase< List, GitHubActionParams>() {
11 | override suspend fun run(params: GitHubActionParams): Either> {
12 | return try {
13 | Either.Right(repo.getRepositoryByUser(params.user))
14 | } catch (e: Exception) {
15 | Either.Left(Failure.NetworkConnection())
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sysdata/kt/ktandroidarchitecture/usecase/LoginUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.sysdata.kt.ktandroidarchitecture.usecase
2 |
3 | import com.sysdata.kt.ktandroidarchitecture.repository.AuthRepository
4 | import com.sysdata.kt.ktandroidarchitecture.repository.AuthRepositoryImpl
5 | import com.sysdata.kt.ktandroidarchitecture.repository.model.UserLogged
6 | import it.sysdata.ktandroidarchitecturecore.exception.Failure
7 | import it.sysdata.ktandroidarchitecturecore.functional.Either
8 | import it.sysdata.ktandroidarchitecturecore.interactor.UseCase
9 |
10 | class LoginUseCase(private val repo:AuthRepository) : UseCase() {
11 | override suspend fun run(params: LoginActionParams): Either {
12 | return try {
13 | Either.Right(repo.login(params.email, params.password))
14 | } catch (e: Exception) {
15 | Either.Left(Failure.NetworkConnection())
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sysdata/kt/ktandroidarchitecture/usecase/Params.kt:
--------------------------------------------------------------------------------
1 | package com.sysdata.kt.ktandroidarchitecture.usecase
2 |
3 | import it.sysdata.ktandroidarchitecturecore.interactor.ActionParams
4 |
5 | class None : ActionParams()
6 | data class LoginActionParams(val email: String, val password: String) : ActionParams()
7 |
8 | data class GitHubActionParams(val user: String) : ActionParams()
--------------------------------------------------------------------------------
/app/src/main/java/com/sysdata/kt/ktandroidarchitecture/viewmodel/GitHubViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.sysdata.kt.ktandroidarchitecture.viewmodel
2 |
3 | import androidx.lifecycle.ViewModel
4 | import com.example.networkmodule.api.model.Repo
5 | import com.sysdata.kt.ktandroidarchitecture.usecase.GitHubActionParams
6 | import com.sysdata.kt.ktandroidarchitecture.usecase.GitHubUseCase
7 | import it.sysdata.ktandroidarchitecturecore.interactor.Action
8 |
9 | class GitHubViewModel(gitHubUseCase: GitHubUseCase) : ViewModel() {
10 | val gitHubAction = Action.Builder, List>()
11 | .useCase(gitHubUseCase)
12 | .buildWithUiModel { it }
13 |
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/sysdata/kt/ktandroidarchitecture/viewmodel/LoginViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.sysdata.kt.ktandroidarchitecture.viewmodel
2 |
3 | import androidx.lifecycle.viewModelScope
4 | import com.sysdata.kt.ktandroidarchitecture.repository.model.UIUserLogged
5 | import com.sysdata.kt.ktandroidarchitecture.repository.model.UserLogged
6 | import com.sysdata.kt.ktandroidarchitecture.usecase.LoginActionParams
7 | import com.sysdata.kt.ktandroidarchitecture.usecase.LoginUseCase
8 | import it.sysdata.ktandroidarchitecturecore.interactor.Action
9 | import it.sysdata.ktandroidarchitecturecore.platform.BaseViewModel
10 |
11 | class LoginViewModel(loginUseCase: LoginUseCase) : BaseViewModel() {
12 |
13 |
14 |
15 |
16 | val actionLogin = Action.Builder()
17 | .useCase(loginUseCase)
18 | .buildWithUiModel { UIUserLogged(it.username) }
19 |
20 |
21 | fun login(username: String, password: String) {
22 | actionLogin.execute(LoginActionParams(username, password), viewModelScope)
23 |
24 | }
25 |
26 |
27 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_git_hub.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_login.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
15 |
16 |
21 |
22 |
28 |
29 |
34 |
35 |
42 |
43 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SysdataSpA/KTAndroidArchitecture/f54c4e833c1d02436870527cb95fe9e5ba4995e8/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SysdataSpA/KTAndroidArchitecture/f54c4e833c1d02436870527cb95fe9e5ba4995e8/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SysdataSpA/KTAndroidArchitecture/f54c4e833c1d02436870527cb95fe9e5ba4995e8/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SysdataSpA/KTAndroidArchitecture/f54c4e833c1d02436870527cb95fe9e5ba4995e8/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SysdataSpA/KTAndroidArchitecture/f54c4e833c1d02436870527cb95fe9e5ba4995e8/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SysdataSpA/KTAndroidArchitecture/f54c4e833c1d02436870527cb95fe9e5ba4995e8/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SysdataSpA/KTAndroidArchitecture/f54c4e833c1d02436870527cb95fe9e5ba4995e8/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SysdataSpA/KTAndroidArchitecture/f54c4e833c1d02436870527cb95fe9e5ba4995e8/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SysdataSpA/KTAndroidArchitecture/f54c4e833c1d02436870527cb95fe9e5ba4995e8/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SysdataSpA/KTAndroidArchitecture/f54c4e833c1d02436870527cb95fe9e5ba4995e8/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | KTAndroidArchitecture
3 | Username
4 | Password
5 | login
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/test/java/com/sysdata/kt/ktandroidarchitecture/AuthRepositoryTest.kt:
--------------------------------------------------------------------------------
1 | package com.sysdata.kt.ktandroidarchitecture
2 |
3 | import com.sysdata.kt.ktandroidarchitecture.di.appModule
4 | import com.sysdata.kt.ktandroidarchitecture.repository.AuthRepository
5 | import com.sysdata.kt.ktandroidarchitecture.repository.model.UserLogged
6 | import junit.framework.TestCase.assertEquals
7 | import org.junit.After
8 | import org.junit.Before
9 | import org.junit.Test
10 | import org.koin.core.context.startKoin
11 | import org.koin.core.context.stopKoin
12 | import org.koin.test.KoinTest
13 | import org.koin.test.inject
14 |
15 | class AuthRepositoryTest : KoinTest {
16 |
17 | val repo: AuthRepository by inject()
18 |
19 |
20 | @Before
21 | fun before() {
22 | startKoin {
23 | printLogger()
24 | modules(appModule)
25 | }
26 | }
27 | @After
28 | fun after() {
29 | stopKoin()
30 | }
31 | @Test
32 | fun login() {
33 | val email = "email"
34 | val password = "test"
35 | val result = repo.login(email, password)
36 | assertEquals(UserLogged(email), result)
37 | }
38 |
39 | @Test
40 | fun runFailUsernameEmptyLogin() {
41 | val email = ""
42 | val password = "test"
43 | try {
44 | val result = repo.login(email, password)
45 |
46 | } catch (e: Exception) {
47 |
48 | assert(e is RuntimeException)
49 | }
50 |
51 | }
52 |
53 |
54 | @Test
55 | fun `runFailPasswordEmptyLogin`() {
56 | val email = "email"
57 | val password = ""
58 | try {
59 | val result = repo.login(email, password)
60 |
61 | } catch (e: Exception) {
62 |
63 | assert(e is RuntimeException)
64 | }
65 |
66 | }
67 | }
68 |
69 |
70 |
--------------------------------------------------------------------------------
/app/src/test/java/com/sysdata/kt/ktandroidarchitecture/CheckModulesTest.kt:
--------------------------------------------------------------------------------
1 | package com.sysdata.kt.ktandroidarchitecture
2 |
3 | import com.example.networkmodule.di.networkModule
4 | import com.sysdata.kt.ktandroidarchitecture.di.appModule
5 | import org.junit.Test
6 | import org.koin.dsl.koinApplication
7 | import org.koin.test.KoinTest
8 | import org.koin.test.check.checkModules
9 |
10 | class CheckModulesTest : KoinTest {
11 |
12 | @Test
13 | fun checkAllModules() {
14 | koinApplication { modules(listOf(appModule, networkModule) )}.checkModules()
15 | }
16 |
17 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/sysdata/kt/ktandroidarchitecture/GitHubRepositoryTest.kt:
--------------------------------------------------------------------------------
1 | package com.sysdata.kt.ktandroidarchitecture
2 |
3 | import com.example.networkmodule.api.api.GitHubService
4 | import com.example.networkmodule.di.networkModule
5 | import com.sysdata.kt.ktandroidarchitecture.di.appModule
6 | import com.sysdata.kt.ktandroidarchitecture.repository.GitHubRepo
7 | import kotlinx.coroutines.ExperimentalCoroutinesApi
8 | import kotlinx.coroutines.runBlocking
9 | import kotlinx.coroutines.test.runBlockingTest
10 | import org.junit.After
11 | import org.junit.Before
12 | import org.junit.Test
13 | import org.koin.core.context.startKoin
14 | import org.koin.core.context.stopKoin
15 | import org.koin.test.KoinTest
16 | import org.koin.test.inject
17 | import org.koin.test.mock.declareMock
18 | import org.mockito.BDDMockito.given
19 |
20 | @ExperimentalCoroutinesApi
21 | class GitHubRepositoryTest : KoinTest {
22 | private val repositoryTest: GitHubRepo by inject()
23 |
24 |
25 | @Before
26 | fun before() {
27 | startKoin {
28 |
29 |
30 | modules(listOf(appModule, networkModule))
31 | }
32 | declareMock {
33 | runBlocking {
34 | given(this@declareMock.listRepos("SysdataSpA")).willReturn(listOf())
35 | }
36 | }
37 |
38 |
39 | }
40 | @After
41 | fun after() {
42 | stopKoin()
43 | }
44 |
45 | @Test
46 | fun testGitListRepoByUser(){
47 | runBlockingTest {
48 | val list = repositoryTest.getRepositoryByUser("SysdataSpA")
49 | assert(list.isEmpty())
50 | }
51 | }
52 |
53 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/sysdata/kt/ktandroidarchitecture/LiveDataTestUtil.kt:
--------------------------------------------------------------------------------
1 | package com.sysdata.kt.ktandroidarchitecture
2 |
3 | import com.jraska.livedata.TestLifecycle
4 | import it.sysdata.ktandroidarchitecturecore.exception.Failure
5 | import it.sysdata.ktandroidarchitecturecore.interactor.Action
6 | import it.sysdata.ktandroidarchitecturecore.interactor.ActionParams
7 | import java.util.concurrent.CountDownLatch
8 | import java.util.concurrent.TimeUnit
9 |
10 | /**
11 | * Safely handles observables from LiveData for testing.
12 | */
13 | object LiveDataTestUtil {
14 |
15 | /**
16 | * Gets the value of a LiveData safely.
17 | */
18 | @Throws(InterruptedException::class)
19 | fun getValue(action: Action, lifecycle: TestLifecycle): T? {
20 | var data: T? = null
21 | val latch = CountDownLatch(1)
22 | val observer: (T) -> Unit = { o ->
23 | data = o
24 | latch.countDown()
25 |
26 | }
27 |
28 | action.observe(lifecycle, observer)
29 |
30 | latch.await(100, TimeUnit.MILLISECONDS)
31 |
32 | return data
33 | }
34 |
35 | @Throws(InterruptedException::class)
36 | fun < Model : Any, Params : ActionParams,UiModel:Any> getFail(action: Action, lifecycle: TestLifecycle): Failure? {
37 | var data: Failure? = null
38 | val latch = CountDownLatch(1)
39 | val observer: (T:Failure) -> Unit = { o ->
40 | data = o
41 | latch.countDown()
42 |
43 | }
44 |
45 | action.observeFailure(lifecycle, observer)
46 |
47 | latch.await(100, TimeUnit.MILLISECONDS)
48 |
49 | return data
50 | }
51 |
52 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/sysdata/kt/ktandroidarchitecture/LoginUseCaseTest.kt:
--------------------------------------------------------------------------------
1 | package com.sysdata.kt.ktandroidarchitecture
2 |
3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule
4 | import com.sysdata.kt.ktandroidarchitecture.di.appModule
5 | import com.sysdata.kt.ktandroidarchitecture.repository.model.UserLogged
6 | import com.sysdata.kt.ktandroidarchitecture.usecase.LoginActionParams
7 | import com.sysdata.kt.ktandroidarchitecture.usecase.LoginUseCase
8 | import it.sysdata.ktandroidarchitecturecore.exception.Failure
9 | import it.sysdata.ktandroidarchitecturecore.functional.map
10 | import junit.framework.TestCase.assertEquals
11 | import kotlinx.coroutines.ExperimentalCoroutinesApi
12 | import kotlinx.coroutines.delay
13 | import kotlinx.coroutines.runBlocking
14 | import kotlinx.coroutines.test.runBlockingTest
15 | import org.junit.After
16 | import org.junit.Before
17 | import org.junit.Rule
18 | import org.junit.Test
19 | import org.koin.core.context.startKoin
20 | import org.koin.core.context.stopKoin
21 | import org.koin.test.KoinTest
22 | import org.koin.test.inject
23 |
24 | @ExperimentalCoroutinesApi
25 | class LoginUseCaseTest : KoinTest {
26 |
27 |
28 | // Executes tasks in the Architecture Components in the same thread
29 | @get:Rule
30 | var instantTaskExecutorRule = InstantTaskExecutorRule()
31 |
32 | val useCase: LoginUseCase by inject()
33 | @Before
34 | fun before() {
35 | startKoin {
36 | printLogger()
37 | modules(appModule)
38 | }
39 | }
40 | @After
41 | fun after() {
42 | stopKoin()
43 | }
44 | @Test
45 | fun runUseCase() = runBlockingTest {
46 | val email = "email"
47 | val password = "test"
48 | val params = LoginActionParams(email, password)
49 | val result = useCase.run(params)
50 | assert(result.isRight)
51 | var userLogged: UserLogged? = null
52 | result.map { res -> userLogged = res }
53 | delay(200)
54 | assertEquals(UserLogged(email), userLogged)
55 | }
56 |
57 | @Test
58 | fun runFailUsernameEmptyUseCase() = runBlockingTest {
59 | val email = ""
60 | val password = "test"
61 | var failureTest: Failure? = null
62 |
63 | val params = LoginActionParams(email, password)
64 | val result = useCase.run(params)
65 | result.either({
66 | failureTest = it
67 | it
68 | }, {})
69 | delay(200)
70 | assert(result.isLeft)
71 | assert(failureTest is Failure.NetworkConnection)
72 |
73 | }
74 |
75 |
76 | @Test
77 | fun runFailPasswordEmptyUseCase() = runBlockingTest {
78 | val email = "email"
79 | val password = ""
80 | var failureTest: Failure? = null
81 |
82 | val params = LoginActionParams(email, password)
83 | val result = useCase.run(params)
84 | result.either({
85 | failureTest = it
86 | it
87 | }, {})
88 | delay(200)
89 | assert(result.isLeft)
90 | assert(failureTest is Failure.NetworkConnection)
91 |
92 | }
93 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/sysdata/kt/ktandroidarchitecture/LoginViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package com.sysdata.kt.ktandroidarchitecture
2 |
3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule
4 | import com.jraska.livedata.TestLifecycle
5 | import com.sysdata.kt.ktandroidarchitecture.di.appModule
6 | import com.sysdata.kt.ktandroidarchitecture.repository.model.UIUserLogged
7 | import com.sysdata.kt.ktandroidarchitecture.viewmodel.LoginViewModel
8 | import it.sysdata.ktandroidarchitecturecore.exception.Failure
9 | import junit.framework.Assert.assertEquals
10 | import org.junit.After
11 | import org.junit.Assert.assertTrue
12 | import org.junit.Before
13 | import org.junit.Rule
14 | import org.junit.Test
15 | import org.koin.core.context.startKoin
16 | import org.koin.core.context.stopKoin
17 | import org.koin.test.KoinTest
18 | import org.koin.test.inject
19 |
20 |
21 | class LoginViewModelTest : KoinTest {
22 | // Executes tasks in the Architecture Components in the same thread
23 | @get:Rule
24 | var instantTaskExecutorRule = InstantTaskExecutorRule()
25 |
26 | private val loginViewModelTest: LoginViewModel by inject()
27 |
28 | @Before
29 | fun before() {
30 | startKoin {
31 | printLogger()
32 | modules(appModule)
33 | }
34 | }
35 |
36 | @After
37 | fun after() {
38 | stopKoin()
39 | }
40 |
41 | @Test
42 | fun `testLogin`() {
43 | val testLifecycle = TestLifecycle.initialized()
44 |
45 | val email = "email"
46 | val password = "test"
47 |
48 | loginViewModelTest.login(email, password)
49 |
50 | testLifecycle.resume()
51 |
52 |
53 | val result = LiveDataTestUtil.getValue(loginViewModelTest.actionLogin, testLifecycle)
54 |
55 | assertEquals(UIUserLogged(email), result)
56 |
57 |
58 | }
59 |
60 | @Test
61 | fun `testLoginWithNoValue`() {
62 | val testLifecycle = TestLifecycle.initialized()
63 |
64 | val email = ""
65 | val password = ""
66 |
67 | loginViewModelTest.login(email, password)
68 |
69 | testLifecycle.resume()
70 |
71 |
72 | val result = LiveDataTestUtil.getFail(loginViewModelTest.actionLogin, testLifecycle)
73 | assertTrue(result is Failure.NetworkConnection)
74 |
75 |
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | ext.kotlin_version = '1.3.61'
5 | // latest stable
6 | ext.koin_version = '2.0.1'
7 | repositories {
8 | google()
9 | jcenter()
10 | }
11 | dependencies {
12 | classpath 'com.android.tools.build:gradle:3.5.3'
13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
14 |
15 | // NOTE: Do not place your application dependencies here; they belong
16 | // in the individual module build.gradle files
17 | }
18 | }
19 |
20 | allprojects {
21 | repositories {
22 | google()
23 | jcenter()
24 | }
25 | }
26 |
27 | task clean(type: Delete) {
28 | delete rootProject.buildDir
29 | }
30 |
--------------------------------------------------------------------------------
/docs/Action.md:
--------------------------------------------------------------------------------
1 | # Action
2 |
3 | ### 1.1 What is an Action?
4 | An **Action** is an object which handles the process of calling a [**UseCase**](UseCase.md) and map the response.
5 |
6 | Since the action is basically built on a Usecase to execute it we have to link it to an interaction through [**ViewModel**](ViewModel.md) such as clicking on a button.
7 |
8 | When the interaction happens UseCase is executed and the result is mapped and passed to the **ViewModel**.
9 |
10 | 
11 |
12 | ### 1.2 What is an ActionQueue?
13 |
14 | Generally an action use only one **UseCase** but is possible to define an **ActionQueue** which is a special type of Action which calls multiple **UseCases** sequentially.
15 |
16 | Into an **ActionQueue** each **UseCase**, except the first, take the result of the previous as parameters and give the output to the next.
17 |
18 | To execute a **ActionQueue** as for the **Action** we have to link it to an interaction, but in this case when the interaction happens the first usecase of the sequence is executed and pass its results to the next one, repeating this for each usecase of the sequence, except the last one which return the mapped result to the **ViewModel**.
19 |
20 | 
21 |
22 | ### 1.3 What is an SingleAction
23 |
24 | An **SingleAction** is an object which handles the process of calling a [**UseCase**](UseCase.md) and map the response.
25 |
26 | This avoids a common problem with events: on configuration change (like rotation) an update
27 |
28 | can be emitted if the observer is active. This LiveData only calls the observable if there's an
29 |
30 | explicit call to setValue() or call().
31 |
32 | Note that only one observer is going to be notified of changes.
33 |
34 | When the interaction happens UseCase is executed and the result is mapped and passed to the **ViewModel**.
35 |
36 | [alt_text] (https://github.com/googlesamples/android-architecture/blob/dev-todo-mvvm-live/todoapp/app/src/main/java/com/example/android/architecture/blueprints/todoapp/SingleLiveEvent.java "SingleLiveEvent")
37 |
38 |
39 | ### 1.4 Sample
40 |
41 | Below an example of Action
42 |
43 |
44 | ```kotlin
45 | val actionLogin = Action.Builder()
46 | .useCase(LoginUseCase::class.java)
47 | .buildWithUiModel { UiModel(it) }
48 | ```
49 |
50 |
51 | Below an example of ActionQueue
52 |
53 |
54 | ```kotlin
55 | val actionQueue = ActionQueue.Builder()
56 | .setFirstUseCase(FirstUseCase::class.java)
57 | .addUseCase(...)
58 | .setLastUseCase(...)
59 | ```
60 |
61 | Below an example of SingleAction
62 |
63 | ```kotlin
64 | val redoOrderAction = SingleAction.Builder()
65 | .useCase(redoOrderUsecase)
66 | .buildWithUiModel { it }
67 | ```
--------------------------------------------------------------------------------
/docs/ActionFlowDiagram-thumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SysdataSpA/KTAndroidArchitecture/f54c4e833c1d02436870527cb95fe9e5ba4995e8/docs/ActionFlowDiagram-thumbnail.png
--------------------------------------------------------------------------------
/docs/ActionFlowDiagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SysdataSpA/KTAndroidArchitecture/f54c4e833c1d02436870527cb95fe9e5ba4995e8/docs/ActionFlowDiagram.png
--------------------------------------------------------------------------------
/docs/DI-Dagger.md:
--------------------------------------------------------------------------------
1 | # DI - Use with Dagger
2 | A Kotlin android architecture with Google Architecture Components
3 |
4 | ## 1 How to use it?
5 |
6 | ### 1.1 Import dependency
7 |
8 | In **app level `build.gradle`** add these dependencies that will be needed to add Dagger and thus Dependency injection.
9 | ```gradle
10 | // dagger2 android
11 | kapt "com.google.dagger:dagger-android-processor:$dagger_version"
12 | implementation "com.google.dagger:dagger-android:$dagger_version"
13 | implementation "com.google.dagger:dagger-android-support:$dagger_version"
14 | kapt "com.google.dagger:dagger-compiler:$dagger_version"
15 | ```
16 | ### 1.2 Basic implementation steps
17 | Define a ViewModelFactory that will inject ViewModel providers as soon as they will be needed and will add them to a map so they are as singleton instances. You will have to add all the ViewModels you create to this map and to the constructor.
18 | ```kotlin
19 | @Singleton
20 | class ViewModelFactory @Inject constructor(application: Application
21 | // ... add other view model providers here
22 | , sampleViewModelProvider: Provider) : ViewModelProvider.Factory {
23 |
24 | private val mMapProvider = HashMap, Provider>()
25 | private val defaultFactory: ViewModelProvider.AndroidViewModelFactory = ViewModelProvider.AndroidViewModelFactory.getInstance(application)
26 |
27 | init {
28 | mMapProvider[SampleViewModel::class.java] = sampleViewModelProvider
29 | // ... add other view model providers to the map here
30 | }
31 |
32 | override fun create(modelClass: Class): T {
33 | return if (mMapProvider.containsKey(modelClass)) {
34 | mMapProvider[modelClass]!!.get() as T
35 | } else {
36 | defaultFactory.create(modelClass)
37 | }
38 | }
39 | }
40 | ```
41 |
42 | Define an application component that will provide the created ViewModelFactory.
43 | ```kotlin
44 | @Singleton
45 | @Component(modules = [AndroidSupportInjectionModule::class])
46 | interface ApplicationComponent : AndroidInjector {
47 | fun getViewModelFactory(): ViewModelFactory
48 | }
49 | ```
50 |
51 | Define a ViewModel. By using Dagger, you can create the instance of the view model through the @Inject annotation. Moreover, usecases will be injected instead of being instantiated through reflection.
52 |
53 | ```kotlin
54 | class SampleViewModel @Inject constructor(sampleUsecase: SampleUsecase
55 | // you can add all possible usecases here as parameters of the ViewModel
56 | ) : ViewModel() {
57 | // actions can use instances of the injected UseCases instead of the class
58 | val sampleAction = Action.Builder()
59 | .useCase(sampleUsecase)
60 | .buildWithUiModel { None }
61 |
62 | }
63 | ```
64 |
65 | UseCases are written in a similar way: the only difference is that we use the @Inject annotation here and we define our repositories as constructor parameters instead of creating them.
66 | ```kotlin
67 | class SampleUsecase @Inject constructor(
68 | // optional repo that you can add as constrtor parameters
69 | private val sampleRepo: SampleRepo) : UseCase() {
70 | override suspend fun run(params: None): Either {
71 | // optionally you can perform async operations here.
72 | return Either.Right(None)
73 | }
74 | }
75 | ```
76 | ```kotlin
77 | @Singleton
78 | class AuthenticationRepo @Inject constructor() {
79 | // define here your repository methods (DataBase calls, wev)
80 | }
81 | ```
82 |
83 | You are now ready to use your view model inside your Activity/Fragment by injecting the ViewModelFactory and lazy initializing it.
84 | ```kotlin
85 | @Inject
86 | lateinit var factory: ViewModelFactory
87 | private val viewModel: SampleViewModel by lazy {
88 | ViewModelProviders.of(this, factory).get(SampleViewModel::class.java)
89 | }
90 | ```
91 |
--------------------------------------------------------------------------------
/docs/InstrumentedTests.md:
--------------------------------------------------------------------------------
1 | # UI Instrumented Tests
2 |
3 | ### 1 Intro
4 | The aim of the document is to provide base indications on UI automated tests.
5 | At the moment of writing, Google suggests to use two frameworks:
6 | * Espresso
7 | * UI Automator
8 |
9 | In the following paragraphs, I'll describe an example of a test executed with Espresso.
10 |
11 | ### 2.1 Preliminary actions
12 | Gradle dependencies
13 |
14 | ```gradle
15 | android.defaultConfig {
16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
17 | // (sostituisce testImplementation "junit:junit:$junit_version")
18 | }
19 | dependencies {
20 | androidTestImplementation "androidx.test:runner:$testrunner_version"
21 | androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version"
22 | androidTestImplementation 'androidx.test:rules:$testrules_version'
23 | }
24 | ```
25 |
26 | ### 2.2 Simulator configuration
27 | On the target device, go to "Settings > System > developer options" and disable the following options:
28 | Window animation scale
29 | Transition animation scale
30 | Animator duration scale
31 |
32 | ### 2.3 Test configuration
33 | UI test classes must be placed inside the "androidTest" package.
34 | Inside the single file, we'll have to place some annotations:
35 | Above the class signature
36 |
37 | ```kotlin
38 | @RunWith(AndroidJUnit4::class)
39 |
40 | @LargeTest
41 | class LoginTest {
42 | // ...
43 | }
44 | ```
45 |
46 | Above the variable containting rules.
47 |
48 | ```kotlin
49 | @Rule
50 | @JvmField
51 | public val rule = getRule()
52 | ```
53 |
54 | Above the method that is executed before all other tests present inside the class.
55 |
56 | ```kotlin
57 | @BeforeClass
58 | @JvmStatic
59 | fun before_class_method() {
60 |
61 | }
62 | ```
63 |
64 | Above the method that is executed after all other tests present inside the class.
65 |
66 | ```kotlin
67 | @AfterClass
68 | @JvmStatic
69 | fun after_class_method() {
70 |
71 | }
72 | ```
73 |
74 | Above the method that is executed before each and every single test inside the class
75 |
76 | ```kotlin
77 | @Before
78 | fun before_test_method() {
79 |
80 | }
81 | ```
82 |
83 | Above the method that is executed after each and every single test inside the class
84 |
85 | ```kotlin
86 | @After
87 | fun after_test_method() {
88 |
89 | }
90 | ```
91 |
92 | Above the methods that contain the real test.
93 |
94 | ```kotlin
95 | @Test
96 | fun login_success() {
97 |
98 | }
99 | ```
100 |
101 | Inside the test in the example, we wanted to target the login UI. The class structure is `LoginActivity` > `LoginFragment`.
102 | In our rules definition (`@Rule`), we retrieved the target Activity:
103 |
104 | ```kotlin
105 | private fun getRule(): ActivityTestRule {
106 | Log.e("Initalising rule","getting LoginActivity")
107 | return ActivityTestRule(LoginActivity::class.java)
108 | }
109 | ```
110 |
111 | In this case, within the Activity, there is automatic loading of the Fragment via NavGraph; this leads to having direct access to all the views present in the fragment without having to instantiate it.
112 |
113 | The configuration is finished. Now let's move on to implementing the test.
114 |
115 | ### 3 @Test
116 | First, let's determine what we need to test.
117 | The login screen has 3 form-fields and a button; the latter is enabled only if all 3 fields are filled out.
118 | The usecase expects to check the status of the button every time a form field is filled.
119 |
120 | Here is the snippet of a test:
121 |
122 | ```kotlin
123 | @Test
124 | fun login_success() {
125 |
126 | Thread.sleep(1000);
127 | onView((withId(R.id.usernameEditText))).perform(click())
128 | onView(withId(R.id.usernameEditText))
129 | .perform(setTextInSDEditText(username_tobe_typed))
130 |
131 | onView(withId(R.id.loginButton)).check(matches(not(isEnabled())));
132 |
133 | Thread.sleep(1000);
134 | onView((withId(R.id.passwordEditText))).perform(click())
135 | onView(withId(R.id.passwordEditText))
136 | .perform(setTextInSDEditText(correct_password))
137 | .perform(closeSoftKeyboard())
138 |
139 | onView(withId(R.id.loginButton)).check(matches(not(isEnabled())));
140 |
141 | Thread.sleep(1000);
142 | onView(withId(R.id.termsAndConditionCheckBox)).perform(click())
143 |
144 | onView(withId(R.id.loginButton)).check(matches(isEnabled()));
145 |
146 | Thread.sleep(1000);
147 | onView(withId(R.id.loginButton))
148 | .perform(click())
149 |
150 | Thread.sleep(1000);
151 | }
152 | ```
153 |
154 | Briefly, the actions that are performed in the test are:
155 |
156 |
157 | Recreate the view -> `onView()`
158 |
159 | The `onView()` method allows you to select one or more Views that respect the conditions expressed as an argument. In the example, the condition is `withId()` which allows to identify the View with a given ID.
160 | There are many ways to select the views.
161 |
162 | Perform actions on a selected view -> `perform()`
163 |
164 | The `onPerform()` method replaces the user interaction with the device. Allows you to modify the selected view. It is therefore possible to insert texts, click, scroll a list, close the keyboard, etc.
165 | There are default methods for standard Android views but it is also possible to create custom actions in case you want to interact with a view created ad hoc. This last operation was performed in the `SdEditTextUtils` class to insert the text inside the `EditText` nested inside the custom view `SDEditText`
166 |
167 | Perform assertions after one or more actions -> `check()`
168 |
169 | The `check()` method allows to check assertions that we defined during the test analysis. The assertion check is passed as a method argument and can be customized in order to understand all the possible scenarios.
170 | In our example, the assertion is that the button is enabled only if all the three form fields are not empty. If an assertion fails, the entire test fails.
171 |
172 | ### 4 RUN
173 | Once the test has been written, the run must be performed.
174 | Right click on the class of the test and click on "Run ''" or on "Debug ''"
175 |
176 | 
177 |
178 | The "Run" tab will contain all the information relating to the compilation and run of the test.
179 | In case of failure, the relevant logs will be shown.
180 |
181 | 
182 |
183 | ### 5 Firebase
184 | Firebase allows to run tests on a variety of devices in an automated way providing the following feedbacks
185 | * test result
186 | * test video
187 | * device log during test execution
188 | * Performance test
189 |
190 | These automations are subject to subscription or payment based on consumption.
191 | Performing tests from the console is quite simple; the only modification necessary is to insert, in the gradle file, the applicationId of the test APK. In this way, after compiling the test and the APK, both APKs will be created in the "build > outputs > apk" directory which will be used during the test configuration in the Firebase console.
192 |
193 | ```gradle
194 | defaultConfig {
195 | // ...
196 | // Specifies the application ID for the test APK.
197 | testApplicationId "it.sysdata.sysdatastoreTest"
198 | // Specifies the fully-qualified class name of the test instrumentation runner.
199 | testInstrumentationRunner "it.sysdata.sysdatastoreInstrumentationTest"
200 | }
201 | ```
202 | 
203 |
204 | Enter the "Test Lab" section and click on "Run a Test" then select the "Run an instrument test" option
205 |
206 | 
207 |
208 | In the following screen, it will be requested to insert the original APK and the test APK.
209 |
210 | 
211 |
212 | After clicking on "Continue", it will be requested to select the target devices that will run the tests.
213 |
214 | 
215 |
216 | The test configuration is completed. Now we can see the result.
217 | In the test screen, we'll be able to see all the selected devices of the previous screen. Clicking upon each of the devices, it will be possible to see the specific results.
218 |
219 | 
220 |
221 | As explained before, inside a single result, it is possible to view all the information of a single test.
222 |
223 | 
--------------------------------------------------------------------------------
/docs/Repository.md:
--------------------------------------------------------------------------------
1 |
2 | # KTAndroidArchitecture - REPOSITORY
3 |
4 | ### 1.1 What is repository?
5 | A Repository handles the process of saving or retrieving data from a datasource, it is managed by one or more UseCase.
6 | The repository is a sort of data storage.
7 |
8 | The datas retrieved by a repository's call, can come from each one of this sources:
9 | * Local Databases
10 | * Remote Web Services
11 | * Bluetooth connection with other devices
12 | * Data from device sensors
13 | * etc
14 |
15 | Each data source has to be called by the repository.
16 |
17 | 
18 |
19 | ### 1.2 How it works?
20 |
21 | As shown in the image above, the flow of retreiving data through repository is pretty linear:
22 | * the UseCase invoke a repository's method which handles the data retrieving (The implementation changes with the data source)
23 | * the repository's method retrieve the datas (i.e. calling a web service through retrofit)
24 | * Is created an instance of Either object which wrap the response or the error object
25 | * the Either object goes to the UseCase which has called the repository
26 |
27 | ### 1.3 Either
28 |
29 | As seen in the previous paragraph, the Either object has the function to convey a data model in case of successfully data retreiving, or return an error object in case of failure.
30 |
31 | Use the object is very simple:
32 |
33 | ```
34 | suspend inline fun retriveDataCall(param: String? = null): Either {
35 |
36 | val bodyRequest = RequestModel().param(param)
37 |
38 | return try {
39 |
40 | val response = adapt(ApiClient.INSTANCE.getSampleApi().getService(bodyRequest)).await()
41 |
42 | Either.Right(response)
43 |
44 | } catch (e: Exception) {
45 | e.printStackTrace()
46 | Either.Left(Failure.ServerError(e.message))
47 | }
48 | }
49 | ```
50 |
51 | When success, you need to invoke the method "Right" with the mapped data model.
52 | When failure, you need to invoke the method "Left" with the model of the error(in the example above the class ServerError in Failure.kt).
53 | If you need a custom error model, you have to extend the class FeatureFailure.
54 |
55 | ### 1.4 Sample
56 |
57 | Below a complete example of Repository:
58 |
59 | ```
60 | class SampleRepo : BaseRepository() {
61 |
62 | /**
63 | * Used to recall instance only whe it need
64 | */
65 | private object Holder {
66 | val INSTANCE = SampleRepo()
67 | }
68 |
69 | /**
70 | * Used to make singleton pattern
71 | */
72 | companion object {
73 | val instance: SampleRepo by lazy {
74 | Holder.INSTANCE
75 | }
76 | }
77 |
78 | /**
79 | * perform a retriveDataCall request
80 | *
81 | * @param param
82 | *
83 | * @return Either
84 | */
85 | suspend inline fun retriveDataCall(param: String? = null): Either {
86 |
87 | val bodyRequest = RequestModel().param(param)
88 |
89 | return try {
90 |
91 | val response = adapt(ApiClient.INSTANCE.getSampleApi().getService(bodyRequest)).await()
92 |
93 | Either.Right(response)
94 |
95 | } catch (e: Exception) {
96 | e.printStackTrace()
97 | Either.Left(Failure.ServerError(e.message))
98 | }
99 | }
100 | }
101 | ```
102 |
--------------------------------------------------------------------------------
/docs/UI.md:
--------------------------------------------------------------------------------
1 | # UI
2 | WIP
3 |
--------------------------------------------------------------------------------
/docs/UI_to_VM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SysdataSpA/KTAndroidArchitecture/f54c4e833c1d02436870527cb95fe9e5ba4995e8/docs/UI_to_VM.png
--------------------------------------------------------------------------------
/docs/UiModel.md:
--------------------------------------------------------------------------------
1 | # KTAndroidArchitecture - UIMODEL
2 |
3 | ### 1.1 What is UiModel?
4 | A UIModel is a object which contains all UI-related datas of a view, a fragment or an activity
5 |
6 | To fully understand the concept of UiModel, let's do a step back:
7 | think about a normal flow of retrieving and showing datas.
8 |
9 | The components of this flow will be a server, which contains the datas, and a client, which shows them.
10 | Likely, the server will be accessed by clients different from ours;
11 | so the datas returned by the server could be dirty, with unnecessary information or parameters which needs to be elaborated after the retrieving.
12 |
13 | This takes us to treat the datas from the web service as "RAW" datas.
14 |
15 | Our graphic interface, instead, need specific and well mapped datas.
16 |
17 | There is the need to create a data model only for the graphic interface, ignoring all unnecessary datas and elaborating others before passing them to the view.
18 |
19 | 
20 |
21 | ### 1.2 When does it happen?
22 |
23 | The trasformation from raw web service's object to uiModel is done into the ViewModel.
24 | Specifically, into the Action flow, after the usecase, inside the "buildWithUiModel" method:
25 |
26 | ```
27 | val actionSample = Action.Builder()
28 | .useCase(SampleUseCase::class.java)
29 | .buildWithUiModel {
30 | //map model into UiModel
31 | UiModel(it)
32 | }
33 |
34 | ```
35 |
36 |
37 | ### 1.3 DataBinding
38 |
39 | We have almost ended the flow of retrieving and showing datas.
40 | There is only the transition of the uiModel from the ViewModel to the view.
41 |
42 | Coming back to the idea of Action, inside the view you need to set an observer of the action's result:
43 |
44 | ```
45 | mViewModel?.actionSample?.observe(this, ::onSampleObserver)
46 | ```
47 |
48 | This observer make possible to listen the action's result and set the UiModel inside the view.
49 |
50 | This operation is called DataBinding:
51 | a uimodel is injected directly into the view.
52 |
53 | ```
54 | fun onSampleObserver(uiModel: UIModel?) {
55 | uiModel?.let {
56 | binding.uiModel = it
57 | binding.executePendingBindings()
58 | }
59 | }
60 | ```
61 |
62 |
63 |
--------------------------------------------------------------------------------
/docs/UnitTests.md:
--------------------------------------------------------------------------------
1 | # Unit Tests with Koin
2 |
3 | ### 1 Intro
4 | The aim of the document is to provide base indications on unit tests.
5 | Unit testing can be done in various ways and with different tools.
6 | We suggest to test your application with [Koin](https://insert-koin.io/) in order to mock your test objects.
7 |
8 | In the following paragraphs, I'll describe an example of a unit test with the ktAndroidArchitecture.
9 |
10 | ### 2.1 Preliminary actions
11 | Gradle dependencies
12 |
13 | ```gradle
14 | android.defaultConfig {
15 | androidTestImplementation 'androidx.test:runner:1.2.0'
16 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
17 | testImplementation 'org.mockito:mockito-core:2.23.0'
18 | testImplementation 'android.arch.core:core-testing:1.1.1'
19 | testImplementation 'com.jraska.livedata:testing-ktx:1.1.0'
20 | testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.0"
21 | // Koin for Unit tests
22 | testImplementation "org.koin:koin-test:$koin_version"
23 | }
24 | ```
25 |
26 | ### 2.2 UseCase test
27 | In order to perform a UseCase test, you have to call the `run()` function of the UseCase
28 | providing the ActionParams inside the method signature.
29 | After that, you can perform all the assertion that are useful to you.
30 | In the following snippet we are testing the LoginUseCase.
31 |
32 | ```kotlin
33 | class LoginUseCaseTest : KoinTest {
34 |
35 | @Before
36 | fun before() {
37 | startKoin {
38 | printLogger()
39 | modules(appModule)
40 | }
41 | }
42 |
43 | @After
44 | fun after() {
45 | stopKoin()
46 | }
47 |
48 | @Test
49 | fun runUseCase() = runBlockingTest {
50 | val email = "email"
51 | val password = "test"
52 | val params = LoginActionParams(email, password)
53 | val result = useCase.run(params)
54 | assert(result.isRight)
55 | var userLogged: UserLogged? = null
56 | result.map { res -> userLogged = res }
57 | delay(200)
58 | assertEquals(UserLogged(email), userLogged)
59 | }
60 |
61 | // further tests here
62 | }
63 | ```
64 |
65 | ### 2.2 ViewModel test
66 | In order to perform a ViewModel test, you have to create a method inside the real ViewModel.
67 |
68 | ```kotlin
69 | class LoginViewModel(loginUseCase: LoginUseCase) : BaseViewModel() {
70 |
71 | fun login(username: String, password: String) {
72 | actionLogin.execute(LoginActionParams(username, password), viewModelScope)
73 | }
74 |
75 | }
76 | ```
77 |
78 | Then you can call the "login()" function inside the ViewModel test class.
79 |
80 | ```kotlin
81 | class LoginViewModelTest : KoinTest {
82 | // Executes tasks in the Architecture Components in the same thread
83 | @get:Rule
84 | var instantTaskExecutorRule = InstantTaskExecutorRule()
85 | private val loginViewModelTest: LoginViewModel by inject()
86 |
87 | @Before
88 | fun before() {
89 | startKoin {
90 | printLogger()
91 | modules(appModule)
92 | }
93 | }
94 |
95 | @After
96 | fun after() {
97 | stopKoin()
98 | }
99 |
100 | @Test
101 | fun `testLogin`() {
102 | val testLifecycle = TestLifecycle.initialized()
103 | val email = "email"
104 | val password = "test"
105 | loginViewModelTest.login(email, password)
106 | testLifecycle.resume()
107 | val result = LiveDataTestUtil.getValue(loginViewModelTest.actionLogin, testLifecycle)
108 | assertEquals(UIUserLogged(email), result)
109 | }
110 |
111 | // further tests here
112 | }
113 | ```
114 |
115 | In the previous snippet, you can see the `loginViewModelTest.login(email, password)` used to test the ViewModel.
116 |
117 |
118 | ### 2.3 Repository test
119 | In order to perform a Repository test, you have to inject the real repository through Koin.
120 | Then, inside the `@Test` annotated function, you can call the repository function that you want to test.
121 |
122 | ```kotlin
123 | class AuthRepositoryTest : KoinTest {
124 |
125 | val repo: AuthRepository by inject()
126 |
127 | @Before
128 | fun before() {
129 | startKoin {
130 | printLogger()
131 | modules(appModule)
132 | }
133 | }
134 |
135 | @After
136 | fun after() {
137 | stopKoin()
138 | }
139 |
140 | @Test
141 | fun login() {
142 | val email = "email"
143 | val password = "test"
144 | val result = repo.login(email, password)
145 | assertEquals(UserLogged(email), result)
146 | }
147 |
148 | }
149 | ```
150 |
--------------------------------------------------------------------------------
/docs/UseCase.md:
--------------------------------------------------------------------------------
1 | # KTAndroidArchitecture - USECASE
2 |
3 | ### 1.1 What is UseCase?
4 | A UseCase is basically a wrapper for business logic operations, such as backend calls, data elaboration and so on.
5 |
6 | In this specific case a usecase only elaborates the datas provided by a repository and return the elaborated datas.
7 | In the execution flow of a usecase we can find this steps:
8 |
9 | * The action execute the usecase;
10 | * The usecase calls the repository to get datas;
11 | * The retrieved data get elaborate, by handling error or success results ie.
12 | * The elaborated datas returns to the usecase's calling action.
13 |
14 |
15 | 
16 |
17 | A usecase have to extend the UseCase abstract class and implement the method **run** where all the logic is placed, the caller action calls the **execute** method which calls the **run** method.
18 |
19 | You can use also **inline usecases** defined in the action execute call.
20 |
21 | ### 1.2 Sample
22 |
23 | Below a complete example of UseCase:
24 |
25 |
26 | ```
27 | class LoginUseCase: UseCase() {
28 | override suspend fun run(params: LoginActionParams): Either {
29 | return AuthRepository.instance.login(params.email, params.password).map { do something }
30 | }
31 | }
32 | ```
33 |
34 | Below a example of inline UseCase:
35 |
36 | ```
37 | Action.Builder()
38 | .useCase{return AuthRepository.instance.login(params.email, params.password).map { do something }}
39 | ...
40 | ```
41 |
--------------------------------------------------------------------------------
/docs/ViewModel.md:
--------------------------------------------------------------------------------
1 | # ViewModels with Livedata
2 | WIP
3 |
--------------------------------------------------------------------------------
/docs/actionQueue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SysdataSpA/KTAndroidArchitecture/f54c4e833c1d02436870527cb95fe9e5ba4995e8/docs/actionQueue.png
--------------------------------------------------------------------------------
/docs/actionSingleUseCase.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SysdataSpA/KTAndroidArchitecture/f54c4e833c1d02436870527cb95fe9e5ba4995e8/docs/actionSingleUseCase.png
--------------------------------------------------------------------------------
/docs/action_execute.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SysdataSpA/KTAndroidArchitecture/f54c4e833c1d02436870527cb95fe9e5ba4995e8/docs/action_execute.png
--------------------------------------------------------------------------------
/docs/action_queue_execute.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SysdataSpA/KTAndroidArchitecture/f54c4e833c1d02436870527cb95fe9e5ba4995e8/docs/action_queue_execute.png
--------------------------------------------------------------------------------
/docs/create_usecase.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SysdataSpA/KTAndroidArchitecture/f54c4e833c1d02436870527cb95fe9e5ba4995e8/docs/create_usecase.gif
--------------------------------------------------------------------------------
/docs/queue_action.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SysdataSpA/KTAndroidArchitecture/f54c4e833c1d02436870527cb95fe9e5ba4995e8/docs/queue_action.gif
--------------------------------------------------------------------------------
/docs/repository.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SysdataSpA/KTAndroidArchitecture/f54c4e833c1d02436870527cb95fe9e5ba4995e8/docs/repository.png
--------------------------------------------------------------------------------
/docs/settings.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SysdataSpA/KTAndroidArchitecture/f54c4e833c1d02436870527cb95fe9e5ba4995e8/docs/settings.zip
--------------------------------------------------------------------------------
/docs/single_action.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SysdataSpA/KTAndroidArchitecture/f54c4e833c1d02436870527cb95fe9e5ba4995e8/docs/single_action.gif
--------------------------------------------------------------------------------
/docs/test/test_apk_location.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SysdataSpA/KTAndroidArchitecture/f54c4e833c1d02436870527cb95fe9e5ba4995e8/docs/test/test_apk_location.png
--------------------------------------------------------------------------------
/docs/test/test_apk_request_firebase.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SysdataSpA/KTAndroidArchitecture/f54c4e833c1d02436870527cb95fe9e5ba4995e8/docs/test/test_apk_request_firebase.png
--------------------------------------------------------------------------------
/docs/test/test_device_results_firebase.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SysdataSpA/KTAndroidArchitecture/f54c4e833c1d02436870527cb95fe9e5ba4995e8/docs/test/test_device_results_firebase.png
--------------------------------------------------------------------------------
/docs/test/test_result.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SysdataSpA/KTAndroidArchitecture/f54c4e833c1d02436870527cb95fe9e5ba4995e8/docs/test/test_result.png
--------------------------------------------------------------------------------
/docs/test/test_run_instrumented_firebase.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SysdataSpA/KTAndroidArchitecture/f54c4e833c1d02436870527cb95fe9e5ba4995e8/docs/test/test_run_instrumented_firebase.png
--------------------------------------------------------------------------------
/docs/test/test_run_test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SysdataSpA/KTAndroidArchitecture/f54c4e833c1d02436870527cb95fe9e5ba4995e8/docs/test/test_run_test.png
--------------------------------------------------------------------------------
/docs/test/test_select_devices_firebase.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SysdataSpA/KTAndroidArchitecture/f54c4e833c1d02436870527cb95fe9e5ba4995e8/docs/test/test_select_devices_firebase.png
--------------------------------------------------------------------------------
/docs/test/test_single_device_result_firebase.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SysdataSpA/KTAndroidArchitecture/f54c4e833c1d02436870527cb95fe9e5ba4995e8/docs/test/test_single_device_result_firebase.png
--------------------------------------------------------------------------------
/docs/uimodel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SysdataSpA/KTAndroidArchitecture/f54c4e833c1d02436870527cb95fe9e5ba4995e8/docs/uimodel.png
--------------------------------------------------------------------------------
/docs/usecase.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SysdataSpA/KTAndroidArchitecture/f54c4e833c1d02436870527cb95fe9e5ba4995e8/docs/usecase.png
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | android.enableJetifier=true
10 | android.useAndroidX=true
11 | org.gradle.jvmargs=-Xmx1536m
12 | # When configured, Gradle will run in incubating parallel mode.
13 | # This option should only be used with decoupled projects. More details, visit
14 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
15 | # org.gradle.parallel=true
16 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SysdataSpA/KTAndroidArchitecture/f54c4e833c1d02436870527cb95fe9e5ba4995e8/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Aug 27 15:02:40 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.4.1-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/ktandroidarchitecturecore/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/ktandroidarchitecturecore/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 |
6 | // This is the library version used when deploying the artifact
7 | version = "1.0.3"
8 |
9 | group = "it.sysdata.mobile"
10 |
11 | buildscript {
12 | dependencies {
13 | repositories {
14 | google()
15 | // serve per org.jfrog.buildinfo:build-info-extractor-gradle:3.1.1
16 | maven {
17 | url "https://plugins.gradle.org/m2/"
18 | }
19 | }
20 | // artifactory
21 | classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
22 | classpath 'org.jfrog.buildinfo:build-info-extractor-gradle:3.1.1'
23 | // bintray
24 | classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4'
25 | classpath 'com.github.dcendents:android-maven-plugin:1.2'
26 | }
27 | }
28 |
29 | android {
30 | compileSdkVersion 28
31 |
32 |
33 | defaultConfig {
34 | minSdkVersion 15
35 | targetSdkVersion 28
36 | versionCode 1
37 | versionName version
38 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
39 |
40 | }
41 |
42 |
43 | buildTypes {
44 | release {
45 | minifyEnabled false
46 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
47 | }
48 | }
49 |
50 | packagingOptions {
51 | exclude 'META-INF/main.kotlin_module'
52 | }
53 |
54 | }
55 |
56 | dependencies {
57 | def kotlin_coroutines_version = '1.3.0-M2'
58 | def lifecycle_version = '2.2.0-rc03'
59 | implementation fileTree(dir: 'libs', include: ['*.jar'])
60 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
61 | // For Kotlin use lifecycle-viewmodel-ktx
62 |
63 | implementation "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"
64 | kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
65 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
66 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
67 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
68 | }
69 |
70 | repositories {
71 | mavenCentral()
72 | google()
73 | }
74 |
75 | // pubblicazione maven
76 | Properties properties = new Properties()
77 | properties.load(project.rootProject.file('local.properties').newDataInputStream())
78 | ext {
79 | // GROUP_ID
80 | publishedGroupId = group
81 | // ARTIFACT_ID
82 | artifact = 'ktandroidarchitecturecore'
83 | // VERSION_ID
84 | libraryVersion = version
85 |
86 | developerId = properties.getProperty("bintray.developer.id")
87 | developerName = properties.getProperty("bintray.developer.name")
88 | developerEmail = properties.getProperty("bintray.developer.email")
89 |
90 | bintrayRepo = 'maven'
91 | bintrayName = 'ktandroidarchitecturecore'
92 | libraryName = 'ktandroidarchitecturecore'
93 | bintrayOrganization = 'sysdata-mobile'
94 | }
95 |
96 | //apply from: 'publishArtifactory.gradle'
97 | apply from: 'publishBintray.gradle'
98 |
--------------------------------------------------------------------------------
/ktandroidarchitecturecore/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 |
--------------------------------------------------------------------------------
/ktandroidarchitecturecore/publishBintray.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | repositories {
3 | google()
4 | jcenter()
5 | }
6 | dependencies {
7 | classpath 'com.android.tools.build:gradle:3.5.3'
8 | classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
9 | classpath 'org.jfrog.buildinfo:build-info-extractor-gradle:3.1.1'
10 | }
11 | }
12 |
13 | apply plugin: 'com.android.library'
14 |
15 | ext {
16 | libraryDescription = ''
17 | siteUrl = 'https://github.com/SysdataSpA/KTAndroidArchitecture'
18 | gitUrl = 'https://github.com/SysdataSpA/KTAndroidArchitecture.git'
19 |
20 | licenseName = 'The Apache Software License, Version 2.0'
21 | licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
22 | allLicenses = ["Apache-2.0"]
23 | }
24 |
25 | //apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/installv1.gradle'
26 | apply plugin: 'com.github.dcendents.android-maven'
27 |
28 | group = publishedGroupId // Maven Group ID for the artifact
29 |
30 | install {
31 | repositories.mavenInstaller {
32 | // This generates POM.xml with proper parameters
33 | pom {
34 | project {
35 | packaging 'aar'
36 | groupId publishedGroupId
37 | artifactId artifact
38 |
39 | // Add your description here
40 | name libraryName
41 | description libraryDescription
42 | url siteUrl
43 |
44 | // Set your license
45 | licenses {
46 | license {
47 | name licenseName
48 | url licenseUrl
49 | }
50 | }
51 | developers {
52 | developer {
53 | id developerId
54 | name developerName
55 | email developerEmail
56 | }
57 | }
58 | scm {
59 | connection gitUrl
60 | developerConnection gitUrl
61 | url siteUrl
62 |
63 | }
64 | }
65 | }
66 | }
67 | }
68 |
69 | //apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/bintrayv1.gradle'
70 | apply plugin: 'com.jfrog.bintray'
71 | apply plugin: 'maven'
72 |
73 | version = libraryVersion
74 |
75 | task sourcesJar(type: Jar) {
76 | dependsOn = ['test', 'connectedAndroidTest']
77 | from android.sourceSets.main.java.srcDirs
78 | classifier = 'sources'
79 | }
80 |
81 | task writeNewPom {
82 | pom {
83 | project {
84 | packaging 'aar'
85 | name 'KTAndroidArchitecture'
86 | url siteUrl
87 | licenses {
88 | license {
89 | name 'The Apache Software License, Version 2.0'
90 | url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
91 | distribution 'repo'
92 | }
93 | }
94 | }
95 | }.writeTo("$buildDir/poms/pom-default.xml")
96 | }
97 |
98 | artifacts {
99 | archives sourcesJar
100 | }
101 |
102 | // Bintray
103 | Properties properties = new Properties()
104 | properties.load(project.rootProject.file('local.properties').newDataInputStream())
105 |
106 | bintray {
107 | user = properties.getProperty("bintray.user")
108 | key = properties.getProperty("bintray.apikey")
109 |
110 | configurations = ['archives']
111 | pkg {
112 | repo = bintrayRepo
113 | name = bintrayName
114 | desc = libraryDescription
115 | userOrg = bintrayOrganization
116 | websiteUrl = siteUrl
117 | vcsUrl = gitUrl
118 | licenses = allLicenses
119 | publish = true
120 | publicDownloadNumbers = true
121 | version {
122 | desc = libraryDescription
123 | gpg {
124 | sign = true //Determines whether to GPG sign the files. The default is false
125 | passphrase = properties.getProperty("bintray.gpg.password")
126 | //Optional. The passphrase for GPG signing'
127 | }
128 | }
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/ktandroidarchitecturecore/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/ktandroidarchitecturecore/src/main/java/it/sysdata/ktandroidarchitecturecore/BaseConfig.kt:
--------------------------------------------------------------------------------
1 | package it.sysdata.ktandroidarchitecturecore
2 |
3 | import it.sysdata.ktandroidarchitecturecore.platform.SafeExecute
4 | import it.sysdata.ktandroidarchitecturecore.platform.SafeExecuteInterface
5 |
6 | object BaseConfig {
7 |
8 |
9 | var safeExecutor: SafeExecuteInterface = SafeExecute()
10 |
11 |
12 | }
--------------------------------------------------------------------------------
/ktandroidarchitecturecore/src/main/java/it/sysdata/ktandroidarchitecturecore/BaseRepository.kt:
--------------------------------------------------------------------------------
1 | package it.sysdata.ktandroidarchitecturecore
2 |
3 | abstract class BaseRepository {
4 | }
--------------------------------------------------------------------------------
/ktandroidarchitecturecore/src/main/java/it/sysdata/ktandroidarchitecturecore/exception/Failure.kt:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2020 Sysdata S.p.a.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package it.sysdata.ktandroidarchitecturecore.exception
17 |
18 | /**
19 | * Base Class for handling errors/failures/exceptions.
20 | * Every feature specific failure should extend [FeatureFailure] class.
21 | */
22 | sealed class Failure {
23 | class NetworkConnection : Failure()
24 | class InternalError(val errorMessage: String?) : Failure()
25 |
26 | class ServerError : Failure()
27 | class NotImplemeted : Failure()
28 | class NullInQueue(val useCaseindex: Int) : Failure()
29 | class NotNull : Failure()
30 |
31 |
32 | /** * Extend this class for feature specific failures.*/
33 | abstract class FeatureFailure : Failure()
34 | }
35 |
--------------------------------------------------------------------------------
/ktandroidarchitecturecore/src/main/java/it/sysdata/ktandroidarchitecturecore/functional/Either.kt:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2020 Sysdata S.p.a.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package it.sysdata.ktandroidarchitecturecore.functional
17 |
18 | /**
19 | * Represents a value of one of two possible types (a disjoint union).
20 | * Instances of [Either] are either an instance of [Left] or [Right].
21 | * FP Convention dictates that [Left] is used for "failure"
22 | * and [Right] is used for "success".
23 | *
24 | * @see Left
25 | * @see Right
26 | */
27 | sealed class Either {
28 | /** * Represents the left side of [Either] class which by convention is a "Failure". */
29 | data class Left(val a: L) : Either()
30 | /** * Represents the right side of [Either] class which by convention is a "Success". */
31 | data class Right(val b: R) : Either()
32 |
33 | val isRight get() = this is Right
34 | val isLeft get() = this is Left
35 |
36 | fun left(a: L) = Left(a)
37 | fun right(b: R) = Right(b)
38 |
39 | fun either(fnL: (L) -> Any, fnR: (R) -> Any): Any =
40 | when (this) {
41 | is Left -> fnL(a)
42 | is Right -> fnR(b)
43 | }
44 | }
45 |
46 | // Credits to Alex Hart -> https://proandroiddev.com/kotlins-nothing-type-946de7d464fb
47 | // Composes 2 functions
48 | fun ((A) -> B).c(f: (B) -> C): (A) -> C = {
49 | f(this(it))
50 | }
51 |
52 | fun Either.flatMap(fn: (R) -> Either): Either =
53 | when (this) {
54 | is Either.Left -> Either.Left(a)
55 | is Either.Right -> fn(b)
56 | }
57 |
58 | fun Either.map(fn: (R) -> (T)): Either = this.flatMap(fn.c(::right))
59 |
--------------------------------------------------------------------------------
/ktandroidarchitecturecore/src/main/java/it/sysdata/ktandroidarchitecturecore/interactor/Action.kt:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2020 Sysdata S.p.a.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package it.sysdata.ktandroidarchitecturecore.interactor
17 |
18 | import androidx.lifecycle.LifecycleOwner
19 | import androidx.lifecycle.MutableLiveData
20 | import androidx.lifecycle.Observer
21 | import it.sysdata.ktandroidarchitecturecore.exception.Failure
22 | import it.sysdata.ktandroidarchitecturecore.functional.Either
23 | import it.sysdata.ktandroidarchitecturecore.platform.SingleLiveEvent
24 | import kotlinx.coroutines.CoroutineScope
25 | import kotlinx.coroutines.GlobalScope
26 |
27 | /**
28 | * [Action] allow to define [UseCase] and [MutableLiveData] for handle the success, loading and failure case
29 | *
30 | * of Use Case and it allow to set a function for mapping Model into UiModel
31 | *
32 | */
33 | class Action private constructor() {
34 |
35 | private val liveData = MutableLiveData()
36 | private val loadingLiveData = MutableLiveData()
37 |
38 | private val failureLiveData = SingleLiveEvent()
39 | private lateinit var uc: UseCase
40 | private var mappingFunction: (Model) -> UiModel? = { null }
41 |
42 | private var lastParams: Params? = null
43 |
44 | /**
45 | * Execute the action
46 | *
47 | * @param params for use case
48 | */
49 | fun execute(params: Params, scope: CoroutineScope = GlobalScope) {
50 | lastParams = params
51 | loadingLiveData.value = true
52 | uc.execute({ it.either(::handleFailure, ::handleSuccess) }, params, scope)
53 | }
54 |
55 | /**
56 | * Execute the action in safe mode. This means tha whenever an exception is thrown, the architecture will return a [Failure.InternalError] failure.
57 | *
58 | * @param params for use case
59 | */
60 | fun safeExecute(params: Params, scope: CoroutineScope = GlobalScope) {
61 | lastParams = params
62 | loadingLiveData.value = true
63 | uc.execute({ it.either(::handleFailure, ::handleSuccess) }, params, scope,true)
64 | }
65 | /**
66 | * Retry the action performed last type
67 | *
68 | * @param params for use case
69 | */
70 | fun retry() {
71 | if (lastParams == null) return
72 | lastParams?.let {
73 | execute(it)
74 | }
75 | }
76 |
77 | /**
78 | * Define the function that will use for handle the result
79 | *
80 | * @param owner for [liveData]
81 | * @param body the function that will use for handle the result
82 | */
83 | fun observe(owner: LifecycleOwner, body: (UiModel) -> Unit) {
84 | liveData.observe(owner, Observer(body))
85 |
86 | }
87 |
88 | /**
89 | * Define the function that will use for handle the result without model
90 | *
91 | * @param owner for [liveData]
92 | * @param body the function that will use for handle the result
93 | */
94 | fun observeWithoutModel(owner: LifecycleOwner, body: () -> Unit) {
95 | liveData.observe(owner, Observer { body.invoke() })
96 |
97 | }
98 |
99 | /**
100 | * Define the function that will use for handle the failure
101 | *
102 | * @param owner for [failureLiveData]
103 | * @param body the function that will use for handle the failure
104 | */
105 | fun observeFailure(owner: LifecycleOwner, body: (Failure) -> Unit) {
106 | failureLiveData.observe(owner, Observer(body))
107 |
108 | }
109 |
110 | /**
111 | * Define the function that will use for handle the failure without type of Failure
112 | *
113 | * @param owner for [failureLiveData]
114 | * @param body the function that will use for handle the failure
115 | */
116 | fun observeFailureWithoutType(owner: LifecycleOwner, body: () -> Unit) {
117 | failureLiveData.observe(owner, Observer { body.invoke() })
118 |
119 | }
120 |
121 | /**
122 | * Define the function that will use for handle the loading
123 | *
124 | * @param owner for [loadingLiveData]
125 | * @param body the function that will use for handle the loading
126 | */
127 | fun observeLoadingStatus(owner: LifecycleOwner, body: (Boolean) -> Unit) {
128 | loadingLiveData.observe(owner, Observer(body))
129 |
130 | }
131 |
132 | /**
133 | * Post loading live date to false
134 | *
135 | * Post the Fail into [MutableLiveData]
136 | * @param fail
137 | */
138 | private fun handleFailure(fail: Failure) {
139 | loadingLiveData.postValue(false)
140 | failureLiveData.postValue(fail)
141 | }
142 |
143 | /**
144 | * Post loading live date to false
145 | *
146 | * Post the Uimodel with @mappingFunction into [MutableLiveData]
147 | * @param model
148 | */
149 | private fun handleSuccess(model: Model) {
150 | loadingLiveData.postValue(false)
151 | liveData.postValue(mappingFunction(model))
152 | }
153 |
154 |
155 | class ActionBuilderMappingUiModel internal constructor(private val action: Action) {
156 | /**
157 | * Set map function for [Action]
158 | * @param handleResult function for mapping Model to UiModel
159 | *
160 | * @return [Builder] instance
161 | */
162 | fun buildWithUiModel(handleResult: (Model) -> UiModel): Action {
163 | action.mappingFunction = handleResult
164 | return action
165 |
166 | }
167 |
168 |
169 | }
170 |
171 |
172 | /**
173 | * Use this class for create a instance of [Action]
174 | */
175 | class Builder {
176 | private val action = Action()
177 |
178 | /**
179 | * Set use case for [Action]
180 | * @param useCaseClass Java class of use case
181 | *
182 | * @return [ActionBuilderMappingUiModel] instance
183 | */
184 | fun useCase(useCaseClass: Class): ActionBuilderMappingUiModel where T : UseCase {
185 | action.uc = useCaseClass.newInstance()
186 |
187 | return ActionBuilderMappingUiModel(action)
188 | }
189 |
190 | /**
191 | * Set use case for [Action]
192 | * @param run function for Action
193 | *
194 | * @return [ActionBuilderMappingUiModel] instance
195 | */
196 | fun useCase(run: (Params) -> Either): ActionBuilderMappingUiModel {
197 | action.uc = object : UseCase() {
198 | override suspend fun run(params: Params): Either {
199 | return run.invoke(params)
200 | }
201 | }
202 | return ActionBuilderMappingUiModel(action)
203 | }
204 |
205 | /**
206 | * Set use case for [Action]
207 | * @param useCase the instance of useCase
208 | *
209 | * @return [ActionBuilderMappingUiModel] instance
210 | */
211 | fun useCase(useCase: T): ActionBuilderMappingUiModel where T : UseCase {
212 | action.uc = useCase
213 |
214 | return ActionBuilderMappingUiModel(action)
215 | }
216 | }
217 |
218 | }
219 |
220 |
221 |
--------------------------------------------------------------------------------
/ktandroidarchitecturecore/src/main/java/it/sysdata/ktandroidarchitecturecore/interactor/ActionParams.kt:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * Copyright (C) 2020 Sysdata S.p.a.
4 | *
5 | * Licensed under the Apache License, Version 2.0 (the "License");
6 | * you may not use this file except in compliance with the License.
7 | * You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 | package it.sysdata.ktandroidarchitecturecore.interactor
18 |
19 | abstract class ActionParams
--------------------------------------------------------------------------------
/ktandroidarchitecturecore/src/main/java/it/sysdata/ktandroidarchitecturecore/interactor/ActionQueue.kt:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2020 Sysdata S.p.a.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package it.sysdata.ktandroidarchitecturecore.interactor
17 |
18 | import androidx.lifecycle.LifecycleOwner
19 | import androidx.lifecycle.MutableLiveData
20 | import androidx.lifecycle.Observer
21 | import it.sysdata.ktandroidarchitecturecore.exception.Failure
22 | import it.sysdata.ktandroidarchitecturecore.functional.Either
23 |
24 | /**
25 | * [ActionQueue] allow to define a queue of [UseCase]
26 | * they will be launched one after the other and the Model of previous [UseCase]
27 | * will be mapping for Params of next [UseCase]
28 | */
29 | class ActionQueue private constructor() {
30 | // LiveData for UiModel
31 | private val liveData = MutableLiveData()
32 |
33 | // LiveData for loading status (true if action is loading)
34 | private val loadingLiveData = MutableLiveData()
35 |
36 | // LiveData for fail status
37 | private val failureLiveData = MutableLiveData()
38 |
39 | //Last use case
40 | private lateinit var lastUseCase: ActionQueueItem<*, *, *>
41 |
42 | //Check if call the last use case
43 | private var isLastAction = false
44 |
45 | private var index = 1
46 |
47 | //Queue of use case
48 | private val useCaseQueue = mutableListOf>()
49 |
50 | //Mapping function for mapping last model of last use case to UiModel
51 | private var mappingFunction: (LastModel) -> UiModel? = { null }
52 |
53 | /**
54 | * Execute the first action
55 | *
56 | * @param params for first use case
57 | */
58 | fun execute(params: Params) {
59 | if (loadingLiveData.value == true) {
60 | return
61 | }
62 | val item = useCaseQueue.first()
63 | loadingLiveData.postValue(true)
64 | if (useCaseQueue.size == 1) //check if the first use case is the only use case (use normal Action for this case)
65 | isLastAction = true
66 | item.executeWithoutMapping(::handleFailure, ::handleUseCase, params)
67 |
68 |
69 | }
70 |
71 |
72 | @Suppress("UNCHECKED_CAST")
73 | private fun handleUseCase(model: T) {
74 |
75 | // If last action set the user action for the result use case
76 | // otherwise call the action with this function as function result
77 |
78 | when {
79 | isLastAction -> {
80 | handleLastUseCase(model as Any)
81 | }
82 | model != null -> {
83 | if (useCaseQueue.size == 1) {
84 | handleLastUseCase(model as Any)
85 | return
86 | }
87 | isLastAction = index + 1 == useCaseQueue.size
88 | val item = useCaseQueue[index]
89 | item.execute(::handleFailure, ::handleUseCase, model)
90 | index++
91 |
92 | }
93 | else -> {
94 | handleFailure(Failure.NullInQueue(index))
95 | resetQueue()
96 | }
97 | }
98 | }
99 |
100 | private fun handleLastUseCase(model: Any) {
101 | isLastAction = true
102 | lastUseCase.execute(::handleFailure, ::handleSuccess, model)
103 | resetQueue()
104 | }
105 |
106 | private fun resetQueue() {
107 | isLastAction = false
108 | index = 1
109 | }
110 |
111 | private fun handleFailure(fail: Failure) {
112 | loadingLiveData.postValue(false)
113 | failureLiveData.postValue(fail)
114 | resetQueue()
115 | }
116 |
117 |
118 | @Suppress("UNCHECKED_CAST")
119 | private fun handleSuccess(model: T) {
120 |
121 | loadingLiveData.postValue(false)
122 | liveData.postValue(mappingFunction(model as LastModel))
123 | }
124 |
125 | /**
126 | * Define the function that will use for handle the result
127 | *
128 | * @param owner for [liveData]
129 | * @param body the function that will use for handle the result
130 | */
131 | fun observe(owner: LifecycleOwner, body: (UiModel) -> Unit) {
132 | liveData.observe(owner, Observer(body))
133 |
134 | }
135 |
136 | /**
137 | * Define the function that will use for handle the result without model
138 | *
139 | * @param owner for [liveData]
140 | * @param body the function that will use for handle the result
141 | */
142 | fun observeWithoutModel(owner: LifecycleOwner, body: () -> Unit) {
143 | liveData.observe(owner, Observer { body.invoke() })
144 |
145 | }
146 |
147 | /**
148 | * Define the function that will use for handle the failure without type of Failure
149 | *
150 | * @param owner for [failureLiveData]
151 | * @param body the function that will use for handle the failure
152 | */
153 | fun observeFailureWithoutType(owner: LifecycleOwner, body: () -> Unit) {
154 | failureLiveData.observe(owner, Observer { body.invoke() })
155 |
156 | }
157 |
158 | /**
159 | * Define the function that will use for handle the failure
160 | *
161 | * @param owner for [failureLiveData]
162 | * @param body the function that will use for handle the failure
163 | */
164 | fun observeFailure(owner: LifecycleOwner, body: (Failure) -> Unit) {
165 | failureLiveData.observe(owner, Observer(body))
166 |
167 | }
168 |
169 | /**
170 | * Define the function that will use for handle the loading
171 | *
172 | * @param owner for [loadingLiveData]
173 | * @param body the function that will use for handle the loading
174 | */
175 | fun observeLoadingStatus(owner: LifecycleOwner, body: (Boolean) -> Unit) {
176 | loadingLiveData.observe(owner, Observer(body))
177 |
178 | }
179 |
180 | class ActionQueueItem internal constructor(
181 | private val useCase: UseCase,
182 | private val handleMapping: ((OldModel) -> Params)? = null
183 | ) {
184 |
185 |
186 | @Suppress("UNCHECKED_CAST")
187 | fun execute(handleFailure: (Failure) -> Unit, handleSuccess: (Any) -> Unit, model: Any) {
188 | try {
189 | useCase.execute({ it.either(handleFailure, handleSuccess) }, handleMapping!!(model as OldModel))
190 |
191 | } catch (e: Exception) {
192 | handleFailure.invoke(Failure.InternalError(e.message))
193 | }
194 | }
195 |
196 | @Suppress("UNCHECKED_CAST")
197 | fun executeWithoutMapping(handleFailure: (Failure) -> Unit, handleSuccess: (Any) -> Unit, params: Any) {
198 | try {
199 | useCase.execute({ it.either(handleFailure, handleSuccess) }, params as Params)
200 |
201 | } catch (e: Exception) {
202 | handleFailure.invoke(Failure.InternalError(e.message))
203 |
204 | }
205 |
206 | }
207 | }
208 |
209 |
210 | class ActionQueueBuilderMappingUiModel internal constructor(
211 | private val actionQueue: ActionQueue
212 | ) {
213 | /**
214 | * Set map function for [ActionQueue]
215 | * @param handleResult function for mapping Model to UiModel
216 | *
217 | * @return [Builder] instance
218 | */
219 | fun buildWithUiModel(handleResult: (Model) -> UiModel): ActionQueue {
220 | actionQueue.mappingFunction = handleResult
221 | return actionQueue
222 |
223 | }
224 |
225 | }
226 |
227 |
228 | class ActionQueueBuilderUseCase internal constructor(private val actionQueue: ActionQueue) {
229 | /**
230 | * Add a use case that will execute after previous use case
231 | *
232 | *
233 | * @param useCaseClass java class of use case
234 | * @param mapping function that mapping previous model use case result with params for this
235 | * use case
236 | * @return [Builder] instance
237 | */
238 |
239 |
240 | fun > addUseCase(
241 | useCaseClass: Class,
242 | mapping: (OldModel) -> ParamsUseCase
243 | ): ActionQueueBuilderUseCase {
244 | actionQueue.useCaseQueue.add(ActionQueueItem(useCaseClass.newInstance(), mapping))
245 |
246 | return ActionQueueBuilderUseCase(actionQueue)
247 |
248 | }
249 |
250 | /**
251 | * Add a use case that will execute after previous use case
252 | *
253 | *
254 | * @param useCaseClass instance of use case
255 | * @param mapping function that mapping previous model use case result with params for this
256 | * use case
257 | * @return [Builder] instance
258 | */
259 |
260 |
261 | fun > addUseCase(
262 | useCaseClass: T,
263 | mapping: (OldModel) -> ParamsUseCase
264 | ): ActionQueueBuilderUseCase {
265 | actionQueue.useCaseQueue.add(ActionQueueItem(useCaseClass, mapping))
266 |
267 | return ActionQueueBuilderUseCase(actionQueue)
268 |
269 | }
270 |
271 | /**
272 | * Add a use case that will execute after previous use case
273 | *
274 | *
275 | * @param run function for use case
276 | * @param mapping function that mapping previous model use case result with params for this
277 | * use case
278 | *
279 | * @return [Builder] instance
280 |
281 | */
282 | fun addUseCase(
283 | run: (ParamsUseCase) -> Either,
284 | mapping: (OldModel) -> ParamsUseCase
285 | ): ActionQueueBuilderUseCase {
286 | val useCase = object : UseCase() {
287 | override suspend fun run(params: ParamsUseCase): Either {
288 | return run.invoke(params)
289 | }
290 | }
291 | actionQueue.useCaseQueue.add(ActionQueueItem(useCase, mapping))
292 | return ActionQueueBuilderUseCase(actionQueue)
293 |
294 |
295 | }
296 |
297 | /**
298 | * Set the last use case that will execute with the function define in [mappingFunction]
299 | *
300 | *
301 | * @param useCaseClass java class of use case
302 | * @param mapping function that mapping previous model use case result with params for this
303 | * use case
304 | *
305 | * @return [Builder] instance
306 |
307 | */
308 |
309 |
310 | fun > setLastUseCase(
311 | useCaseClass: Class,
312 | mapping: (OldModel) -> ParamsUseCase
313 | ): ActionQueueBuilderMappingUiModel {
314 | val finalActionQueue = ActionQueue()
315 |
316 | finalActionQueue.lastUseCase = ActionQueueItem(useCaseClass.newInstance(), mapping)
317 | finalActionQueue.useCaseQueue.addAll(actionQueue.useCaseQueue)
318 |
319 | return ActionQueueBuilderMappingUiModel(finalActionQueue)
320 | }
321 |
322 | /**
323 | * Set the last use case that will execute with the function define in [mappingFunction]
324 | *
325 | *
326 | * @param useCaseClass instance of use case
327 | * @param mapping function that mapping previous model use case result with params for this
328 | * use case
329 | *
330 | * @return [Builder] instance
331 |
332 | */
333 |
334 |
335 | fun > setLastUseCase(
336 | useCaseClass: T,
337 | mapping: (OldModel) -> ParamsUseCase
338 | ): ActionQueueBuilderMappingUiModel {
339 | val finalActionQueue = ActionQueue()
340 |
341 | finalActionQueue.lastUseCase = ActionQueueItem(useCaseClass, mapping)
342 | finalActionQueue.useCaseQueue.addAll(actionQueue.useCaseQueue)
343 |
344 | return ActionQueueBuilderMappingUiModel(finalActionQueue)
345 | }
346 |
347 | /**
348 | * Set the last use case that will execute with the function define in [mappingFunction]
349 | *
350 | *
351 | * @param run function for use case
352 | * @param mapping function that mapping previous model use case result with params for this
353 | * use case
354 | *
355 | * @return [ActionQueueBuilder] instance
356 |
357 | */
358 | fun setLastUseCase(
359 | run: (ParamsUseCase) -> Either,
360 | mapping: (OldModel) -> ParamsUseCase
361 | ): ActionQueueBuilderMappingUiModel {
362 | val useCase = object : UseCase() {
363 | override suspend fun run(params: ParamsUseCase): Either {
364 | return run.invoke(params)
365 | }
366 | }
367 | val finalActionQueue = ActionQueue()
368 |
369 | finalActionQueue.lastUseCase = ActionQueueItem(useCase, mapping)
370 | finalActionQueue.useCaseQueue.addAll(actionQueue.useCaseQueue)
371 |
372 | return ActionQueueBuilderMappingUiModel(finalActionQueue)
373 |
374 |
375 | }
376 |
377 |
378 | }
379 |
380 |
381 | /**
382 | * Use this class for create a instance of [ActionQueue]
383 | */
384 | class Builder {
385 | private val actionQueue = ActionQueue()
386 |
387 |
388 | /**
389 | * Set the first use case that will execute
390 | *
391 | *
392 | * @param useCaseClass java class of use case
393 | * use case
394 | * @return [Builder] instance
395 | */
396 |
397 |
398 | fun > setFirstUseCase(useCaseClass: Class): ActionQueueBuilderUseCase {
399 | actionQueue.useCaseQueue.add(0, ActionQueueItem(useCaseClass.newInstance()))
400 |
401 | return ActionQueueBuilderUseCase(actionQueue)
402 | }
403 |
404 | /**
405 | * Set the first use case that will execute
406 | *
407 | *
408 | * @param useCaseClass java class of use case
409 | * use case
410 | * @return [Builder] instance
411 | */
412 | fun > setFirstUseCase(useCaseClass: T): ActionQueueBuilderUseCase {
413 | actionQueue.useCaseQueue.add(0, ActionQueueItem(useCaseClass))
414 |
415 | return ActionQueueBuilderUseCase(actionQueue)
416 | }
417 |
418 | /**
419 | * Set the first use case that will execute
420 | *
421 | *
422 | * @param run function for use case
423 | * use case
424 | * @return [Builder] instance
425 | */
426 | fun setFirstUseCase(run: (Params) -> Either): ActionQueueBuilderUseCase {
427 | val useCase = object : UseCase() {
428 | override suspend fun run(params: Params): Either {
429 | return run.invoke(params)
430 | }
431 | }
432 | actionQueue.useCaseQueue.add(0, ActionQueueItem(useCase))
433 | return ActionQueueBuilderUseCase(actionQueue)
434 |
435 |
436 | }
437 |
438 |
439 | }
440 | }
--------------------------------------------------------------------------------
/ktandroidarchitecturecore/src/main/java/it/sysdata/ktandroidarchitecturecore/interactor/SingleAction.kt:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2020 Sysdata S.p.a.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package it.sysdata.ktandroidarchitecturecore.interactor
17 |
18 | import androidx.lifecycle.LifecycleOwner
19 | import androidx.lifecycle.MutableLiveData
20 | import androidx.lifecycle.Observer
21 | import it.sysdata.ktandroidarchitecturecore.exception.Failure
22 | import it.sysdata.ktandroidarchitecturecore.functional.Either
23 | import it.sysdata.ktandroidarchitecturecore.platform.SingleLiveEvent
24 |
25 | /**
26 | * [SingleAction] allow to define [UseCase] and [MutableLiveData] for handle the success, loading and failure case
27 | *
28 | * of Use Case and it allow to set a function for mapping Model into UiModel
29 | *
30 | */
31 | class SingleAction private constructor() {
32 |
33 | private val liveData = SingleLiveEvent()
34 | private val loadingLiveData = MutableLiveData()
35 |
36 | private val failureLiveData = SingleLiveEvent()
37 | private lateinit var uc: UseCase
38 | private var mappingFunction: (Model) -> UiModel? = { null }
39 |
40 | private var lastParams: Params? = null
41 |
42 | /**
43 | * Execute the action
44 | *
45 | * @param params for use case
46 | */
47 | fun execute(params: Params) {
48 | lastParams = params
49 | // loadingLiveData.postValue(true)
50 | // TODO to check modify
51 | loadingLiveData.value = true
52 | uc.execute({ it.either(::handleFailure, ::handleSuccess) }, params)
53 | }
54 |
55 | /**
56 | * Retry the action performed last type
57 | *
58 | * @param params for use case
59 | */
60 | fun retry() {
61 | if (lastParams == null) return
62 | lastParams?.let {
63 | execute(it)
64 | }
65 | }
66 |
67 | /**
68 | * Define the function that will use for handle the result
69 | *
70 | * @param owner for [liveData]
71 | * @param body the function that will use for handle the result
72 | */
73 | fun observe(owner: LifecycleOwner, body: (UiModel) -> Unit) {
74 | liveData.observe(owner, Observer(body))
75 |
76 | }
77 |
78 | /**
79 | * Define the function that will use for handle the result without model
80 | *
81 | * @param owner for [liveData]
82 | * @param body the function that will use for handle the result
83 | */
84 | fun observeWithoutModel(owner: LifecycleOwner, body: () -> Unit) {
85 | liveData.observe(owner, Observer { body.invoke() })
86 |
87 | }
88 |
89 | /**
90 | * Define the function that will use for handle the failure
91 | *
92 | * @param owner for [failureLiveData]
93 | * @param body the function that will use for handle the failure
94 | */
95 | fun observeFailure(owner: LifecycleOwner, body: (Failure) -> Unit) {
96 | failureLiveData.observe(owner, Observer(body))
97 |
98 | }
99 |
100 | /**
101 | * Define the function that will use for handle the failure without type of Failure
102 | *
103 | * @param owner for [failureLiveData]
104 | * @param body the function that will use for handle the failure
105 | */
106 | fun observeFailureWithoutType(owner: LifecycleOwner, body: () -> Unit) {
107 | failureLiveData.observe(owner, Observer { body.invoke() })
108 |
109 | }
110 |
111 | /**
112 | * Define the function that will use for handle the loading
113 | *
114 | * @param owner for [loadingLiveData]
115 | * @param body the function that will use for handle the loading
116 | */
117 | fun observeLoadingStatus(owner: LifecycleOwner, body: (Boolean) -> Unit) {
118 | loadingLiveData.observe(owner, Observer(body))
119 |
120 | }
121 |
122 | /**
123 | * Post loading live date to false
124 | *
125 | * Post the Fail into [MutableLiveData]
126 | * @param fail
127 | */
128 | private fun handleFailure(fail: Failure) {
129 | loadingLiveData.postValue(false)
130 | failureLiveData.postValue(fail)
131 | }
132 |
133 | /**
134 | * Post loading live date to false
135 | *
136 | * Post the Uimodel with @mappingFunction into [MutableLiveData]
137 | * @param model
138 | */
139 | private fun handleSuccess(model: Model) {
140 | loadingLiveData.postValue(false)
141 | liveData.postValue(mappingFunction(model))
142 | }
143 |
144 |
145 | class ActionBuilderMappingUiModel internal constructor(private val action: SingleAction) {
146 | /**
147 | * Set map function for [SingleAction]
148 | * @param handleResult function for mapping Model to UiModel
149 | *
150 | * @return [Builder] instance
151 | */
152 | fun buildWithUiModel(handleResult: (Model) -> UiModel): SingleAction {
153 | action.mappingFunction = handleResult
154 | return action
155 |
156 | }
157 |
158 |
159 | }
160 |
161 |
162 | /**
163 | * Use this class for create a instance of [SingleAction]
164 | */
165 | class Builder {
166 | private val action = SingleAction()
167 |
168 | /**
169 | * Set use case for [SingleAction]
170 | * @param useCaseClass Java class of use case
171 | *
172 | * @return [ActionBuilderMappingUiModel] instance
173 | */
174 | fun useCase(useCaseClass: Class): ActionBuilderMappingUiModel where T : UseCase {
175 | action.uc = useCaseClass.newInstance()
176 |
177 | return ActionBuilderMappingUiModel(action)
178 | }
179 |
180 | /**
181 | * Set use case for [SingleAction]
182 | * @param run function for Action
183 | *
184 | * @return [ActionBuilderMappingUiModel] instance
185 | */
186 | fun useCase(run: (Params) -> Either): ActionBuilderMappingUiModel {
187 | action.uc = object : UseCase() {
188 | override suspend fun run(params: Params): Either {
189 | return run.invoke(params)
190 | }
191 | }
192 | return ActionBuilderMappingUiModel(action)
193 | }
194 |
195 | /**
196 | * Set use case for [SingleAction]
197 | * @param useCase the instance of useCase
198 | *
199 | * @return [ActionBuilderMappingUiModel] instance
200 | */
201 | fun useCase(useCase: T): ActionBuilderMappingUiModel where T : UseCase {
202 | action.uc = useCase
203 |
204 | return ActionBuilderMappingUiModel(action)
205 | }
206 | }
207 |
208 | }
209 |
210 |
211 |
--------------------------------------------------------------------------------
/ktandroidarchitecturecore/src/main/java/it/sysdata/ktandroidarchitecturecore/interactor/UseCase.kt:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2020 Sysdata S.p.a.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package it.sysdata.ktandroidarchitecturecore.interactor
17 |
18 | import it.sysdata.ktandroidarchitecturecore.BaseConfig
19 | import it.sysdata.ktandroidarchitecturecore.exception.Failure
20 | import it.sysdata.ktandroidarchitecturecore.functional.Either
21 | import kotlinx.coroutines.*
22 |
23 |
24 | /**
25 | * Abstract class for a Use Case (Interactor in terms of Clean Architecture).
26 | * This abstraction represents an execution unit for different use cases (this means than any use
27 | * case in the application should implement this contract).
28 | *
29 | * By convention each [UseCase] implementation will execute its job in a background thread
30 | * (kotlin coroutine) and will post the result in the CommonPool or UI thread.
31 | */
32 | abstract class UseCase where Type : Any, Params : ActionParams {
33 |
34 | abstract suspend fun run(params: Params): Either
35 |
36 | fun execute(
37 | onResult: (Either) -> Unit,
38 | params: Params,
39 | scope: CoroutineScope = GlobalScope,
40 | safeExecute: Boolean = false
41 | ) {
42 | val job = scope.async(
43 | Dispatchers.Default,
44 | CoroutineStart.DEFAULT
45 | ) { run(params) }
46 | scope.launch(Dispatchers.Default, CoroutineStart.DEFAULT) {
47 |
48 |
49 | if (safeExecute)
50 | onResult.invoke(BaseConfig.safeExecutor.safeExecute(job))
51 | else
52 | onResult.invoke(job.await())
53 |
54 | }
55 |
56 |
57 | }
58 |
59 |
60 | }
61 |
62 |
63 |
--------------------------------------------------------------------------------
/ktandroidarchitecturecore/src/main/java/it/sysdata/ktandroidarchitecturecore/platform/BaseViewModel.kt:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2020 Sysdata S.p.a.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package it.sysdata.ktandroidarchitecturecore.platform
17 |
18 | import androidx.lifecycle.ViewModel
19 |
20 | /**
21 | * Base ViewModel class.
22 | * @see ViewModel
23 | *
24 | */
25 | abstract class BaseViewModel : ViewModel()
--------------------------------------------------------------------------------
/ktandroidarchitecturecore/src/main/java/it/sysdata/ktandroidarchitecturecore/platform/KParcelable.kt:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright (C) 2020 Sysdata S.p.a.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | @file:Suppress("NOTHING_TO_INLINE")
17 |
18 | package it.sysdata.ktandroidarchitecturecore.platform
19 |
20 | import android.os.Parcel
21 | import android.os.Parcelable
22 | import java.math.BigDecimal
23 | import java.math.BigInteger
24 | import java.util.*
25 |
26 | //Interesting article about Parcelable and Kotlin:
27 | //https://medium.com/@BladeCoder/reducing-parcelable-boilerplate-code-using-kotlin-741c3124a49a
28 | interface KParcelable : Parcelable {
29 | override fun describeContents() = 0
30 | override fun writeToParcel(dest: Parcel, flags: Int)
31 | }
32 |
33 | // Creator factory functions
34 | inline fun parcelableCreator(crossinline create: (Parcel) -> T) =
35 | object : Parcelable.Creator {
36 | override fun createFromParcel(source: Parcel) = create(source)
37 | override fun newArray(size: Int) = arrayOfNulls(size)
38 | }
39 |
40 | inline fun parcelableClassLoaderCreator(crossinline create: (Parcel, ClassLoader) -> T) =
41 | object : Parcelable.ClassLoaderCreator {
42 | override fun createFromParcel(source: Parcel, loader: ClassLoader) = create(source, loader)
43 | override fun createFromParcel(source: Parcel) = createFromParcel(source, T::class.java.classLoader)
44 | override fun newArray(size: Int) = arrayOfNulls(size)
45 | }
46 |
47 | // Parcel extensions
48 |
49 | inline fun Parcel.readBoolean() = readInt() != 0
50 |
51 | inline fun Parcel.writeBoolean(value: Boolean) = writeInt(if (value) 1 else 0)
52 |
53 | inline fun > Parcel.readEnum() = readInt().let { if (it >= 0) enumValues()[it] else null }
54 |
55 | inline fun > Parcel.writeEnum(value: T?) = writeInt(value?.ordinal ?: -1)
56 |
57 | inline fun Parcel.readNullable(reader: () -> T) = if (readInt() != 0) reader() else null
58 |
59 | inline fun Parcel.writeNullable(value: T?, writer: (T) -> Unit) {
60 | if (value != null) {
61 | writeInt(1)
62 | writer(value)
63 | } else {
64 | writeInt(0)
65 | }
66 | }
67 |
68 | fun Parcel.readDate() = readNullable { Date(readLong()) }
69 |
70 | fun Parcel.writeDate(value: Date?) = writeNullable(value) { writeLong(it.time) }
71 |
72 | fun Parcel.readBigInteger() = readNullable { BigInteger(createByteArray()) }
73 |
74 | fun Parcel.writeBigInteger(value: BigInteger?) = writeNullable(value) { writeByteArray(it.toByteArray()) }
75 |
76 | fun Parcel.readBigDecimal() = readNullable { BigDecimal(BigInteger(createByteArray()), readInt()) }
77 |
78 | fun Parcel.writeBigDecimal(value: BigDecimal?) = writeNullable(value) {
79 | writeByteArray(it.unscaledValue().toByteArray())
80 | writeInt(it.scale())
81 | }
82 |
83 | fun Parcel.readTypedObjectCompat(c: Parcelable.Creator) = readNullable { c.createFromParcel(this) }
84 |
85 | fun Parcel.writeTypedObjectCompat(value: T?, parcelableFlags: Int) = writeNullable(value) { it.writeToParcel(this, parcelableFlags) }
86 |
--------------------------------------------------------------------------------
/ktandroidarchitecturecore/src/main/java/it/sysdata/ktandroidarchitecturecore/platform/SafeExecute.kt:
--------------------------------------------------------------------------------
1 | package it.sysdata.ktandroidarchitecturecore.platform
2 |
3 | import it.sysdata.ktandroidarchitecturecore.exception.Failure
4 | import it.sysdata.ktandroidarchitecturecore.functional.Either
5 | import kotlinx.coroutines.Deferred
6 |
7 | /**
8 | * This class is used as a default implementation for the safe execute behaviour.
9 | * Whenever an exception is thrown, this will be wrapped inside a [Failure.InternalError] failure.
10 | * If you want to define a custom behaviour for all the exception, you can define a custom class that inherits from [SafeExecuteInterface]
11 | */
12 | class SafeExecute : SafeExecuteInterface {
13 |
14 |
15 | override suspend fun safeExecute(job: Deferred>): Either {
16 | return try {
17 | job.await()
18 |
19 | } catch (e: Exception) {
20 | e.printStackTrace()
21 | Either.Left(Failure.InternalError(e.message))
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/ktandroidarchitecturecore/src/main/java/it/sysdata/ktandroidarchitecturecore/platform/SafeExecuteInterface.kt:
--------------------------------------------------------------------------------
1 | package it.sysdata.ktandroidarchitecturecore.platform
2 |
3 | import it.sysdata.ktandroidarchitecturecore.exception.Failure
4 | import it.sysdata.ktandroidarchitecturecore.functional.Either
5 | import kotlinx.coroutines.Deferred
6 |
7 | /**
8 | * This interface is used for defining an implementation when the Usecase throws an exception.
9 | * By default, the ktAndroid architecture will wrap the safe execute behaviour with the [SafeExecute] implementation.
10 | */
11 | interface SafeExecuteInterface {
12 |
13 | suspend fun safeExecute(job: Deferred>): Either }
--------------------------------------------------------------------------------
/ktandroidarchitecturecore/src/main/java/it/sysdata/ktandroidarchitecturecore/platform/SingleLiveEvent.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 Google Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package it.sysdata.ktandroidarchitecturecore.platform
18 |
19 | import android.util.Log
20 | import androidx.annotation.MainThread
21 | import androidx.lifecycle.LifecycleOwner
22 | import androidx.lifecycle.MutableLiveData
23 | import androidx.lifecycle.Observer
24 | import java.util.concurrent.atomic.AtomicBoolean
25 |
26 | /**
27 | * A lifecycle-aware observable that sends only new updates after subscription, used for events like
28 | * navigation and Snackbar messages.
29 | *
30 | *
31 | * This avoids a common problem with events: on configuration change (like rotation) an update
32 | * can be emitted if the observer is active. This LiveData only calls the observable if there's an
33 | * explicit call to setValue() or call().
34 | *
35 | *
36 | * Note that only one observer is going to be notified of changes.
37 | */
38 | class SingleLiveEvent : MutableLiveData() {
39 |
40 | private val mPending = AtomicBoolean(false)
41 |
42 | @MainThread
43 | override fun observe(owner: LifecycleOwner, observer: Observer) {
44 | if (hasActiveObservers()) {
45 | Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
46 | }
47 |
48 | // Observe the internal MutableLiveData
49 | super.observe(owner, Observer { t ->
50 | if (mPending.compareAndSet(true, false)) {
51 | observer.onChanged(t)
52 | }
53 | })
54 | }
55 |
56 | @MainThread
57 | override fun setValue(t: T?) {
58 | mPending.set(true)
59 | super.setValue(t)
60 | }
61 |
62 | /**
63 | * Used for cases where T is Void, to make calls cleaner.
64 | */
65 | @MainThread
66 | fun call() {
67 | value = null
68 | }
69 |
70 | companion object {
71 |
72 | private val TAG = "SingleLiveEvent"
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/ktandroidarchitecturecore/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/networkmodule/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/networkmodule/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 |
5 | android {
6 | compileSdkVersion 29
7 |
8 | defaultConfig {
9 | minSdkVersion 15
10 | targetSdkVersion 29
11 | versionCode 1
12 | versionName "1.0"
13 |
14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
15 | consumerProguardFiles 'consumer-rules.pro'
16 | }
17 |
18 | buildTypes {
19 | release {
20 | minifyEnabled false
21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
22 | }
23 | }
24 |
25 | }
26 |
27 | dependencies {
28 | implementation fileTree(dir: 'libs', include: ['*.jar'])
29 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
30 | implementation 'androidx.appcompat:appcompat:1.1.0'
31 | implementation 'androidx.core:core-ktx:1.1.0'
32 | testImplementation 'junit:junit:4.13'
33 | androidTestImplementation 'androidx.test:runner:1.2.0'
34 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
35 | // Koin for Kotlin
36 | api "org.koin:koin-core:$koin_version"
37 | // Koin extended & experimental features
38 | api "org.koin:koin-core-ext:$koin_version"
39 | // Koin for Unit tests
40 | testApi "org.koin:koin-test:$koin_version"
41 | // Koin for Android
42 | api "org.koin:koin-android:$koin_version"
43 | // Koin AndroidX Scope features
44 | api "org.koin:koin-androidx-scope:$koin_version"
45 | // Koin AndroidX ViewModel features
46 | api "org.koin:koin-androidx-viewmodel:$koin_version"
47 | // Koin AndroidX Experimental features
48 | api "org.koin:koin-androidx-ext:$koin_version"
49 | //Retrofit2
50 | implementation "com.squareup.retrofit2:retrofit:2.6.2"
51 | implementation 'com.squareup.retrofit2:converter-gson:2.6.2'
52 | implementation "com.squareup.okhttp3:logging-interceptor:4.1.1"
53 | }
54 |
--------------------------------------------------------------------------------
/networkmodule/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SysdataSpA/KTAndroidArchitecture/f54c4e833c1d02436870527cb95fe9e5ba4995e8/networkmodule/consumer-rules.pro
--------------------------------------------------------------------------------
/networkmodule/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 |
--------------------------------------------------------------------------------
/networkmodule/src/androidTest/java/com/example/networkmodule/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.example.networkmodule
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.example.networkmodule.test", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/networkmodule/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/networkmodule/src/main/java/com/example/networkmodule/api/api/GitHubService.kt:
--------------------------------------------------------------------------------
1 | package com.example.networkmodule.api.api
2 |
3 | import com.example.networkmodule.api.model.Repo
4 | import retrofit2.http.GET
5 | import retrofit2.http.Path
6 |
7 | interface GitHubService {
8 |
9 | @GET("users/{user}/repos")
10 | suspend fun listRepos(@Path("user") user: String): List
11 | }
--------------------------------------------------------------------------------
/networkmodule/src/main/java/com/example/networkmodule/api/api/GitHubServiceAPI.kt:
--------------------------------------------------------------------------------
1 | package com.example.networkmodule.api.api
2 |
3 | import okhttp3.OkHttpClient
4 | import okhttp3.logging.HttpLoggingInterceptor
5 | import retrofit2.Retrofit
6 | import retrofit2.converter.gson.GsonConverterFactory
7 |
8 |
9 | object GitHubServiceAPI {
10 |
11 | fun getGitHubService(): GitHubService {
12 | val interceptor = HttpLoggingInterceptor()
13 | interceptor.level = HttpLoggingInterceptor.Level.BODY
14 | val client = OkHttpClient.Builder().addInterceptor(interceptor).build()
15 | val retrofit = Retrofit.Builder()
16 | .baseUrl("https://api.github.com/")
17 | .client(client)
18 | .addConverterFactory(GsonConverterFactory.create())
19 | .build()
20 |
21 | return retrofit.create(GitHubService::class.java)
22 | }
23 | }
--------------------------------------------------------------------------------
/networkmodule/src/main/java/com/example/networkmodule/api/model/Repo.kt:
--------------------------------------------------------------------------------
1 | package com.example.networkmodule.api.model
2 |
3 | data class Repo(
4 | val `private`: Boolean,
5 | val archive_url: String,
6 | val archived: Boolean,
7 | val assignees_url: String,
8 | val blobs_url: String,
9 | val branches_url: String,
10 | val clone_url: String,
11 | val collaborators_url: String,
12 | val comments_url: String,
13 | val commits_url: String,
14 | val compare_url: String,
15 | val contents_url: String,
16 | val contributors_url: String,
17 | val created_at: String,
18 | val default_branch: String,
19 | val deployments_url: String,
20 | val description: String,
21 | val disabled: Boolean,
22 | val downloads_url: String,
23 | val events_url: String,
24 | val fork: Boolean,
25 | val forks: Int,
26 | val forks_count: Int,
27 | val forks_url: String,
28 | val full_name: String,
29 | val git_commits_url: String,
30 | val git_refs_url: String,
31 | val git_tags_url: String,
32 | val git_url: String,
33 | val has_downloads: Boolean,
34 | val has_issues: Boolean,
35 | val has_pages: Boolean,
36 | val has_projects: Boolean,
37 | val has_wiki: Boolean,
38 | val homepage: Any,
39 | val hooks_url: String,
40 | val html_url: String,
41 | val id: Int,
42 | val issue_comment_url: String,
43 | val issue_events_url: String,
44 | val issues_url: String,
45 | val keys_url: String,
46 | val labels_url: String,
47 | val language: String,
48 | val languages_url: String,
49 | val license: Any,
50 | val merges_url: String,
51 | val milestones_url: String,
52 | val mirror_url: Any,
53 | val name: String,
54 | val node_id: String,
55 | val notifications_url: String,
56 | val open_issues: Int,
57 | val open_issues_count: Int,
58 | val owner: Owner,
59 | val pulls_url: String,
60 | val pushed_at: String,
61 | val releases_url: String,
62 | val size: Int,
63 | val ssh_url: String,
64 | val stargazers_count: Int,
65 | val stargazers_url: String,
66 | val statuses_url: String,
67 | val subscribers_url: String,
68 | val subscription_url: String,
69 | val svn_url: String,
70 | val tags_url: String,
71 | val teams_url: String,
72 | val trees_url: String,
73 | val updated_at: String,
74 | val url: String,
75 | val watchers: Int,
76 | val watchers_count: Int
77 | )
78 |
79 | data class Owner(
80 | val avatar_url: String,
81 | val events_url: String,
82 | val followers_url: String,
83 | val following_url: String,
84 | val gists_url: String,
85 | val gravatar_id: String,
86 | val html_url: String,
87 | val id: Int,
88 | val login: String,
89 | val node_id: String,
90 | val organizations_url: String,
91 | val received_events_url: String,
92 | val repos_url: String,
93 | val site_admin: Boolean,
94 | val starred_url: String,
95 | val subscriptions_url: String,
96 | val type: String,
97 | val url: String
98 | )
99 |
--------------------------------------------------------------------------------
/networkmodule/src/main/java/com/example/networkmodule/di/NetworkModule.kt:
--------------------------------------------------------------------------------
1 | package com.example.networkmodule.di
2 |
3 | import com.example.networkmodule.api.api.GitHubServiceAPI
4 | import org.koin.dsl.module
5 |
6 | val networkModule = module {
7 | single { GitHubServiceAPI.getGitHubService() }
8 | }
--------------------------------------------------------------------------------
/networkmodule/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | NetworkModule
3 |
4 |
--------------------------------------------------------------------------------
/networkmodule/src/test/java/com/example/networkmodule/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.example.networkmodule
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':ktandroidarchitecturecore', ':networkmodule'
2 |
--------------------------------------------------------------------------------