├── .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 | [![Build Status](https://travis-ci.org/joemccann/dillinger.svg?branch=master)](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 | --------------------------------------------------------------------------------