├── .gitignore
├── README.md
├── build.gradle
├── buildSystem
└── dependencies.gradle
├── data
├── .gitignore
├── build.gradle
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── christopher
│ │ └── elias
│ │ └── data
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── christopher
│ │ │ └── elias
│ │ │ └── data
│ │ │ ├── di
│ │ │ ├── MapperDataModule.kt
│ │ │ ├── NetworkModule.kt
│ │ │ ├── PreferencesModule.kt
│ │ │ └── RepositoryModule.kt
│ │ │ ├── network
│ │ │ ├── end_points
│ │ │ │ ├── EndPoints.kt
│ │ │ │ ├── EndPointsImpl.kt
│ │ │ │ ├── EndPointsService.kt
│ │ │ │ └── SupportInterceptor.kt
│ │ │ ├── mapper
│ │ │ │ ├── TodoMapper.kt
│ │ │ │ └── TodoMapperImpl.kt
│ │ │ ├── response
│ │ │ │ └── TodoResponse.kt
│ │ │ └── utils
│ │ │ │ ├── ConnectionUtils.kt
│ │ │ │ └── ConnectionUtilsImpl.kt
│ │ │ ├── preferences
│ │ │ ├── SecurePreferences.kt
│ │ │ └── SecurePreferencesImpl.kt
│ │ │ └── repository
│ │ │ └── TodoRepositoryImpl.kt
│ └── res
│ │ └── values
│ │ └── strings.xml
│ └── test
│ └── java
│ └── com
│ └── christopher
│ └── elias
│ └── data
│ ├── ExampleUnitTest.kt
│ ├── base
│ └── BaseUseCaseUniTest.kt
│ ├── features
│ └── TodoUseCasesUnitTest.kt
│ └── mock
│ ├── ConnectionUtilsImplMocked.kt
│ ├── FakeNetworkModule.kt
│ └── SecurePreferencesImplMocked.kt
├── domain
├── .gitignore
├── build.gradle
└── src
│ └── main
│ └── java
│ └── com
│ └── christopher
│ └── elias
│ └── domain
│ ├── di
│ └── UseCaseModule.kt
│ ├── entity
│ ├── Either.kt
│ ├── Failure.kt
│ └── TodoEntity.kt
│ ├── repository
│ └── TodoRepository.kt
│ └── usecase
│ ├── BaseUseCase.kt
│ ├── GetTodoUseCase.kt
│ └── GetTodosUseCase.kt
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── presentation
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── christopher
│ │ └── elias
│ │ └── base
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── christopher
│ │ │ └── elias
│ │ │ └── base
│ │ │ ├── TodoApplication.kt
│ │ │ ├── di
│ │ │ ├── MapperPresentationModule.kt
│ │ │ └── ViewModelModule.kt
│ │ │ ├── mapper
│ │ │ ├── TodoModelMapper.kt
│ │ │ └── TodoModelMapperImpl.kt
│ │ │ ├── model
│ │ │ └── TodoModel.kt
│ │ │ └── ui
│ │ │ ├── adapter
│ │ │ └── TodoAdapter.kt
│ │ │ ├── base
│ │ │ ├── BaseActivity.kt
│ │ │ ├── BaseFragment.kt
│ │ │ └── BaseViewModel.kt
│ │ │ ├── bindingtools
│ │ │ ├── RecyclerViewBTools.kt
│ │ │ └── TextViewBTools.kt
│ │ │ └── destinations
│ │ │ ├── activities
│ │ │ └── main
│ │ │ │ └── MainActivity.kt
│ │ │ └── fragments
│ │ │ └── todo
│ │ │ ├── detail
│ │ │ ├── TodoDetailFragment.kt
│ │ │ └── TodoDetailViewModel.kt
│ │ │ └── list
│ │ │ ├── TodoListFragment.kt
│ │ │ ├── TodoListNavigator.kt
│ │ │ └── TodoListViewModel.kt
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ ├── ic_launcher_background.xml
│ │ ├── ic_todo_done.xml
│ │ └── ic_todo_pending.xml
│ │ ├── layout
│ │ ├── activity_main.xml
│ │ ├── fragment_todo_detail.xml
│ │ ├── fragment_todo_list.xml
│ │ └── item_todo.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
│ └── christopher
│ └── elias
│ └── base
│ └── ExampleUnitTest.kt
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | #built application files
2 | *.apk
3 | *.ap_
4 |
5 | # files for the dex VM
6 | *.dex
7 |
8 | # Java class files
9 | *.class
10 |
11 | # generated files
12 | bin/
13 | gen/
14 |
15 | # Local configuration file (sdk path, etc)
16 | local.properties
17 |
18 | # Windows thumbnail db
19 | Thumbs.db
20 |
21 | # OSX files
22 | .DS_Store
23 |
24 | # Android Studio
25 | *.iml
26 | .idea
27 | #.idea/workspace.xml - remove # and delete .idea if it better suit your needs.
28 | .gradle
29 | build/
30 | .navigation
31 | captures/
32 | output.json
33 |
34 | #NDK
35 | obj/
36 | .externalNativeBuild
37 |
38 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Coroutines Base Project
2 |
3 | [](https://travis-ci.org/joemccann/dillinger)
4 |
5 | This project contains the following.
6 |
7 | - [Kotlin Coroutines]
8 | - [MVVM]
9 | - [Clean Architecture]
10 | - Modularization per layer (Basic).
11 | - Unit Test
12 |
13 | Thanks to [Fernando Cejas], [Paulo Sato], [Mario Lorenzo] and [Ryan M.Key] for provide a lot of great articles, videos, blogs that help incredibly to make this project. You guys really rock.
14 |
15 | ### Development
16 |
17 | Want to contribute? Great!
18 |
19 | find and fix bugs, make a PR.
20 |
21 |
22 | ### Todos
23 |
24 | - Write MORE Tests.
25 | - Write Instrumented Tests.
26 | - Prevent ciclic imports.
27 | - Improve the code.
28 |
29 | License
30 | ----
31 | ```
32 | Copyright 2019 Christopher Elias.
33 |
34 | Licensed to the Apache Software Foundation (ASF) under one or more contributor
35 | license agreements. See the NOTICE file distributed with this work for
36 | additional information regarding copyright ownership. The ASF licenses this
37 | file to you under the Apache License, Version 2.0 (the "License"); you may not
38 | use this file except in compliance with the License. You may obtain a copy of
39 | the License at
40 |
41 | http://www.apache.org/licenses/LICENSE-2.0
42 |
43 | Unless required by applicable law or agreed to in writing, software
44 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
45 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
46 | License for the specific language governing permissions and limitations under
47 | the License.
48 | ```
49 | [Fernando Cejas]:
50 | [Mario Lorenzo]:
51 | [git-repo-url]:
52 | [Paulo Sato]:
53 | [Ryan M.Key]:
54 | [Kotlin Coroutines]:
55 | [MVVM]:
56 | [Clean Architecture]:
57 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | apply from: 'buildSystem/dependencies.gradle'
3 |
4 | buildscript {
5 | ext.kotlin_version = '1.3.61'
6 | repositories {
7 | google()
8 | jcenter()
9 |
10 | }
11 | dependencies {
12 | classpath 'com.android.tools.build:gradle:3.5.3'
13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
14 | // NOTE: Do not place your application dependencies here; they belong
15 | // in the individual module build.gradle files
16 | }
17 | }
18 |
19 | allprojects {
20 | repositories {
21 | google()
22 | jcenter()
23 |
24 | }
25 | }
26 |
27 | task clean(type: Delete) {
28 | delete rootProject.buildDir
29 | }
30 |
--------------------------------------------------------------------------------
/buildSystem/dependencies.gradle:
--------------------------------------------------------------------------------
1 | ext {
2 |
3 | // Android Dx
4 | def lifecycleVersion = '2.2.0-alpha05'
5 |
6 | //Koin
7 | def koinVersion = '2.0.1'
8 |
9 | // Material Components
10 | def materialVersion = '1.0.0'
11 |
12 | // Kotlin Coroutines
13 | def coroutinesVersion = '1.3.3'
14 |
15 | // Fragment version for use FragmentContainerView instead of FrameLayout
16 | def fragmentVersion = '1.2.1'
17 |
18 |
19 | /*
20 | * Presentation dependencies
21 | */
22 | presentationDependencies = [
23 | constraintLayout : 'androidx.constraintlayout:constraintlayout:1.1.3',
24 | appCompat : 'androidx.appcompat:appcompat:1.1.0',
25 | androidCore : 'androidx.core:core-ktx:1.1.0',
26 | // ViewModel Scope and other ViewModel FEATURES
27 | viewModel : "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion",
28 | // LiveData extensions (including coroutines support)
29 | livedata: "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion",
30 | // Material Components
31 | material : "com.google.android.material:material:$materialVersion",
32 | // For Android
33 | koin : "org.koin:koin-android:$koinVersion",
34 | // For ViewModel features
35 | koinForViewModel : "org.koin:koin-androidx-viewmodel:$koinVersion",
36 | // New fragment features
37 | fragmentKotlin: "androidx.fragment:fragment-ktx:$fragmentVersion"
38 | ]
39 |
40 | presentationTestDependencies = [
41 | junit : 'junit:junit:4.12',
42 | androidxArchcore : 'androidx.arch.core:core-testing:2.1.0'
43 | ]
44 |
45 | presentationTestImplementationDependencies = [
46 | androidXRunner : 'androidx.test:runner:1.2.0',
47 | androidXEspresso: 'androidx.test.espresso:espresso-core:3.2.0'
48 | ]
49 |
50 | /*
51 | * Domain Dependencies
52 | */
53 | domainDependencies = [
54 | coroutinesCore : "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion",
55 | coroutinesAndroid : "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion",
56 | koin : "org.koin:koin-android:$koinVersion",
57 | ]
58 |
59 | domainTestDependencies = [
60 | junit : 'junit:junit:4.12',
61 | androidXcore : 'androidx.arch.core:core-testing:2.1.0'
62 | ]
63 |
64 |
65 | def retrofitVersion = "2.7.1"
66 | def roomVersion = '2.1.0'
67 | def loggingInterceptorVersion = '4.2.1'// Requests logging
68 | roomDependencies = [
69 | roomCompiler : "androidx.room:room-compiler:$roomVersion"
70 | ]
71 |
72 | /*
73 | * Data Dependencies
74 | */
75 | dataDependencies = [
76 | koin : "org.koin:koin-android:$koinVersion",
77 | retrofit : "com.squareup.retrofit2:retrofit:$retrofitVersion",
78 | loggingInterceptor : "com.squareup.okhttp3:logging-interceptor:$loggingInterceptorVersion",
79 | gsonConverter : "com.squareup.retrofit2:converter-gson:$retrofitVersion",
80 | roomDataBase : "androidx.room:room-runtime:$roomVersion",
81 | coroutinesCore : "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion",
82 | coroutinesAndroid : "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
83 | ]
84 |
85 | dataTestDependencies = [
86 | junit : 'junit:junit:4.12',
87 | androidXcore : 'androidx.arch.core:core-testing:2.1.0',
88 | coroutinesTest: "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion",
89 | koinTest: "org.koin:koin-test:$koinVersion",
90 | jsonForJVM: 'org.json:json:20140107'
91 | ]
92 | }
--------------------------------------------------------------------------------
/data/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/data/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 |
3 | apply plugin: 'kotlin-android'
4 |
5 | apply plugin: 'kotlin-android-extensions'
6 |
7 | apply plugin: 'kotlin-kapt'
8 |
9 | android {
10 | compileSdkVersion 29
11 | buildToolsVersion "29.0.2"
12 |
13 |
14 | defaultConfig {
15 | minSdkVersion 22
16 | targetSdkVersion 29
17 | versionCode 1
18 | versionName "1.0"
19 |
20 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
21 | consumerProguardFiles 'consumer-rules.pro'
22 | }
23 |
24 | buildTypes {
25 | release {
26 | minifyEnabled false
27 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
28 | //Change this for release URL
29 | buildConfigField("String", "BaseURL", "\"https://jsonplaceholder.typicode.com/\"")
30 | }
31 | debug {
32 | minifyEnabled false
33 | debuggable true
34 | buildConfigField("String", "BaseURL", "\"https://jsonplaceholder.typicode.com/\"")
35 | }
36 | qa {
37 | minifyEnabled false
38 | debuggable true
39 | //Change this for qa URL
40 | buildConfigField("String", "BaseURL", "\"https://jsonplaceholder.typicode.com/\"")
41 | }
42 | }
43 |
44 | kotlinOptions {
45 | jvmTarget = "1.8"
46 | }
47 |
48 | lintOptions {
49 | abortOnError false
50 | }
51 |
52 | }
53 |
54 | dependencies {
55 | implementation fileTree(dir: 'libs', include: ['*.jar'])
56 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
57 | implementation project(":domain")
58 | api dataDependencies.values()
59 | kapt rootProject.ext.roomDependencies.roomCompiler
60 | testImplementation dataTestDependencies.values()
61 | }
62 |
--------------------------------------------------------------------------------
/data/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChristopherME/base-project/e39ed2dae17c7043cd494052858577a54ac03ad4/data/consumer-rules.pro
--------------------------------------------------------------------------------
/data/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 |
--------------------------------------------------------------------------------
/data/src/androidTest/java/com/christopher/elias/data/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.data
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.christopher.elias.data.test", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/data/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/data/src/main/java/com/christopher/elias/data/di/MapperDataModule.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.data.di
2 |
3 | import com.christopher.elias.data.network.mapper.TodoMapper
4 | import com.christopher.elias.data.network.mapper.TodoMapperImpl
5 | import org.koin.dsl.module
6 | /**
7 | * Created by Christopher Elias on 27/01/2020.
8 | * christopher.mike.96@gmail.com
9 | *
10 | * Peru Apps
11 | * Lima, Peru.
12 | **/
13 |
14 | val mapperDataModule = module {
15 | single{ TodoMapperImpl() }
16 | }
17 |
--------------------------------------------------------------------------------
/data/src/main/java/com/christopher/elias/data/di/NetworkModule.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.data.di
2 |
3 | import com.christopher.elias.data.BuildConfig
4 | import com.christopher.elias.data.network.end_points.EndPoints
5 | import com.christopher.elias.data.network.end_points.EndPointsImpl
6 | import com.christopher.elias.data.network.end_points.EndPointsService
7 | import com.christopher.elias.data.network.end_points.SupportInterceptor
8 | import com.christopher.elias.data.network.utils.ConnectionUtils
9 | import com.christopher.elias.data.network.utils.ConnectionUtilsImpl
10 | import okhttp3.OkHttpClient
11 | import okhttp3.logging.HttpLoggingInterceptor
12 | import org.koin.android.ext.koin.androidContext
13 | import org.koin.dsl.module
14 | import retrofit2.Retrofit
15 | import retrofit2.converter.gson.GsonConverterFactory
16 | import java.util.concurrent.TimeUnit
17 |
18 | /**
19 | * Created by Christopher Elias on 27/01/2020.
20 | * christopher.mike.96@gmail.com
21 | *
22 | * Peru Apps
23 | * Lima, Peru.
24 | **/
25 |
26 |
27 | val networkModule = module {
28 | single {
29 | ConnectionUtilsImpl(
30 | androidContext()
31 | )
32 | }
33 | factory { SupportInterceptor(get()) }
34 | single { provideOkHttpClient(get()) }
35 | single { provideApi(get()) }
36 | single { provideRetrofit(get()) }
37 | single { EndPointsImpl(get(), get()) }
38 | }
39 |
40 | fun provideRetrofit(okHttpClient: OkHttpClient) : Retrofit {
41 | return Retrofit.Builder()
42 | .baseUrl(BuildConfig.BaseURL)
43 | .addConverterFactory(GsonConverterFactory.create())
44 | .client(okHttpClient)
45 | .build()
46 | }
47 |
48 | fun provideOkHttpClient(authInterceptor: SupportInterceptor): OkHttpClient {
49 | val interceptor = HttpLoggingInterceptor()
50 | interceptor.level = HttpLoggingInterceptor.Level.BODY
51 | val builder = OkHttpClient.Builder()
52 | builder.addInterceptor(interceptor)
53 | .connectTimeout(30, TimeUnit.SECONDS)
54 | .readTimeout(30, TimeUnit.SECONDS)
55 | .addInterceptor(authInterceptor)
56 | return builder.build()
57 | }
58 |
59 | fun provideApi(retrofit: Retrofit): EndPointsService = retrofit.create(EndPointsService::class.java)
--------------------------------------------------------------------------------
/data/src/main/java/com/christopher/elias/data/di/PreferencesModule.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.data.di
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import android.content.SharedPreferences
6 | import com.christopher.elias.data.preferences.SecurePreferences
7 | import com.christopher.elias.data.preferences.SecurePreferencesImpl
8 | import org.koin.android.ext.koin.androidApplication
9 | import org.koin.core.qualifier.named
10 | import org.koin.dsl.module
11 |
12 | /**
13 | * Created by Christopher Elias on 27/01/2020.
14 | * christopher.mike.96@gmail.com
15 | *
16 | * Peru Apps
17 | * Lima, Peru.
18 | **/
19 |
20 | /**
21 | * Multiple preferences files
22 | * more information: https://medium.com/@prus.piotr/multiple-shared-preferences-how-to-manage-them-with-koin-di-dbebeb95b121
23 | */
24 | val preferencesModule = module {
25 | single(named("securePrefs")) { provideSecurePreferences(androidApplication()) }
26 | single { SecurePreferencesImpl(get(named("securePrefs"))) }
27 | }
28 |
29 | private const val SECURE_PREFS_FILE_KEY = "com.christopher.elias.secure_preferences"
30 |
31 | const val PREF_KEY_ACCESS_TOKEN = "key_user_access_token"
32 | const val PREF_KEY_USER_NAME = "key_user_name"
33 |
34 | private fun provideSecurePreferences(app: Application): SharedPreferences =
35 | app.getSharedPreferences(SECURE_PREFS_FILE_KEY, Context.MODE_PRIVATE)
--------------------------------------------------------------------------------
/data/src/main/java/com/christopher/elias/data/di/RepositoryModule.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.data.di
2 |
3 | import com.christopher.elias.data.repository.TodoRepositoryImpl
4 | import com.christopher.elias.domain.repository.TodoRepository
5 | import org.koin.dsl.module
6 |
7 |
8 | /**
9 | * Created by Christopher Elias on 27/01/2020.
10 | * christopher.mike.96@gmail.com
11 | *
12 | * Peru Apps
13 | * Lima, Peru.
14 | **/
15 |
16 | val repositoryModule = module {
17 | single { TodoRepositoryImpl(get(), get()) }
18 | }
--------------------------------------------------------------------------------
/data/src/main/java/com/christopher/elias/data/network/end_points/EndPoints.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.data.network.end_points
2 |
3 | import com.christopher.elias.data.network.response.TodoResponse
4 | import com.christopher.elias.domain.entity.Either
5 | import com.christopher.elias.domain.entity.Failure
6 | import retrofit2.Response
7 |
8 | /**
9 | * Created by Christopher Elias on 27/01/2020.
10 | * christopher.mike.96@gmail.com
11 | *
12 | * Peru Apps
13 | * Lima, Peru.
14 | **/
15 |
16 | /**
17 | * We are using [Response] retrofit object as wrapper for every POJO we expect.
18 | */
19 | interface EndPoints {
20 |
21 | suspend fun getTodo(todoId: Int) : Either
22 |
23 | suspend fun getTodos() : Either>
24 | }
--------------------------------------------------------------------------------
/data/src/main/java/com/christopher/elias/data/network/end_points/EndPointsImpl.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.data.network.end_points
2 |
3 | import com.christopher.elias.data.network.response.TodoResponse
4 | import com.christopher.elias.data.network.utils.ConnectionUtils
5 | import com.christopher.elias.domain.entity.Either
6 | import com.christopher.elias.domain.entity.Either.Right
7 | import com.christopher.elias.domain.entity.Either.Left
8 | import com.christopher.elias.domain.entity.Failure
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.withContext
11 | import org.json.JSONObject
12 | import retrofit2.Response
13 | import java.net.SocketTimeoutException
14 | import javax.net.ssl.SSLException
15 | import javax.net.ssl.SSLHandshakeException
16 |
17 | /**
18 | * Created by Christopher Elias on 27/01/2020.
19 | * christopher.mike.96@gmail.com
20 | *
21 | * Peru Apps
22 | * Lima, Peru.
23 | **/
24 | class EndPointsImpl(private val endPoints: EndPointsService,
25 | private val networkUtils: ConnectionUtils) : EndPoints {
26 |
27 | companion object {
28 | private const val KEY_CODE = "code"
29 | private const val KEY_MESSAGE = "message"
30 | }
31 |
32 | override suspend fun getTodos(): Either>
33 | = callService { endPoints.getTodos() }
34 |
35 | override suspend fun getTodo(todoId: Int): Either
36 | = callService { endPoints.getTodo(todoId) }
37 |
38 |
39 | /**
40 | * Invoke the retrofit endpoint service in IO Context and after the response has been invoked
41 | * verify if its successful and if has a valid body.
42 | */
43 | private suspend inline fun callService(crossinline retrofitCall: suspend ()-> Response) : Either {
44 | return when(networkUtils.isNetworkAvailable()) {
45 | true -> {
46 | try {
47 | withContext(Dispatchers.IO) {
48 | val response = retrofitCall.invoke()
49 | if(response.isSuccessful && response.body() != null) {
50 | return@withContext Right(response.body()!!)
51 | } else {
52 | return@withContext Left(getErrorMessageFromServer(response.errorBody()?.string()))
53 | }
54 | }
55 | } catch (e: Exception) {
56 | return Left(parseException(e))
57 | }
58 | }
59 | false -> Left(Failure.NoNetworkDetected)
60 | }
61 | }
62 |
63 | /**
64 | * Parse Server Error to [Failure.ServerBodyError] if [errorBody] [isServerErrorValid].
65 | * @return [Failure] object.
66 | */
67 | private suspend fun getErrorMessageFromServer(errorBody: String?) : Failure {
68 | return if (errorBody != null) {
69 | return withContext(Dispatchers.IO) {
70 | val serverErrorJson = JSONObject(errorBody)
71 | when {
72 | isServerErrorValid(serverErrorJson.toString()) -> {
73 | val code = serverErrorJson[KEY_CODE].toString().toInt()
74 | if (code == 401 || code == 403) {
75 | return@withContext Failure.UnauthorizedOrForbidden
76 | } else {
77 | return@withContext Failure.ServerBodyError(code, serverErrorJson[KEY_MESSAGE].toString())
78 | }
79 | }
80 | serverErrorJson.toString().contains("\"$KEY_MESSAGE\"") -> {
81 | return@withContext Failure.ServiceUncaughtFailure(serverErrorJson[KEY_MESSAGE].toString())
82 | }
83 | else -> return@withContext Failure.None
84 | }
85 | }
86 | } else {
87 | //No error body was found.
88 | Failure.None
89 | }
90 | }
91 |
92 | private fun isServerErrorValid(error: String) : Boolean{
93 | return error.contains("\"$KEY_CODE\"") && error.contains("\"$KEY_MESSAGE\"")
94 | }
95 |
96 | private fun parseException(throwable: Throwable) : Failure {
97 | return when(throwable) {
98 | is SocketTimeoutException -> Failure.TimeOut
99 | is SSLException -> Failure.NetworkConnectionLostSuddenly
100 | is SSLHandshakeException -> Failure.SSLError
101 | else -> Failure.ServiceUncaughtFailure(throwable.message?:"Service response doesn't match with response object.")
102 | }
103 | }
104 | }
--------------------------------------------------------------------------------
/data/src/main/java/com/christopher/elias/data/network/end_points/EndPointsService.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.data.network.end_points
2 |
3 | import com.christopher.elias.data.network.response.TodoResponse
4 | import retrofit2.Response
5 | import retrofit2.http.GET
6 | import retrofit2.http.Path
7 |
8 | /**
9 | * Created by Christopher Elias on 27/01/2020.
10 | * christopher.mike.96@gmail.com
11 | *
12 | * Peru Apps
13 | * Lima, Peru.
14 | **/
15 | interface EndPointsService {
16 |
17 | @GET("todos")
18 | suspend fun getTodos() : Response>
19 |
20 | @GET("todos/{id}")
21 | suspend fun getTodo(@Path("id") todoId: Int) : Response
22 | }
--------------------------------------------------------------------------------
/data/src/main/java/com/christopher/elias/data/network/end_points/SupportInterceptor.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.data.network.end_points
2 |
3 | import com.christopher.elias.data.preferences.SecurePreferences
4 | import okhttp3.Interceptor
5 | import okhttp3.Response
6 |
7 | /**
8 | * Created by Christopher Elias on 27/01/2020.
9 | * christopher.mike.96@gmail.com
10 | *
11 | * Peru Apps
12 | * Lima, Peru.
13 | **/
14 | class SupportInterceptor(private val preferences : SecurePreferences) : Interceptor {
15 |
16 | /**
17 | * Interceptor class for setting of the headers for every request
18 | */
19 | override fun intercept(chain: Interceptor.Chain): Response {
20 | var request = chain.request()
21 | request = request.newBuilder()
22 | .addHeader("Content-Type", "application/json")
23 | .addHeader("Accept", "application/json")
24 | .addHeader("Authorization", preferences.getAccessToken())
25 | .build()
26 | return chain.proceed(request)
27 | }
28 | }
--------------------------------------------------------------------------------
/data/src/main/java/com/christopher/elias/data/network/mapper/TodoMapper.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.data.network.mapper
2 |
3 | import com.christopher.elias.data.network.response.TodoResponse
4 | import com.christopher.elias.domain.entity.TodoEntity
5 |
6 | /**
7 | * Created by Christopher Elias on 27/01/2020.
8 | * christopher.mike.96@gmail.com
9 | *
10 | * Peru Apps
11 | * Lima, Peru.
12 | **/
13 | interface TodoMapper {
14 |
15 | suspend fun todoDataToDomain(todo: TodoResponse) : TodoEntity
16 |
17 | suspend fun todoListDataToDomain(todos: List) : List
18 | }
--------------------------------------------------------------------------------
/data/src/main/java/com/christopher/elias/data/network/mapper/TodoMapperImpl.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.data.network.mapper
2 |
3 | import com.christopher.elias.data.network.response.TodoResponse
4 | import com.christopher.elias.domain.entity.TodoEntity
5 |
6 | /**
7 | * Created by Christopher Elias on 27/01/2020.
8 | * christopher.mike.96@gmail.com
9 | *
10 | * Peru Apps
11 | * Lima, Peru.
12 | **/
13 | class TodoMapperImpl : TodoMapper {
14 |
15 | override suspend fun todoDataToDomain(todo: TodoResponse): TodoEntity {
16 | return TodoEntity(todo.id,
17 | todo.userId,
18 | todo.title,
19 | todo.completed)
20 | }
21 |
22 | override suspend fun todoListDataToDomain(todos: List): List {
23 | return todos.map { TodoEntity(it.id, it.userId, it.title, it.completed) }
24 | }
25 | }
--------------------------------------------------------------------------------
/data/src/main/java/com/christopher/elias/data/network/response/TodoResponse.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.data.network.response
2 |
3 | /**
4 | * Created by Christopher Elias on 27/01/2020.
5 | * christopher.mike.96@gmail.com
6 | *
7 | * Peru Apps
8 | * Lima, Peru.
9 | **/
10 | data class TodoResponse(val id: Int,
11 | val userId: Int,
12 | val title: String,
13 | val completed: Boolean)
--------------------------------------------------------------------------------
/data/src/main/java/com/christopher/elias/data/network/utils/ConnectionUtils.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.data.network.utils
2 |
3 | /**
4 | * Created by Christopher Elias on 27/01/2020.
5 | * christopher.mike.96@gmail.com
6 | *
7 | * Peru Apps
8 | * Lima, Peru.
9 | **/
10 | interface ConnectionUtils {
11 | fun isNetworkAvailable() : Boolean
12 | }
--------------------------------------------------------------------------------
/data/src/main/java/com/christopher/elias/data/network/utils/ConnectionUtilsImpl.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.data.network.utils
2 |
3 | import android.content.Context
4 | import android.net.ConnectivityManager
5 | import android.net.NetworkCapabilities
6 | import android.os.Build
7 | import android.util.Log
8 | import com.christopher.elias.data.network.utils.ConnectionUtils
9 | import java.lang.Exception
10 |
11 | /**
12 | * Created by Christopher Elias on 27/01/2020.
13 | * christopher.mike.96@gmail.com
14 | *
15 | * Peru Apps
16 | * Lima, Peru.
17 | **/
18 | class ConnectionUtilsImpl(private val applicationContext: Context) :
19 | ConnectionUtils {
20 | override fun isNetworkAvailable(): Boolean {
21 | try {
22 | val connectivityManager = applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
23 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
24 | val nw = connectivityManager.activeNetwork ?: return false
25 | val actNw = connectivityManager.getNetworkCapabilities(nw) ?: return false
26 | return when {
27 | actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
28 | actNw.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
29 | //for other device how are able to connect with Ethernet
30 | actNw.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
31 | else -> false
32 | }
33 | } else {
34 | val nwInfo = connectivityManager.activeNetworkInfo ?: return false
35 | return nwInfo.isConnected
36 | }
37 | } catch (e: Exception) {
38 | Log.e("NetworkUtils", "Exception happened: ${e.message}")
39 | return false
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/data/src/main/java/com/christopher/elias/data/preferences/SecurePreferences.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.data.preferences
2 |
3 | /**
4 | * Created by Christopher Elias on 30/09/2019.
5 | * christopher.mike.96@gmail.com
6 | *
7 | * Peru Apps
8 | * Lima, Peru.
9 | **/
10 | interface SecurePreferences {
11 | fun saveLogInInfo(token: String, name: String)
12 | fun getAccessToken() : String
13 | fun getClientName(): String
14 | /**
15 | * Clear all values from pref file.
16 | */
17 | fun logOut()
18 | }
--------------------------------------------------------------------------------
/data/src/main/java/com/christopher/elias/data/preferences/SecurePreferencesImpl.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.data.preferences
2 |
3 | import android.content.SharedPreferences
4 | import com.christopher.elias.data.di.PREF_KEY_ACCESS_TOKEN
5 | import com.christopher.elias.data.di.PREF_KEY_USER_NAME
6 |
7 | /**
8 | * Created by Christopher Elias on 30/09/2019.
9 | * christopher.mike.96@gmail.com
10 | *
11 | * Peru Apps
12 | * Lima, Peru.
13 | **/
14 |
15 | class SecurePreferencesImpl(private val prefs : SharedPreferences) : SecurePreferences {
16 |
17 | override fun saveLogInInfo(token: String, name: String) {
18 | prefs.edit().putString(PREF_KEY_ACCESS_TOKEN, "Bearer $token").apply()
19 | prefs.edit().putString(PREF_KEY_USER_NAME, name).apply()
20 | }
21 |
22 | override fun getAccessToken() = prefs.getString(PREF_KEY_ACCESS_TOKEN, "")?:""
23 |
24 | override fun getClientName() = prefs.getString(PREF_KEY_USER_NAME, "")?:""
25 |
26 | override fun logOut() = prefs.edit().clear().apply()
27 | }
--------------------------------------------------------------------------------
/data/src/main/java/com/christopher/elias/data/repository/TodoRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.data.repository
2 |
3 | import com.christopher.elias.data.network.end_points.EndPoints
4 | import com.christopher.elias.data.network.mapper.TodoMapper
5 | import com.christopher.elias.domain.entity.Either
6 | import com.christopher.elias.domain.entity.Failure
7 | import com.christopher.elias.domain.entity.TodoEntity
8 | import com.christopher.elias.domain.repository.TodoRepository
9 |
10 | /**
11 | * Created by Christopher Elias on 27/01/2020.
12 | * christopher.mike.96@gmail.com
13 | *
14 | * Peru Apps
15 | * Lima, Peru.
16 | **/
17 | class TodoRepositoryImpl(private val endPoints: EndPoints,
18 | private val mapper: TodoMapper) : TodoRepository {
19 |
20 | override suspend fun getAllTodos(): Either> {
21 | return when(val response = endPoints.getTodos()){
22 | is Either.Right -> Either.Right(mapper.todoListDataToDomain(response.b))
23 | is Either.Left -> Either.Left(response.a)
24 | }
25 | }
26 |
27 | override suspend fun getTodo(id: Int): Either {
28 | return when(val response = endPoints.getTodo(id)){
29 | is Either.Right -> Either.Right(mapper.todoDataToDomain(response.b))
30 | is Either.Left -> Either.Left(response.a)
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/data/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | data
3 |
4 |
--------------------------------------------------------------------------------
/data/src/test/java/com/christopher/elias/data/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.data
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 |
--------------------------------------------------------------------------------
/data/src/test/java/com/christopher/elias/data/base/BaseUseCaseUniTest.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.data.base
2 |
3 | import com.christopher.elias.data.di.mapperDataModule
4 | import com.christopher.elias.data.di.repositoryModule
5 | import com.christopher.elias.data.mock.fakeNetworkModule
6 | import com.christopher.elias.data.mock.SecurePreferencesImplMocked
7 | import com.christopher.elias.data.preferences.SecurePreferences
8 | import com.christopher.elias.domain.di.useCasesModule
9 | import com.christopher.elias.domain.entity.Failure
10 | import org.junit.Before
11 | import org.koin.core.context.startKoin
12 | import org.koin.core.context.stopKoin
13 | import org.koin.dsl.bind
14 | import org.koin.dsl.module
15 | import org.koin.test.AutoCloseKoinTest
16 | import java.net.SocketTimeoutException
17 | import javax.net.ssl.SSLException
18 | import javax.net.ssl.SSLHandshakeException
19 |
20 | /**
21 | * Created by Christopher Elias on 27/01/2020.
22 | * christopher.mike.96@gmail.com
23 | *
24 | * Peru Apps
25 | * Lima, Peru.
26 | **/
27 | abstract class BaseUseCaseUniTest : AutoCloseKoinTest() {
28 |
29 | @Before
30 | fun before() {
31 | stopKoin() // to remove 'A Koin Application has already been started' exception at the beginning of the test.
32 | val fakePreferencesModule = module {
33 | single { SecurePreferencesImplMocked() } bind SecurePreferences::class
34 | }
35 | startKoin {
36 | modules(arrayListOf(fakePreferencesModule,
37 | fakeNetworkModule, mapperDataModule, repositoryModule, useCasesModule))
38 | }
39 | }
40 |
41 |
42 | protected fun printUseCaseSuccessObject(someDataObject: T) {
43 | println("Use case invocation: Success!")
44 | println(someDataObject)
45 | }
46 |
47 | protected fun printUseCaseSuccessList(someList: List) {
48 | println("Use case invocation: Success!")
49 | println("List size: ${someList.size}")
50 | println("List content:")
51 | someList.forEach { println(it) }
52 | }
53 |
54 | protected fun printUseCaseFailure(error: Failure) {
55 | println("Use case invocation: Failure :(")
56 | when(error) {
57 | is Failure.None -> throw Exception("Ups! Something went REALLY wrong. Contact support.")
58 | is Failure.NetworkConnectionLostSuddenly -> throw SSLException("The internet connection is suddenly lost.")
59 | is Failure.SSLError -> throw SSLHandshakeException("Verify the SSL.")
60 | is Failure.TimeOut -> throw SocketTimeoutException("Time out exception. The server took too long to answer.")
61 | is Failure.UnauthorizedOrForbidden -> throw Exception("Force a log out. server throw 401 - 403")
62 | is Failure.ServerBodyError -> throw Exception("Service Error Response (Error Body) -> CODE: ${error.code} - MESSAGE: ${error.message}")
63 | is Failure.DataToDomainMapperFailure -> throw IllegalArgumentException("DataToDomainMapperFailure: ${error.mapperException}")
64 | is Failure.ServiceUncaughtFailure -> throw Exception("500 - ServiceUncaughtFailure: ${error.uncaughtFailureMessage}")
65 | }
66 | }
67 | }
--------------------------------------------------------------------------------
/data/src/test/java/com/christopher/elias/data/features/TodoUseCasesUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.data.features
2 |
3 | import com.christopher.elias.data.base.BaseUseCaseUniTest
4 | import com.christopher.elias.domain.usecase.GetTodoUseCase
5 | import com.christopher.elias.domain.usecase.GetTodosUseCase
6 | import kotlinx.coroutines.runBlocking
7 | import org.junit.Test
8 | import org.koin.test.inject
9 |
10 | /**
11 | * Created by Christopher Elias on 27/01/2020.
12 | * christopher.mike.96@gmail.com
13 | *
14 | * Peru Apps
15 | * Lima, Peru.
16 | **/
17 | class TodoUseCasesUnitTest : BaseUseCaseUniTest() {
18 |
19 | private val getTodoUseCase by inject()
20 | private val getTodosListUseCase by inject()
21 |
22 | @Test
23 | fun `GET TODO LIST`() = runBlocking {
24 | getTodosListUseCase.invoke(this, "") {
25 | it.either(::printUseCaseFailure, ::printUseCaseSuccessList)
26 | }
27 | }
28 |
29 | @Test
30 | fun `GET TODO object of id 1`() = runBlocking {
31 | val params = GetTodoUseCase.Params(1)
32 | getTodoUseCase.invoke(this, params) {
33 | it.either(::printUseCaseFailure, ::printUseCaseSuccessObject)
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/data/src/test/java/com/christopher/elias/data/mock/ConnectionUtilsImplMocked.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.data.mock
2 |
3 | import com.christopher.elias.data.network.utils.ConnectionUtils
4 |
5 | /**
6 | * Created by Christopher Elias on 26/10/2019.
7 | * christopher.mike.96@gmail.com
8 | *
9 | * Peru Apps
10 | * Lima, Peru.
11 | **/
12 | /**
13 | * This class is just for "MOCK" the result of wifi connection and
14 | * return always true.
15 | * This class is just for UnitTest of the use cases.
16 | */
17 | class ConnectionUtilsImplMocked : ConnectionUtils {
18 | override fun isNetworkAvailable() = true
19 | }
--------------------------------------------------------------------------------
/data/src/test/java/com/christopher/elias/data/mock/FakeNetworkModule.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.data.mock
2 |
3 | import com.christopher.elias.data.BuildConfig
4 | import com.christopher.elias.data.network.end_points.EndPoints
5 | import com.christopher.elias.data.network.end_points.EndPointsImpl
6 | import com.christopher.elias.data.network.end_points.EndPointsService
7 | import com.christopher.elias.data.network.end_points.SupportInterceptor
8 | import com.christopher.elias.data.network.utils.ConnectionUtils
9 | import okhttp3.OkHttpClient
10 | import okhttp3.logging.HttpLoggingInterceptor
11 | import org.koin.dsl.bind
12 | import org.koin.dsl.module
13 | import retrofit2.Retrofit
14 | import retrofit2.converter.gson.GsonConverterFactory
15 | import java.util.concurrent.TimeUnit
16 |
17 | /**
18 | * Created by Christopher Elias on 27/01/2020.
19 | * christopher.mike.96@gmail.com
20 | *
21 | * Peru Apps
22 | * Lima, Peru.
23 | **/
24 |
25 |
26 |
27 | val fakeNetworkModule = module {
28 | single { ConnectionUtilsImplMocked() } bind ConnectionUtils::class
29 | factory { SupportInterceptor(get()) }
30 | single { provideOkHttpClient(get()) }
31 | single { provideApi(get()) }
32 | single { provideRetrofit(get()) }
33 | single { EndPointsImpl(get(), get()) }
34 | }
35 |
36 | fun provideRetrofit(okHttpClient: OkHttpClient) : Retrofit {
37 | return Retrofit.Builder()
38 | .baseUrl(BuildConfig.BaseURL)
39 | .addConverterFactory(GsonConverterFactory.create())
40 | .client(okHttpClient)
41 | .build()
42 | }
43 |
44 | fun provideOkHttpClient(authInterceptor: SupportInterceptor): OkHttpClient {
45 | val interceptor = HttpLoggingInterceptor()
46 | interceptor.level = HttpLoggingInterceptor.Level.BODY
47 | val builder = OkHttpClient.Builder()
48 | builder.addInterceptor(interceptor)
49 | .connectTimeout(30, TimeUnit.SECONDS)
50 | .readTimeout(30, TimeUnit.SECONDS)
51 | .addInterceptor(authInterceptor)
52 | return builder.build()
53 | }
54 |
55 | fun provideApi(retrofit: Retrofit): EndPointsService = retrofit.create(EndPointsService::class.java)
--------------------------------------------------------------------------------
/data/src/test/java/com/christopher/elias/data/mock/SecurePreferencesImplMocked.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.data.mock
2 |
3 | import com.christopher.elias.data.preferences.SecurePreferences
4 |
5 | /**
6 | * Created by Christopher Elias on 15/11/2019.
7 | * christopher.mike.96@gmail.com
8 | *
9 | * Peru Apps
10 | * Lima, Peru.
11 | **/
12 | class SecurePreferencesImplMocked(): SecurePreferences {
13 |
14 | override fun saveLogInInfo(token: String, name: String) {
15 | /*
16 | * Do something
17 | */
18 | println("Saving Login Info")
19 | println("Name: $name")
20 | println("Token: $token")
21 | }
22 |
23 | override fun getAccessToken(): String {
24 | return ""
25 | }
26 |
27 | override fun getClientName(): String {
28 | return "Get Client Name"
29 | }
30 |
31 | override fun logOut() {
32 | /*
33 | * Clear all prefs
34 | */
35 | println("log out.")
36 | }
37 | }
--------------------------------------------------------------------------------
/domain/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/domain/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'kotlin'
2 |
3 | dependencies {
4 | implementation fileTree(dir: 'libs', include: ['*.jar'])
5 | def domainDependencies = rootProject.ext.domainDependencies
6 | implementation domainDependencies.values()
7 | }
8 |
9 | sourceCompatibility = "7"
10 | targetCompatibility = "7"
11 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/christopher/elias/domain/di/UseCaseModule.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.domain.di
2 |
3 | import com.christopher.elias.domain.usecase.GetTodoUseCase
4 | import com.christopher.elias.domain.usecase.GetTodosUseCase
5 | import org.koin.dsl.module
6 |
7 | /**
8 | * Created by Christopher Elias on 27/01/2020.
9 | * christopher.mike.96@gmail.com
10 | *
11 | * Peru Apps
12 | * Lima, Peru.
13 | **/
14 |
15 |
16 | val useCasesModule = module {
17 | factory { GetTodoUseCase(get()) }
18 | factory { GetTodosUseCase(get()) }
19 | }
--------------------------------------------------------------------------------
/domain/src/main/java/com/christopher/elias/domain/entity/Either.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.domain.entity
2 |
3 | /**
4 | * Created by Christopher Elias on 27/01/2020.
5 | * christopher.mike.96@gmail.com
6 | *
7 | * Peru Apps
8 | * Lima, Peru.
9 | **/
10 |
11 | /**
12 | * Represents a value of one of two possible types (a disjoint union).
13 | * Instances of [Either] are either an instance of [Left] or [Right].
14 | * FP Convention dictates that [Left] is used for "failure"
15 | * and [Right] is used for "success".
16 | *
17 | * @see Left
18 | * @see Right
19 | *
20 | * https://danielwestheide.com/blog/the-neophytes-guide-to-scala-part-7-the-either-type/
21 | */
22 | sealed class Either {
23 | /** * Represents the left side of [Either] class which by convention is a "Failure". */
24 | data class Left(val a: L) : Either()
25 | /** * Represents the right side of [Either] class which by convention is a "Success". */
26 | data class Right(val b: R) : Either()
27 |
28 | val isRight get() = this is Right
29 | val isLeft get() = this is Left
30 |
31 | fun left(a: L) = Left(a)
32 | fun right(b: R) = Right(b)
33 |
34 | fun either(fnL: (L) -> Any, fnR: (R) -> Any): Any =
35 | when (this) {
36 | is Left -> fnL(a)
37 | is Right -> fnR(b)
38 | }
39 | }
--------------------------------------------------------------------------------
/domain/src/main/java/com/christopher/elias/domain/entity/Failure.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.domain.entity
2 |
3 | /**
4 | * Created by Christopher Elias on 27/01/2020.
5 | * christopher.mike.96@gmail.com
6 | *
7 | * Peru Apps
8 | * Lima, Peru.
9 | **/
10 |
11 | sealed class Failure {
12 |
13 | /** When service return 401 or 403 this will force the client to log out of the app.*/
14 | object UnauthorizedOrForbidden : Failure()
15 |
16 | /** Weird and strange error that we don´t know the cause.*/
17 | object None : Failure()
18 |
19 | /** When suddenly the connection is lost.*/
20 | object NetworkConnectionLostSuddenly : Failure()
21 |
22 | /** When there is no internet network detected.*/
23 | object NoNetworkDetected : Failure()
24 |
25 | object SSLError: Failure()
26 |
27 | /** When service is taking to long on return the response.*/
28 | object TimeOut: Failure()
29 |
30 | /** This class is for feature specific failures.*/
31 | data class ServiceUncaughtFailure(val uncaughtFailureMessage: String) : Failure()
32 |
33 | /** This class is for feature specific SERVICE ERROR BODY RESPONSE.*/
34 | data class ServerBodyError(val code: Int, val message: String) : Failure()
35 |
36 | /** This class is for feature specific DATA -> DOMAIN MAPPERS exceptions.*/
37 | data class DataToDomainMapperFailure(val mapperException: String?) : Failure()
38 | }
--------------------------------------------------------------------------------
/domain/src/main/java/com/christopher/elias/domain/entity/TodoEntity.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.domain.entity
2 |
3 | /**
4 | * Created by Christopher Elias on 27/01/2020.
5 | * christopher.mike.96@gmail.com
6 | *
7 | * Peru Apps
8 | * Lima, Peru.
9 | **/
10 | data class TodoEntity(val id: Int,
11 | val userId: Int,
12 | val title: String,
13 | val completed: Boolean)
--------------------------------------------------------------------------------
/domain/src/main/java/com/christopher/elias/domain/repository/TodoRepository.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.domain.repository
2 |
3 | import com.christopher.elias.domain.entity.Either
4 | import com.christopher.elias.domain.entity.Failure
5 | import com.christopher.elias.domain.entity.TodoEntity
6 |
7 | /**
8 | * Created by Christopher Elias on 27/01/2020.
9 | * christopher.mike.96@gmail.com
10 | *
11 | * Peru Apps
12 | * Lima, Peru.
13 | **/
14 | interface TodoRepository {
15 |
16 | suspend fun getTodo(id: Int) : Either
17 |
18 | suspend fun getAllTodos() : Either>
19 | }
--------------------------------------------------------------------------------
/domain/src/main/java/com/christopher/elias/domain/usecase/BaseUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.domain.usecase
2 |
3 | import com.christopher.elias.domain.entity.Either
4 | import com.christopher.elias.domain.entity.Failure
5 | import kotlinx.coroutines.*
6 |
7 | /**
8 | * Created by Christopher Elias on 27/01/2020.
9 | * christopher.mike.96@gmail.com
10 | *
11 | * Peru Apps
12 | * Lima, Peru.
13 | **/
14 | /**
15 | * By convention each [BaseUseCase] implementation will execute its job in a background thread
16 | * (kotlin coroutine) and will post the result in the UI thread.
17 | */
18 | abstract class BaseUseCase where Type : Any {
19 |
20 | abstract suspend fun run(params: Params): Either
21 |
22 | open operator fun invoke(
23 | scope: CoroutineScope,
24 | params: Params,
25 | onResult: (Either) -> Unit = {}
26 | ) {
27 | /*
28 | * Credits to Paulo.
29 | * https://proandroiddev.com/i-exchanged-rxjava-for-coroutines-in-my-android-application-why-you-probably-should-do-the-same-5526dfb38d0e#cf27
30 | *
31 | * Basically. All exceptions that could occur while invoking the service will be handled on EndPointImpl because
32 | * the response.call function is wrapped inside a try catch block. If something else occur outside of that block (like Data -> Domain mappers)
33 | * those exceptions will be caught here. On the Launch scope.
34 | */
35 |
36 | val backgroundJob = scope.async(Dispatchers.IO) { run(params) }
37 | scope.launch { onResult(backgroundJob.await()) }
38 | }
39 | }
--------------------------------------------------------------------------------
/domain/src/main/java/com/christopher/elias/domain/usecase/GetTodoUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.domain.usecase
2 |
3 | import com.christopher.elias.domain.entity.TodoEntity
4 | import com.christopher.elias.domain.repository.TodoRepository
5 |
6 | /**
7 | * Created by Christopher Elias on 27/01/2020.
8 | * christopher.mike.96@gmail.com
9 | *
10 | * Peru Apps
11 | * Lima, Peru.
12 | **/
13 | class GetTodoUseCase(private val todosRepository: TodoRepository) : BaseUseCase() {
14 |
15 | override suspend fun run(params: Params) = todosRepository.getTodo(params.id)
16 |
17 | data class Params(val id: Int)
18 | }
--------------------------------------------------------------------------------
/domain/src/main/java/com/christopher/elias/domain/usecase/GetTodosUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.domain.usecase
2 |
3 | import com.christopher.elias.domain.entity.Either
4 | import com.christopher.elias.domain.entity.Failure
5 | import com.christopher.elias.domain.entity.TodoEntity
6 | import com.christopher.elias.domain.repository.TodoRepository
7 |
8 | /**
9 | * Created by Christopher Elias on 27/01/2020.
10 | * christopher.mike.96@gmail.com
11 | *
12 | * Peru Apps
13 | * Lima, Peru.
14 | **/
15 | class GetTodosUseCase(private val todosRepository: TodoRepository) : BaseUseCase, Any>() {
16 |
17 | override suspend fun run(params: Any): Either>
18 | = todosRepository.getAllTodos()
19 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
22 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChristopherME/base-project/e39ed2dae17c7043cd494052858577a54ac03ad4/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Jan 27 13:48:41 COT 2020
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 |
--------------------------------------------------------------------------------
/presentation/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/presentation/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | apply plugin: 'kotlin-android'
4 |
5 | apply plugin: 'kotlin-android-extensions'
6 |
7 | apply plugin: 'kotlin-kapt'
8 |
9 | android {
10 | compileSdkVersion 29
11 | buildToolsVersion "29.0.2"
12 | defaultConfig {
13 | applicationId "com.christopher.elias.base"
14 | minSdkVersion 22
15 | targetSdkVersion 29
16 | versionCode 1
17 | versionName "1.0"
18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
19 | }
20 | buildTypes {
21 | //TODO: Generate signingConfig per each build type.
22 | release {
23 | minifyEnabled false
24 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
25 | }
26 |
27 | debug {
28 | applicationIdSuffix ".debug"
29 | versionNameSuffix "-debug"
30 | debuggable true
31 | }
32 | qa {
33 | applicationIdSuffix ".qa"
34 | versionNameSuffix "-qa"
35 | debuggable true
36 | }
37 | }
38 |
39 | kotlinOptions {
40 | jvmTarget = "1.8"
41 | }
42 |
43 | androidExtensions {
44 | experimental = true
45 | }
46 |
47 | dataBinding {
48 | enabled = true
49 | }
50 | }
51 |
52 | dependencies {
53 | implementation fileTree(dir: 'libs', include: ['*.jar'])
54 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
55 | implementation project(":domain")
56 | implementation project(":data")//TODO: Improve this.
57 | implementation presentationDependencies.values()
58 | testImplementation presentationTestDependencies.values()
59 | androidTestImplementation presentationTestImplementationDependencies.androidXRunner
60 | androidTestImplementation presentationTestImplementationDependencies.androidXEspresso
61 | }
62 |
--------------------------------------------------------------------------------
/presentation/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/presentation/src/androidTest/java/com/christopher/elias/base/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.base
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.christopher.elias.base", appContext.packageName)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/presentation/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/christopher/elias/base/TodoApplication.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.base
2 |
3 | import android.app.Application
4 | import com.christopher.elias.base.di.mapperPresentationModule
5 | import com.christopher.elias.base.di.viewModelModule
6 | import com.christopher.elias.data.di.mapperDataModule
7 | import com.christopher.elias.data.di.networkModule
8 | import com.christopher.elias.data.di.preferencesModule
9 | import com.christopher.elias.data.di.repositoryModule
10 | import com.christopher.elias.domain.di.useCasesModule
11 | import org.koin.android.ext.koin.androidContext
12 | import org.koin.core.context.startKoin
13 |
14 | /**
15 | * Created by Christopher Elias on 3/02/2020.
16 | * christopher.mike.96@gmail.com
17 | *
18 | * Peru Apps
19 | * Lima, Peru.
20 | **/
21 | class TodoApplication : Application() {
22 | override fun onCreate() {
23 | super.onCreate()
24 | startKoin {
25 | androidContext(this@TodoApplication)
26 | modules(arrayListOf(networkModule, preferencesModule, mapperDataModule, repositoryModule,
27 | useCasesModule, mapperPresentationModule, viewModelModule))
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/presentation/src/main/java/com/christopher/elias/base/di/MapperPresentationModule.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.base.di
2 |
3 | import com.christopher.elias.base.mapper.TodoModelMapper
4 | import com.christopher.elias.base.mapper.TodoModelMapperImpl
5 | import org.koin.dsl.module
6 |
7 | /**
8 | * Created by Christopher Elias on 3/02/2020.
9 | * christopher.mike.96@gmail.com
10 | *
11 | * Peru Apps
12 | * Lima, Peru.
13 | **/
14 |
15 | val mapperPresentationModule = module {
16 | single{ TodoModelMapperImpl() }
17 | }
--------------------------------------------------------------------------------
/presentation/src/main/java/com/christopher/elias/base/di/ViewModelModule.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.base.di
2 |
3 | import com.christopher.elias.base.ui.destinations.fragments.todo.detail.TodoDetailViewModel
4 | import com.christopher.elias.base.ui.destinations.fragments.todo.list.TodoListViewModel
5 | import org.koin.androidx.viewmodel.dsl.viewModel
6 | import org.koin.dsl.module
7 |
8 | /**
9 | * Created by Christopher Elias on 3/02/2020.
10 | * christopher.mike.96@gmail.com
11 | *
12 | * Peru Apps
13 | * Lima, Peru.
14 | **/
15 |
16 | val viewModelModule = module {
17 | viewModel { TodoListViewModel(get(), get()) }
18 | viewModel { TodoDetailViewModel(get(), get()) }
19 | }
--------------------------------------------------------------------------------
/presentation/src/main/java/com/christopher/elias/base/mapper/TodoModelMapper.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.base.mapper
2 |
3 | import com.christopher.elias.base.model.TodoModel
4 | import com.christopher.elias.domain.entity.TodoEntity
5 |
6 | /**
7 | * Created by Christopher Elias on 3/02/2020.
8 | * christopher.mike.96@gmail.com
9 | *
10 | * Peru Apps
11 | * Lima, Peru.
12 | **/
13 | interface TodoModelMapper {
14 |
15 | suspend fun todoDomainToPresentation(todo: TodoEntity) : TodoModel
16 |
17 | suspend fun todoListDomainToPresentation(todos: List) : List
18 |
19 | }
--------------------------------------------------------------------------------
/presentation/src/main/java/com/christopher/elias/base/mapper/TodoModelMapperImpl.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.base.mapper
2 |
3 | import com.christopher.elias.base.model.TodoModel
4 | import com.christopher.elias.domain.entity.TodoEntity
5 |
6 | /**
7 | * Created by Christopher Elias on 3/02/2020.
8 | * christopher.mike.96@gmail.com
9 | *
10 | * Peru Apps
11 | * Lima, Peru.
12 | **/
13 | class TodoModelMapperImpl : TodoModelMapper {
14 |
15 | override suspend fun todoDomainToPresentation(todo: TodoEntity): TodoModel {
16 | return TodoModel(id = todo.id,
17 | userId = "${todo.userId}",
18 | title = todo.title,
19 | completed = todo.completed)
20 | }
21 |
22 | override suspend fun todoListDomainToPresentation(todos: List): List {
23 | return todos.map { todo ->
24 | TodoModel(id = todo.id, userId = "${todo.userId}",
25 | title = todo.title, completed = todo.completed)
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/presentation/src/main/java/com/christopher/elias/base/model/TodoModel.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.base.model
2 |
3 | import android.os.Parcelable
4 | import kotlinx.android.parcel.Parcelize
5 |
6 | /**
7 | * Created by Christopher Elias on 3/02/2020.
8 | * christopher.mike.96@gmail.com
9 | *
10 | * Peru Apps
11 | * Lima, Peru.
12 | **/
13 |
14 | @Parcelize
15 | data class TodoModel(val id: Int,
16 | val userId: String,
17 | val title: String,
18 | val completed: Boolean) : Parcelable
--------------------------------------------------------------------------------
/presentation/src/main/java/com/christopher/elias/base/ui/adapter/TodoAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.base.ui.adapter
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 | import androidx.databinding.DataBindingUtil
6 | import androidx.recyclerview.widget.RecyclerView
7 | import com.christopher.elias.base.BR
8 | import com.christopher.elias.base.R
9 | import com.christopher.elias.base.databinding.ItemTodoBinding
10 | import com.christopher.elias.base.model.TodoModel
11 |
12 | /**
13 | * Created by Christopher Elias on 3/02/2020.
14 | * christopher.mike.96@gmail.com
15 | *
16 | * Peru Apps
17 | * Lima, Peru.
18 | **/
19 |
20 | /**
21 | * @param items list of [TodoModel] for populate RV.
22 | * @param callback function that replaces the necessity of a interface for handling RV Items touch listener.
23 | */
24 | class TodoAdapter(private val items: MutableList,
25 | val callback:(model: TodoModel, position: Int) -> Unit) : RecyclerView.Adapter() {
26 |
27 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoHolder {
28 | val binding : ItemTodoBinding? = DataBindingUtil.bind(LayoutInflater.from(parent.context).inflate(R.layout.item_todo, parent, false))
29 | return TodoHolder(binding!!)
30 | }
31 |
32 | override fun getItemCount() = items.size
33 |
34 | override fun onBindViewHolder(holder: TodoHolder, position: Int) {
35 | val todoModel = items[position]
36 | holder.setItem(model = todoModel)
37 | holder.itemTodoBinding.root.setOnClickListener {
38 | callback(todoModel, position)
39 | }
40 | }
41 |
42 | fun addItems(newItems: List) {
43 | items.clear()
44 | items.addAll(newItems)
45 | notifyDataSetChanged()
46 | }
47 |
48 | inner class TodoHolder(val itemTodoBinding: ItemTodoBinding) : RecyclerView.ViewHolder(itemTodoBinding.root) {
49 | fun setItem(model: TodoModel) {
50 | itemTodoBinding.setVariable(BR.model, model)
51 | itemTodoBinding.executePendingBindings()
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/presentation/src/main/java/com/christopher/elias/base/ui/base/BaseActivity.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.base.ui.base
2 |
3 | import android.os.Bundle
4 | import androidx.appcompat.app.AppCompatActivity
5 | import androidx.databinding.DataBindingUtil
6 | import androidx.databinding.ViewDataBinding
7 |
8 | /**
9 | * Created by Christopher Elias on 3/02/2020.
10 | * christopher.mike.96@gmail.com
11 | *
12 | * Peru Apps
13 | * Lima, Peru.
14 | **/
15 | abstract class BaseActivity> : AppCompatActivity() {
16 |
17 | private lateinit var viewDataBinding: T
18 | // private variable just for set the viewModel variable to the view (Data binding)
19 | private var _viewModel: V? = null
20 |
21 | abstract val getLayoutId : Int
22 |
23 | abstract val getViewModel : V
24 |
25 | abstract val getBindingVariable : Int
26 |
27 | override fun onCreate(savedInstanceState: Bundle?) {
28 | super.onCreate(savedInstanceState)
29 | viewDataBinding = DataBindingUtil.setContentView(this, getLayoutId)
30 | _viewModel = if (_viewModel == null) getViewModel else _viewModel
31 | viewDataBinding.setVariable(getBindingVariable, _viewModel)
32 | /**
33 | * use Fragment.viewLifecycleOwner for fragments
34 | */
35 | viewDataBinding.lifecycleOwner = this
36 | viewDataBinding.executePendingBindings()
37 | }
38 |
39 | }
--------------------------------------------------------------------------------
/presentation/src/main/java/com/christopher/elias/base/ui/base/BaseFragment.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.base.ui.base
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.databinding.DataBindingUtil
8 | import androidx.databinding.ViewDataBinding
9 | import androidx.fragment.app.Fragment
10 | import org.koin.androidx.viewmodel.ext.android.viewModel
11 | import kotlin.reflect.KClass
12 |
13 | /**
14 | * Created by Christopher Elias on 3/02/2020.
15 | * christopher.mike.96@gmail.com
16 | *
17 | * Peru Apps
18 | * Lima, Peru.
19 | **/
20 |
21 | /**
22 | * @see [https://kotlinlang.org/docs/reference/generics.html] for understand the usages of [T] - [ViewModelType], out keyword, etc.
23 | * @see [https://developer.android.com/topic/libraries/data-binding] for DataBinding.
24 | * @see [https://developer.android.com/topic/libraries/architecture/viewmodel?gclid=EAIaIQobChMIg-T2i-W15wIVCoeGCh06rwkuEAAYASAAEgI7P_D_BwE] for ViewModel.
25 | * @see [https://github.com/InsertKoinIO/koin/issues/56] for ViewModelType implementation
26 | */
27 | abstract class BaseFragment>(clazz: KClass) : Fragment() {
28 |
29 | private lateinit var viewDataBinding: T
30 | private lateinit var rootView: View
31 | val myViewModel : ViewModelType by viewModel(clazz)
32 |
33 | /**
34 | * This function associate the xml file to the class.
35 | * In order to pass it to onCreateView and bind it.
36 | * @return layout id
37 | */
38 | abstract val getLayoutId: Int
39 |
40 | /**
41 | * Return the binding variable of the XML
42 | * associated to this class.
43 | * @return binding variable ID
44 | */
45 | abstract val getBindingVariable: Int
46 |
47 | override fun onCreate(savedInstanceState: Bundle?) {
48 | super.onCreate(savedInstanceState)
49 | // Binding the viewModel
50 | if (myViewModel == null) {
51 | throw Exception("View Model must not be null.")
52 | }
53 | }
54 |
55 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
56 | viewDataBinding = DataBindingUtil.inflate(inflater, getLayoutId, container, false)
57 | rootView = viewDataBinding.root
58 | viewDataBinding.setVariable(getBindingVariable, myViewModel)
59 | return rootView
60 | }
61 |
62 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
63 | super.onViewCreated(view, savedInstanceState)
64 | viewDataBinding.lifecycleOwner = this.viewLifecycleOwner
65 | viewDataBinding.executePendingBindings()
66 | if (savedInstanceState == null)
67 | onFragmentViewReady(view)
68 | }
69 |
70 | abstract fun onFragmentViewReady(view: View)
71 |
72 | /**
73 | * Add a fragment above the current fragment. Also allows to use the back stack.
74 | *
75 | * @param containerId the frameLayout id.
76 | * @param fragment the fragment to add.
77 | * @param fromParent if true the we have to use activity?.supportFragmentManager.
78 | */
79 | protected fun addFragment(containerId: Int,
80 | fragment: Fragment,
81 | fromParent: Boolean = false) {
82 | val fragmentTransaction = if (fromParent) {
83 | activity?.supportFragmentManager?.beginTransaction()
84 | } else {
85 | fragmentManager?.beginTransaction()
86 | }
87 | fragmentTransaction?.add(containerId, fragment)
88 | ?.addToBackStack(null)
89 | ?.commit()
90 | }
91 | }
--------------------------------------------------------------------------------
/presentation/src/main/java/com/christopher/elias/base/ui/base/BaseViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.base.ui.base
2 |
3 | import android.util.Log
4 | import androidx.lifecycle.LiveData
5 | import androidx.lifecycle.MutableLiveData
6 | import androidx.lifecycle.ViewModel
7 | import com.christopher.elias.domain.entity.Failure
8 | import java.lang.ref.WeakReference
9 |
10 | /**
11 | * Created by Christopher Elias on 3/02/2020.
12 | * christopher.mike.96@gmail.com
13 | *
14 | * Peru Apps
15 | * Lima, Peru.
16 | **/
17 | abstract class BaseViewModel : ViewModel() {
18 |
19 | //TODO: Change this. Use Navigation Components from Android JetPack.
20 | // This is just a temporal solution to fix memory leaks due to the reference to activity.
21 | // The final solution will be using Navigation with multiple fragments and one activity.
22 | // By making navigator weakReference the garbage collector will be able to clean the "context"
23 | // from memory after the viewModel has been cleared..
24 | private var navigator: WeakReference? = null
25 |
26 | // Shows or hide progress loading bar if the have it.
27 | private val _isLoading = MutableLiveData(false)
28 | val isLoading : LiveData
29 | get() = _isLoading
30 |
31 | // Shows or hide and empty view layout if the view have it
32 | private val _showEmptyView = MutableLiveData(false)
33 | val showEmptyView : LiveData
34 | get() = _showEmptyView
35 |
36 | //Shows, hide, init or stop refreshing of Swipe refresh layout if the view have it.
37 | private val _isRefreshing = MutableLiveData(false)
38 | val isRefreshing : LiveData
39 | get() = _isRefreshing
40 |
41 | //Shows, hide, error message view.
42 | private val _showErrorCause = MutableLiveData(false)
43 | val showErrorCause : LiveData
44 | get() = _showErrorCause
45 |
46 | // The resource default value of the error or any error(Exception, server side, etc).
47 | private val _errorCause = MutableLiveData()
48 | val errorCause: LiveData
49 | get() = _errorCause
50 |
51 | protected fun logError(errorMessage: String?) {
52 | Log.e(this.javaClass.simpleName, errorMessage?:"error message is null.")
53 | }
54 |
55 | protected fun logInfo(infoMessage: String?) {
56 | Log.i(this.javaClass.simpleName, infoMessage?:"info message is null.")
57 | }
58 |
59 | fun getNavigator() : T? {
60 | return navigator?.get()
61 | }
62 |
63 | fun setNavigator(navigator: T) {
64 | this.navigator = WeakReference(navigator)
65 | }
66 |
67 | /*
68 | * The following functions are just for presentation purposes
69 | */
70 | protected fun setRefreshing(refreshValue: Boolean) {
71 | _isRefreshing.value = refreshValue
72 | }
73 |
74 | protected fun showLoading(loadingValue: Boolean) {
75 | _isLoading.value = loadingValue
76 | }
77 |
78 | protected fun shouldShowEmptyView(show: Boolean?) {
79 | _showEmptyView.value = show
80 | }
81 |
82 | protected fun showErrorCause(show: Boolean) {
83 | _showErrorCause.value = show
84 | }
85 |
86 | /**
87 | * This will perform common actions such as stop loading, refreshing, hide empty view, and show error cause.
88 | * In case of a failure from any Use Case.
89 | */
90 | protected fun handleUseCaseFailureFromBase(failure: Failure){
91 | when(failure) {
92 | is Failure.UnauthorizedOrForbidden -> logError("Log Out") /* Log out of the app*/
93 | is Failure.None -> setError("None"/*R.string.snack_bar_error_failure_none*/)
94 | is Failure.NetworkConnectionLostSuddenly -> setError("Connection lost suddenly. Check the wifi or mobile data."/*R.string.snack_bar_error_failure_network_connection_lost_suddenly*/)
95 | is Failure.NoNetworkDetected -> setError("No network detected"/*R.string.snack_bar_error_failure_no_network_detected*/)
96 | is Failure.SSLError -> setError("WARNING: SSL Exception"/*R.string.snack_bar_error_failure_ssl*/)
97 | is Failure.TimeOut -> setError("Time out."/*R.string.snack_bar_error_failure_time_out*/)
98 | is Failure.ServerBodyError -> setError(failure.message)
99 | is Failure.DataToDomainMapperFailure -> setError("Data to domain mapper failure: ${failure.mapperException}"/*failure.mapperException?:R.string.snack_bar_error_general*/)
100 | is Failure.ServiceUncaughtFailure -> setError(failure.uncaughtFailureMessage)
101 | }
102 | showLoading(false)
103 | setRefreshing(false)
104 | shouldShowEmptyView(false)
105 | showErrorCause(true)
106 | }
107 |
108 | /**
109 | * Set [_errorCause] value in order to observe the changes on lifecycle owner.
110 | *
111 | * @param cause the error cause can be a plain [String] or Int(string resource id)
112 | */
113 | protected fun setError(cause: Any) {
114 | //Print directly on console if cause is String.
115 | if (cause is String) {
116 | logError(cause)
117 | }
118 | _errorCause.value = cause
119 | }
120 |
121 | override fun onCleared() {
122 | // Clear object for prevention of memory leaks.
123 | navigator?.clear()
124 | super.onCleared()
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/christopher/elias/base/ui/bindingtools/RecyclerViewBTools.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.base.ui.bindingtools
2 |
3 | import androidx.databinding.BindingAdapter
4 | import androidx.recyclerview.widget.LinearLayoutManager
5 | import androidx.recyclerview.widget.RecyclerView
6 | import com.christopher.elias.base.ui.adapter.TodoAdapter
7 |
8 | /**
9 | * Created by Christopher Elias on 3/02/2020.
10 | * christopher.mike.96@gmail.com
11 | *
12 | * Peru Apps
13 | * Lima, Peru.
14 | **/
15 |
16 | @BindingAdapter("todoAdapter")
17 | fun setTodoAdapter(recyclerView: RecyclerView, adapter: TodoAdapter){
18 | recyclerView.layoutManager = LinearLayoutManager(recyclerView.context)
19 | recyclerView.adapter = adapter
20 | }
--------------------------------------------------------------------------------
/presentation/src/main/java/com/christopher/elias/base/ui/bindingtools/TextViewBTools.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.base.ui.bindingtools
2 |
3 | import android.widget.TextView
4 | import androidx.databinding.BindingAdapter
5 |
6 | /**
7 | * Created by Christopher Elias on 3/02/2020.
8 | * christopher.mike.96@gmail.com
9 | *
10 | * Peru Apps
11 | * Lima, Peru.
12 | **/
13 |
14 | @BindingAdapter("setCustomErrorMessage")
15 | fun setCustomErrorMessage(tv: TextView, message: Any?) {
16 | tv.text = when(message) {
17 | is String -> message
18 | is Int -> tv.resources.getString(message)
19 | else -> ""
20 | }
21 | }
--------------------------------------------------------------------------------
/presentation/src/main/java/com/christopher/elias/base/ui/destinations/activities/main/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.base.ui.destinations.activities.main
2 |
3 | import androidx.appcompat.app.AppCompatActivity
4 | import android.os.Bundle
5 | import com.christopher.elias.base.R
6 | import com.christopher.elias.base.ui.destinations.fragments.todo.list.TodoListFragment
7 |
8 | class MainActivity : AppCompatActivity() {
9 |
10 | override fun onCreate(savedInstanceState: Bundle?) {
11 | super.onCreate(savedInstanceState)
12 | setContentView(R.layout.activity_main)
13 | supportFragmentManager.beginTransaction()
14 | .replace(R.id.frame_main, TodoListFragment())
15 | .commit()
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/christopher/elias/base/ui/destinations/fragments/todo/detail/TodoDetailFragment.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.base.ui.destinations.fragments.todo.detail
2 |
3 | import android.os.Bundle
4 | import android.view.View
5 | import com.christopher.elias.base.BR
6 | import com.christopher.elias.base.R
7 | import com.christopher.elias.base.databinding.FragmentTodoDetailBinding
8 | import com.christopher.elias.base.model.TodoModel
9 | import com.christopher.elias.base.ui.base.BaseFragment
10 |
11 | /**
12 | * Created by Christopher Elias on 3/02/2020.
13 | * christopher.mike.96@gmail.com
14 | *
15 | * Peru Apps
16 | * Lima, Peru.
17 | **/
18 | class TodoDetailFragment : BaseFragment(TodoDetailViewModel::class) {
19 |
20 | override val getBindingVariable: Int
21 | get() = BR.todoDetailViewModel
22 |
23 | override val getLayoutId: Int
24 | get() = R.layout.fragment_todo_detail
25 |
26 | companion object {
27 | fun newInstance(model: TodoModel) = TodoDetailFragment().apply {
28 | arguments = Bundle().apply {
29 | putParcelable("todoModel", model)
30 | }
31 | }
32 | }
33 |
34 | override fun onFragmentViewReady(view: View) {
35 | myViewModel.getValuesFromArguments(arguments?.getParcelable("todoModel"))
36 | }
37 | }
--------------------------------------------------------------------------------
/presentation/src/main/java/com/christopher/elias/base/ui/destinations/fragments/todo/detail/TodoDetailViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.base.ui.destinations.fragments.todo.detail
2 |
3 | import androidx.lifecycle.*
4 | import com.christopher.elias.base.mapper.TodoModelMapper
5 | import com.christopher.elias.base.model.TodoModel
6 | import com.christopher.elias.base.ui.base.BaseViewModel
7 | import com.christopher.elias.domain.entity.TodoEntity
8 | import com.christopher.elias.domain.usecase.GetTodoUseCase
9 | import kotlinx.coroutines.Dispatchers
10 |
11 | /**
12 | * Created by Christopher Elias on 3/02/2020.
13 | * christopher.mike.96@gmail.com
14 | *
15 | * Peru Apps
16 | * Lima, Peru.
17 | **/
18 | class TodoDetailViewModel(private val getTodoUseCase: GetTodoUseCase,
19 | private val mapper: TodoModelMapper) : BaseViewModel() {
20 |
21 | private var _todoId = -1
22 |
23 | private val _todoModelFromNetwork = MutableLiveData()
24 | val todoModelFromNetwork : LiveData = _todoModelFromNetwork.switchMap {
25 | liveData(Dispatchers.IO) {
26 | emit(mapper.todoDomainToPresentation(it))
27 | }
28 | }
29 |
30 | private val _todoModelFromArgs = MutableLiveData()
31 | val todoModelFromArgs: LiveData
32 | get() = _todoModelFromArgs
33 |
34 | fun refreshData() {
35 | setRefreshing(true)
36 | }
37 |
38 | /**
39 | * @param todoModel to publish new value on [_todoModelFromArgs]
40 | */
41 | fun getValuesFromArguments(todoModel: TodoModel?) {
42 | _todoId = todoModel?.id?:-1
43 | _todoModelFromArgs.value = todoModel
44 | executeGetTodoPerIdUseCase()
45 | }
46 |
47 | private fun executeGetTodoPerIdUseCase() {
48 | showLoading(true)
49 | val params = GetTodoUseCase.Params(_todoId)
50 | getTodoUseCase.invoke(viewModelScope, params){
51 | it.either(::handleUseCaseFailureFromBase, ::handleUseCaseSuccess)
52 | }
53 | }
54 |
55 | private fun handleUseCaseSuccess(todoEntity: TodoEntity) {
56 | _todoModelFromNetwork.value = todoEntity
57 | showLoading(false)
58 | shouldShowEmptyView(false)
59 | showErrorCause(false)
60 | }
61 | }
--------------------------------------------------------------------------------
/presentation/src/main/java/com/christopher/elias/base/ui/destinations/fragments/todo/list/TodoListFragment.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.base.ui.destinations.fragments.todo.list
2 |
3 | import android.view.View
4 | import androidx.lifecycle.Observer
5 | import com.christopher.elias.base.BR
6 | import com.christopher.elias.base.R
7 | import com.christopher.elias.base.databinding.FragmentTodoListBinding
8 | import com.christopher.elias.base.model.TodoModel
9 | import com.christopher.elias.base.ui.base.BaseFragment
10 | import com.christopher.elias.base.ui.destinations.fragments.todo.detail.TodoDetailFragment
11 |
12 | /**
13 | * Created by Christopher Elias on 3/02/2020.
14 | * christopher.mike.96@gmail.com
15 | *
16 | * Peru Apps
17 | * Lima, Peru.
18 | **/
19 | class TodoListFragment : BaseFragment(TodoListViewModel::class), TodoListNavigator {
20 |
21 | override val getBindingVariable: Int
22 | get() = BR.todoListViewModel
23 |
24 | override val getLayoutId: Int
25 | get() = R.layout.fragment_todo_list
26 |
27 | override fun onFragmentViewReady(view: View) {
28 | myViewModel.setNavigator(this)
29 | myViewModel.todoList.observe(this, Observer {
30 | myViewModel.bindItemsAfterMapping(it)
31 | })
32 | }
33 |
34 | override fun onTodoObjectClicked(todo: TodoModel) {
35 | addFragment(R.id.frame_main, TodoDetailFragment.newInstance(todo), true)
36 | }
37 | }
--------------------------------------------------------------------------------
/presentation/src/main/java/com/christopher/elias/base/ui/destinations/fragments/todo/list/TodoListNavigator.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.base.ui.destinations.fragments.todo.list
2 |
3 | import com.christopher.elias.base.model.TodoModel
4 |
5 | /**
6 | * Created by Christopher Elias on 3/02/2020.
7 | * christopher.mike.96@gmail.com
8 | *
9 | * Peru Apps
10 | * Lima, Peru.
11 | **/
12 | interface TodoListNavigator {
13 | fun onTodoObjectClicked(todo: TodoModel)
14 | }
--------------------------------------------------------------------------------
/presentation/src/main/java/com/christopher/elias/base/ui/destinations/fragments/todo/list/TodoListViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.base.ui.destinations.fragments.todo.list
2 |
3 |
4 | import androidx.lifecycle.*
5 | import com.christopher.elias.base.mapper.TodoModelMapper
6 | import com.christopher.elias.base.model.TodoModel
7 | import com.christopher.elias.base.ui.adapter.TodoAdapter
8 | import com.christopher.elias.base.ui.base.BaseViewModel
9 | import com.christopher.elias.domain.entity.TodoEntity
10 | import com.christopher.elias.domain.usecase.GetTodosUseCase
11 | import kotlinx.coroutines.Dispatchers
12 |
13 | /**
14 | * Created by Christopher Elias on 3/02/2020.
15 | * christopher.mike.96@gmail.com
16 | *
17 | * Peru Apps
18 | * Lima, Peru.
19 | **/
20 | class TodoListViewModel(private val getTodosUseCase: GetTodosUseCase,
21 | private val mapper: TodoModelMapper) : BaseViewModel() {
22 |
23 | /*
24 | * Underscore for private fields as good coding practice.
25 | * What we are doing here is when _todoList has a value or the current value has changed,
26 | * todoList listen to these changes and apply a "switchMap" which can invoke a "liveData" coroutine scope
27 | * an call other coroutines. Inside the liveDataScope we are calling mapper functions in Dispatchers.IO but the result will be
28 | * on Dispatchers.Main.
29 | *
30 | * @see https://developer.android.com/reference/androidx/lifecycle/Transformations
31 | */
32 | private val _todoList = MutableLiveData>()
33 | val todoList : LiveData> = _todoList.switchMap {
34 | liveData(Dispatchers.IO) {
35 | emit(mapper.todoListDomainToPresentation(it))
36 | }
37 | }
38 |
39 | // For show the quantity of items returned by the service.
40 | val todoListSize : LiveData = Transformations.map(_todoList) {
41 | "${it.size} TODO's"
42 | }
43 |
44 | // adapter consumed directly on XML through data binding.
45 | val adapter = TodoAdapter(arrayListOf()) { model, _ ->
46 | getNavigator()?.onTodoObjectClicked(todo = model)
47 | }
48 |
49 | init {
50 | executeGetTodoListUseCase()
51 | }
52 |
53 | /**
54 | * After the mapping of mapper.todoListDomainToPresentation() has finish we can populate the [adapter] of our rv.
55 | *
56 | * @param todoListMapped list from presentation layer.
57 | */
58 | fun bindItemsAfterMapping(todoListMapped: List) {
59 | setRefreshing(false)
60 | showLoading(false)
61 | shouldShowEmptyView(todoListMapped.isEmpty())
62 | showErrorCause(false)
63 | adapter.addItems(todoListMapped)
64 | }
65 |
66 | fun refreshData() {
67 | setRefreshing(true)
68 | executeGetTodoListUseCase()
69 | }
70 |
71 | private fun executeGetTodoListUseCase() {
72 | showLoading(true)
73 | getTodosUseCase.invoke(viewModelScope,""){
74 | it.either(::handleUseCaseFailureFromBase,::handleUseCaseSuccess )
75 | }
76 | }
77 |
78 | /**
79 | * Fill a new value for [_todoList].
80 | *
81 | * @param todos the RIGHT side of the Either object returned from [executeGetTodoListUseCase].
82 | */
83 | private fun handleUseCaseSuccess(todos : List){
84 | _todoList.value = todos
85 | }
86 | }
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/presentation/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 |
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable/ic_todo_done.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable/ic_todo_pending.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/presentation/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/presentation/src/main/res/layout/fragment_todo_detail.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 |
21 |
22 |
29 |
30 |
39 |
40 |
51 |
52 |
53 |
54 |
64 |
65 |
74 |
75 |
84 |
85 |
92 |
93 |
102 |
103 |
112 |
113 |
127 |
128 |
129 |
130 |
131 |
132 |
--------------------------------------------------------------------------------
/presentation/src/main/res/layout/fragment_todo_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 |
21 |
22 |
29 |
30 |
39 |
40 |
51 |
52 |
53 |
54 |
64 |
65 |
80 |
81 |
94 |
95 |
109 |
110 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
--------------------------------------------------------------------------------
/presentation/src/main/res/layout/item_todo.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
11 |
12 |
13 |
14 |
18 |
19 |
29 |
30 |
37 |
38 |
46 |
47 |
56 |
57 |
65 |
66 |
75 |
76 |
82 |
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/presentation/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/presentation/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/presentation/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChristopherME/base-project/e39ed2dae17c7043cd494052858577a54ac03ad4/presentation/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/presentation/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChristopherME/base-project/e39ed2dae17c7043cd494052858577a54ac03ad4/presentation/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/presentation/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChristopherME/base-project/e39ed2dae17c7043cd494052858577a54ac03ad4/presentation/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/presentation/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChristopherME/base-project/e39ed2dae17c7043cd494052858577a54ac03ad4/presentation/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/presentation/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChristopherME/base-project/e39ed2dae17c7043cd494052858577a54ac03ad4/presentation/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/presentation/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChristopherME/base-project/e39ed2dae17c7043cd494052858577a54ac03ad4/presentation/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/presentation/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChristopherME/base-project/e39ed2dae17c7043cd494052858577a54ac03ad4/presentation/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/presentation/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChristopherME/base-project/e39ed2dae17c7043cd494052858577a54ac03ad4/presentation/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/presentation/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChristopherME/base-project/e39ed2dae17c7043cd494052858577a54ac03ad4/presentation/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/presentation/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChristopherME/base-project/e39ed2dae17c7043cd494052858577a54ac03ad4/presentation/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/presentation/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #008577
4 | #00574B
5 | #D81B60
6 | #ffffff
7 |
8 |
--------------------------------------------------------------------------------
/presentation/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Base
3 | User ID
4 | Title
5 | Is completed
6 | List of TODO\'s!
7 | Detail of the TODO
8 | The TODO list is empty.
9 | Todo from Network
10 | Todo from arguments
11 |
12 |
--------------------------------------------------------------------------------
/presentation/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/presentation/src/test/java/com/christopher/elias/base/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.christopher.elias.base
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 ':presentation', ':data', ':domain'
2 | rootProject.name='Base'
3 |
--------------------------------------------------------------------------------