├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── colors.xml │ │ │ │ └── styles.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 │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ │ └── drawable │ │ │ │ └── ic_launcher_background.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── clean │ │ │ └── project │ │ │ ├── AppComponent.kt │ │ │ ├── NasaApp.kt │ │ │ └── AppModule.kt │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── clean │ │ │ └── project │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── com │ │ └── clean │ │ └── project │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro └── build.gradle ├── data ├── .gitignore ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── clean │ │ │ │ └── data │ │ │ │ ├── cache │ │ │ │ ├── DBConstants.kt │ │ │ │ ├── NasaDataBase.kt │ │ │ │ ├── AsteroidEntity.kt │ │ │ │ ├── NasaCache.kt │ │ │ │ └── AsteroidDao.kt │ │ │ │ ├── model │ │ │ │ └── Asteroid.kt │ │ │ │ ├── remote │ │ │ │ ├── NasaService.kt │ │ │ │ └── NasaRemote.kt │ │ │ │ ├── StringProviderImpl.kt │ │ │ │ ├── config │ │ │ │ ├── DataComponent.kt │ │ │ │ └── DataModule.kt │ │ │ │ ├── mapper │ │ │ │ └── AsteroidMapper.kt │ │ │ │ └── NasaRepositoryImpl.kt │ │ └── res │ │ │ └── values │ │ │ └── strings.xml │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── clean │ │ │ └── data │ │ │ └── ExampleUnitTest.java │ └── androidTest │ │ └── java │ │ └── com │ │ └── clean │ │ └── data │ │ └── ExampleInstrumentedTest.java ├── proguard-rules.pro └── build.gradle ├── domain ├── .gitignore ├── src │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── clean │ │ │ └── domain │ │ │ └── asteroid │ │ │ ├── model │ │ │ ├── Asteroid.kt │ │ │ ├── AsteroidViewEvent.kt │ │ │ ├── AsteroidViewState.kt │ │ │ └── AsteroidViewResult.kt │ │ │ ├── StringProvider.kt │ │ │ ├── usecase │ │ │ ├── AsteroidUseCase.kt │ │ │ ├── GetAsteroidOfTheDay.kt │ │ │ └── SaveAsteroid.kt │ │ │ ├── NasaRepository.kt │ │ │ ├── AsteroidViewEventHandler.kt │ │ │ ├── AsteroidViewStateReducer.kt │ │ │ └── AsteroidViewFlow.kt │ └── test │ │ └── kotlin │ │ └── com │ │ └── clean │ │ └── domain │ │ └── asteroid │ │ ├── StringProviderFake.kt │ │ ├── NasaRepositoryFake.kt │ │ └── AsteroidViewFlowTest.kt └── build.gradle ├── presentation ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ └── strings.xml │ │ │ └── layout │ │ │ │ └── activity_main.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── clean │ │ │ │ └── asteroids │ │ │ │ ├── config │ │ │ │ ├── CoreComponentProvider.kt │ │ │ │ ├── Scopes.kt │ │ │ │ ├── CoreComponent.kt │ │ │ │ ├── ViewModelKey.kt │ │ │ │ ├── PresentationComponent.kt │ │ │ │ └── PresentationModule.kt │ │ │ │ ├── AsteroidEffectHandler.kt │ │ │ │ ├── ViewModelFactory.kt │ │ │ │ ├── AsteroidViewModel.kt │ │ │ │ ├── AsteroidViewRenderer.kt │ │ │ │ └── AsteroidActivity.kt │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── clean │ │ │ └── asteroids │ │ │ └── ExampleUnitTest.java │ └── androidTest │ │ └── java │ │ └── com │ │ └── clean │ │ └── asteroids │ │ └── ExampleInstrumentedTest.java ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .idea ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── vcs.xml ├── misc.xml ├── runConfigurations.xml └── gradle.xml ├── .gitignore ├── gradle.properties ├── gradlew.bat ├── dependencies.gradle └── gradlew /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /domain/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /presentation/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':domain', ':presentation', ':data' 2 | -------------------------------------------------------------------------------- /data/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | CleanProject 3 | 4 | -------------------------------------------------------------------------------- /presentation/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | asteroids 3 | 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdrianoCelentano/CleanProject/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdrianoCelentano/CleanProject/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdrianoCelentano/CleanProject/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdrianoCelentano/CleanProject/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdrianoCelentano/CleanProject/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdrianoCelentano/CleanProject/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdrianoCelentano/CleanProject/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdrianoCelentano/CleanProject/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdrianoCelentano/CleanProject/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdrianoCelentano/CleanProject/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdrianoCelentano/CleanProject/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /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 | ) -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #008577 4 | #00574B 5 | #D81B60 6 | 7 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /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/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/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/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 | ) -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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) -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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) -------------------------------------------------------------------------------- /presentation/src/test/java/com/clean/asteroids/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.clean.asteroids; 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 | } -------------------------------------------------------------------------------- /presentation/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 15 | 16 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/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/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 18 | 19 | 26 |