diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..2b75303ac --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 000000000..34dc27cb6 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 000000000..79ee123c2 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 000000000..7ac24c777 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 000000000..af0bbdde1 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 000000000..7f68460d8 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 000000000..35eb1ddfb --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 7c98a093e..64a843f91 100644 --- a/README.md +++ b/README.md @@ -16,12 +16,26 @@ Please clone the repository, complete the exercise, and submit a PR for us to re A) Describe the strategy used to consume the API endpoints and the data management. +I used retrofit to consume the API and ROOM for save the responses locally. Also I used mappers to manage the data +between cloud, database and domain. + B) Explain which library was used for the routing and why. Would you use the same for a consumer facing app targeting thousands of users? Why? +I used activities and Intents for routing, but I will use fragments if a more flexible UI that could support tablets is needed. Also, +The navigation library from Jetpack is pretty interesting applications. +Anyway, the three options are viable for apps with thousands of users. + C) Have you used any strategy to optimize the performance of the list generated for the first feature? +I used the paging library for improve the performance of the list rendered in the first feature. + D) Would you like to add any further comments or observations? +* Tested with mockk +* With Clean Architecture in mind +* MVVM for the presentation layer +* Clean Code + ## Overview: diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 000000000..2dcdcdb46 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,65 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' + +android { + compileSdkVersion 28 + defaultConfig { + applicationId "com.granitosdearena.matiaslev.cocktails" + minSdkVersion 21 + targetSdkVersion 28 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0' + + // All - Modules + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'org.koin:koin-core:2.0.0-rc-2' + implementation 'org.koin:koin-android-viewmodel:2.0.0-rc-2' + + // All - Testing + testImplementation 'junit:junit:4.12' + testImplementation "io.mockk:mockk:1.9" + + // Presentation + def lifecycle_version = "2.0.0" + + implementation 'androidx.appcompat:appcompat:1.0.0-beta01' + implementation 'androidx.core:core-ktx:1.1.0-alpha05' + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'io.reactivex.rxjava2:rxandroid:2.1.0' + implementation 'androidx.recyclerview:recyclerview:1.0.0' + implementation 'com.github.bumptech.glide:glide:4.9.0' + kapt 'com.github.bumptech.glide:compiler:4.9.0' + implementation 'androidx.cardview:cardview:1.0.0' + + // Data + def room_version = "2.1.0-alpha06" + def paging_version = "2.1.0" + + implementation 'com.squareup.retrofit2:retrofit:2.5.0' + implementation 'com.squareup.retrofit2:converter-gson:2.5.0' + implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0' + + implementation "androidx.room:room-runtime:$room_version" + kapt "androidx.room:room-compiler:$room_version" // For Kotlin use kapt instead of annotationProcessor + implementation "androidx.room:room-rxjava2:$room_version" + + implementation "androidx.paging:paging-runtime-ktx:$paging_version" // For Kotlin use paging-runtime-ktx + implementation "androidx.paging:paging-rxjava2-ktx:$paging_version" // For Kotlin use paging-rxjava2-ktx +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 000000000..f1b424510 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..19ebcd6a3 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/granitosdearena/matiaslev/cocktails/CocktailsApp.kt b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/CocktailsApp.kt new file mode 100644 index 000000000..cf76a257d --- /dev/null +++ b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/CocktailsApp.kt @@ -0,0 +1,18 @@ +package com.granitosdearena.matiaslev.cocktails + +import android.app.Application +import com.granitosdearena.matiaslev.cocktails.presentation.appModule +import org.koin.android.ext.koin.androidContext +import org.koin.android.ext.koin.androidLogger +import org.koin.core.context.startKoin + +class CocktailsApp: Application() { + override fun onCreate() { + super.onCreate() + startKoin { + androidLogger() + androidContext(this@CocktailsApp) + modules(appModule) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/CocktailsRepositoryImpl.kt b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/CocktailsRepositoryImpl.kt new file mode 100644 index 000000000..3b24fac93 --- /dev/null +++ b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/CocktailsRepositoryImpl.kt @@ -0,0 +1,65 @@ +package com.granitosdearena.matiaslev.cocktails.data + +import android.util.Log +import androidx.paging.PagedList +import androidx.paging.toObservable +import com.granitosdearena.matiaslev.cocktails.data.cloud.CocktailsApi +import com.granitosdearena.matiaslev.cocktails.data.database.AppDatabase +import com.granitosdearena.matiaslev.cocktails.data.mappers.cocktail.CocktailCloudToDatabaseMapper +import com.granitosdearena.matiaslev.cocktails.data.mappers.cocktail.ToCocktailFromDatabaseMapper +import com.granitosdearena.matiaslev.cocktails.data.mappers.cocktailPreview.CocktailPreviewCloudToDatabaseMapper +import com.granitosdearena.matiaslev.cocktails.data.mappers.cocktailPreview.ToCocktailPreviewFromDatabaseMapper +import com.granitosdearena.matiaslev.cocktails.domain.Cocktail +import com.granitosdearena.matiaslev.cocktails.domain.CocktailPreview +import com.granitosdearena.matiaslev.cocktails.domain.CocktailsRepository +import com.granitosdearena.matiaslev.cocktails.presentation.viewModels.CocktailPreviewViewModel +import io.reactivex.Observable +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.rxkotlin.subscribeBy +import io.reactivex.schedulers.Schedulers + +class CocktailsRepositoryImpl(val cocktailsApi: CocktailsApi, + val database: AppDatabase, + val disposables: CompositeDisposable, + val cocktailPreviewCloudToDatabaseMapper: CocktailPreviewCloudToDatabaseMapper, + val cocktailCloudToDatabaseMapper: CocktailCloudToDatabaseMapper, + val toCocktailPreviewFromDatabaseMapper: ToCocktailPreviewFromDatabaseMapper, + val toCocktailFromDatabaseMapper: ToCocktailFromDatabaseMapper): CocktailsRepository { + + override fun syncCockailsPreview(): Observable> { + val disposable = cocktailsApi.getCockailsPreview() + .subscribeOn(Schedulers.io()) + .doOnError { Log.d(CocktailPreviewViewModel::class.java.canonicalName, it.message) } + .map { database.cocktailPreviewDao().insertAll(cocktailPreviewCloudToDatabaseMapper.transform(it)) } + .subscribeBy( + onError = { it.printStackTrace() }, + onSuccess = { } + ) + disposables.add(disposable) + + return database.cocktailPreviewDao().getAll() + .map { toCocktailPreviewFromDatabaseMapper.transform(it) } + .toObservable(10) + } + + override fun searchCocktailsPreviewByName(name: String): Observable> = + database.cocktailPreviewDao().searchCocktailPreviewByName("%$name%") + .map { toCocktailPreviewFromDatabaseMapper.transform(it) } + .toObservable(10) + + override fun getCocktail(drinkId: String): Observable { + val disposable = cocktailsApi.getCocktail(drinkId) + .subscribeOn(Schedulers.io()) + .doOnError { Log.d(CocktailPreviewViewModel::class.java.canonicalName, it.message) } + .filter { it.drinks.isNotEmpty() } + .map { database.cocktailDao().insert(cocktailCloudToDatabaseMapper.transform(it.drinks.first())) } + .subscribeBy( + onError = { it.printStackTrace() }, + onSuccess = { } + ) + disposables.add(disposable) + + return database.cocktailDao().searchCocktailById(drinkId.toInt()) + .map { toCocktailFromDatabaseMapper.transform(it) } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/cloud/CocktailsApi.kt b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/cloud/CocktailsApi.kt new file mode 100644 index 000000000..5be68a45b --- /dev/null +++ b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/cloud/CocktailsApi.kt @@ -0,0 +1,17 @@ +package com.granitosdearena.matiaslev.cocktails.data.cloud + +import com.granitosdearena.matiaslev.cocktails.data.cloud.model.CocktailCloudList +import com.granitosdearena.matiaslev.cocktails.data.cloud.model.CocktailPreviewCloudList +import io.reactivex.Single +import retrofit2.http.GET +import retrofit2.http.Query + +interface CocktailsApi { + + @GET("filter.php?g=Cocktail_glass") + fun getCockailsPreview(): Single + + @GET("lookup.php?") + fun getCocktail(@Query("i") id: String): Single + +} \ No newline at end of file diff --git a/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/cloud/model/CocktailCloud.kt b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/cloud/model/CocktailCloud.kt new file mode 100644 index 000000000..fb373e8f1 --- /dev/null +++ b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/cloud/model/CocktailCloud.kt @@ -0,0 +1,40 @@ +package com.granitosdearena.matiaslev.cocktails.data.cloud.model + +data class CocktailCloudList(val drinks: List) + +data class CocktailCloud( + var idDrink: String, + var strDrink: String, + var strDrinkThumb: String, + var strInstructions: String, + var strIngredient1: String?, + var strIngredient2: String?, + var strIngredient3: String?, + var strIngredient4: String?, + var strIngredient5: String?, + var strIngredient6: String?, + var strIngredient7: String?, + var strIngredient8: String?, + var strIngredient9: String?, + var strIngredient10: String?, + var strIngredient11: String?, + var strIngredient12: String?, + var strIngredient13: String?, + var strIngredient14: String?, + var strIngredient15: String?, + var strMeasure1: String?, + var strMeasure2: String?, + var strMeasure3: String?, + var strMeasure4: String?, + var strMeasure5: String?, + var strMeasure6: String?, + var strMeasure7: String?, + var strMeasure8: String?, + var strMeasure9: String?, + var strMeasure10: String?, + var strMeasure11: String?, + var strMeasure12: String?, + var strMeasure13: String?, + var strMeasure14: String?, + var strMeasure15: String? +) \ No newline at end of file diff --git a/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/cloud/model/CocktailPreviewCloud.kt b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/cloud/model/CocktailPreviewCloud.kt new file mode 100644 index 000000000..f6e26712d --- /dev/null +++ b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/cloud/model/CocktailPreviewCloud.kt @@ -0,0 +1,9 @@ +package com.granitosdearena.matiaslev.cocktails.data.cloud.model + +data class CocktailPreviewCloudList(val drinks: List) + +data class CocktailPreviewCloud( + var strDrink: String, + var strDrinkThumb: String, + var idDrink: String +) \ No newline at end of file diff --git a/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/database/AppDatabase.kt b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/database/AppDatabase.kt new file mode 100644 index 000000000..7fbbf5db1 --- /dev/null +++ b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/database/AppDatabase.kt @@ -0,0 +1,21 @@ +package com.granitosdearena.matiaslev.cocktails.data.database + +import androidx.room.Database +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import com.granitosdearena.matiaslev.cocktails.data.database.model.CocktailDatabase +import com.granitosdearena.matiaslev.cocktails.data.database.model.CocktailPreviewDatabase +import com.granitosdearena.matiaslev.cocktails.data.database.model.StringListConverter + +@Database( + entities = arrayOf( + CocktailPreviewDatabase::class, + CocktailDatabase::class + ), + version = 1 +) +@TypeConverters(StringListConverter::class) +abstract class AppDatabase : RoomDatabase() { + abstract fun cocktailPreviewDao(): CocktailPreviewDao + abstract fun cocktailDao(): CocktailDao +} \ No newline at end of file diff --git a/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/database/CocktailDao.kt b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/database/CocktailDao.kt new file mode 100644 index 000000000..334cd1bae --- /dev/null +++ b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/database/CocktailDao.kt @@ -0,0 +1,19 @@ +package com.granitosdearena.matiaslev.cocktails.data.database + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.granitosdearena.matiaslev.cocktails.data.database.model.CocktailDatabase +import io.reactivex.Observable + +@Dao +interface CocktailDao { + + @Query("SELECT * FROM CocktailDatabase WHERE drinkId = :drinkId") + fun searchCocktailById(drinkId: Int): Observable + + @Insert(onConflict = OnConflictStrategy.ABORT) + fun insert(cocktailPreviewDatabaseList: CocktailDatabase) + +} \ No newline at end of file diff --git a/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/database/CocktailPreviewDao.kt b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/database/CocktailPreviewDao.kt new file mode 100644 index 000000000..b93c2807a --- /dev/null +++ b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/database/CocktailPreviewDao.kt @@ -0,0 +1,27 @@ +package com.granitosdearena.matiaslev.cocktails.data.database + +import androidx.paging.DataSource +import androidx.room.* +import com.granitosdearena.matiaslev.cocktails.data.database.model.CocktailDatabase +import com.granitosdearena.matiaslev.cocktails.data.database.model.CocktailPreviewDatabase +import io.reactivex.Observable + +@Dao +interface CocktailPreviewDao { + + @Query("SELECT * FROM CocktailPreviewDatabase") + fun getAll(): DataSource.Factory + + @Query("SELECT * FROM CocktailPreviewDatabase WHERE drinkName LIKE :name") + fun searchCocktailPreviewByName(name: String): DataSource.Factory + + @Query("SELECT * FROM CocktailPreviewDatabase WHERE idDrink IN (:userIds)") + fun loadAllByIds(userIds: IntArray): List + + @Insert(onConflict = OnConflictStrategy.ABORT) + fun insertAll(cocktailPreviewDatabaseList: List) + + @Delete + fun delete(user: CocktailPreviewDatabase) + +} \ No newline at end of file diff --git a/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/database/model/CocktailDatabase.kt b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/database/model/CocktailDatabase.kt new file mode 100644 index 000000000..7120cdf31 --- /dev/null +++ b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/database/model/CocktailDatabase.kt @@ -0,0 +1,14 @@ +package com.granitosdearena.matiaslev.cocktails.data.database.model + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity +data class CocktailDatabase( + @PrimaryKey var drinkId: Int, + var drinkName: String, + var drinkThumb: String, + var drinkInstructions: String, + var drinkIngredients: List, + var drinkMeasures: List +) \ No newline at end of file diff --git a/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/database/model/CocktailPreviewDatabase.kt b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/database/model/CocktailPreviewDatabase.kt new file mode 100644 index 000000000..17df1e881 --- /dev/null +++ b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/database/model/CocktailPreviewDatabase.kt @@ -0,0 +1,11 @@ +package com.granitosdearena.matiaslev.cocktails.data.database.model + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity +data class CocktailPreviewDatabase( + @PrimaryKey var idDrink: Int, + var drinkName: String, + var drinkThumb: String +) \ No newline at end of file diff --git a/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/database/model/StringListConverter.kt b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/database/model/StringListConverter.kt new file mode 100644 index 000000000..d03ee9960 --- /dev/null +++ b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/database/model/StringListConverter.kt @@ -0,0 +1,18 @@ +package com.granitosdearena.matiaslev.cocktails.data.database.model + +import androidx.room.TypeConverter +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken + + +class StringListConverter { + + @TypeConverter + fun fromStringList(stringList: List): String = Gson().toJson(stringList) + + @TypeConverter + fun toStringList(jsonString: String): List = Gson().fromJson(jsonString, getStringListType()) + + private fun getStringListType() = object: TypeToken>() {}.type + +} \ No newline at end of file diff --git a/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/mappers/BaseMapper.kt b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/mappers/BaseMapper.kt new file mode 100644 index 000000000..04e031354 --- /dev/null +++ b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/mappers/BaseMapper.kt @@ -0,0 +1,5 @@ +package com.granitosdearena.matiaslev.cocktails.data.mappers + +interface BaseMapper { + fun transform(input: E): D +} \ No newline at end of file diff --git a/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/mappers/cocktail/CocktailCloudToDatabaseMapper.kt b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/mappers/cocktail/CocktailCloudToDatabaseMapper.kt new file mode 100644 index 000000000..4b98196ef --- /dev/null +++ b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/mappers/cocktail/CocktailCloudToDatabaseMapper.kt @@ -0,0 +1,57 @@ +package com.granitosdearena.matiaslev.cocktails.data.mappers.cocktail + +import com.granitosdearena.matiaslev.cocktails.data.cloud.model.CocktailCloud +import com.granitosdearena.matiaslev.cocktails.data.database.model.CocktailDatabase +import com.granitosdearena.matiaslev.cocktails.data.mappers.BaseMapper + +class CocktailCloudToDatabaseMapper: BaseMapper { + + override fun transform(input: CocktailCloud): CocktailDatabase = + CocktailDatabase( + input.idDrink.toInt(), + input.strDrink, + input.strDrinkThumb, + input.strInstructions, + getIngredients(input), + getMeasures(input)) + + private fun getIngredients(input: CocktailCloud): List { + val mutableListOfIngredients = mutableListOf() + if(!input.strIngredient1.isNullOrEmpty()) input.strIngredient1?.let { mutableListOfIngredients.add(it) } + if(!input.strIngredient2.isNullOrEmpty()) input.strIngredient2?.let { mutableListOfIngredients.add(it) } + if(!input.strIngredient3.isNullOrEmpty()) input.strIngredient3?.let { mutableListOfIngredients.add(it) } + if(!input.strIngredient4.isNullOrEmpty()) input.strIngredient4?.let { mutableListOfIngredients.add(it) } + if(!input.strIngredient5.isNullOrEmpty()) input.strIngredient5?.let { mutableListOfIngredients.add(it) } + if(!input.strIngredient6.isNullOrEmpty()) input.strIngredient6?.let { mutableListOfIngredients.add(it) } + if(!input.strIngredient7.isNullOrEmpty()) input.strIngredient7?.let { mutableListOfIngredients.add(it) } + if(!input.strIngredient8.isNullOrEmpty()) input.strIngredient8?.let { mutableListOfIngredients.add(it) } + if(!input.strIngredient9.isNullOrEmpty()) input.strIngredient9?.let { mutableListOfIngredients.add(it) } + if(!input.strIngredient10.isNullOrEmpty()) input.strIngredient1?.let { mutableListOfIngredients.add(it) } + if(!input.strIngredient11.isNullOrEmpty()) input.strIngredient1?.let { mutableListOfIngredients.add(it) } + if(!input.strIngredient12.isNullOrEmpty()) input.strIngredient1?.let { mutableListOfIngredients.add(it) } + if(!input.strIngredient13.isNullOrEmpty()) input.strIngredient1?.let { mutableListOfIngredients.add(it) } + if(!input.strIngredient14.isNullOrEmpty()) input.strIngredient1?.let { mutableListOfIngredients.add(it) } + if(!input.strIngredient15.isNullOrEmpty()) input.strIngredient1?.let { mutableListOfIngredients.add(it) } + return mutableListOfIngredients + } + + private fun getMeasures(input: CocktailCloud): List { + val mutableListOfMeasures = mutableListOf() + if(!input.strMeasure1.isNullOrEmpty()) input.strMeasure1?.let { mutableListOfMeasures.add(it) } + if(!input.strMeasure2.isNullOrEmpty()) input.strMeasure2?.let { mutableListOfMeasures.add(it) } + if(!input.strMeasure3.isNullOrEmpty()) input.strMeasure3?.let { mutableListOfMeasures.add(it) } + if(!input.strMeasure4.isNullOrEmpty()) input.strMeasure4?.let { mutableListOfMeasures.add(it) } + if(!input.strMeasure5.isNullOrEmpty()) input.strMeasure5?.let { mutableListOfMeasures.add(it) } + if(!input.strMeasure6.isNullOrEmpty()) input.strMeasure6?.let { mutableListOfMeasures.add(it) } + if(!input.strMeasure7.isNullOrEmpty()) input.strMeasure7?.let { mutableListOfMeasures.add(it) } + if(!input.strMeasure8.isNullOrEmpty()) input.strMeasure8?.let { mutableListOfMeasures.add(it) } + if(!input.strMeasure9.isNullOrEmpty()) input.strMeasure9?.let { mutableListOfMeasures.add(it) } + if(!input.strMeasure10.isNullOrEmpty()) input.strMeasure10?.let { mutableListOfMeasures.add(it) } + if(!input.strMeasure11.isNullOrEmpty()) input.strMeasure11?.let { mutableListOfMeasures.add(it) } + if(!input.strMeasure12.isNullOrEmpty()) input.strMeasure12?.let { mutableListOfMeasures.add(it) } + if(!input.strMeasure13.isNullOrEmpty()) input.strMeasure13?.let { mutableListOfMeasures.add(it) } + if(!input.strMeasure14.isNullOrEmpty()) input.strMeasure14?.let { mutableListOfMeasures.add(it) } + if(!input.strMeasure15.isNullOrEmpty()) input.strMeasure15?.let { mutableListOfMeasures.add(it) } + return mutableListOfMeasures + } +} \ No newline at end of file diff --git a/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/mappers/cocktail/ToCocktailFromDatabaseMapper.kt b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/mappers/cocktail/ToCocktailFromDatabaseMapper.kt new file mode 100644 index 000000000..77a79d7ca --- /dev/null +++ b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/mappers/cocktail/ToCocktailFromDatabaseMapper.kt @@ -0,0 +1,18 @@ +package com.granitosdearena.matiaslev.cocktails.data.mappers.cocktail + +import com.granitosdearena.matiaslev.cocktails.data.database.model.CocktailDatabase +import com.granitosdearena.matiaslev.cocktails.data.mappers.BaseMapper +import com.granitosdearena.matiaslev.cocktails.domain.Cocktail + +class ToCocktailFromDatabaseMapper: BaseMapper { + + override fun transform(input: CocktailDatabase): Cocktail = Cocktail( + input.drinkId.toString(), + input.drinkName, + input.drinkThumb, + input.drinkInstructions, + input.drinkIngredients.filter { it.isNotBlank() }, + input.drinkMeasures.filter { it.isNotBlank() } + ) + +} \ No newline at end of file diff --git a/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/mappers/cocktailPreview/CocktailPreviewCloudToDatabaseMapper.kt b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/mappers/cocktailPreview/CocktailPreviewCloudToDatabaseMapper.kt new file mode 100644 index 000000000..aec39ca6c --- /dev/null +++ b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/mappers/cocktailPreview/CocktailPreviewCloudToDatabaseMapper.kt @@ -0,0 +1,14 @@ +package com.granitosdearena.matiaslev.cocktails.data.mappers.cocktailPreview + +import com.granitosdearena.matiaslev.cocktails.data.cloud.model.CocktailPreviewCloud +import com.granitosdearena.matiaslev.cocktails.data.cloud.model.CocktailPreviewCloudList +import com.granitosdearena.matiaslev.cocktails.data.database.model.CocktailPreviewDatabase +import com.granitosdearena.matiaslev.cocktails.data.mappers.BaseMapper + +class CocktailPreviewCloudToDatabaseMapper: + BaseMapper> { + + override fun transform(input: CocktailPreviewCloudList): List = + input.drinks.map { CocktailPreviewDatabase(it.idDrink.toInt(), it.strDrink, it.strDrinkThumb) } + +} diff --git a/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/mappers/cocktailPreview/ToCocktailPreviewFromDatabaseMapper.kt b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/mappers/cocktailPreview/ToCocktailPreviewFromDatabaseMapper.kt new file mode 100644 index 000000000..eae0dd466 --- /dev/null +++ b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/data/mappers/cocktailPreview/ToCocktailPreviewFromDatabaseMapper.kt @@ -0,0 +1,13 @@ +package com.granitosdearena.matiaslev.cocktails.data.mappers.cocktailPreview + +import com.granitosdearena.matiaslev.cocktails.data.database.model.CocktailPreviewDatabase +import com.granitosdearena.matiaslev.cocktails.data.mappers.BaseMapper +import com.granitosdearena.matiaslev.cocktails.domain.CocktailPreview + +class ToCocktailPreviewFromDatabaseMapper: + BaseMapper { + + override fun transform(input: CocktailPreviewDatabase): CocktailPreview = + CocktailPreview(input.drinkName, input.drinkThumb, input.idDrink.toString()) + +} \ No newline at end of file diff --git a/app/src/main/java/com/granitosdearena/matiaslev/cocktails/domain/Cocktail.kt b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/domain/Cocktail.kt new file mode 100644 index 000000000..ad1165cf8 --- /dev/null +++ b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/domain/Cocktail.kt @@ -0,0 +1,25 @@ +package com.granitosdearena.matiaslev.cocktails.domain + +data class Cocktail( + override var drinkId: String = EMPTY_STRING, + override var drinkName: String = EMPTY_STRING, + override var drinkThumb: String = EMPTY_STRING, + var drinkInstructions: String, + var drinkIngredients: List, + var drinkMeasures: List +): CocktailPreview(drinkName, drinkThumb, drinkId) { + + companion object { + // could be an global object Util, but for now is used just here. + val EMPTY_STRING = "" + } + + fun getIngredientsWithMeasures(): String { + var ingredientsWithMeasuresText = EMPTY_STRING + drinkMeasures.forEachIndexed { index, measure -> + ingredientsWithMeasuresText += "$measure - ${drinkIngredients[index]} \n" + } + return ingredientsWithMeasuresText + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/granitosdearena/matiaslev/cocktails/domain/CocktailPreview.kt b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/domain/CocktailPreview.kt new file mode 100644 index 000000000..ae76e61b1 --- /dev/null +++ b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/domain/CocktailPreview.kt @@ -0,0 +1,7 @@ +package com.granitosdearena.matiaslev.cocktails.domain + +open class CocktailPreview( + open var drinkName: String, + open var drinkThumb: String, + open var drinkId: String +) \ No newline at end of file diff --git a/app/src/main/java/com/granitosdearena/matiaslev/cocktails/domain/CocktailsRepository.kt b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/domain/CocktailsRepository.kt new file mode 100644 index 000000000..e29efca6e --- /dev/null +++ b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/domain/CocktailsRepository.kt @@ -0,0 +1,11 @@ +package com.granitosdearena.matiaslev.cocktails.domain + +import androidx.paging.PagedList +import io.reactivex.Observable + +interface CocktailsRepository { + // TODO: Not Use PagedList in domain!! we are coupled for this reason. + fun syncCockailsPreview(): Observable> + fun searchCocktailsPreviewByName(name: String): Observable> + fun getCocktail(drinkId: String): Observable +} \ No newline at end of file diff --git a/app/src/main/java/com/granitosdearena/matiaslev/cocktails/domain/GetCocktailUseCase.kt b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/domain/GetCocktailUseCase.kt new file mode 100644 index 000000000..b4c423bdb --- /dev/null +++ b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/domain/GetCocktailUseCase.kt @@ -0,0 +1,5 @@ +package com.granitosdearena.matiaslev.cocktails.domain + +class GetCocktailUseCase(val cocktailsRepository: CocktailsRepository) { + operator fun invoke(drinkId: String) = cocktailsRepository.getCocktail(drinkId) +} \ No newline at end of file diff --git a/app/src/main/java/com/granitosdearena/matiaslev/cocktails/domain/GetCocktailsPreviewUseCase.kt b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/domain/GetCocktailsPreviewUseCase.kt new file mode 100644 index 000000000..82b3bd026 --- /dev/null +++ b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/domain/GetCocktailsPreviewUseCase.kt @@ -0,0 +1,5 @@ +package com.granitosdearena.matiaslev.cocktails.domain + +class GetCocktailsPreviewUseCase(val cockatilsRepository: CocktailsRepository) { + operator fun invoke() = cockatilsRepository.syncCockailsPreview() +} \ No newline at end of file diff --git a/app/src/main/java/com/granitosdearena/matiaslev/cocktails/domain/SearchCocktailsPreviewByNameUseCase.kt b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/domain/SearchCocktailsPreviewByNameUseCase.kt new file mode 100644 index 000000000..8ba0c0289 --- /dev/null +++ b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/domain/SearchCocktailsPreviewByNameUseCase.kt @@ -0,0 +1,5 @@ +package com.granitosdearena.matiaslev.cocktails.domain + +class SearchCocktailsPreviewByNameUseCase(val cocktailsRepository: CocktailsRepository) { + operator fun invoke(name: String) = cocktailsRepository.searchCocktailsPreviewByName(name) +} \ No newline at end of file diff --git a/app/src/main/java/com/granitosdearena/matiaslev/cocktails/presentation/DI.kt b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/presentation/DI.kt new file mode 100644 index 000000000..dad2ae613 --- /dev/null +++ b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/presentation/DI.kt @@ -0,0 +1,72 @@ +package com.granitosdearena.matiaslev.cocktails.presentation + +import androidx.room.Room +import com.granitosdearena.matiaslev.cocktails.data.CocktailsRepositoryImpl +import com.granitosdearena.matiaslev.cocktails.data.cloud.CocktailsApi +import com.granitosdearena.matiaslev.cocktails.data.database.AppDatabase +import com.granitosdearena.matiaslev.cocktails.data.mappers.cocktail.CocktailCloudToDatabaseMapper +import com.granitosdearena.matiaslev.cocktails.data.mappers.cocktail.ToCocktailFromDatabaseMapper +import com.granitosdearena.matiaslev.cocktails.data.mappers.cocktailPreview.CocktailPreviewCloudToDatabaseMapper +import com.granitosdearena.matiaslev.cocktails.data.mappers.cocktailPreview.ToCocktailPreviewFromDatabaseMapper +import com.granitosdearena.matiaslev.cocktails.domain.CocktailsRepository +import com.granitosdearena.matiaslev.cocktails.domain.GetCocktailUseCase +import com.granitosdearena.matiaslev.cocktails.domain.GetCocktailsPreviewUseCase +import com.granitosdearena.matiaslev.cocktails.domain.SearchCocktailsPreviewByNameUseCase +import com.granitosdearena.matiaslev.cocktails.presentation.cocktailPreviewRecycler.CocktailPreviewAdapter +import com.granitosdearena.matiaslev.cocktails.presentation.viewModels.CocktailPreviewViewModel +import com.granitosdearena.matiaslev.cocktails.presentation.viewModels.CocktailViewModel +import io.reactivex.disposables.CompositeDisposable +import okhttp3.OkHttpClient +import org.koin.android.ext.koin.androidContext +import org.koin.android.viewmodel.dsl.viewModel +import org.koin.dsl.module +import retrofit2.Retrofit +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import retrofit2.converter.gson.GsonConverterFactory +import java.util.concurrent.TimeUnit + + +object RetrofitApiFactory { + val http = OkHttpClient.Builder() + .readTimeout(1, TimeUnit.MINUTES) + .build() + + val retrofit = Retrofit.Builder() + .baseUrl("https://www.thecocktaildb.com/api/json/v1/1/") + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .client(http) + .build() +} + +val appModule = module { + + factory { CocktailPreviewAdapter() } + + single { RetrofitApiFactory.retrofit.create(CocktailsApi::class.java) } + + single { Room.databaseBuilder(androidContext(), AppDatabase::class.java, "database-name").build() } + + single { CocktailPreviewCloudToDatabaseMapper() } + + single { CocktailCloudToDatabaseMapper() } + + single { ToCocktailPreviewFromDatabaseMapper() } + + single { ToCocktailFromDatabaseMapper() } + + single { CompositeDisposable() } + + single { CocktailsRepositoryImpl(get(), get(), get(), get(), get(), get(), get()) } + + single { GetCocktailsPreviewUseCase(get()) } + + single { GetCocktailUseCase(get()) } + + single { SearchCocktailsPreviewByNameUseCase(get()) } + + viewModel { CocktailPreviewViewModel(get(), get()) } + + viewModel { CocktailViewModel(get()) } + +} \ No newline at end of file diff --git a/app/src/main/java/com/granitosdearena/matiaslev/cocktails/presentation/SafeClickListener.kt b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/presentation/SafeClickListener.kt new file mode 100644 index 000000000..bdeb1a24c --- /dev/null +++ b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/presentation/SafeClickListener.kt @@ -0,0 +1,25 @@ +package com.granitosdearena.matiaslev.cocktails.presentation + +import android.os.SystemClock +import android.view.View + +class SafeClickListener( + private var defaultInterval: Int = 1000, + private val onSafeCLick: (View) -> Unit +) : View.OnClickListener { + private var lastTimeClicked: Long = 0 + override fun onClick(v: View) { + if (SystemClock.elapsedRealtime() - lastTimeClicked < defaultInterval) { + return + } + lastTimeClicked = SystemClock.elapsedRealtime() + onSafeCLick(v) + } +} + +fun View.setSafeOnClickListener(onSafeClick: (View) -> Unit) { + val safeClickListener = SafeClickListener { + onSafeClick(it) + } + setOnClickListener(safeClickListener) +} \ No newline at end of file diff --git a/app/src/main/java/com/granitosdearena/matiaslev/cocktails/presentation/activities/CocktailActivity.kt b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/presentation/activities/CocktailActivity.kt new file mode 100644 index 000000000..ec4bfbec4 --- /dev/null +++ b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/presentation/activities/CocktailActivity.kt @@ -0,0 +1,70 @@ +package com.granitosdearena.matiaslev.cocktails.presentation.activities + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import com.bumptech.glide.Glide +import com.granitosdearena.matiaslev.cocktails.R +import com.granitosdearena.matiaslev.cocktails.presentation.setSafeOnClickListener +import com.granitosdearena.matiaslev.cocktails.presentation.viewModels.CocktailViewModel +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import kotlinx.android.synthetic.main.activity_cocktail.* +import org.koin.android.ext.android.get +import org.koin.android.ext.android.inject + +class CocktailActivity : AppCompatActivity() { + + val cocktailViewModel by inject() + private var disposables = get() + + + companion object { + val COCKTAIL_ID = "COCKTAIL_ID" + val COCKTAIL_NAME = "COCKTAIL_NAME" + + fun getCocktailActivityIntent(context: Context, cocktailId: String, cocktailName: String): Intent { + return Intent(context, CocktailActivity::class.java).apply { + putExtra(COCKTAIL_ID, cocktailId) + putExtra(COCKTAIL_NAME, cocktailName) + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_cocktail) + toolbarTittle.text = intent.getStringExtra(COCKTAIL_NAME) + backArrow.setSafeOnClickListener { onBackPressed() } + + val disposable = cocktailViewModel.getCockailById(intent.getStringExtra(COCKTAIL_ID)) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + Glide.with(this).load(it.drinkThumb) + .centerCrop() + .placeholder(R.drawable.ic_drink_placeholder) + .into(drinkThumb) + + ingredientsWithMeasures.text = it.getIngredientsWithMeasures() + instructions.text = it.drinkInstructions + + setViewVisibilityAsLoaded() + } + disposables.add(disposable) + } + + private fun setViewVisibilityAsLoaded() { + progressBar.visibility = View.GONE + drinkThumb.visibility = View.VISIBLE + ingredientsWithMeasures.visibility = View.VISIBLE + howToPrepare.visibility = View.VISIBLE + instructions.visibility = View.VISIBLE + } + + override fun onDestroy() { + super.onDestroy() + disposables.clear() + } +} diff --git a/app/src/main/java/com/granitosdearena/matiaslev/cocktails/presentation/activities/CocktailPreviewActivity.kt b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/presentation/activities/CocktailPreviewActivity.kt new file mode 100644 index 000000000..7fb45d479 --- /dev/null +++ b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/presentation/activities/CocktailPreviewActivity.kt @@ -0,0 +1,67 @@ +package com.granitosdearena.matiaslev.cocktails.presentation.activities + +import android.os.Bundle +import android.view.KeyEvent +import android.view.WindowManager +import androidx.appcompat.app.AppCompatActivity +import androidx.core.widget.addTextChangedListener +import androidx.recyclerview.widget.LinearLayoutManager +import com.granitosdearena.matiaslev.cocktails.R +import com.granitosdearena.matiaslev.cocktails.presentation.cocktailPreviewRecycler.CocktailPreviewAdapter +import com.granitosdearena.matiaslev.cocktails.presentation.viewModels.CocktailPreviewViewModel +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import kotlinx.android.synthetic.main.activity_cocktail_preview.* +import org.koin.android.ext.android.get +import org.koin.android.ext.android.inject + +class CocktailPreviewActivity : AppCompatActivity() { + + private val cocktailPreviewAdapter by inject() + private val cocktailPreviewViewModel by inject() + private var disposables = get() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_cocktail_preview) + supportActionBar?.title = resources.getString(R.string.drinks_by_mati_lev) + + getAllCocktailsPreview() + + search.addTextChangedListener { + it?.let { + if (it.isEmpty()) { + getAllCocktailsPreview() + } else { + searchCocktailsPreviewByName(it.toString()) + } + } + } + } + + private fun searchCocktailsPreviewByName(name: String) { + val disposable = cocktailPreviewViewModel.searchCocktailsPreviewByName(name) + .subscribe { + cocktailPreviewRecycler.adapter = cocktailPreviewAdapter + cocktailPreviewRecycler.layoutManager = LinearLayoutManager(this) + cocktailPreviewAdapter.submitList(it) + } + disposables.add(disposable) + } + + private fun getAllCocktailsPreview() { + val disposable = cocktailPreviewViewModel.getCockailsPreview() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + cocktailPreviewRecycler.adapter = cocktailPreviewAdapter + cocktailPreviewRecycler.layoutManager = LinearLayoutManager(this) + cocktailPreviewAdapter.submitList(it) + } + disposables.add(disposable) + } + + override fun onDestroy() { + super.onDestroy() + disposables.clear() + } +} diff --git a/app/src/main/java/com/granitosdearena/matiaslev/cocktails/presentation/cocktailPreviewRecycler/CocktailPreviewAdapter.kt b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/presentation/cocktailPreviewRecycler/CocktailPreviewAdapter.kt new file mode 100644 index 000000000..83c4871ab --- /dev/null +++ b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/presentation/cocktailPreviewRecycler/CocktailPreviewAdapter.kt @@ -0,0 +1,37 @@ +package com.granitosdearena.matiaslev.cocktails.presentation.cocktailPreviewRecycler + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.paging.PagedListAdapter +import androidx.recyclerview.widget.DiffUtil +import com.granitosdearena.matiaslev.cocktails.R +import com.granitosdearena.matiaslev.cocktails.domain.CocktailPreview + +class CocktailPreviewAdapter: PagedListAdapter(DIFF_CALLBACK) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = CocktailPreviewViewHolder( + parent.context, + LayoutInflater.from(parent.context) + .inflate(R.layout.item_cocktail_preview, parent, false) + ) + + override fun onBindViewHolder(holder: CocktailPreviewViewHolder, position: Int) { + holder.bindTo(cocktailPreview = getItem(position)) + } + + companion object { + private val DIFF_CALLBACK = object : + DiffUtil.ItemCallback() { + // CocktailPreview details may have changed if reloaded from the database, + // but ID is fixed. + override fun areItemsTheSame(oldCocktailPreview: CocktailPreview, + newCocktailPreview: CocktailPreview) = + oldCocktailPreview.drinkId == newCocktailPreview.drinkId + + override fun areContentsTheSame(oldCocktailPreview: CocktailPreview, + newCocktailPreview: CocktailPreview) = + oldCocktailPreview == newCocktailPreview + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/granitosdearena/matiaslev/cocktails/presentation/cocktailPreviewRecycler/CocktailPreviewViewHolder.kt b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/presentation/cocktailPreviewRecycler/CocktailPreviewViewHolder.kt new file mode 100644 index 000000000..57adf1a28 --- /dev/null +++ b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/presentation/cocktailPreviewRecycler/CocktailPreviewViewHolder.kt @@ -0,0 +1,30 @@ +package com.granitosdearena.matiaslev.cocktails.presentation.cocktailPreviewRecycler + +import android.content.Context +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.bumptech.glide.load.resource.bitmap.CenterCrop +import com.bumptech.glide.load.resource.bitmap.RoundedCorners +import com.granitosdearena.matiaslev.cocktails.R +import com.granitosdearena.matiaslev.cocktails.domain.CocktailPreview +import com.granitosdearena.matiaslev.cocktails.presentation.activities.CocktailActivity +import com.granitosdearena.matiaslev.cocktails.presentation.setSafeOnClickListener +import kotlinx.android.synthetic.main.item_cocktail_preview.view.* + +class CocktailPreviewViewHolder(val context: Context, itemView: View): RecyclerView.ViewHolder(itemView) { + + val roundedCorner = context.resources.getDimension(R.dimen.radius_10).toInt() + + fun bindTo(cocktailPreview: CocktailPreview?) = cocktailPreview?.let { cocktailPreview -> + itemView.drinkName.text = cocktailPreview.drinkName + Glide.with(context).load(cocktailPreview.drinkThumb) + .transforms(CenterCrop(), RoundedCorners(roundedCorner)) + .placeholder(R.drawable.ic_drink_placeholder) + .into(itemView.drinkThumb) + itemView.setSafeOnClickListener { + context.startActivity(CocktailActivity.getCocktailActivityIntent(context, + cocktailPreview.drinkId, cocktailPreview.drinkName)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/granitosdearena/matiaslev/cocktails/presentation/viewModels/CocktailPreviewViewModel.kt b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/presentation/viewModels/CocktailPreviewViewModel.kt new file mode 100644 index 000000000..4a6238436 --- /dev/null +++ b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/presentation/viewModels/CocktailPreviewViewModel.kt @@ -0,0 +1,20 @@ +package com.granitosdearena.matiaslev.cocktails.presentation.viewModels + +import androidx.lifecycle.ViewModel +import androidx.paging.PagedList +import com.granitosdearena.matiaslev.cocktails.domain.CocktailPreview +import com.granitosdearena.matiaslev.cocktails.domain.GetCocktailsPreviewUseCase +import com.granitosdearena.matiaslev.cocktails.domain.SearchCocktailsPreviewByNameUseCase +import io.reactivex.Observable + +class CocktailPreviewViewModel( + private val getCocktailsPreviewUseCase: GetCocktailsPreviewUseCase, + private val searchCocktailsPreviewByNameUseCase: SearchCocktailsPreviewByNameUseCase +): ViewModel() { + + fun getCockailsPreview(): Observable> = getCocktailsPreviewUseCase() + + fun searchCocktailsPreviewByName(name: String): Observable> = + searchCocktailsPreviewByNameUseCase(name) + +} \ No newline at end of file diff --git a/app/src/main/java/com/granitosdearena/matiaslev/cocktails/presentation/viewModels/CocktailViewModel.kt b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/presentation/viewModels/CocktailViewModel.kt new file mode 100644 index 000000000..72265cd3d --- /dev/null +++ b/app/src/main/java/com/granitosdearena/matiaslev/cocktails/presentation/viewModels/CocktailViewModel.kt @@ -0,0 +1,13 @@ +package com.granitosdearena.matiaslev.cocktails.presentation.viewModels + +import androidx.lifecycle.ViewModel +import com.granitosdearena.matiaslev.cocktails.domain.Cocktail +import com.granitosdearena.matiaslev.cocktails.domain.CocktailsRepository +import com.granitosdearena.matiaslev.cocktails.domain.GetCocktailUseCase +import io.reactivex.Observable + +class CocktailViewModel(val getCocktailUseCase: GetCocktailUseCase): ViewModel() { + + fun getCockailById(cocktailId: String): Observable = getCocktailUseCase(cocktailId) + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_arrow_back.xml b/app/src/main/res/drawable/ic_arrow_back.xml new file mode 100644 index 000000000..71d5bbd29 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_back.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_drink_placeholder.xml b/app/src/main/res/drawable/ic_drink_placeholder.xml new file mode 100644 index 000000000..4452fd83c --- /dev/null +++ b/app/src/main/res/drawable/ic_drink_placeholder.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..445973d07 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 000000000..445973d07 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_cocktail.xml b/app/src/main/res/layout/activity_cocktail.xml new file mode 100644 index 000000000..74fcf0514 --- /dev/null +++ b/app/src/main/res/layout/activity_cocktail.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_cocktail_preview.xml b/app/src/main/res/layout/activity_cocktail_preview.xml new file mode 100644 index 000000000..814d64ef5 --- /dev/null +++ b/app/src/main/res/layout/activity_cocktail_preview.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_cocktail_preview.xml b/app/src/main/res/layout/item_cocktail_preview.xml new file mode 100644 index 000000000..e7e32a966 --- /dev/null +++ b/app/src/main/res/layout/item_cocktail_preview.xml @@ -0,0 +1,38 @@ + + + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..bbd3e0212 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..bbd3e0212 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..898f3ed59 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 000000000..dffca3601 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..64ba76f75 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 000000000..dae5e0823 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..e5ed46597 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 000000000..14ed0af35 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..b0907cac3 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..d8ae03154 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..2c18de9e6 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..beed3cdd2 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 000000000..395e251e2 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #81a9ea + #81a9ea + #FFFFFF + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 000000000..315052a39 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,21 @@ + + + 28dp + 20dp + 5dp + 10dp + 200dp + 10dp + 0dp + 8dp + 15dp + 24dp + 20dp + 32dp + 1dp + 325dp + 20dp + 10dp + 20sp + 16sp + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..cfaddad2f --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,9 @@ + + Cocktails + How To Prepare + ingredientsWithMeasures + Drink Name + instructions + Drinks By Mati Lev + Search Cocktail By Name + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 000000000..0eb88fe33 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/test/java/com/granitosdearena/matiaslev/cocktails/CocktailFactory.kt b/app/src/test/java/com/granitosdearena/matiaslev/cocktails/CocktailFactory.kt new file mode 100644 index 000000000..dde983ebb --- /dev/null +++ b/app/src/test/java/com/granitosdearena/matiaslev/cocktails/CocktailFactory.kt @@ -0,0 +1,59 @@ +package com.granitosdearena.matiaslev.cocktails + +import com.granitosdearena.matiaslev.cocktails.data.cloud.model.CocktailCloud +import com.granitosdearena.matiaslev.cocktails.data.cloud.model.CocktailCloudList +import com.granitosdearena.matiaslev.cocktails.data.database.model.CocktailPreviewDatabase +import com.granitosdearena.matiaslev.cocktails.data.cloud.model.CocktailPreviewCloud +import com.granitosdearena.matiaslev.cocktails.data.cloud.model.CocktailPreviewCloudList +import com.granitosdearena.matiaslev.cocktails.data.database.model.CocktailDatabase +import com.granitosdearena.matiaslev.cocktails.domain.Cocktail +import com.granitosdearena.matiaslev.cocktails.domain.CocktailPreview + +object CocktailFactory { + + val id = "1" + val name = "Crazy Cocktail" + val thumb = "Crazy_Cocktail.jpg" + val instruction = "instruction" + val ingredient = "ingredient" + val ingredientBlank = " " + val measure = "measure" + val measureBlank = " " + + fun newCocktail() = Cocktail(id, name, thumb, + instruction, listOf(ingredient, ingredient), listOf(measure, measure)) + + fun newCocktailCloudClass() = CocktailCloudList(newCocktailCloudList()) + + fun newCocktailCloudList() = listOf(newCocktailCloud()) + + fun newCocktailCloud() = CocktailCloud(id, name, thumb, + instruction, ingredient, ingredient, ingredient, ingredient, ingredient, ingredient, ingredient, ingredient, + ingredient, ingredient, ingredient, ingredient, ingredient, ingredient, ingredient, + measure, measure, measure, measure, measure, measure, measure, measure, measure, measure, measure, measure, + measure, measure, measure + ) + + fun newCocktailDatabase() = CocktailDatabase(id.toInt(), name, thumb, + instruction, listOf(ingredient, ingredientBlank, ingredient), listOf(measure, measureBlank ,measure) + ) + + fun newCocktailPreview() = CocktailPreview(name, thumb, id) + + fun newCocktailPreviewCloudClass() = CocktailPreviewCloudList(newCocktailPreviewCloudList()) + + fun newCocktailPreviewCloudList() = listOf( + newCocktailPreviewCloud(), + newCocktailPreviewCloud(), + newCocktailPreviewCloud(), + newCocktailPreviewCloud() + ) + + fun newCocktailPreviewCloud() = CocktailPreviewCloud(name, thumb, id) + + fun newCocktailPreviewDatabaseList() = listOf(newCocktailPreviewDatabase()) + + fun newCocktailPreviewDatabase() = + CocktailPreviewDatabase(id.toInt(), name, thumb) + +} \ No newline at end of file diff --git a/app/src/test/java/com/granitosdearena/matiaslev/cocktails/data/CocktailRepositoryImplTest.kt b/app/src/test/java/com/granitosdearena/matiaslev/cocktails/data/CocktailRepositoryImplTest.kt new file mode 100644 index 000000000..645adb56a --- /dev/null +++ b/app/src/test/java/com/granitosdearena/matiaslev/cocktails/data/CocktailRepositoryImplTest.kt @@ -0,0 +1,81 @@ +package com.granitosdearena.matiaslev.cocktails.data + +import com.granitosdearena.matiaslev.cocktails.CocktailFactory +import com.granitosdearena.matiaslev.cocktails.data.cloud.CocktailsApi +import com.granitosdearena.matiaslev.cocktails.data.database.AppDatabase +import com.granitosdearena.matiaslev.cocktails.data.mappers.cocktail.CocktailCloudToDatabaseMapper +import com.granitosdearena.matiaslev.cocktails.data.mappers.cocktail.ToCocktailFromDatabaseMapper +import com.granitosdearena.matiaslev.cocktails.data.mappers.cocktailPreview.CocktailPreviewCloudToDatabaseMapper +import com.granitosdearena.matiaslev.cocktails.data.mappers.cocktailPreview.ToCocktailPreviewFromDatabaseMapper +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import io.reactivex.Single +import io.reactivex.disposables.CompositeDisposable +import org.junit.Test + +class CocktailRepositoryImplTest { + + val id = "1" + val cocktailsApi = mockk(relaxed = true) + val database = mockk(relaxed = true) + val cocktailPreviewCloudToDatabaseMapper = mockk() + val cocktailCloudToDatabaseMapper = mockk() + val toCocktailPreviewFromDatabaseMapper = mockk() + val toCocktailFromDatabaseMapper = mockk() + val disposables = mockk(relaxed = true) + val repositoryUnderTest = CocktailsRepositoryImpl(cocktailsApi, database, disposables, cocktailPreviewCloudToDatabaseMapper, + cocktailCloudToDatabaseMapper, toCocktailPreviewFromDatabaseMapper, toCocktailFromDatabaseMapper) + + @Test + fun `getCockailsPreview should call the getCockailsPreview api`() { + repositoryUnderTest.syncCockailsPreview() + verify { cocktailsApi.getCockailsPreview() } + } + + @Test + fun `getCockailsPreview should insert the data returned by the api`() { + val apiResponse = Single.just(CocktailFactory.newCocktailPreviewCloudClass()) + every { cocktailsApi.getCockailsPreview() } returns apiResponse + every { cocktailPreviewCloudToDatabaseMapper.transform(CocktailFactory.newCocktailPreviewCloudClass()) } returns CocktailFactory.newCocktailPreviewDatabaseList() + + repositoryUnderTest.syncCockailsPreview() + + verify { database.cocktailPreviewDao().insertAll(CocktailFactory.newCocktailPreviewDatabaseList()) } + } + + @Test + fun `getCockailsPreview should get the data from the database`() { + repositoryUnderTest.syncCockailsPreview() + verify { database.cocktailPreviewDao().getAll() } + } + + @Test + fun `getCocktail should call the getCocktail api`() { + repositoryUnderTest.getCocktail(id) + verify { cocktailsApi.getCocktail(id) } + } + + @Test + fun `getCocktail should insert the data returned by the api`() { + val apiResponse = Single.just(CocktailFactory.newCocktailCloudClass()) + every { cocktailsApi.getCocktail(id) } returns apiResponse + every { cocktailCloudToDatabaseMapper.transform(CocktailFactory.newCocktailCloud()) } returns CocktailFactory.newCocktailDatabase() + + repositoryUnderTest.getCocktail(id) + + verify { database.cocktailDao().insert(CocktailFactory.newCocktailDatabase()) } + } + + @Test + fun `getCocktail should get the data from the database`() { + repositoryUnderTest.getCocktail(id) + verify { database.cocktailDao().searchCocktailById(id.toInt()) } + } + + @Test + fun `searchCocktailsPreviewByName should get the names that match to the search from the database with the % prefixes`() { + repositoryUnderTest.searchCocktailsPreviewByName("Some Name") + verify { database.cocktailPreviewDao().searchCocktailPreviewByName("%Some Name%") } + } +} \ No newline at end of file diff --git a/app/src/test/java/com/granitosdearena/matiaslev/cocktails/data/mappers/cocktail/CocktailCloudToDatabaseMapperTest.kt b/app/src/test/java/com/granitosdearena/matiaslev/cocktails/data/mappers/cocktail/CocktailCloudToDatabaseMapperTest.kt new file mode 100644 index 000000000..9f969be0d --- /dev/null +++ b/app/src/test/java/com/granitosdearena/matiaslev/cocktails/data/mappers/cocktail/CocktailCloudToDatabaseMapperTest.kt @@ -0,0 +1,19 @@ +package com.granitosdearena.matiaslev.cocktails.data.mappers.cocktail + +import com.granitosdearena.matiaslev.cocktails.CocktailFactory +import com.granitosdearena.matiaslev.cocktails.data.cloud.model.CocktailCloud +import com.granitosdearena.matiaslev.cocktails.data.database.model.CocktailDatabase +import org.junit.Assert.assertEquals +import org.junit.Test + +class CocktailCloudToDatabaseMapperTest { + + private val mapperUnderTest = CocktailCloudToDatabaseMapper() + + @Test + fun `transform should get CocktailCloud and return CocktailDatabase`() { + val mapperResult = mapperUnderTest.transform(CocktailFactory.newCocktailCloud()) + assertEquals(CocktailDatabase::class, mapperResult::class) + } + +} \ No newline at end of file diff --git a/app/src/test/java/com/granitosdearena/matiaslev/cocktails/data/mappers/cocktail/ToCocktailFromDatabaseMapperTest.kt b/app/src/test/java/com/granitosdearena/matiaslev/cocktails/data/mappers/cocktail/ToCocktailFromDatabaseMapperTest.kt new file mode 100644 index 000000000..c1da0e439 --- /dev/null +++ b/app/src/test/java/com/granitosdearena/matiaslev/cocktails/data/mappers/cocktail/ToCocktailFromDatabaseMapperTest.kt @@ -0,0 +1,26 @@ +package com.granitosdearena.matiaslev.cocktails.data.mappers.cocktail + +import com.granitosdearena.matiaslev.cocktails.CocktailFactory +import com.granitosdearena.matiaslev.cocktails.data.database.model.CocktailDatabase +import com.granitosdearena.matiaslev.cocktails.domain.Cocktail +import org.junit.Assert.assertEquals +import org.junit.Test + +class ToCocktailFromDatabaseMapperTest { + + private val mapperUnderTest = ToCocktailFromDatabaseMapper() + + @Test + fun `transform should get CocktailDatabase and return Cocktail`() { + val mapperResult = mapperUnderTest.transform(CocktailFactory.newCocktailDatabase()) + assertEquals(Cocktail::class, mapperResult::class) + } + + @Test + fun `transform should filter blank ingredients and measures`() { + val mapperResult = mapperUnderTest.transform(CocktailFactory.newCocktailDatabase()) + assertEquals(0, mapperResult.drinkIngredients.filter { it.isBlank() }.size) + assertEquals(0, mapperResult.drinkMeasures.filter { it.isBlank() }.size) + } + +} \ No newline at end of file diff --git a/app/src/test/java/com/granitosdearena/matiaslev/cocktails/data/mappers/cocktailPreview/CocktailPreviewCloudToDatabaseMapperTest.kt b/app/src/test/java/com/granitosdearena/matiaslev/cocktails/data/mappers/cocktailPreview/CocktailPreviewCloudToDatabaseMapperTest.kt new file mode 100644 index 000000000..17207d93b --- /dev/null +++ b/app/src/test/java/com/granitosdearena/matiaslev/cocktails/data/mappers/cocktailPreview/CocktailPreviewCloudToDatabaseMapperTest.kt @@ -0,0 +1,20 @@ +package com.granitosdearena.matiaslev.cocktails.data.mappers.cocktailPreview + +import com.granitosdearena.matiaslev.cocktails.CocktailFactory +import com.granitosdearena.matiaslev.cocktails.data.cloud.model.CocktailPreviewCloudList +import com.granitosdearena.matiaslev.cocktails.data.database.model.CocktailPreviewDatabase +import org.junit.Assert.assertEquals +import org.junit.Test + +class CocktailPreviewCloudToDatabaseMapperTest { + + private val mapperUnderTest = + CocktailPreviewCloudToDatabaseMapper() + + @Test + fun `transform should get CocktailPreviewDataList and return a list of CocktailPreview`() { + val mapperResult = mapperUnderTest.transform(CocktailFactory.newCocktailPreviewCloudClass()) + assertEquals(CocktailPreviewDatabase::class, mapperResult.first()::class) + } + +} \ No newline at end of file diff --git a/app/src/test/java/com/granitosdearena/matiaslev/cocktails/data/mappers/cocktailPreview/ToCocktailPreviewFromDatabaseMapperTest.kt b/app/src/test/java/com/granitosdearena/matiaslev/cocktails/data/mappers/cocktailPreview/ToCocktailPreviewFromDatabaseMapperTest.kt new file mode 100644 index 000000000..44eeab97d --- /dev/null +++ b/app/src/test/java/com/granitosdearena/matiaslev/cocktails/data/mappers/cocktailPreview/ToCocktailPreviewFromDatabaseMapperTest.kt @@ -0,0 +1,21 @@ +package com.granitosdearena.matiaslev.cocktails.data.mappers.cocktailPreview + +import com.granitosdearena.matiaslev.cocktails.CocktailFactory +import com.granitosdearena.matiaslev.cocktails.data.database.model.CocktailPreviewDatabase +import com.granitosdearena.matiaslev.cocktails.data.mappers.cocktailPreview.ToCocktailPreviewFromDatabaseMapper +import com.granitosdearena.matiaslev.cocktails.domain.CocktailPreview +import org.junit.Assert +import org.junit.Test + +class ToCocktailPreviewFromDatabaseMapperTest { + + private val mapperUnderTest = + ToCocktailPreviewFromDatabaseMapper() + + @Test + fun `transform should get CocktailPreviewDatabase and return CocktailPreview`() { + val mapperResult = mapperUnderTest.transform(CocktailFactory.newCocktailPreviewDatabase()) + Assert.assertEquals(CocktailPreview::class, mapperResult::class) + } + +} \ No newline at end of file diff --git a/app/src/test/java/com/granitosdearena/matiaslev/cocktails/domain/CocktailTest.kt b/app/src/test/java/com/granitosdearena/matiaslev/cocktails/domain/CocktailTest.kt new file mode 100644 index 000000000..b9f3ecc5f --- /dev/null +++ b/app/src/test/java/com/granitosdearena/matiaslev/cocktails/domain/CocktailTest.kt @@ -0,0 +1,29 @@ +package com.granitosdearena.matiaslev.cocktails.domain + +import org.junit.Assert.assertEquals +import org.junit.Test + +class CocktailTest { + + private val INGREDIENT = "ingredient" + private val INGREDIENT_BLANK = " " + private val MEASURE = "measure" + private val MEASURE_BLANK = " " + private val cocktail = Cocktail( + "123", + "test drink", + "http://fake.url.com", + "shake shake and drink!", + listOf(INGREDIENT, INGREDIENT), + listOf(MEASURE, MEASURE) + ) + + @Test + fun `getIngredientsWithMeasures should return a string with measure + instruction on each line`() { + assertEquals( + "$MEASURE - $INGREDIENT \n$MEASURE - $INGREDIENT \n", + cocktail.getIngredientsWithMeasures() + ) + } + +} \ No newline at end of file diff --git a/app/src/test/java/com/granitosdearena/matiaslev/cocktails/domain/GetCocktailUseCaseTest.kt b/app/src/test/java/com/granitosdearena/matiaslev/cocktails/domain/GetCocktailUseCaseTest.kt new file mode 100644 index 000000000..36e38a2a4 --- /dev/null +++ b/app/src/test/java/com/granitosdearena/matiaslev/cocktails/domain/GetCocktailUseCaseTest.kt @@ -0,0 +1,17 @@ +package com.granitosdearena.matiaslev.cocktails.domain + +import io.mockk.mockk +import io.mockk.verify +import org.junit.Test + +class GetCocktailUseCaseTest { + + private val repository = mockk(relaxed = true) + private val useCaseUnderTest = GetCocktailUseCase(repository) + + @Test + fun `should delegate the work to the repository`() { + useCaseUnderTest("1") + verify { repository.getCocktail("1") } + } +} \ No newline at end of file diff --git a/app/src/test/java/com/granitosdearena/matiaslev/cocktails/domain/GetCocktailsPreviewUseCaseTest.kt b/app/src/test/java/com/granitosdearena/matiaslev/cocktails/domain/GetCocktailsPreviewUseCaseTest.kt new file mode 100644 index 000000000..776d44c3b --- /dev/null +++ b/app/src/test/java/com/granitosdearena/matiaslev/cocktails/domain/GetCocktailsPreviewUseCaseTest.kt @@ -0,0 +1,16 @@ +package com.granitosdearena.matiaslev.cocktails.domain + +import io.mockk.mockk +import io.mockk.verify +import org.junit.Test + +class GetCocktailsPreviewUseCaseTest { + private val repository = mockk(relaxed = true) + private val useCaseUnderTest = GetCocktailsPreviewUseCase(repository) + + @Test + fun `should delegate the work to the repository`() { + useCaseUnderTest() + verify { repository.syncCockailsPreview() } + } +} \ No newline at end of file diff --git a/app/src/test/java/com/granitosdearena/matiaslev/cocktails/domain/SearchCocktailPreviewByNameUseCase.kt b/app/src/test/java/com/granitosdearena/matiaslev/cocktails/domain/SearchCocktailPreviewByNameUseCase.kt new file mode 100644 index 000000000..c61f76d22 --- /dev/null +++ b/app/src/test/java/com/granitosdearena/matiaslev/cocktails/domain/SearchCocktailPreviewByNameUseCase.kt @@ -0,0 +1,18 @@ +package com.granitosdearena.matiaslev.cocktails.domain + +import io.mockk.mockk +import io.mockk.verify +import org.junit.Test + +class SearchCocktailPreviewByNameUseCase { + + private val repository = mockk(relaxed = true) + private val useCaseUnderTest = SearchCocktailsPreviewByNameUseCase(repository) + + @Test + fun `should delegate the work to the repository`() { + useCaseUnderTest("Some Name") + verify { repository.searchCocktailsPreviewByName("Some Name") } + } + +} \ No newline at end of file diff --git a/app/src/test/java/com/granitosdearena/matiaslev/cocktails/presentation/CocktailPreviewViewModelTest.kt b/app/src/test/java/com/granitosdearena/matiaslev/cocktails/presentation/CocktailPreviewViewModelTest.kt new file mode 100644 index 000000000..bbebfd146 --- /dev/null +++ b/app/src/test/java/com/granitosdearena/matiaslev/cocktails/presentation/CocktailPreviewViewModelTest.kt @@ -0,0 +1,28 @@ +package com.granitosdearena.matiaslev.cocktails.presentation + +import com.granitosdearena.matiaslev.cocktails.domain.GetCocktailsPreviewUseCase +import com.granitosdearena.matiaslev.cocktails.domain.SearchCocktailsPreviewByNameUseCase +import com.granitosdearena.matiaslev.cocktails.presentation.viewModels.CocktailPreviewViewModel +import io.mockk.mockk +import io.mockk.verify +import org.junit.Test + +class CocktailPreviewViewModelTest { + + val cocktailsPreviewUseCase = mockk(relaxed = true) + val searchCocktailsPreviewByNameUseCase = mockk(relaxed = true) + val viewModelUnderTest = CocktailPreviewViewModel(cocktailsPreviewUseCase, searchCocktailsPreviewByNameUseCase) + + @Test + fun `getCockailsPreview should ask to the repository for sync the CocktailsPreview`() { + viewModelUnderTest.getCockailsPreview() + verify { cocktailsPreviewUseCase() } + } + + @Test + fun `searchCocktailsPreviewByName should ask to the repository for get the cocktails that match with the search`() { + viewModelUnderTest.searchCocktailsPreviewByName("Some Name") + verify { searchCocktailsPreviewByNameUseCase("Some Name") } + } + +} \ No newline at end of file diff --git a/app/src/test/java/com/granitosdearena/matiaslev/cocktails/presentation/CocktailViewModelTest.kt b/app/src/test/java/com/granitosdearena/matiaslev/cocktails/presentation/CocktailViewModelTest.kt new file mode 100644 index 000000000..df47f9416 --- /dev/null +++ b/app/src/test/java/com/granitosdearena/matiaslev/cocktails/presentation/CocktailViewModelTest.kt @@ -0,0 +1,21 @@ +package com.granitosdearena.matiaslev.cocktails.presentation + +import com.granitosdearena.matiaslev.cocktails.domain.GetCocktailUseCase +import com.granitosdearena.matiaslev.cocktails.presentation.viewModels.CocktailViewModel +import io.mockk.mockk +import io.mockk.verify +import org.junit.Test + +class CocktailViewModelTest { + + val id = "1" + val cocktailUseCase = mockk(relaxed = true) + val viewModelUnderTest = CocktailViewModel(cocktailUseCase) + + @Test + fun `getCockailsPreview should ask to the repository for sync the CocktailsPreview`() { + viewModelUnderTest.getCockailById(id) + verify { cocktailUseCase(id) } + } + +} \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..a6b2983d1 --- /dev/null +++ b/build.gradle @@ -0,0 +1,28 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + ext.kotlin_version = '1.3.21' + repositories { + google() + jcenter() + + } + dependencies { + classpath 'com.android.tools.build:gradle:3.3.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + google() + jcenter() + + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 000000000..23339e0df --- /dev/null +++ b/gradle.properties @@ -0,0 +1,21 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..f6b961fd5 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..ba88ad913 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Apr 08 21:50:20 ART 2019 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip diff --git a/gradlew b/gradlew new file mode 100644 index 000000000..cccdd3d51 --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000..f9553162f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 000000000..e7b4def49 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':app'