├── .gitignore ├── .idea ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── gradle.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── clean │ │ └── project │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── clean │ │ │ └── project │ │ │ ├── AppComponent.kt │ │ │ ├── AppModule.kt │ │ │ └── NasaApp.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.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 │ └── clean │ └── project │ └── ExampleUnitTest.kt ├── build.gradle ├── data ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── clean │ │ └── data │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── clean │ │ │ └── data │ │ │ ├── NasaRepositoryImpl.kt │ │ │ ├── StringProviderImpl.kt │ │ │ ├── cache │ │ │ ├── AsteroidDao.kt │ │ │ ├── AsteroidEntity.kt │ │ │ ├── DBConstants.kt │ │ │ ├── NasaCache.kt │ │ │ └── NasaDataBase.kt │ │ │ ├── config │ │ │ ├── DataComponent.kt │ │ │ └── DataModule.kt │ │ │ ├── mapper │ │ │ └── AsteroidMapper.kt │ │ │ ├── model │ │ │ └── Asteroid.kt │ │ │ └── remote │ │ │ ├── NasaRemote.kt │ │ │ └── NasaService.kt │ └── res │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── clean │ └── data │ └── ExampleUnitTest.java ├── dependencies.gradle ├── domain ├── .gitignore ├── build.gradle └── src │ ├── main │ └── java │ │ └── com │ │ └── clean │ │ └── domain │ │ └── asteroid │ │ ├── AsteroidViewEventHandler.kt │ │ ├── AsteroidViewFlow.kt │ │ ├── AsteroidViewStateReducer.kt │ │ ├── NasaRepository.kt │ │ ├── StringProvider.kt │ │ ├── model │ │ ├── Asteroid.kt │ │ ├── AsteroidViewEvent.kt │ │ ├── AsteroidViewResult.kt │ │ └── AsteroidViewState.kt │ │ └── usecase │ │ ├── AsteroidUseCase.kt │ │ ├── GetAsteroidOfTheDay.kt │ │ └── SaveAsteroid.kt │ └── test │ └── kotlin │ └── com │ └── clean │ └── domain │ └── asteroid │ ├── AsteroidViewFlowTest.kt │ ├── NasaRepositoryFake.kt │ └── StringProviderFake.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 │ │ └── clean │ │ └── asteroids │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── clean │ │ │ └── asteroids │ │ │ ├── AsteroidActivity.kt │ │ │ ├── AsteroidEffectHandler.kt │ │ │ ├── AsteroidViewModel.kt │ │ │ ├── AsteroidViewRenderer.kt │ │ │ ├── ViewModelFactory.kt │ │ │ └── config │ │ │ ├── CoreComponent.kt │ │ │ ├── CoreComponentProvider.kt │ │ │ ├── PresentationComponent.kt │ │ │ ├── PresentationModule.kt │ │ │ ├── Scopes.kt │ │ │ └── ViewModelKey.kt │ └── res │ │ ├── layout │ │ └── activity_main.xml │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── clean │ └── asteroids │ └── ExampleUnitTest.java └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | xmlns:android 14 | 15 | ^$ 16 | 17 | 18 | 19 |
20 |
21 | 22 | 23 | 24 | xmlns:.* 25 | 26 | ^$ 27 | 28 | 29 | BY_NAME 30 | 31 |
32 |
33 | 34 | 35 | 36 | .*:id 37 | 38 | http://schemas.android.com/apk/res/android 39 | 40 | 41 | 42 |
43 |
44 | 45 | 46 | 47 | .*:name 48 | 49 | http://schemas.android.com/apk/res/android 50 | 51 | 52 | 53 |
54 |
55 | 56 | 57 | 58 | name 59 | 60 | ^$ 61 | 62 | 63 | 64 |
65 |
66 | 67 | 68 | 69 | style 70 | 71 | ^$ 72 | 73 | 74 | 75 |
76 |
77 | 78 | 79 | 80 | .* 81 | 82 | ^$ 83 | 84 | 85 | BY_NAME 86 | 87 |
88 |
89 | 90 | 91 | 92 | .* 93 | 94 | http://schemas.android.com/apk/res/android 95 | 96 | 97 | ANDROID_ATTRIBUTE_ORDER 98 | 99 |
100 |
101 | 102 | 103 | 104 | .* 105 | 106 | .* 107 | 108 | 109 | BY_NAME 110 | 111 |
112 |
113 |
114 |
115 | 116 | 118 |
119 |
-------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/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 28 11 | defaultConfig { 12 | applicationId "com.clean.project" 13 | minSdkVersion 21 14 | targetSdkVersion 28 15 | versionCode 1 16 | versionName "1.0" 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | } 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | 26 | compileOptions { 27 | sourceCompatibility = 1.8 28 | targetCompatibility = 1.8 29 | } 30 | } 31 | 32 | configurations.all { 33 | resolutionStrategy { 34 | force "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 35 | } 36 | } 37 | 38 | dependencies { 39 | implementation 'androidx.appcompat:appcompat:1.0.2' 40 | implementation 'com.facebook.stetho:stetho:1.5.1' 41 | implementation "io.reactivex.rxjava2:rxjava:2.2.9" 42 | implementation 'com.google.dagger:dagger:2.20' 43 | kapt 'com.google.dagger:dagger-compiler:2.20' 44 | 45 | implementation project(path: ':domain') 46 | implementation project(path: ':data') 47 | implementation project(path: ':presentation') 48 | } 49 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/clean/project/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.clean.project 2 | 3 | import android.support.test.InstrumentationRegistry 4 | import android.support.test.runner.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.getTargetContext() 22 | assertEquals("com.clean.project", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/clean/project/AppComponent.kt: -------------------------------------------------------------------------------- 1 | package com.clean.project 2 | 3 | import com.clean.asteroids.config.CoreComponent 4 | import com.clean.data.config.DataComponent 5 | import com.clean.domain.asteroid.AsteroidViewFlow 6 | import dagger.Component 7 | import javax.inject.Scope 8 | 9 | @AppSingleton 10 | @Component( 11 | modules = arrayOf(AppModule::class), 12 | dependencies = arrayOf(DataComponent::class) 13 | ) 14 | interface AppComponent : CoreComponent { 15 | 16 | override fun provideAsteriodFlow(): AsteroidViewFlow 17 | 18 | @Component.Builder 19 | interface Builder { 20 | 21 | fun dataComponent(dataComponent: DataComponent): Builder 22 | fun build(): AppComponent 23 | } 24 | } 25 | 26 | @Scope 27 | @MustBeDocumented 28 | @Retention 29 | annotation class AppSingleton -------------------------------------------------------------------------------- /app/src/main/java/com/clean/project/AppModule.kt: -------------------------------------------------------------------------------- 1 | package com.clean.project 2 | 3 | import com.clean.domain.asteroid.model.AsteroidViewEvent 4 | import com.clean.domain.asteroid.usecase.AsteroidUseCase 5 | import com.clean.domain.asteroid.usecase.GetAsteroidOfTheDay 6 | import com.clean.domain.asteroid.usecase.SaveAsteroid 7 | import dagger.Module 8 | import dagger.Provides 9 | import dagger.multibindings.IntoSet 10 | 11 | @Module 12 | object AppModule { 13 | 14 | @Provides 15 | @JvmStatic 16 | @IntoSet 17 | fun provideGetAsteroidOfTheDayUseCase(getAsteroidOfTheDay: GetAsteroidOfTheDay): AsteroidUseCase { 18 | return getAsteroidOfTheDay as AsteroidUseCase 19 | } 20 | 21 | @Provides 22 | @JvmStatic 23 | @IntoSet 24 | fun provideSaveAsteroid(saveAsteroid: SaveAsteroid): AsteroidUseCase { 25 | return saveAsteroid as AsteroidUseCase 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/clean/project/NasaApp.kt: -------------------------------------------------------------------------------- 1 | package com.clean.project 2 | 3 | import android.app.Application 4 | import com.clean.asteroids.config.CoreComponent 5 | import com.clean.asteroids.config.CoreComponentProvider 6 | import com.clean.data.config.DaggerDataComponent 7 | import com.facebook.stetho.Stetho 8 | 9 | class NasaApp : Application(), CoreComponentProvider { 10 | 11 | val appComponent: AppComponent by lazy { 12 | DaggerAppComponent.builder() 13 | .dataComponent(dataComponent()) 14 | .build() 15 | } 16 | 17 | override fun onCreate() { 18 | super.onCreate() 19 | Stetho.initializeWithDefaults(this); 20 | } 21 | 22 | override fun provide(): CoreComponent { 23 | return appComponent 24 | } 25 | 26 | private fun dataComponent() = DaggerDataComponent.builder().application(this).build() 27 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdrianoCelentano/CleanProject/ab415e4d84025175ae578a5c9ac3861cee84bfde/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdrianoCelentano/CleanProject/ab415e4d84025175ae578a5c9ac3861cee84bfde/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdrianoCelentano/CleanProject/ab415e4d84025175ae578a5c9ac3861cee84bfde/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdrianoCelentano/CleanProject/ab415e4d84025175ae578a5c9ac3861cee84bfde/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdrianoCelentano/CleanProject/ab415e4d84025175ae578a5c9ac3861cee84bfde/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdrianoCelentano/CleanProject/ab415e4d84025175ae578a5c9ac3861cee84bfde/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdrianoCelentano/CleanProject/ab415e4d84025175ae578a5c9ac3861cee84bfde/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdrianoCelentano/CleanProject/ab415e4d84025175ae578a5c9ac3861cee84bfde/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdrianoCelentano/CleanProject/ab415e4d84025175ae578a5c9ac3861cee84bfde/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdrianoCelentano/CleanProject/ab415e4d84025175ae578a5c9ac3861cee84bfde/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #008577 4 | #00574B 5 | #D81B60 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | CleanProject 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/test/java/com/clean/project/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.clean.project 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 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | 2 | buildscript { 3 | ext.kotlin_version = '1.3.30' 4 | repositories { 5 | google() 6 | jcenter() 7 | } 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.4.1' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | apply from: 'dependencies.gradle' 15 | 16 | allprojects { 17 | repositories { 18 | google() 19 | jcenter() 20 | } 21 | } 22 | 23 | task clean(type: Delete) { 24 | delete rootProject.buildDir 25 | } 26 | -------------------------------------------------------------------------------- /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 28 11 | 12 | defaultConfig { 13 | minSdkVersion 21 14 | targetSdkVersion 28 15 | versionCode 1 16 | versionName "1.0" 17 | 18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 19 | 20 | } 21 | 22 | buildTypes { 23 | release { 24 | minifyEnabled false 25 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 26 | } 27 | } 28 | 29 | compileOptions { 30 | sourceCompatibility = 1.8 31 | targetCompatibility = 1.8 32 | } 33 | 34 | } 35 | 36 | kapt { 37 | javacOptions { 38 | // Increase the max count of errors from annotation processors. 39 | // Default is 100. 40 | option("-Xmaxerrs", 500) 41 | } 42 | } 43 | 44 | dependencies { 45 | def dataDependencies = rootProject.ext.dataDependencies 46 | def dataTestDependencies = rootProject.ext.dataTestDependencies 47 | 48 | implementation project(":domain") 49 | implementation dataDependencies.javaxAnnotation 50 | implementation dataDependencies.javaxInject 51 | implementation dataDependencies.kotlin 52 | implementation dataDependencies.rxKotlin 53 | implementation dataDependencies.rxJava 54 | implementation dataDependencies.gson 55 | implementation dataDependencies.okHttp 56 | implementation dataDependencies.okHttpLogger 57 | implementation dataDependencies.retrofit 58 | implementation dataDependencies.retrofitConverter 59 | implementation dataDependencies.retrofitAdapter 60 | implementation dataDependencies.roomRuntime 61 | implementation dataDependencies.roomKotlin 62 | implementation dataDependencies.roomRxJava 63 | kapt dataDependencies.roomCompiler 64 | implementation dataDependencies.dagger 65 | kapt dataDependencies.daggerCompiler 66 | testImplementation dataTestDependencies.junit 67 | testImplementation dataTestDependencies.mockito 68 | testImplementation dataTestDependencies.assertj 69 | } 70 | -------------------------------------------------------------------------------- /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/clean/data/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.clean.data; 2 | 3 | import android.content.Context; 4 | import androidx.test.InstrumentationRegistry; 5 | import androidx.test.runner.AndroidJUnit4; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | 9 | import static org.junit.Assert.assertEquals; 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * @see Testing documentation 15 | */ 16 | @RunWith(AndroidJUnit4.class) 17 | public class ExampleInstrumentedTest { 18 | @Test 19 | public void useAppContext() { 20 | // Context of the app under test. 21 | Context appContext = InstrumentationRegistry.getTargetContext(); 22 | 23 | assertEquals("com.clean.data.test", appContext.getPackageName()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /data/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /data/src/main/java/com/clean/data/NasaRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.clean.data 2 | 3 | import com.clean.data.cache.NasaCache 4 | import com.clean.data.mapper.AsteroidMapper 5 | import com.clean.data.remote.NasaRemote 6 | import com.clean.domain.asteroid.NasaRepository 7 | import com.clean.domain.asteroid.model.Asteroid 8 | import io.reactivex.Completable 9 | import io.reactivex.Observable 10 | import javax.inject.Inject 11 | 12 | class NasaRepositoryImpl @Inject constructor( 13 | val nasaRemote: NasaRemote, 14 | val nasaCache: NasaCache, 15 | private val asteroidMapper: AsteroidMapper 16 | ) : NasaRepository { 17 | 18 | override fun getAsteroidOfTheDay(): Observable { 19 | return nasaRemote.getAsteroidOfTheDay().map { asteroidMapper.mapDataToDomain(it) } 20 | } 21 | 22 | override fun saveAsteroid(asteroid: Asteroid): Completable { 23 | return Observable.just(asteroid) 24 | .map { asteroidMapper.mapDomaintoEntity(asteroid) } 25 | .flatMapCompletable { nasaCache.saveAsteroid(it) } 26 | } 27 | 28 | override fun getSavedAsteroid(): Observable> { 29 | return nasaCache.getAsteroids() 30 | .map { asteroids -> asteroids.map { asteroidMapper.mapEntitiyToDomain(it) } } 31 | } 32 | } -------------------------------------------------------------------------------- /data/src/main/java/com/clean/data/StringProviderImpl.kt: -------------------------------------------------------------------------------- 1 | package com.clean.data 2 | 3 | import android.app.Application 4 | import com.clean.domain.asteroid.StringProvider 5 | import javax.inject.Inject 6 | 7 | class StringProviderImpl @Inject constructor(private val application: Application) : StringProvider { 8 | 9 | override val serverError: String 10 | get() = application.getString(R.string.server_errror) 11 | override val generalError: String 12 | get() = application.getString(R.string.general_errror) 13 | override val storeAsteroidSuccess: String 14 | get() = application.getString(R.string.store_asteroid_success) 15 | override val messageInABottle: String 16 | get() = application.getString(R.string.message_in_a_bottle) 17 | 18 | } -------------------------------------------------------------------------------- /data/src/main/java/com/clean/data/cache/AsteroidDao.kt: -------------------------------------------------------------------------------- 1 | package com.clean.data.cache 2 | 3 | import androidx.room.* 4 | import com.clean.data.cache.DBConstants.ASTEROID_TABLE_NAME 5 | import io.reactivex.Completable 6 | import io.reactivex.Observable 7 | 8 | @Dao 9 | interface AsteroidDao { 10 | 11 | @Query("SELECT * FROM ${ASTEROID_TABLE_NAME}") 12 | fun getAll(): Observable> 13 | 14 | @Insert(onConflict = OnConflictStrategy.REPLACE) 15 | fun insert(asteroidEntity: AsteroidEntity): Completable 16 | 17 | @Delete 18 | fun delete(asteroidEntity: AsteroidEntity): Completable 19 | } -------------------------------------------------------------------------------- /data/src/main/java/com/clean/data/cache/AsteroidEntity.kt: -------------------------------------------------------------------------------- 1 | package com.clean.data.cache 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | import com.clean.data.cache.DBConstants.ASTEROID_TABLE_NAME 6 | 7 | @Entity(tableName = ASTEROID_TABLE_NAME) 8 | data class AsteroidEntity( 9 | @PrimaryKey(autoGenerate = true) 10 | val id: Int = 0, 11 | val title: String? = null, 12 | val url: String? = null 13 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/clean/data/cache/DBConstants.kt: -------------------------------------------------------------------------------- 1 | package com.clean.data.cache 2 | 3 | object DBConstants { 4 | 5 | const val ASTEROID_TABLE_NAME = "asteroid_table" 6 | const val DB_NAME = "nasaDB" 7 | } -------------------------------------------------------------------------------- /data/src/main/java/com/clean/data/cache/NasaCache.kt: -------------------------------------------------------------------------------- 1 | package com.clean.data.cache 2 | 3 | import io.reactivex.Completable 4 | import io.reactivex.Observable 5 | import javax.inject.Inject 6 | 7 | class NasaCache @Inject constructor( 8 | private val nasaDataBase: NasaDataBase 9 | ) { 10 | 11 | fun saveAsteroid(asteroidEntity: AsteroidEntity): Completable { 12 | return nasaDataBase.asteroidDao().insert(asteroidEntity) 13 | } 14 | 15 | fun getAsteroids(): Observable> { 16 | return nasaDataBase.asteroidDao().getAll() 17 | } 18 | } -------------------------------------------------------------------------------- /data/src/main/java/com/clean/data/cache/NasaDataBase.kt: -------------------------------------------------------------------------------- 1 | package com.clean.data.cache 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | 6 | @Database(entities = arrayOf(AsteroidEntity::class), version = 1) 7 | abstract class NasaDataBase : RoomDatabase() { 8 | abstract fun asteroidDao(): AsteroidDao 9 | } -------------------------------------------------------------------------------- /data/src/main/java/com/clean/data/config/DataComponent.kt: -------------------------------------------------------------------------------- 1 | package com.clean.data.config 2 | 3 | import android.app.Application 4 | import com.clean.domain.asteroid.NasaRepository 5 | import com.clean.domain.asteroid.StringProvider 6 | import dagger.BindsInstance 7 | import dagger.Component 8 | import javax.inject.Scope 9 | 10 | @Component(modules = arrayOf(DataModule::class)) 11 | @DataSingleton 12 | interface DataComponent { 13 | 14 | fun provideNasaRepository(): NasaRepository 15 | 16 | fun provideStringProvider(): StringProvider 17 | 18 | @Component.Builder 19 | interface Builder { 20 | 21 | @BindsInstance 22 | fun application(application: Application): Builder 23 | fun build(): DataComponent 24 | } 25 | } 26 | 27 | @Scope 28 | @MustBeDocumented 29 | @Retention 30 | annotation class DataSingleton -------------------------------------------------------------------------------- /data/src/main/java/com/clean/data/config/DataModule.kt: -------------------------------------------------------------------------------- 1 | package com.clean.data.config 2 | 3 | import android.app.Application 4 | import androidx.room.Room 5 | import com.clean.data.BuildConfig 6 | import com.clean.data.NasaRepositoryImpl 7 | import com.clean.data.StringProviderImpl 8 | import com.clean.data.cache.DBConstants.DB_NAME 9 | import com.clean.data.cache.NasaDataBase 10 | import com.clean.data.remote.NasaService 11 | import com.clean.domain.asteroid.NasaRepository 12 | import com.clean.domain.asteroid.StringProvider 13 | import com.google.gson.Gson 14 | import dagger.Binds 15 | import dagger.Module 16 | import dagger.Provides 17 | import okhttp3.OkHttpClient 18 | import okhttp3.logging.HttpLoggingInterceptor 19 | import retrofit2.Retrofit 20 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory 21 | import retrofit2.converter.gson.GsonConverterFactory 22 | import java.util.concurrent.TimeUnit 23 | 24 | @Module(includes = [DataBindsModule::class]) 25 | object DataModule { 26 | 27 | @Provides 28 | @JvmStatic 29 | fun provideIsDebug() = BuildConfig.DEBUG 30 | 31 | @Provides 32 | @JvmStatic 33 | @DataSingleton 34 | fun provideNasaDB(application: Application): NasaDataBase { 35 | return Room.databaseBuilder( 36 | application, 37 | NasaDataBase::class.java, DB_NAME 38 | ).build() 39 | } 40 | 41 | @Provides 42 | @JvmStatic 43 | fun provideNasaService(okHttpClient: OkHttpClient, gson: Gson): NasaService { 44 | val retrofit = Retrofit.Builder() 45 | .baseUrl("https://api.nasa.gov/planetary/") 46 | .client(okHttpClient) 47 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) 48 | .addConverterFactory(GsonConverterFactory.create(gson)) 49 | .build() 50 | return retrofit.create(NasaService::class.java) 51 | } 52 | 53 | @Provides 54 | @JvmStatic 55 | fun provideOkHttpClient(httpLoggingInterceptor: HttpLoggingInterceptor): OkHttpClient { 56 | return OkHttpClient.Builder() 57 | .addInterceptor(httpLoggingInterceptor) 58 | .connectTimeout(5, TimeUnit.SECONDS) 59 | .readTimeout(5, TimeUnit.SECONDS) 60 | .build() 61 | } 62 | 63 | @Provides 64 | @JvmStatic 65 | fun provideGson(): Gson { 66 | return Gson() 67 | } 68 | 69 | @Provides 70 | @JvmStatic 71 | fun provideLoggingInterceptor(isDebug: Boolean): HttpLoggingInterceptor { 72 | val logging = HttpLoggingInterceptor() 73 | logging.level = if (isDebug) { 74 | HttpLoggingInterceptor.Level.BODY 75 | } else { 76 | HttpLoggingInterceptor.Level.NONE 77 | } 78 | return logging 79 | } 80 | } 81 | 82 | @Module 83 | interface DataBindsModule { 84 | 85 | @Binds 86 | fun bindNasaRepository(naseRepositoryImpl: NasaRepositoryImpl): NasaRepository 87 | 88 | @Binds 89 | fun bindStringProvider(stringProviderImpl: StringProviderImpl): StringProvider 90 | } 91 | -------------------------------------------------------------------------------- /data/src/main/java/com/clean/data/mapper/AsteroidMapper.kt: -------------------------------------------------------------------------------- 1 | package com.clean.data.mapper 2 | 3 | import com.clean.data.cache.AsteroidEntity 4 | import javax.inject.Inject 5 | import com.clean.data.model.Asteroid as DataAsteroid 6 | import com.clean.domain.asteroid.model.Asteroid as DomainAsteroid 7 | 8 | class AsteroidMapper @Inject constructor() { 9 | 10 | fun mapDataToDomain(dataAsteroid: DataAsteroid): DomainAsteroid { 11 | return DomainAsteroid( 12 | title = dataAsteroid.title, 13 | imageUrl = dataAsteroid.url 14 | ) 15 | } 16 | 17 | fun mapDomaintoEntity(domainAsteroid: DomainAsteroid): AsteroidEntity { 18 | return AsteroidEntity( 19 | title = domainAsteroid.title, 20 | url = domainAsteroid.imageUrl 21 | ) 22 | } 23 | 24 | fun mapEntitiyToDomain(asteroidEntity: AsteroidEntity): DomainAsteroid { 25 | return DomainAsteroid( 26 | title = asteroidEntity.title, 27 | imageUrl = asteroidEntity.url 28 | ) 29 | } 30 | } -------------------------------------------------------------------------------- /data/src/main/java/com/clean/data/model/Asteroid.kt: -------------------------------------------------------------------------------- 1 | package com.clean.data.model 2 | 3 | data class Asteroid( 4 | val copyright: String? = null, 5 | val date: String? = null, 6 | val explanation: String? = null, 7 | val hdurl: String? = null, 8 | val media_type: String? = null, 9 | val service_version: String? = null, 10 | val title: String? = null, 11 | val url: String? = null 12 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/clean/data/remote/NasaRemote.kt: -------------------------------------------------------------------------------- 1 | package com.clean.data.remote 2 | 3 | import com.clean.data.model.Asteroid 4 | import com.clean.domain.asteroid.DataError 5 | import io.reactivex.Observable 6 | import retrofit2.Response 7 | import javax.inject.Inject 8 | 9 | class NasaRemote @Inject constructor(private val nasaService: NasaService) { 10 | 11 | fun getAsteroidOfTheDay(): Observable { 12 | return nasaService.getAsteroidOfTheDay() 13 | .map { response -> 14 | if (isResponseSuccessful(response)) { 15 | return@map response.body()!! 16 | } else { 17 | val message = buildErrorMessage(response) 18 | throw DataError(message) 19 | } 20 | } 21 | } 22 | 23 | private fun buildErrorMessage(response: Response) = 24 | "there was a network error, code: ${response.code()}, message: ${response.message()}" 25 | 26 | private fun isResponseSuccessful(response: Response) = 27 | response.isSuccessful && response.body() != null 28 | } 29 | -------------------------------------------------------------------------------- /data/src/main/java/com/clean/data/remote/NasaService.kt: -------------------------------------------------------------------------------- 1 | package com.clean.data.remote 2 | 3 | import com.clean.data.model.Asteroid 4 | import io.reactivex.Observable 5 | import io.reactivex.Single 6 | import retrofit2.Response 7 | import retrofit2.http.GET 8 | 9 | interface NasaService { 10 | @GET("apod?api_key=wSNEtcl1BcDve7JggX1VQgScHRoVSmQNcLvM1rI4") 11 | fun getAsteroidOfTheDay(): Observable> 12 | 13 | } -------------------------------------------------------------------------------- /data/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | data 3 | "Error loading Asteroid from Server" 4 | "Error loading Asteroid" 5 | "Asteroid saved" 6 | "Message in a bottle" 7 | 8 | -------------------------------------------------------------------------------- /data/src/test/java/com/clean/data/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.clean.data; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /dependencies.gradle: -------------------------------------------------------------------------------- 1 | ext { 2 | //Android 3 | androidMinSdkVersion = 21 4 | androidTargetSdkVersion = 28 5 | androidCompileSdkVersion = 28 6 | 7 | //Libraries 8 | kotlinVersion = '1.3.31' 9 | rxKotlinVersion = '2.3.0' 10 | rxAndroidVersion = '2.1.1' 11 | rxJavaVersion = '2.2.8' 12 | rxRelayVersion = '2.1.0' 13 | rxBindingsVersion = '2.2.0' 14 | ktxFragmentVersion = '1.1.0-alpha09' 15 | roomVersion = '1.0.0-alpha9-1' 16 | gsonVersion = '2.8.1' 17 | okHttpVersion = '3.8.1' 18 | retrofitVersion = '2.5.0' 19 | roomVersion = '2.1.0-rc01' 20 | appcompatVersion = '1.0.2' 21 | materialVersion = '1.0.0' 22 | livecycleExtensionsVersion = '2.0.0' 23 | constraintlayoutVersion = '1.1.3' 24 | glideVersion = '4.3.1' 25 | timberVersion = '4.5.1' 26 | glideVersion = '4.0.0' 27 | daggerVersion = '2.20' 28 | javaxAnnotationVersion = '1.0' 29 | javaxInjectVersion = '1' 30 | 31 | //Testing 32 | jUnitVersion = '4.12' 33 | assertJVersion = '3.8.0' 34 | espressoVersion = '3.0.0' 35 | mockitoKotlinVersion = '1.5.0' 36 | 37 | dataDependencies = [ 38 | javaxAnnotation : "javax.annotation:jsr250-api:${javaxAnnotationVersion}", 39 | javaxInject : "javax.inject:javax.inject:${javaxInjectVersion}", 40 | okHttp : "com.squareup.okhttp3:okhttp:${okHttpVersion}", 41 | okHttpLogger : "com.squareup.okhttp3:logging-interceptor:${okHttpVersion}", 42 | gson : "com.google.code.gson:gson:${gsonVersion}", 43 | roomRuntime : "androidx.room:room-runtime:${roomVersion}", 44 | roomKotlin : "androidx.room:room-ktx:${roomVersion}", 45 | roomCompiler : "androidx.room:room-compiler:${roomVersion}", 46 | roomRxJava : "androidx.room:room-rxjava2:${roomVersion}", 47 | rxKotlin : "io.reactivex.rxjava2:rxkotlin:${rxKotlinVersion}", 48 | rxJava : "io.reactivex.rxjava2:rxjava:${rxJavaVersion}", 49 | kotlin : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${kotlinVersion}", 50 | retrofit : "com.squareup.retrofit2:retrofit:${retrofitVersion}", 51 | retrofitConverter: "com.squareup.retrofit2:converter-gson:${retrofitVersion}", 52 | retrofitAdapter : "com.squareup.retrofit2:adapter-rxjava2:${retrofitVersion}", 53 | dagger : "com.google.dagger:dagger:${daggerVersion}", 54 | daggerCompiler : "com.google.dagger:dagger-compiler:${daggerVersion}" 55 | ] 56 | 57 | dataTestDependencies = [ 58 | junit : "junit:junit:${jUnitVersion}", 59 | kotlinJUnit: "org.jetbrains.kotlin:kotlin-test-junit:${kotlin_version}", 60 | assertj : "org.assertj:assertj-core:${assertJVersion}", 61 | mockito : "com.nhaarman:mockito-kotlin:${mockitoKotlinVersion}", 62 | ] 63 | 64 | domainDependencies = [ 65 | javaxAnnotation: "javax.annotation:jsr250-api:${javaxAnnotationVersion}", 66 | javaxInject : "javax.inject:javax.inject:${javaxInjectVersion}", 67 | rxKotlin : "io.reactivex.rxjava2:rxkotlin:${rxKotlinVersion}", 68 | rxJava : "io.reactivex.rxjava2:rxjava:${rxJavaVersion}", 69 | kotlin : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${kotlinVersion}", 70 | ] 71 | 72 | domainTestDependencies = [ 73 | junit : "junit:junit:${jUnitVersion}", 74 | kotlinJUnit: "org.jetbrains.kotlin:kotlin-test-junit:${kotlin_version}", 75 | assertj : "org.assertj:assertj-core:${assertJVersion}", 76 | mockito : "com.nhaarman:mockito-kotlin:${mockitoKotlinVersion}", 77 | ] 78 | 79 | presentationDependencies = [ 80 | glide : "com.github.bumptech.glide:glide:${glideVersion}", 81 | glideCompiler : "com.github.bumptech.glide:compiler:${glideVersion}", 82 | constrainLayout : "androidx.constraintlayout:constraintlayout:${constraintlayoutVersion}", 83 | appcompat : "androidx.appcompat:appcompat:${appcompatVersion}", 84 | ktxFragment : "androidx.fragment:fragment-ktx:${ktxFragmentVersion}", 85 | lifecycleExtensions: "androidx.lifecycle:lifecycle-extensions:${livecycleExtensionsVersion}", 86 | javaxAnnotation : "javax.annotation:jsr250-api:${javaxAnnotationVersion}", 87 | javaxInject : "javax.inject:javax.inject:${javaxInjectVersion}", 88 | rxKotlin : "io.reactivex.rxjava2:rxkotlin:${rxKotlinVersion}", 89 | rxJava : "io.reactivex.rxjava2:rxjava:${rxJavaVersion}", 90 | rxRelay : "com.jakewharton.rxrelay2:rxrelay:${rxRelayVersion}", 91 | rxAndroid : "io.reactivex.rxjava2:rxandroid:${rxAndroidVersion}", 92 | rxBindings : "com.jakewharton.rxbinding2:rxbinding-kotlin:${rxBindingsVersion}", 93 | kotlin : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${kotlinVersion}", 94 | dagger : "com.google.dagger:dagger:${daggerVersion}", 95 | daggerCompiler : "com.google.dagger:dagger-compiler:${daggerVersion}", 96 | material : "com.google.android.material:material:${materialVersion}" 97 | ] 98 | 99 | presentationTestDependencies = [ 100 | junit : "junit:junit:${jUnitVersion}", 101 | kotlinJUnit: "org.jetbrains.kotlin:kotlin-test-junit:${kotlin_version}", 102 | assertj : "org.assertj:assertj-core:${assertJVersion}", 103 | mockito : "com.nhaarman:mockito-kotlin:${mockitoKotlinVersion}", 104 | ] 105 | } -------------------------------------------------------------------------------- /domain/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /domain/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'kotlin' 2 | 3 | sourceCompatibility = 1.8 4 | targetCompatibility = 1.8 5 | 6 | configurations.all { 7 | resolutionStrategy { 8 | force "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 9 | } 10 | } 11 | 12 | dependencies { 13 | def domainDependencies = rootProject.ext.domainDependencies 14 | def domainTestDependencies = rootProject.ext.domainTestDependencies 15 | 16 | implementation domainDependencies.javaxAnnotation 17 | implementation domainDependencies.javaxInject 18 | implementation domainDependencies.kotlin 19 | implementation domainDependencies.rxKotlin 20 | implementation domainDependencies.rxJava 21 | testImplementation domainTestDependencies.junit 22 | testImplementation domainTestDependencies.mockito 23 | testImplementation domainTestDependencies.assertj 24 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/clean/domain/asteroid/AsteroidViewEventHandler.kt: -------------------------------------------------------------------------------- 1 | package com.clean.domain.asteroid 2 | 3 | import com.clean.domain.asteroid.model.AsteroidViewEvent 4 | import com.clean.domain.asteroid.model.AsteroidViewResult 5 | import com.clean.domain.asteroid.usecase.AsteroidUseCase 6 | import io.reactivex.Observable 7 | import javax.inject.Inject 8 | 9 | class AsteroidViewEventHandler @Inject constructor( 10 | private val useCases: Set<@JvmSuppressWildcards AsteroidUseCase> 11 | ) { 12 | 13 | fun handleEvent(viewEvent: AsteroidViewEvent): Observable { 14 | return useCases 15 | .find { it.isForEvent(viewEvent) } 16 | .let { requireNotNull(it) { "No UseCase supports the ViewEvent: $viewEvent" } } 17 | .execute(viewEvent) 18 | } 19 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/clean/domain/asteroid/AsteroidViewFlow.kt: -------------------------------------------------------------------------------- 1 | package com.clean.domain.asteroid 2 | 3 | import com.clean.domain.asteroid.model.AsteroidViewEvent 4 | import com.clean.domain.asteroid.model.AsteroidViewResult 5 | import com.clean.domain.asteroid.model.AsteroidViewState 6 | import io.reactivex.Observable 7 | import javax.inject.Inject 8 | 9 | class AsteroidViewFlow @Inject constructor( 10 | private val eventHandler: AsteroidViewEventHandler, 11 | private val viewStateReducer: AsteroidViewStateReducer 12 | ) { 13 | 14 | fun start( 15 | eventEmitter: Observable 16 | ): Pair, Observable> { 17 | val sharedResultEmitter = eventEmitter 18 | .handleEvent(eventHandler) 19 | .share() 20 | val effectEmitter = effectEmitter(sharedResultEmitter) 21 | val viewStateEmitter = viewStateEmitter(sharedResultEmitter, viewStateReducer) 22 | return effectEmitter to viewStateEmitter 23 | } 24 | 25 | private fun Observable.handleEvent( 26 | eventToResultProcessor: AsteroidViewEventHandler 27 | ): Observable { 28 | return doOnNext { println("flow intent: $it") } 29 | .flatMap(eventToResultProcessor::handleEvent) 30 | .doOnNext { println("flow result: $it") } 31 | } 32 | 33 | private fun effectEmitter( 34 | share: Observable 35 | ): Observable { 36 | return share.ofType(AsteroidViewResult.AsteroidViewEffect::class.java) 37 | } 38 | 39 | private fun viewStateEmitter( 40 | share: Observable, 41 | viewStateReducer: AsteroidViewStateReducer 42 | ): Observable { 43 | return share.ofType(AsteroidViewResult.AsteroidPartialState::class.java) 44 | .compose(viewStateReducer.reduce()) 45 | .doOnNext { println("flow viewstate: $it") } 46 | .distinctUntilChanged() 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/clean/domain/asteroid/AsteroidViewStateReducer.kt: -------------------------------------------------------------------------------- 1 | package com.clean.domain.asteroid 2 | 3 | import com.clean.domain.asteroid.model.AsteroidViewResult 4 | import com.clean.domain.asteroid.model.AsteroidViewState 5 | import com.clean.domain.asteroid.model.ViewData 6 | import io.reactivex.ObservableTransformer 7 | import javax.inject.Inject 8 | 9 | class AsteroidViewStateReducer @Inject constructor() { 10 | 11 | fun reduce(): ObservableTransformer { 12 | return ObservableTransformer { partialStateObservable -> 13 | partialStateObservable.scan(AsteroidViewState.init()) 14 | { oldviewstate: AsteroidViewState, result: AsteroidViewResult.AsteroidPartialState -> 15 | when (result) { 16 | is AsteroidViewResult.AsteroidPartialState.NewAsteroid -> { 17 | oldviewstate.copy(data = ViewData(result.asteroid), loading = false, errorMessage = null) 18 | } 19 | is AsteroidViewResult.AsteroidPartialState.Error -> { 20 | oldviewstate.copy(errorMessage = result.message, loading = false, data = null) 21 | } 22 | AsteroidViewResult.AsteroidPartialState.Loading -> { 23 | oldviewstate.copy(loading = true, data = null, errorMessage = null) 24 | } 25 | } 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/clean/domain/asteroid/NasaRepository.kt: -------------------------------------------------------------------------------- 1 | package com.clean.domain.asteroid 2 | 3 | import com.clean.domain.asteroid.model.Asteroid 4 | import io.reactivex.Completable 5 | import io.reactivex.Observable 6 | 7 | interface NasaRepository { 8 | 9 | fun getAsteroidOfTheDay(): Observable 10 | 11 | fun getSavedAsteroid(): Observable> 12 | 13 | fun saveAsteroid(asteroid: Asteroid): Completable 14 | } 15 | 16 | class DataError(message: String) : Exception(message) -------------------------------------------------------------------------------- /domain/src/main/java/com/clean/domain/asteroid/StringProvider.kt: -------------------------------------------------------------------------------- 1 | package com.clean.domain.asteroid 2 | 3 | interface StringProvider { 4 | 5 | val serverError: String 6 | val generalError: String 7 | val storeAsteroidSuccess: String 8 | val messageInABottle: String 9 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/clean/domain/asteroid/model/Asteroid.kt: -------------------------------------------------------------------------------- 1 | package com.clean.domain.asteroid.model 2 | 3 | data class Asteroid( 4 | val title: String? = null, 5 | val imageUrl: String? = null 6 | ) -------------------------------------------------------------------------------- /domain/src/main/java/com/clean/domain/asteroid/model/AsteroidViewEvent.kt: -------------------------------------------------------------------------------- 1 | package com.clean.domain.asteroid.model 2 | 3 | sealed class AsteroidViewEvent { 4 | data class Store(val asteroid: Asteroid) : AsteroidViewEvent() 5 | object Load : AsteroidViewEvent() 6 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/clean/domain/asteroid/model/AsteroidViewResult.kt: -------------------------------------------------------------------------------- 1 | package com.clean.domain.asteroid.model 2 | 3 | sealed class AsteroidViewResult { 4 | 5 | sealed class AsteroidPartialState : AsteroidViewResult() { 6 | data class NewAsteroid(val asteroid: Asteroid) : AsteroidPartialState() 7 | data class Error(val message: String) : AsteroidPartialState() 8 | object Loading : AsteroidPartialState() 9 | } 10 | 11 | sealed class AsteroidViewEffect : AsteroidViewResult() { 12 | data class UserMessage(val message: String) : AsteroidViewEffect() 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /domain/src/main/java/com/clean/domain/asteroid/model/AsteroidViewState.kt: -------------------------------------------------------------------------------- 1 | package com.clean.domain.asteroid.model 2 | 3 | data class AsteroidViewState( 4 | val loading: Boolean = false, 5 | val errorMessage: String? = null, 6 | val data: ViewData? = null 7 | ) { 8 | companion object { 9 | fun init(): AsteroidViewState { 10 | return AsteroidViewState(false, null, null) 11 | } 12 | } 13 | } 14 | 15 | data class ViewData(val asteroid: Asteroid) -------------------------------------------------------------------------------- /domain/src/main/java/com/clean/domain/asteroid/usecase/AsteroidUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.clean.domain.asteroid.usecase 2 | 3 | import com.clean.domain.asteroid.model.AsteroidViewEvent 4 | import com.clean.domain.asteroid.model.AsteroidViewResult 5 | import io.reactivex.Observable 6 | 7 | interface AsteroidUseCase { 8 | 9 | fun execute(event: T): Observable 10 | 11 | fun isForEvent(event: AsteroidViewEvent): Boolean 12 | 13 | } 14 | -------------------------------------------------------------------------------- /domain/src/main/java/com/clean/domain/asteroid/usecase/GetAsteroidOfTheDay.kt: -------------------------------------------------------------------------------- 1 | package com.clean.domain.asteroid.usecase 2 | 3 | import com.clean.domain.asteroid.NasaRepository 4 | import com.clean.domain.asteroid.StringProvider 5 | import com.clean.domain.asteroid.model.AsteroidViewEvent 6 | import com.clean.domain.asteroid.model.AsteroidViewResult 7 | import io.reactivex.Observable 8 | import javax.inject.Inject 9 | 10 | class GetAsteroidOfTheDay @Inject constructor( 11 | private val nasaRepository: NasaRepository, 12 | private val stringProvider: StringProvider 13 | ) : AsteroidUseCase { 14 | 15 | override fun isForEvent(event: AsteroidViewEvent): Boolean { 16 | return event is AsteroidViewEvent.Load 17 | } 18 | 19 | override fun execute(event: AsteroidViewEvent.Load): Observable { 20 | return Observable.concat(emitLoading(), emitAsteroid()) 21 | .onErrorReturnItem(AsteroidViewResult.AsteroidPartialState.Error(stringProvider.generalError)) 22 | .doOnError { println(it.message) } 23 | } 24 | 25 | private fun emitLoading(): Observable { 26 | return Observable.just(AsteroidViewResult.AsteroidPartialState.Loading) 27 | } 28 | 29 | private fun emitAsteroid(): Observable { 30 | return nasaRepository.getAsteroidOfTheDay() 31 | .map { AsteroidViewResult.AsteroidPartialState.NewAsteroid(it) } 32 | } 33 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/clean/domain/asteroid/usecase/SaveAsteroid.kt: -------------------------------------------------------------------------------- 1 | package com.clean.domain.asteroid.usecase 2 | 3 | import com.clean.domain.asteroid.NasaRepository 4 | import com.clean.domain.asteroid.StringProvider 5 | import com.clean.domain.asteroid.model.AsteroidViewEvent 6 | import com.clean.domain.asteroid.model.AsteroidViewResult 7 | import io.reactivex.Observable 8 | import javax.inject.Inject 9 | 10 | class SaveAsteroid @Inject constructor( 11 | private val nasaRepository: NasaRepository, 12 | private val stringProvider: StringProvider 13 | ) : AsteroidUseCase { 14 | 15 | override fun isForEvent(event: AsteroidViewEvent): Boolean { 16 | return event is AsteroidViewEvent.Store 17 | } 18 | 19 | override fun execute(event: AsteroidViewEvent.Store): Observable { 20 | return Observable.concat(saveAsteroid(event), emitUserMessageEffect()) 21 | .onErrorReturnItem(AsteroidViewResult.AsteroidViewEffect.UserMessage(stringProvider.generalError)) 22 | .doOnError { println(it.message) } 23 | } 24 | 25 | private fun saveAsteroid(event: AsteroidViewEvent.Store): Observable { 26 | return nasaRepository.saveAsteroid(event.asteroid).toObservable() 27 | } 28 | 29 | private fun emitUserMessageEffect(): Observable { 30 | return Observable.just(AsteroidViewResult.AsteroidViewEffect.UserMessage(stringProvider.storeAsteroidSuccess)) 31 | } 32 | } -------------------------------------------------------------------------------- /domain/src/test/kotlin/com/clean/domain/asteroid/AsteroidViewFlowTest.kt: -------------------------------------------------------------------------------- 1 | package com.clean.domain.asteroid 2 | 3 | import com.clean.domain.asteroid.model.Asteroid 4 | import com.clean.domain.asteroid.model.AsteroidViewEvent 5 | import com.clean.domain.asteroid.model.AsteroidViewResult 6 | import com.clean.domain.asteroid.model.AsteroidViewState 7 | import com.clean.domain.asteroid.usecase.AsteroidUseCase 8 | import com.clean.domain.asteroid.usecase.GetAsteroidOfTheDay 9 | import com.clean.domain.asteroid.usecase.SaveAsteroid 10 | import io.reactivex.Observable 11 | import io.reactivex.subjects.PublishSubject 12 | import org.assertj.core.api.Assertions 13 | import org.junit.Test 14 | 15 | class AsteroidViewFlowTest { 16 | 17 | private val repository: NasaRepository = NasaRepositoryFake() 18 | 19 | private val stringProvider: StringProvider = StringProviderFake() 20 | 21 | private val asteroidViewFlow: AsteroidViewFlow = asteroidViewFlow() 22 | 23 | @Test 24 | fun `when event is store then return effect user message`() { 25 | val (effectsEmiter, viewstateEmiter) = 26 | asteroidViewFlow.start(Observable.just(AsteroidViewEvent.Store(asteroid()))) 27 | 28 | emitsUserMessageEffect(effectsEmiter) 29 | } 30 | 31 | @Test 32 | fun `when event is store then save Asteroid`() { 33 | val (effectsEmiter, viewstateEmiter) = 34 | asteroidViewFlow.start(Observable.just(AsteroidViewEvent.Store(asteroid()))) 35 | 36 | val viewStateSubscriber = viewstateEmiter.test() 37 | 38 | asteroidIsStored(asteroid()) 39 | } 40 | 41 | @Test 42 | fun `when event is store then viewState is not changing`() { 43 | val viewEventEmiter = PublishSubject.create() 44 | val (effectsEmiter, viewstateEmiter) = 45 | asteroidViewFlow.start(viewEventEmiter) 46 | 47 | val viewStateTestSubscriber = viewstateEmiter.test() 48 | viewEventEmiter.onNext(AsteroidViewEvent.Load) 49 | val oldItemCount = viewStateTestSubscriber.valueCount() 50 | viewEventEmiter.onNext(AsteroidViewEvent.Store(asteroid())) 51 | val newItemCount = viewStateTestSubscriber.valueCount() 52 | Assertions.assertThat(oldItemCount).isEqualTo(newItemCount) 53 | } 54 | 55 | @Test 56 | fun `when event is load then return effect user message`() { 57 | val (effectsEmiter, viewstateEmiter) = 58 | asteroidViewFlow.start(Observable.just(AsteroidViewEvent.Load)) 59 | 60 | emitsNoEffects(effectsEmiter) 61 | } 62 | 63 | @Test 64 | fun `when event is load then return partial states init, loading, data`() { 65 | val (effectsEmiter, viewstate) = 66 | asteroidViewFlow.start(Observable.just(AsteroidViewEvent.Load)) 67 | 68 | val viewStateSubscriber = viewstate.test() 69 | viewStateSubscriber.assertValueCount(3) 70 | viewStateSubscriber.assertValueAt(0, AsteroidViewState.init()) 71 | viewStateSubscriber.assertValueAt(1, { asteroidViewState -> isLoading(asteroidViewState) }) 72 | viewStateSubscriber.assertValueAt(2, { asteroidViewState -> hasData(asteroidViewState) }) 73 | } 74 | 75 | @Test 76 | fun `when event is second load then return partial states loading, data`() { 77 | val (effectsEmiter, viewstate) = 78 | asteroidViewFlow.start(Observable.just(AsteroidViewEvent.Load, AsteroidViewEvent.Load)) 79 | 80 | val viewStateSubscriber = viewstate.test() 81 | viewStateSubscriber.assertValueCount(5) 82 | viewStateSubscriber.assertValueAt(3, { asteroidViewState -> isLoading(asteroidViewState) }) 83 | viewStateSubscriber.assertValueAt(4, { asteroidViewState -> hasData(asteroidViewState) }) 84 | } 85 | 86 | private fun asteroid() = Asteroid("title", "url") 87 | 88 | private fun asteroidIsStored(asteroid: Asteroid) { 89 | val savedAsteroids = repository.getSavedAsteroid().test() 90 | savedAsteroids.assertValueAt(0, { asteroids -> asteroids.contains(asteroid) }) 91 | } 92 | 93 | private fun emitsUserMessageEffect(effects: Observable) { 94 | val effectsSubscriber = effects.test() 95 | effectsSubscriber.assertValueCount(1) 96 | .assertValue(AsteroidViewResult.AsteroidViewEffect.UserMessage(stringProvider.storeAsteroidSuccess)) 97 | } 98 | 99 | private fun emitsNoEffects(effects: Observable) { 100 | val effectsSubscriber = effects.test() 101 | effectsSubscriber.assertValueCount(0) 102 | } 103 | 104 | private fun isLoading(asteroidViewState: AsteroidViewState): Boolean = 105 | asteroidViewState.loading == true && asteroidViewState.errorMessage == null && asteroidViewState.data == null 106 | 107 | private fun hasData(asteroidViewState: AsteroidViewState) = 108 | asteroidViewState.loading == false && asteroidViewState.errorMessage == null && asteroidViewState.data != null 109 | 110 | private fun asteroidViewFlow(): AsteroidViewFlow { 111 | 112 | return AsteroidViewFlow( 113 | AsteroidViewEventHandler( 114 | setOf( 115 | GetAsteroidOfTheDay( 116 | nasaRepository = repository, 117 | stringProvider = stringProvider 118 | ) as AsteroidUseCase, 119 | SaveAsteroid( 120 | nasaRepository = repository, 121 | stringProvider = stringProvider 122 | ) as AsteroidUseCase 123 | ) 124 | ), 125 | AsteroidViewStateReducer() 126 | ) 127 | } 128 | } -------------------------------------------------------------------------------- /domain/src/test/kotlin/com/clean/domain/asteroid/NasaRepositoryFake.kt: -------------------------------------------------------------------------------- 1 | package com.clean.domain.asteroid 2 | 3 | import com.clean.domain.asteroid.model.Asteroid 4 | import io.reactivex.Completable 5 | import io.reactivex.Observable 6 | 7 | class NasaRepositoryFake : NasaRepository { 8 | 9 | val cache: MutableList = mutableListOf() 10 | 11 | override fun saveAsteroid(asteroid: Asteroid): Completable { 12 | cache.add(asteroid) 13 | return Completable.complete() 14 | } 15 | 16 | override fun getAsteroidOfTheDay(): Observable { 17 | return Observable.just(Asteroid(title = "title", imageUrl = "imageUrl")) 18 | } 19 | 20 | override fun getSavedAsteroid(): Observable> { 21 | return Observable.just(cache) 22 | } 23 | } -------------------------------------------------------------------------------- /domain/src/test/kotlin/com/clean/domain/asteroid/StringProviderFake.kt: -------------------------------------------------------------------------------- 1 | package com.clean.domain.asteroid 2 | 3 | class StringProviderFake() : StringProvider { 4 | 5 | override val serverError: String 6 | get() = "serverError" 7 | override val generalError: String 8 | get() = "generalError" 9 | override val storeAsteroidSuccess: String 10 | get() = "storeAsteroidSuccess" 11 | override val messageInABottle: String 12 | get() = "messageInABottle" 13 | } -------------------------------------------------------------------------------- /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 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 10 | org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m 11 | # When configured, Gradle will run in incubating parallel mode. 12 | # This option should only be used with decoupled projects. More details, visit 13 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 14 | # org.gradle.parallel=true 15 | # Kotlin code style for this project: "official" or "obsolete": 16 | kotlin.code.style=official 17 | android.useAndroidX=true 18 | android.enableJetifier=true 19 | # When configured, Gradle will run in incubating parallel mode. 20 | # This option should only be used with decoupled projects. More details, visit 21 | org.gradle.parallel=true 22 | # When set to true the Gradle daemon is used to run the build. For local developer builds this is our favorite property. 23 | # The developer environment is optimized for speed and feedback so we nearly always run Gradle jobs with the daemon. 24 | org.gradle.daemon=true 25 | kapt.incremental.apt=true 26 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdrianoCelentano/CleanProject/ab415e4d84025175ae578a5c9ac3861cee84bfde/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri May 03 13:22:22 CEST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip 7 | -------------------------------------------------------------------------------- /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.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 28 11 | 12 | 13 | defaultConfig { 14 | minSdkVersion 21 15 | targetSdkVersion 28 16 | versionCode 1 17 | versionName "1.0" 18 | 19 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 20 | 21 | } 22 | 23 | buildTypes { 24 | release { 25 | minifyEnabled false 26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 27 | } 28 | } 29 | 30 | compileOptions { 31 | sourceCompatibility = 1.8 32 | targetCompatibility = 1.8 33 | } 34 | 35 | } 36 | 37 | kapt { 38 | javacOptions { 39 | // Increase the max count of errors from annotation processors. 40 | // Default is 100. 41 | option("-Xmaxerrs", 500) 42 | } 43 | } 44 | 45 | configurations.all { 46 | resolutionStrategy { 47 | force "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 48 | } 49 | } 50 | 51 | dependencies { 52 | def presentationDependencies = rootProject.ext.presentationDependencies 53 | def domainTestDependencies = rootProject.ext.presentationTestDependencies 54 | 55 | implementation project(path: ':domain') 56 | implementation presentationDependencies.material 57 | implementation presentationDependencies.appcompat 58 | implementation presentationDependencies.lifecycleExtensions 59 | implementation presentationDependencies.glide 60 | implementation presentationDependencies.ktxFragment 61 | kapt presentationDependencies.glideCompiler 62 | implementation presentationDependencies.constrainLayout 63 | implementation presentationDependencies.javaxAnnotation 64 | implementation presentationDependencies.javaxInject 65 | implementation presentationDependencies.kotlin 66 | implementation presentationDependencies.rxKotlin 67 | implementation presentationDependencies.rxJava 68 | implementation presentationDependencies.rxRelay 69 | implementation presentationDependencies.rxAndroid 70 | implementation presentationDependencies.rxBindings 71 | implementation presentationDependencies.dagger 72 | kapt presentationDependencies.daggerCompiler 73 | 74 | testImplementation domainTestDependencies.junit 75 | testImplementation domainTestDependencies.mockito 76 | testImplementation domainTestDependencies.assertj 77 | } 78 | -------------------------------------------------------------------------------- /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/clean/asteroids/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.clean.asteroids; 2 | 3 | import android.content.Context; 4 | import androidx.test.InstrumentationRegistry; 5 | import androidx.test.runner.AndroidJUnit4; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | 9 | import static org.junit.Assert.assertEquals; 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * @see Testing documentation 15 | */ 16 | @RunWith(AndroidJUnit4.class) 17 | public class ExampleInstrumentedTest { 18 | @Test 19 | public void useAppContext() { 20 | // Context of the app under test. 21 | Context appContext = InstrumentationRegistry.getTargetContext(); 22 | 23 | assertEquals("com.clean.asteroids.test", appContext.getPackageName()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /presentation/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/clean/asteroids/AsteroidActivity.kt: -------------------------------------------------------------------------------- 1 | package com.clean.asteroids 2 | 3 | import android.os.Bundle 4 | import androidx.activity.viewModels 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.lifecycle.ViewModelProvider 7 | import com.clean.asteroids.config.CoreComponentProvider 8 | import com.clean.asteroids.config.DaggerPresentationComponent 9 | import com.clean.asteroids.config.PresentationComponent 10 | import com.clean.domain.asteroid.model.AsteroidViewEvent 11 | import com.clean.domain.asteroid.model.AsteroidViewState 12 | import com.jakewharton.rxbinding2.view.clicks 13 | import io.reactivex.Observable 14 | import io.reactivex.android.schedulers.AndroidSchedulers 15 | import io.reactivex.disposables.CompositeDisposable 16 | import io.reactivex.rxkotlin.addTo 17 | import io.reactivex.rxkotlin.subscribeBy 18 | import io.reactivex.schedulers.Schedulers 19 | import kotlinx.android.synthetic.main.activity_main.* 20 | import javax.inject.Inject 21 | import androidx.lifecycle.Observer as LifecycleObserver 22 | 23 | class AsteroidActivity : AppCompatActivity() { 24 | 25 | @Inject 26 | lateinit var viewModelFactory: ViewModelProvider.Factory 27 | 28 | @Inject 29 | lateinit var asteroidViewRenderer: AsteroidViewRenderer 30 | 31 | @Inject 32 | lateinit var asteroidEffectHandler: AsteroidEffectHandler 33 | 34 | private val asteroidViewModel: AsteroidViewModel by viewModels(::viewModelFactory) 35 | 36 | private var component: PresentationComponent? = null 37 | 38 | private val disposables = CompositeDisposable() 39 | 40 | override fun onCreate(savedInstanceState: Bundle?) { 41 | injectMembers() 42 | super.onCreate(savedInstanceState) 43 | setContentView(R.layout.activity_main) 44 | observeModel() 45 | } 46 | 47 | override fun onResume() { 48 | super.onResume() 49 | observeEvents() 50 | observeEffects() 51 | } 52 | 53 | override fun onPause() { 54 | super.onPause() 55 | disposables.clear() 56 | } 57 | 58 | override fun onDestroy() { 59 | super.onDestroy() 60 | component = null 61 | } 62 | 63 | private fun observeEvents() { 64 | Observable.merge(RefreshButtonObservable(), StoreButtonObservable()) 65 | .observeOn(Schedulers.io()) 66 | .subscribeBy( 67 | onNext = { asteroidViewModel.processEvent(it) } 68 | ).addTo(disposables) 69 | } 70 | 71 | private fun observeModel() { 72 | asteroidViewModel.viewStateLive.observe(this, object : LifecycleObserver { 73 | override fun onChanged(asteroidViewState: AsteroidViewState) { 74 | asteroidViewRenderer.render(asteroidViewState) 75 | } 76 | }) 77 | } 78 | 79 | private fun observeEffects() { 80 | asteroidViewModel.viewEffectEmitter 81 | .subscribeOn(Schedulers.io()) 82 | .observeOn(AndroidSchedulers.mainThread()) 83 | .subscribeBy( 84 | onNext = { effect -> 85 | asteroidEffectHandler.handleEffect(effect) 86 | } 87 | ).addTo(disposables) 88 | } 89 | 90 | private fun StoreButtonObservable(): Observable { 91 | return StoreButton.clicks() 92 | .filter { asteroidViewModel.asteroidOfTheDay != null } 93 | .map { AsteroidViewEvent.Store(asteroidViewModel.asteroidOfTheDay!!) } 94 | } 95 | 96 | private fun RefreshButtonObservable(): Observable { 97 | return RefreshButton.clicks() 98 | .map { AsteroidViewEvent.Load } 99 | } 100 | 101 | private fun injectMembers() { 102 | component = DaggerPresentationComponent.builder() 103 | .activity(this) 104 | .coreComponent((application as CoreComponentProvider).provide()) 105 | .build() 106 | .also { it.inject(this) } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/clean/asteroids/AsteroidEffectHandler.kt: -------------------------------------------------------------------------------- 1 | package com.clean.asteroids 2 | 3 | import android.app.Activity 4 | import android.widget.Toast 5 | import com.clean.asteroids.config.ActivityScope 6 | import com.clean.domain.asteroid.model.AsteroidViewResult 7 | import javax.inject.Inject 8 | 9 | @ActivityScope 10 | class AsteroidEffectHandler @Inject constructor(private val activity: Activity) { 11 | 12 | fun handleEffect(effect: AsteroidViewResult.AsteroidViewEffect?) { 13 | when (effect) { 14 | is AsteroidViewResult.AsteroidViewEffect.UserMessage -> showToast(effect) 15 | } 16 | } 17 | 18 | private fun showToast(effect: AsteroidViewResult.AsteroidViewEffect.UserMessage) { 19 | Toast.makeText(activity, effect.message, Toast.LENGTH_SHORT).show() 20 | } 21 | 22 | 23 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/clean/asteroids/AsteroidViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.clean.asteroids 2 | 3 | import android.util.Log 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.ViewModel 7 | import com.clean.domain.asteroid.AsteroidViewFlow 8 | import com.clean.domain.asteroid.model.Asteroid 9 | import com.clean.domain.asteroid.model.AsteroidViewEvent 10 | import com.clean.domain.asteroid.model.AsteroidViewResult 11 | import com.clean.domain.asteroid.model.AsteroidViewState 12 | import com.jakewharton.rxrelay2.PublishRelay 13 | import io.reactivex.Observable 14 | import io.reactivex.disposables.CompositeDisposable 15 | import io.reactivex.rxkotlin.addTo 16 | import io.reactivex.rxkotlin.subscribeBy 17 | import io.reactivex.schedulers.Schedulers 18 | import javax.inject.Inject 19 | 20 | class AsteroidViewModel @Inject constructor( 21 | asteroidViewFlow: AsteroidViewFlow 22 | ) : ViewModel() { 23 | 24 | val viewStateLive: LiveData 25 | get() = viewStateMutableLive 26 | 27 | private val viewStateMutableLive by lazy { MutableLiveData() } 28 | 29 | val viewEffectEmitter: Observable 30 | 31 | val asteroidOfTheDay: Asteroid? get() = viewStateLive.value?.data?.asteroid 32 | 33 | private val eventRelay: PublishRelay = PublishRelay.create() 34 | 35 | private val eventEmitter get() = eventRelay.hide().startWith(AsteroidViewEvent.Load) 36 | 37 | private val disposables: CompositeDisposable = CompositeDisposable() 38 | 39 | init { 40 | val (effectEmitter, viewStateEmitter) = 41 | asteroidViewFlow.start(eventEmitter) 42 | 43 | observeViewState(viewStateEmitter) 44 | 45 | this.viewEffectEmitter = effectEmitter 46 | } 47 | 48 | fun processEvent(viewEvent: AsteroidViewEvent) { 49 | eventRelay.accept(viewEvent) 50 | } 51 | 52 | private fun observeViewState(viewStateEmitter: Observable) { 53 | viewStateEmitter 54 | .subscribeOn(Schedulers.io()) 55 | .subscribeBy( 56 | onNext = { viewState -> viewStateMutableLive.postValue(viewState) }, 57 | onError = { error -> Log.e("qwer", "error viewstate", error) } 58 | ).addTo(disposables) 59 | } 60 | 61 | override fun onCleared() { 62 | super.onCleared() 63 | disposables.clear() 64 | } 65 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/clean/asteroids/AsteroidViewRenderer.kt: -------------------------------------------------------------------------------- 1 | package com.clean.asteroids 2 | 3 | import android.app.Activity 4 | import android.view.View 5 | import android.widget.ImageView 6 | import android.widget.ProgressBar 7 | import android.widget.TextView 8 | import androidx.constraintlayout.widget.ConstraintLayout 9 | import com.bumptech.glide.Glide 10 | import com.clean.asteroids.config.ActivityScope 11 | import com.clean.domain.asteroid.model.AsteroidViewState 12 | import com.clean.domain.asteroid.model.ViewData 13 | import com.google.android.material.snackbar.Snackbar 14 | import javax.inject.Inject 15 | 16 | @ActivityScope 17 | class AsteroidViewRenderer @Inject constructor(private val activity: Activity) { 18 | 19 | private val loadingIndicator: ProgressBar by lazy(LazyThreadSafetyMode.NONE) 20 | { activity.findViewById(R.id.loadingIndicator) } 21 | 22 | private val asteroidTitleTextView: TextView by lazy(LazyThreadSafetyMode.NONE) 23 | { activity.findViewById(R.id.asteroidTitle) } 24 | 25 | private val asteroidImage: ImageView by lazy(LazyThreadSafetyMode.NONE) 26 | { activity.findViewById(R.id.asteroidImage) } 27 | 28 | private val rootLayout: ConstraintLayout by lazy(LazyThreadSafetyMode.NONE) 29 | { activity.findViewById(R.id.rootLayout) } 30 | 31 | private var errorSnackBar: Snackbar? = null 32 | 33 | fun render(asteroidViewState: AsteroidViewState) { 34 | renderLoading(asteroidViewState.loading) 35 | renderData(asteroidViewState.data) 36 | renderError(asteroidViewState.errorMessage) 37 | } 38 | 39 | private fun renderData(viewData: ViewData?) { 40 | if (viewData != null) { 41 | showData(viewData) 42 | } else { 43 | hideData() 44 | } 45 | } 46 | 47 | private fun renderError(errorMessage: String?) { 48 | if (errorMessage.isNullOrBlank()) { 49 | hideErrorMessage() 50 | } else { 51 | showErrorMessage(errorMessage) 52 | } 53 | } 54 | 55 | private fun renderLoading(loading: Boolean) { 56 | if (loading) { 57 | showLoading() 58 | } else { 59 | hideLoading() 60 | } 61 | } 62 | 63 | private fun hideData() { 64 | asteroidTitleTextView.visibility = View.INVISIBLE 65 | asteroidImage.visibility = View.INVISIBLE 66 | } 67 | 68 | private fun showData(viewData: ViewData) { 69 | val asteroid = viewData.asteroid 70 | asteroidTitleTextView.text = asteroid.title 71 | Glide.with(activity).load(asteroid.imageUrl).into(asteroidImage); 72 | asteroidTitleTextView.visibility = View.VISIBLE 73 | asteroidImage.visibility = View.VISIBLE 74 | } 75 | 76 | private fun showErrorMessage(errorMessage: String) { 77 | hideLoading() 78 | hideData() 79 | showSnackBar(errorMessage) 80 | } 81 | 82 | private fun hideErrorMessage() { 83 | errorSnackBar?.dismiss() 84 | } 85 | 86 | private fun showLoading() { 87 | loadingIndicator.visibility = View.VISIBLE 88 | } 89 | 90 | private fun hideLoading() { 91 | loadingIndicator.visibility = View.INVISIBLE 92 | } 93 | 94 | private fun showSnackBar(errorMessage: String) { 95 | errorSnackBar = Snackbar.make(rootLayout, errorMessage, Snackbar.LENGTH_INDEFINITE) 96 | .also { it.show() }; 97 | } 98 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/clean/asteroids/ViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.clean.asteroids 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import javax.inject.Inject 6 | import javax.inject.Provider 7 | 8 | @Suppress("UNCHECKED_CAST") 9 | class ViewModelFactory 10 | @Inject constructor( 11 | private val creators: Map, @JvmSuppressWildcards Provider> 12 | ) : ViewModelProvider.Factory { 13 | 14 | override fun create(modelClass: Class): T { 15 | var creator: Provider? = creators[modelClass] 16 | if (creator == null) { 17 | creator = creators.entries 18 | .find { modelClass.isAssignableFrom(it.key) }?.value 19 | } 20 | if (creator == null) { 21 | throw IllegalArgumentException("unknown model class " + modelClass) 22 | } 23 | try { 24 | return creator.get() as T 25 | } catch (e: Exception) { 26 | throw RuntimeException(e) 27 | } 28 | 29 | } 30 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/clean/asteroids/config/CoreComponent.kt: -------------------------------------------------------------------------------- 1 | package com.clean.asteroids.config 2 | 3 | import com.clean.domain.asteroid.AsteroidViewFlow 4 | 5 | interface CoreComponent { 6 | 7 | fun provideAsteriodFlow(): AsteroidViewFlow 8 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/clean/asteroids/config/CoreComponentProvider.kt: -------------------------------------------------------------------------------- 1 | package com.clean.asteroids.config 2 | 3 | interface CoreComponentProvider { 4 | 5 | fun provide(): CoreComponent 6 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/clean/asteroids/config/PresentationComponent.kt: -------------------------------------------------------------------------------- 1 | package com.clean.asteroids.config 2 | 3 | import com.clean.asteroids.AsteroidActivity 4 | import dagger.BindsInstance 5 | import dagger.Component 6 | 7 | @Component( 8 | modules = arrayOf(PresentationModule::class), 9 | dependencies = arrayOf(CoreComponent::class) 10 | ) 11 | @ActivityScope 12 | interface PresentationComponent { 13 | 14 | fun inject(asteroidActivity: AsteroidActivity) 15 | 16 | @Component.Builder 17 | interface Builder { 18 | 19 | fun coreComponent(coreComponent: CoreComponent): Builder 20 | 21 | @BindsInstance 22 | fun activity(asteroidActivity: AsteroidActivity): Builder 23 | 24 | fun build(): PresentationComponent 25 | } 26 | } -------------------------------------------------------------------------------- /presentation/src/main/java/com/clean/asteroids/config/PresentationModule.kt: -------------------------------------------------------------------------------- 1 | package com.clean.asteroids.config 2 | 3 | import android.app.Activity 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.ViewModelProvider 6 | import com.clean.asteroids.AsteroidActivity 7 | import com.clean.asteroids.AsteroidViewModel 8 | import com.clean.asteroids.ViewModelFactory 9 | import dagger.Binds 10 | import dagger.Module 11 | import dagger.multibindings.ClassKey 12 | import dagger.multibindings.IntoMap 13 | 14 | @Module(includes = [PresentationBindsModule::class]) 15 | object PresentationModule { 16 | 17 | 18 | } 19 | 20 | @Module 21 | interface PresentationBindsModule { 22 | 23 | @Binds 24 | @IntoMap 25 | @ClassKey(AsteroidViewModel::class) 26 | fun bindMainViewModel(asteroidViewModel: AsteroidViewModel): ViewModel 27 | 28 | @Binds 29 | fun bindViewModelFactory(viewModelFactory: ViewModelFactory): ViewModelProvider.Factory 30 | 31 | @Binds 32 | fun bindActivity(asteroidActivity: AsteroidActivity): Activity 33 | } 34 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/clean/asteroids/config/Scopes.kt: -------------------------------------------------------------------------------- 1 | package com.clean.asteroids.config 2 | 3 | import javax.inject.Scope 4 | 5 | @Scope 6 | @MustBeDocumented 7 | @Retention 8 | annotation class ActivityScope 9 | -------------------------------------------------------------------------------- /presentation/src/main/java/com/clean/asteroids/config/ViewModelKey.kt: -------------------------------------------------------------------------------- 1 | package com.clean.asteroids.config 2 | 3 | import androidx.lifecycle.ViewModel 4 | import dagger.MapKey 5 | import kotlin.reflect.KClass 6 | 7 | @MustBeDocumented 8 | @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) 9 | @kotlin.annotation.Retention(AnnotationRetention.RUNTIME) 10 | @MapKey 11 | annotation class ViewModelKey(val value: KClass) -------------------------------------------------------------------------------- /presentation/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 18 | 19 | 26 |