diff --git a/.idea/checkstyle-idea.xml b/.idea/checkstyle-idea.xml new file mode 100644 index 0000000..1d3774c --- /dev/null +++ b/.idea/checkstyle-idea.xml @@ -0,0 +1,16 @@ + + + + 10.3.1 + JavaOnly + true + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index bd44bdc..37ed9c1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,7 +3,6 @@ plugins { id 'org.jetbrains.kotlin.android' id 'androidx.navigation.safeargs' id 'kotlin-kapt' - id 'dagger.hilt.android.plugin' id 'org.jetbrains.kotlin.plugin.serialization' version '1.6.10' } @@ -12,7 +11,7 @@ android { defaultConfig { applicationId "com.ilein.thecocktailapp" - minSdk 21 + minSdk 26 targetSdk 32 versionCode 1 versionName "1.0" @@ -54,7 +53,7 @@ dependencies { implementation 'androidx.legacy:legacy-support-v4:1.0.0' def nav_version = "2.4.2" - def ktor_version = "1.6.8" + def ktor_version = "2.0.3" def room_version = "2.4.2" def koin_version = "3.2.0" @@ -74,16 +73,6 @@ dependencies { implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" implementation "androidx.navigation:navigation-ui-ktx:$nav_version" - // hilt - implementation "com.google.dagger:hilt-android:2.40.5" - kapt "com.google.dagger:hilt-compiler:2.40.5" - - // retrofit - implementation 'com.squareup.retrofit2:retrofit:2.9.0' - implementation 'com.squareup.retrofit2:adapter-rxjava3:2.9.0' - implementation 'com.squareup.retrofit2:converter-gson:2.9.0' - implementation 'com.squareup.okhttp3:logging-interceptor:4.9.3' - // gson implementation 'com.google.code.gson:gson:2.9.0' @@ -98,12 +87,12 @@ dependencies { implementation("io.ktor:ktor-server-netty:$ktor_version") implementation("ch.qos.logback:logback-classic:1.2.5") implementation("io.ktor:ktor-client-logging:$ktor_version") - implementation("io.ktor:ktor-client-serialization:$ktor_version") + implementation("io.ktor:ktor-client-content-negotiation:$ktor_version") + implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version") implementation("io.ktor:ktor-client-cio:$ktor_version") // Room - implementation "androidx.room:room-runtime:$room_version" - implementation "androidx.room:room-rxjava3:$room_version" + implementation "androidx.room:room-ktx:$room_version" kapt "androidx.room:room-compiler:$room_version" // Koin @@ -116,7 +105,6 @@ dependencies { // map implementation "org.osmdroid:osmdroid-android:6.1.13" - implementation 'com.google.android.gms:play-services-location:17.1.0' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 198cc6a..f6a5ae0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,7 +18,7 @@ android:supportsRtl="true" android:theme="@style/Theme.TheCocktailApp"> diff --git a/app/src/main/java/com/ilein/thecocktailapp/App.kt b/app/src/main/java/com/ilein/thecocktailapp/App.kt index bc1c38c..51de837 100644 --- a/app/src/main/java/com/ilein/thecocktailapp/App.kt +++ b/app/src/main/java/com/ilein/thecocktailapp/App.kt @@ -1,15 +1,14 @@ package com.ilein.thecocktailapp import android.app.Application -import com.ilein.thecocktailapp.koin.databaseModule -import com.ilein.thecocktailapp.koin.networkModule -import com.ilein.thecocktailapp.koin.vmModule -import dagger.hilt.android.HiltAndroidApp +import com.ilein.thecocktailapp.di.databaseModule +import com.ilein.thecocktailapp.di.networkModule +import com.ilein.thecocktailapp.di.useCaseModule +import com.ilein.thecocktailapp.di.vmModule import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidLogger import org.koin.core.context.startKoin -@HiltAndroidApp class App: Application() { override fun onCreate() { @@ -18,7 +17,7 @@ class App: Application() { startKoin(){ androidLogger() androidContext(this@App) - modules(databaseModule, networkModule, vmModule) + modules(databaseModule, networkModule, useCaseModule, vmModule) } } } \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/data/DrinkRepositoryImpl.kt b/app/src/main/java/com/ilein/thecocktailapp/data/DrinkRepositoryImpl.kt new file mode 100644 index 0000000..dfefc60 --- /dev/null +++ b/app/src/main/java/com/ilein/thecocktailapp/data/DrinkRepositoryImpl.kt @@ -0,0 +1,37 @@ +package com.ilein.thecocktailapp.data + +import com.ilein.thecocktailapp.data.db.LocalDataSource +import com.ilein.thecocktailapp.data.network.RemoteDataSource +import com.ilein.thecocktailapp.domain.Drink +import com.ilein.thecocktailapp.domain.FavoriteDrink +import com.ilein.thecocktailapp.domain.DrinkRepository +import kotlinx.coroutines.flow.Flow + +class DrinkRepositoryImpl( + private val localDataSource: LocalDataSource, + private val remoteDataSource: RemoteDataSource +): DrinkRepository { + override suspend fun insertDrinkFavorites(drink: FavoriteDrink) = + localDataSource.insertDrinkFavorites(drink.toEntity()) + + override suspend fun getDrinkFavorites(): List = + localDataSource.getDrinkFavorites().map { it.toDomain() } + + override suspend fun getDrinkFavoritesDesc(): List = + localDataSource.getDrinkFavoritesDesc().map { it.toDomain() } + + override suspend fun getFavoriteDrink(drinkId: Int): FavoriteDrink? = + localDataSource.getDrink(drinkId)?.toDomain() + + override suspend fun deleteFavoriteDrink(drinkId: Int) = + localDataSource.deleteDrink(drinkId) + + override suspend fun getDrink(drinkId: Int): Flow = + remoteDataSource.getDrink(drinkId) + + override suspend fun getOneDrink(drinkId: Int): Drink = + remoteDataSource.getOneDrink(drinkId) + + override suspend fun searchDrinks(query: String): Flow> = + remoteDataSource.searchDrinks(query) +} \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/data/MapperExt.kt b/app/src/main/java/com/ilein/thecocktailapp/data/MapperExt.kt new file mode 100644 index 0000000..3e56603 --- /dev/null +++ b/app/src/main/java/com/ilein/thecocktailapp/data/MapperExt.kt @@ -0,0 +1,31 @@ +package com.ilein.thecocktailapp.data + +import com.ilein.thecocktailapp.data.db.DrinkEntity +import com.ilein.thecocktailapp.data.network.model.DrinkModel +import com.ilein.thecocktailapp.data.network.model.DrinksModel +import com.ilein.thecocktailapp.domain.Drink +import com.ilein.thecocktailapp.domain.FavoriteDrink + +fun FavoriteDrink.toEntity() = DrinkEntity( + id = this.id, + name = this.name, + image = this.image, + createDate = this.createDate, + note = this.note +) + +fun DrinkEntity.toDomain() = FavoriteDrink( + id = this.id, + name = this.name, + image = this.image, + createDate = this.createDate, + note = this.note +) + +fun DrinkModel.toDomain() = Drink( + id = this.idDrink, + name = this.strDrink, + image = this.strDrinkThumb, + ingredients = this.getIngredientsText(), + instruction = this.strInstructions +) \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/db/Converters.kt b/app/src/main/java/com/ilein/thecocktailapp/data/db/Converters.kt similarity index 93% rename from app/src/main/java/com/ilein/thecocktailapp/db/Converters.kt rename to app/src/main/java/com/ilein/thecocktailapp/data/db/Converters.kt index bb64bf4..fe0a609 100644 --- a/app/src/main/java/com/ilein/thecocktailapp/db/Converters.kt +++ b/app/src/main/java/com/ilein/thecocktailapp/data/db/Converters.kt @@ -1,4 +1,4 @@ -package com.ilein.thecocktailapp.db +package com.ilein.thecocktailapp.data.db import android.os.Build import androidx.annotation.RequiresApi diff --git a/app/src/main/java/com/ilein/thecocktailapp/db/DrinkLikeEntity.kt b/app/src/main/java/com/ilein/thecocktailapp/data/db/DrinkEntity.kt similarity index 85% rename from app/src/main/java/com/ilein/thecocktailapp/db/DrinkLikeEntity.kt rename to app/src/main/java/com/ilein/thecocktailapp/data/db/DrinkEntity.kt index d12c929..3a54eff 100644 --- a/app/src/main/java/com/ilein/thecocktailapp/db/DrinkLikeEntity.kt +++ b/app/src/main/java/com/ilein/thecocktailapp/data/db/DrinkEntity.kt @@ -1,12 +1,12 @@ -package com.ilein.thecocktailapp.db +package com.ilein.thecocktailapp.data.db import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey import java.time.LocalDateTime -@Entity(tableName = DrinkLikeDb.DRINK_LIKE_TABLE) -data class DrinkLikeEntity ( +@Entity(tableName = DrinkFavoriteDb.DRINK_LIKE_TABLE) +data class DrinkEntity ( @PrimaryKey @ColumnInfo(name = COLUMN_ID) val id: Int, diff --git a/app/src/main/java/com/ilein/thecocktailapp/data/db/DrinkFavoriteDao.kt b/app/src/main/java/com/ilein/thecocktailapp/data/db/DrinkFavoriteDao.kt new file mode 100644 index 0000000..68dd2cc --- /dev/null +++ b/app/src/main/java/com/ilein/thecocktailapp/data/db/DrinkFavoriteDao.kt @@ -0,0 +1,24 @@ +package com.ilein.thecocktailapp.data.db + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query + +@Dao +abstract class DrinkFavoriteDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract suspend fun insertDrink(drinkFavorite: DrinkEntity) + + @Query("SELECT * FROM drink_like") + abstract suspend fun getDrinkFavorites(): List + + @Query("SELECT * FROM drink_like order by update_date desc") + abstract suspend fun getDrinkFavoritesDesc(): List + + @Query("SELECT * FROM drink_like WHERE id = :drinkId") + abstract suspend fun getDrink(drinkId: Int): DrinkEntity? + + @Query("DELETE FROM drink_like WHERE id = :drinkId") + abstract suspend fun deleteDrink(drinkId: Int) +} \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/db/DrinkLikeDb.kt b/app/src/main/java/com/ilein/thecocktailapp/data/db/DrinkFavoriteDb.kt similarity index 66% rename from app/src/main/java/com/ilein/thecocktailapp/db/DrinkLikeDb.kt rename to app/src/main/java/com/ilein/thecocktailapp/data/db/DrinkFavoriteDb.kt index 093fc97..9af2045 100644 --- a/app/src/main/java/com/ilein/thecocktailapp/db/DrinkLikeDb.kt +++ b/app/src/main/java/com/ilein/thecocktailapp/data/db/DrinkFavoriteDb.kt @@ -1,4 +1,4 @@ -package com.ilein.thecocktailapp.db +package com.ilein.thecocktailapp.data.db import android.content.Context import androidx.room.Database @@ -10,27 +10,27 @@ import androidx.room.TypeConverters version = 1, exportSchema = false, entities = [ - DrinkLikeEntity::class + DrinkEntity::class ] ) @TypeConverters(Converters::class) -abstract class DrinkLikeDb: RoomDatabase() { +abstract class DrinkFavoriteDb: RoomDatabase() { companion object { private const val databaseName = "drink_database" const val DRINK_LIKE_TABLE = "drink_like" - fun createDatabaseInstance(context: Context): DrinkLikeDb { + fun createDatabaseInstance(context: Context): DrinkFavoriteDb { return Room .databaseBuilder( context, - DrinkLikeDb::class.java, + DrinkFavoriteDb::class.java, databaseName ) .build() } } - abstract fun drinkLikeDao(): DrinkLikeDao + abstract fun drinkFavoriteDao(): DrinkFavoriteDao } \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/data/db/LocalDataSource.kt b/app/src/main/java/com/ilein/thecocktailapp/data/db/LocalDataSource.kt new file mode 100644 index 0000000..6c4644c --- /dev/null +++ b/app/src/main/java/com/ilein/thecocktailapp/data/db/LocalDataSource.kt @@ -0,0 +1,9 @@ +package com.ilein.thecocktailapp.data.db + +interface LocalDataSource { + suspend fun insertDrinkFavorites(drinkLike: DrinkEntity) + suspend fun getDrinkFavorites(): List + suspend fun getDrinkFavoritesDesc(): List + suspend fun getDrink(drinkId: Int): DrinkEntity? + suspend fun deleteDrink(drinkId: Int) +} \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/data/db/LocalDataSourceImpl.kt b/app/src/main/java/com/ilein/thecocktailapp/data/db/LocalDataSourceImpl.kt new file mode 100644 index 0000000..1f4328f --- /dev/null +++ b/app/src/main/java/com/ilein/thecocktailapp/data/db/LocalDataSourceImpl.kt @@ -0,0 +1,25 @@ +package com.ilein.thecocktailapp.data.db + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext + +class LocalDataSourceImpl( + private val dao: DrinkFavoriteDao, + private val ioDispatcher: CoroutineDispatcher +): LocalDataSource { + override suspend fun insertDrinkFavorites(drinkLike: DrinkEntity) = + withContext(ioDispatcher) { dao.insertDrink(drinkLike) } + + override suspend fun getDrinkFavorites(): List = + withContext(ioDispatcher) { dao.getDrinkFavorites()} + + override suspend fun getDrinkFavoritesDesc(): List = + withContext(ioDispatcher) { dao.getDrinkFavoritesDesc() } + + override suspend fun getDrink(drinkId: Int): DrinkEntity? = + withContext(ioDispatcher) { dao.getDrink(drinkId) } + + override suspend fun deleteDrink(drinkId: Int) = + withContext(ioDispatcher) { dao.deleteDrink(drinkId) } + +} \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/data/network/ClientProvider.kt b/app/src/main/java/com/ilein/thecocktailapp/data/network/ClientProvider.kt new file mode 100644 index 0000000..44c4411 --- /dev/null +++ b/app/src/main/java/com/ilein/thecocktailapp/data/network/ClientProvider.kt @@ -0,0 +1,57 @@ +package com.ilein.thecocktailapp.data.network + +import com.ilein.thecocktailapp.BuildConfig +import com.ilein.thecocktailapp.data.network.model.DrinksModel +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.plugins.cache.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.plugins.logging.* +import io.ktor.client.request.* +import io.ktor.serialization.kotlinx.json.* +import kotlinx.serialization.json.Json + +class ClientProvider { + private val baseUrl = BuildConfig.API_BASE_URL + private val drinks = "api/json/${BuildConfig.API_VERSION}/${BuildConfig.API_KEY}/search.php" + private val drink = "api/json/${BuildConfig.API_VERSION}/${BuildConfig.API_KEY}/lookup.php" + + + private val ktorClient = HttpClient() { + install(Logging) { + level = LogLevel.ALL + } + + install(ContentNegotiation) { + json(Json{ ignoreUnknownKeys = true + prettyPrint = true + isLenient = true}) + } + install(HttpCache) + } + + suspend fun getDrink(drinkId: Int): DrinksModel = ktorClient.use { + it.get { host = baseUrl + url(port = 8080, path = buildPath(drink, mapOf(Pair("i", drinkId)))) }.body() + } + + suspend fun searchDrinks(searchText: String): DrinksModel = ktorClient.use { + it.get { host = baseUrl + url(port = 8080, path = buildPath(drinks, mapOf(Pair("s", searchText)))) }.body() + } + + private fun buildPath( + endpoint: String, + query: Map = emptyMap(), + ): String = + buildString { + append("$endpoint?") + append( + query.entries.joinToString( + separator = "&" + ) { (key, value) -> + "$key=$value" + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/network/DrinksReq.kt b/app/src/main/java/com/ilein/thecocktailapp/data/network/DrinksReq.kt similarity index 67% rename from app/src/main/java/com/ilein/thecocktailapp/network/DrinksReq.kt rename to app/src/main/java/com/ilein/thecocktailapp/data/network/DrinksReq.kt index 9566248..4762545 100644 --- a/app/src/main/java/com/ilein/thecocktailapp/network/DrinksReq.kt +++ b/app/src/main/java/com/ilein/thecocktailapp/data/network/DrinksReq.kt @@ -1,6 +1,6 @@ -package com.ilein.thecocktailapp.network +package com.ilein.thecocktailapp.data.network -import com.ilein.thecocktailapp.network.model.Drinks +import com.ilein.thecocktailapp.data.network.model.DrinksModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -10,13 +10,13 @@ import kotlinx.coroutines.flow.retryWhen class DrinksReq { - fun requestDrinks(query: String): Flow { + fun requestDrinks(query: String): Flow { return requestDrinksDr(query) .flowOn(Dispatchers.IO) .retryWithDelay() } - fun requestDrink(drinkId: Int): Flow { + fun requestDrink(drinkId: Int): Flow { return requestDrinkDr(drinkId) .flowOn(Dispatchers.IO) .retryWithDelay() @@ -24,18 +24,18 @@ class DrinksReq { /* additional */ - private fun requestDrinksDr(query: String): Flow { + private fun requestDrinksDr(query: String): Flow { return flow { emit( - Api().getDrinks(query) + ClientProvider().searchDrinks(query) ) } } - private fun requestDrinkDr(drinkId: Int): Flow { + private fun requestDrinkDr(drinkId: Int): Flow { return flow { emit( - Api().getDrink(drinkId) + ClientProvider().getDrink(drinkId) ) } } diff --git a/app/src/main/java/com/ilein/thecocktailapp/network/NetworkUtil.kt b/app/src/main/java/com/ilein/thecocktailapp/data/network/NetworkUtil.kt similarity index 96% rename from app/src/main/java/com/ilein/thecocktailapp/network/NetworkUtil.kt rename to app/src/main/java/com/ilein/thecocktailapp/data/network/NetworkUtil.kt index 81abb3a..edb6623 100644 --- a/app/src/main/java/com/ilein/thecocktailapp/network/NetworkUtil.kt +++ b/app/src/main/java/com/ilein/thecocktailapp/data/network/NetworkUtil.kt @@ -1,4 +1,4 @@ -package com.ilein.thecocktailapp.network +package com.ilein.thecocktailapp.data.network import android.content.Context import android.net.ConnectivityManager diff --git a/app/src/main/java/com/ilein/thecocktailapp/data/network/RemoteDataSource.kt b/app/src/main/java/com/ilein/thecocktailapp/data/network/RemoteDataSource.kt new file mode 100644 index 0000000..e41b034 --- /dev/null +++ b/app/src/main/java/com/ilein/thecocktailapp/data/network/RemoteDataSource.kt @@ -0,0 +1,10 @@ +package com.ilein.thecocktailapp.data.network + +import com.ilein.thecocktailapp.domain.Drink +import kotlinx.coroutines.flow.Flow + +interface RemoteDataSource { + fun searchDrinks(query: String): Flow> + fun getDrink(drinkId: Int): Flow + suspend fun getOneDrink(drinkId: Int): Drink +} \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/data/network/RemoteDataSourceImpl.kt b/app/src/main/java/com/ilein/thecocktailapp/data/network/RemoteDataSourceImpl.kt new file mode 100644 index 0000000..950935c --- /dev/null +++ b/app/src/main/java/com/ilein/thecocktailapp/data/network/RemoteDataSourceImpl.kt @@ -0,0 +1,50 @@ +package com.ilein.thecocktailapp.data.network + +import com.ilein.thecocktailapp.data.toDomain +import com.ilein.thecocktailapp.domain.Drink +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.retryWhen + +class RemoteDataSourceImpl( + private val client: ClientProvider, + private val ioDispatcher: CoroutineDispatcher +): RemoteDataSource { + + override fun searchDrinks(query: String): Flow> { + return flow { emit(client.searchDrinks(query).results.map { t -> t.toDomain() }) } + .flowOn(ioDispatcher) + .retryWithDelay() + } + + override fun getDrink(drinkId: Int): Flow { + return flow { emit(client.getDrink(drinkId).results.first().toDomain()) } + .flowOn(ioDispatcher) + .retryWithDelay() + } + + override suspend fun getOneDrink(drinkId: Int): Drink { + return client.getDrink(drinkId).results.first().toDomain() + } + + private fun Flow.retryWithDelay( + delay: Long = DEFAULT_DELAY, + filter: (Throwable) -> Boolean = { true } + ): Flow { + return retryWhen { cause, _ -> + if (filter(cause)) { + delay(delay) + true + } else { + throw cause + } + } + } + + companion object { + private const val DEFAULT_DELAY = 2000L + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/network/model/Drink.kt b/app/src/main/java/com/ilein/thecocktailapp/data/network/model/DrinkModel.kt similarity index 66% rename from app/src/main/java/com/ilein/thecocktailapp/network/model/Drink.kt rename to app/src/main/java/com/ilein/thecocktailapp/data/network/model/DrinkModel.kt index 1ee703c..70ba909 100644 --- a/app/src/main/java/com/ilein/thecocktailapp/network/model/Drink.kt +++ b/app/src/main/java/com/ilein/thecocktailapp/data/network/model/DrinkModel.kt @@ -1,10 +1,10 @@ -package com.ilein.thecocktailapp.network.model +package com.ilein.thecocktailapp.data.network.model import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class Drink ( +data class DrinkModel ( @SerialName("idDrink") val idDrink: Int, @SerialName("strDrink") @@ -56,26 +56,20 @@ data class Drink ( @SerialName("strMeasure10") val strMeasure10: String? = null, ) { - fun getIngredientsText(): String { - val ingredientList = listOf(Pair(strIngredient1, strMeasure1), - Pair(strIngredient2, strMeasure2), - Pair(strIngredient3, strMeasure3), - Pair(strIngredient4, strMeasure4), - Pair(strIngredient5, strMeasure5), - Pair(strIngredient6, strMeasure6), - Pair(strIngredient7, strMeasure7), - Pair(strIngredient8, strMeasure8), - Pair(strIngredient9, strMeasure9), - Pair(strIngredient10, strMeasure10) - ) - - var i = 0 - val text: String = ingredientList.joinToString(separator = "") { itPair -> - itPair.first?.let { - i++ - return@let "${i}. $it ${itPair.second?.let {"- $it"} ?: ""}\n" - } ?: "" + fun getIngredientsText(): String = listOf( + Pair(strIngredient1, strMeasure1), + Pair(strIngredient2, strMeasure2), + Pair(strIngredient3, strMeasure3), + Pair(strIngredient4, strMeasure4), + Pair(strIngredient5, strMeasure5), + Pair(strIngredient6, strMeasure6), + Pair(strIngredient7, strMeasure7), + Pair(strIngredient8, strMeasure8), + Pair(strIngredient9, strMeasure9), + Pair(strIngredient10, strMeasure10)) + .filter { it.first.orEmpty().isNotEmpty() } + .mapIndexed { index, pair -> + "${index+1}. ${pair.first} ${pair.second?.let {"- $it"} ?: ""}" } - return text - } + .joinToString(separator = "\n") } \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/data/network/model/DrinksModel.kt b/app/src/main/java/com/ilein/thecocktailapp/data/network/model/DrinksModel.kt new file mode 100644 index 0000000..bd82c33 --- /dev/null +++ b/app/src/main/java/com/ilein/thecocktailapp/data/network/model/DrinksModel.kt @@ -0,0 +1,22 @@ +package com.ilein.thecocktailapp.data.network.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonTransformingSerializer + +@Serializable +data class DrinksModel( + @SerialName("drinks") + @Serializable(with = DrinkListSerializer::class) + val results: List = ArrayList() +) { + + object DrinkListSerializer : JsonTransformingSerializer>(ListSerializer(DrinkModel.serializer())) { + // If response is not an array, then it is an empty array + override fun transformDeserialize(element: JsonElement): JsonElement = + if (element !is JsonArray) JsonArray(listOf()) else element + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/db/DrinkLikeDao.kt b/app/src/main/java/com/ilein/thecocktailapp/db/DrinkLikeDao.kt deleted file mode 100644 index 3a5990c..0000000 --- a/app/src/main/java/com/ilein/thecocktailapp/db/DrinkLikeDao.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.ilein.thecocktailapp.db - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import io.reactivex.rxjava3.core.Completable -import io.reactivex.rxjava3.core.Maybe -import io.reactivex.rxjava3.core.Observable - -@Dao -abstract class DrinkLikeDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun insertDrinkLike(drinkLike: DrinkLikeEntity): Completable - - @Query("SELECT * FROM drink_like") - abstract fun observeDrinkLike(): Observable> - - @Query("SELECT * FROM drink_like order by update_date desc") - abstract fun observeDrinkLikeDesc(): Observable> - - @Query("SELECT * FROM drink_like WHERE id = :drinkId") - abstract fun getDrinkLike(drinkId: Int): Maybe - - @Query("DELETE FROM drink_like WHERE id = :drinkId") - abstract fun deleteDrinkLike(drinkId: Int): Completable -} \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/di/Module.kt b/app/src/main/java/com/ilein/thecocktailapp/di/Module.kt new file mode 100644 index 0000000..cb5c19f --- /dev/null +++ b/app/src/main/java/com/ilein/thecocktailapp/di/Module.kt @@ -0,0 +1,76 @@ +package com.ilein.thecocktailapp.di + +import com.ilein.thecocktailapp.data.DrinkRepositoryImpl +import com.ilein.thecocktailapp.data.db.DrinkFavoriteDao +import com.ilein.thecocktailapp.data.db.DrinkFavoriteDb +import com.ilein.thecocktailapp.data.db.LocalDataSource +import com.ilein.thecocktailapp.data.db.LocalDataSourceImpl +import com.ilein.thecocktailapp.data.network.ClientProvider +import com.ilein.thecocktailapp.data.network.DrinksReq +import com.ilein.thecocktailapp.data.network.NetworkUtil +import com.ilein.thecocktailapp.data.network.RemoteDataSource +import com.ilein.thecocktailapp.data.network.RemoteDataSourceImpl +import com.ilein.thecocktailapp.domain.DrinkRepository +import com.ilein.thecocktailapp.domain.usecase.DeleteDrinkUseCase +import com.ilein.thecocktailapp.domain.usecase.GetDrinkFavoriteUseCase +import com.ilein.thecocktailapp.domain.usecase.GetDrinkFavoritesUseCase +import com.ilein.thecocktailapp.domain.usecase.GetDrinkUseCase +import com.ilein.thecocktailapp.domain.usecase.GetOneDrinkUseCase +import com.ilein.thecocktailapp.domain.usecase.InsertDrinkUseCase +import com.ilein.thecocktailapp.domain.usecase.SearchDrinkUseCase +import com.ilein.thecocktailapp.presentation.ui.detail.DrinkItemDetailViewModel +import com.ilein.thecocktailapp.presentation.ui.detail.DrinkLikeItemDetailViewModel +import com.ilein.thecocktailapp.presentation.ui.liked.LikedDrinksViewModel +import com.ilein.thecocktailapp.presentation.ui.search.SearchDrinksViewModel +import kotlinx.coroutines.Dispatchers +import org.koin.android.ext.koin.androidContext +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.dsl.module + +val databaseModule = module { + single { DrinkFavoriteDb.createDatabaseInstance(context = androidContext()) } + + single { get().drinkFavoriteDao() } + single { LocalDataSourceImpl(get(), Dispatchers.IO) } + + single { ClientProvider() } + single { RemoteDataSourceImpl(get(), Dispatchers.IO) } + + single { DrinkRepositoryImpl(get() as LocalDataSource, get() as RemoteDataSource) } +} + +val useCaseModule = module { + single { InsertDrinkUseCase(get() as DrinkRepository, Dispatchers.IO) } + single { DeleteDrinkUseCase(get() as DrinkRepository, Dispatchers.IO) } + single { SearchDrinkUseCase(get() as DrinkRepository, Dispatchers.IO) } + single { GetDrinkFavoritesUseCase(get() as DrinkRepository, Dispatchers.IO) } + single { GetDrinkFavoriteUseCase(get() as DrinkRepository, Dispatchers.IO) } + single { GetDrinkUseCase(get() as DrinkRepository, Dispatchers.IO) } + single { GetOneDrinkUseCase(get() as DrinkRepository, Dispatchers.IO) } + +} + +val networkModule = module { + single { DrinksReq() } + + single { NetworkUtil(context = androidContext()) } + + single { get().isOnline() } +} + +val vmModule = module { + viewModel { + SearchDrinksViewModel(get() as SearchDrinkUseCase, get() as GetDrinkFavoritesUseCase, + get() as InsertDrinkUseCase, get() as DeleteDrinkUseCase, get() as NetworkUtil) + } + viewModel { + LikedDrinksViewModel(get() as DeleteDrinkUseCase, get() as GetDrinkFavoritesUseCase) + } + viewModel { + DrinkItemDetailViewModel(get() as DrinkFavoriteDao, get() as DrinksReq, get() as NetworkUtil) + } + viewModel { + DrinkLikeItemDetailViewModel(get() as InsertDrinkUseCase, get() as GetDrinkFavoriteUseCase, + get() as GetDrinkUseCase, get() as GetOneDrinkUseCase, get() as NetworkUtil) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/domain/Drink.kt b/app/src/main/java/com/ilein/thecocktailapp/domain/Drink.kt new file mode 100644 index 0000000..4ead256 --- /dev/null +++ b/app/src/main/java/com/ilein/thecocktailapp/domain/Drink.kt @@ -0,0 +1,9 @@ +package com.ilein.thecocktailapp.domain + +data class Drink ( + val id: Int, + val name: String?, + val image: String?, + val instruction: String?, + val ingredients: String? +) \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/domain/DrinkRepository.kt b/app/src/main/java/com/ilein/thecocktailapp/domain/DrinkRepository.kt new file mode 100644 index 0000000..a39f617 --- /dev/null +++ b/app/src/main/java/com/ilein/thecocktailapp/domain/DrinkRepository.kt @@ -0,0 +1,15 @@ +package com.ilein.thecocktailapp.domain + +import kotlinx.coroutines.flow.Flow + + +interface DrinkRepository { + suspend fun insertDrinkFavorites(drink: FavoriteDrink) + suspend fun getDrinkFavorites(): List + suspend fun getDrinkFavoritesDesc(): List + suspend fun getFavoriteDrink(drinkId: Int): FavoriteDrink? + suspend fun deleteFavoriteDrink(drinkId: Int) + suspend fun getDrink(drinkId: Int): Flow + suspend fun getOneDrink(drinkId: Int): Drink + suspend fun searchDrinks(query: String): Flow> +} \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/domain/FavoriteDrink.kt b/app/src/main/java/com/ilein/thecocktailapp/domain/FavoriteDrink.kt new file mode 100644 index 0000000..a9fa996 --- /dev/null +++ b/app/src/main/java/com/ilein/thecocktailapp/domain/FavoriteDrink.kt @@ -0,0 +1,12 @@ +package com.ilein.thecocktailapp.domain + + +import java.time.LocalDateTime + +data class FavoriteDrink( + val id: Int, + val name: String?, + val image: String?, + val createDate: LocalDateTime?, + val note: String?, +) diff --git a/app/src/main/java/com/ilein/thecocktailapp/domain/base/FlowUseCaseBase.kt b/app/src/main/java/com/ilein/thecocktailapp/domain/base/FlowUseCaseBase.kt new file mode 100644 index 0000000..1169a21 --- /dev/null +++ b/app/src/main/java/com/ilein/thecocktailapp/domain/base/FlowUseCaseBase.kt @@ -0,0 +1,15 @@ +package com.ilein.thecocktailapp.domain.base + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flowOn + +abstract class FlowUseCaseBase(private val dispatcher: CoroutineDispatcher) { + + operator fun invoke(arg: P): Flow> = execute(arg) + .catch { e -> emit(Result.Error(e)) } + .flowOn(dispatcher) + + protected abstract fun execute(arg: P): Flow> +} \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/domain/base/Result.kt b/app/src/main/java/com/ilein/thecocktailapp/domain/base/Result.kt new file mode 100644 index 0000000..70648bd --- /dev/null +++ b/app/src/main/java/com/ilein/thecocktailapp/domain/base/Result.kt @@ -0,0 +1,12 @@ +package com.ilein.thecocktailapp.domain.base + +sealed class Result { + data class Success(val data: T) : Result() + data class Error(val throwable: Throwable) : Result() +} + +val Result.data: T? + get() = (this as? Result.Success)?.data + +val Result.error: Throwable? + get() = (this as? Result.Error)?.throwable \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/domain/base/SuspendUseCaseBase.kt b/app/src/main/java/com/ilein/thecocktailapp/domain/base/SuspendUseCaseBase.kt new file mode 100644 index 0000000..30deb21 --- /dev/null +++ b/app/src/main/java/com/ilein/thecocktailapp/domain/base/SuspendUseCaseBase.kt @@ -0,0 +1,22 @@ +package com.ilein.thecocktailapp.domain.base + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext + +abstract class SuspendUseCaseBase(private val dispatcher: CoroutineDispatcher) { + + /** Выполняет UseCase асинхронно и возвращает [Result] с типом [R] */ + suspend operator fun invoke(arg: P): Result { + return try { + withContext(dispatcher) { + execute(arg).let { Result.Success(it) } + } + } catch (throwable: Throwable) { + Result.Error(throwable) + } + } + + /** Переопределить этод метод в наследнике для выполнения UseСase - а */ + @Throws(RuntimeException::class) + protected abstract suspend fun execute(arg: P): R +} \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/domain/usecase/DeleteDrinkUseCase.kt b/app/src/main/java/com/ilein/thecocktailapp/domain/usecase/DeleteDrinkUseCase.kt new file mode 100644 index 0000000..0620a50 --- /dev/null +++ b/app/src/main/java/com/ilein/thecocktailapp/domain/usecase/DeleteDrinkUseCase.kt @@ -0,0 +1,14 @@ +package com.ilein.thecocktailapp.domain.usecase + +import com.ilein.thecocktailapp.domain.DrinkRepository +import com.ilein.thecocktailapp.domain.base.SuspendUseCaseBase +import kotlinx.coroutines.CoroutineDispatcher + +class DeleteDrinkUseCase( + private val repository: DrinkRepository, + dispatcher: CoroutineDispatcher +): SuspendUseCaseBase(dispatcher) { + override suspend fun execute(arg: Int) { + repository.deleteFavoriteDrink(arg) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/domain/usecase/GetDrinkFavoriteUseCase.kt b/app/src/main/java/com/ilein/thecocktailapp/domain/usecase/GetDrinkFavoriteUseCase.kt new file mode 100644 index 0000000..b6eb0e5 --- /dev/null +++ b/app/src/main/java/com/ilein/thecocktailapp/domain/usecase/GetDrinkFavoriteUseCase.kt @@ -0,0 +1,15 @@ +package com.ilein.thecocktailapp.domain.usecase + +import com.ilein.thecocktailapp.domain.FavoriteDrink +import com.ilein.thecocktailapp.domain.DrinkRepository +import com.ilein.thecocktailapp.domain.base.SuspendUseCaseBase +import kotlinx.coroutines.CoroutineDispatcher + +class GetDrinkFavoriteUseCase( + private val repository: DrinkRepository, + dispatcher: CoroutineDispatcher +): SuspendUseCaseBase(dispatcher) { + override suspend fun execute(arg: Int): FavoriteDrink? { + return repository.getFavoriteDrink(arg) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/domain/usecase/GetDrinkFavoritesUseCase.kt b/app/src/main/java/com/ilein/thecocktailapp/domain/usecase/GetDrinkFavoritesUseCase.kt new file mode 100644 index 0000000..8ccb7c7 --- /dev/null +++ b/app/src/main/java/com/ilein/thecocktailapp/domain/usecase/GetDrinkFavoritesUseCase.kt @@ -0,0 +1,15 @@ +package com.ilein.thecocktailapp.domain.usecase + +import com.ilein.thecocktailapp.domain.FavoriteDrink +import com.ilein.thecocktailapp.domain.DrinkRepository +import com.ilein.thecocktailapp.domain.base.SuspendUseCaseBase +import kotlinx.coroutines.CoroutineDispatcher + +class GetDrinkFavoritesUseCase( + private val repository: DrinkRepository, + dispatcher: CoroutineDispatcher +): SuspendUseCaseBase>(dispatcher) { + override suspend fun execute(arg: String): List { + return repository.getDrinkFavorites() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/domain/usecase/GetDrinkUseCase.kt b/app/src/main/java/com/ilein/thecocktailapp/domain/usecase/GetDrinkUseCase.kt new file mode 100644 index 0000000..b2ab3fd --- /dev/null +++ b/app/src/main/java/com/ilein/thecocktailapp/domain/usecase/GetDrinkUseCase.kt @@ -0,0 +1,16 @@ +package com.ilein.thecocktailapp.domain.usecase + +import com.ilein.thecocktailapp.domain.Drink +import com.ilein.thecocktailapp.domain.DrinkRepository +import com.ilein.thecocktailapp.domain.base.SuspendUseCaseBase +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow + +class GetDrinkUseCase ( + private val repository: DrinkRepository, + dispatcher: CoroutineDispatcher +): SuspendUseCaseBase>(dispatcher) { + override suspend fun execute(arg: Int): Flow { + return repository.getDrink(arg) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/domain/usecase/GetOneDrinkUseCase.kt b/app/src/main/java/com/ilein/thecocktailapp/domain/usecase/GetOneDrinkUseCase.kt new file mode 100644 index 0000000..933f5b1 --- /dev/null +++ b/app/src/main/java/com/ilein/thecocktailapp/domain/usecase/GetOneDrinkUseCase.kt @@ -0,0 +1,15 @@ +package com.ilein.thecocktailapp.domain.usecase + +import com.ilein.thecocktailapp.domain.Drink +import com.ilein.thecocktailapp.domain.DrinkRepository +import com.ilein.thecocktailapp.domain.base.SuspendUseCaseBase +import kotlinx.coroutines.CoroutineDispatcher + +class GetOneDrinkUseCase( + private val repository: DrinkRepository, + dispatcher: CoroutineDispatcher +): SuspendUseCaseBase(dispatcher) { + override suspend fun execute(arg: Int): Drink? { + return repository.getOneDrink(arg) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/domain/usecase/InsertDrinkUseCase.kt b/app/src/main/java/com/ilein/thecocktailapp/domain/usecase/InsertDrinkUseCase.kt new file mode 100644 index 0000000..e504852 --- /dev/null +++ b/app/src/main/java/com/ilein/thecocktailapp/domain/usecase/InsertDrinkUseCase.kt @@ -0,0 +1,15 @@ +package com.ilein.thecocktailapp.domain.usecase + +import com.ilein.thecocktailapp.domain.FavoriteDrink +import com.ilein.thecocktailapp.domain.DrinkRepository +import com.ilein.thecocktailapp.domain.base.SuspendUseCaseBase +import kotlinx.coroutines.CoroutineDispatcher + +class InsertDrinkUseCase( + private val repository: DrinkRepository, + dispatcher: CoroutineDispatcher +): SuspendUseCaseBase(dispatcher) { + override suspend fun execute(arg: FavoriteDrink) { + repository.insertDrinkFavorites(arg) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/domain/usecase/SearchDrinkUseCase.kt b/app/src/main/java/com/ilein/thecocktailapp/domain/usecase/SearchDrinkUseCase.kt new file mode 100644 index 0000000..1a18393 --- /dev/null +++ b/app/src/main/java/com/ilein/thecocktailapp/domain/usecase/SearchDrinkUseCase.kt @@ -0,0 +1,16 @@ +package com.ilein.thecocktailapp.domain.usecase + +import com.ilein.thecocktailapp.domain.Drink +import com.ilein.thecocktailapp.domain.DrinkRepository +import com.ilein.thecocktailapp.domain.base.SuspendUseCaseBase +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow + +class SearchDrinkUseCase ( + private val repository: DrinkRepository, + dispatcher: CoroutineDispatcher +): SuspendUseCaseBase>>(dispatcher) { + override suspend fun execute(arg: String): Flow> { + return repository.searchDrinks(arg) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/koin/Module.kt b/app/src/main/java/com/ilein/thecocktailapp/koin/Module.kt deleted file mode 100644 index 8ad19dc..0000000 --- a/app/src/main/java/com/ilein/thecocktailapp/koin/Module.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.ilein.thecocktailapp.koin - -import android.os.Build -import androidx.annotation.RequiresApi -import com.ilein.thecocktailapp.db.DrinkLikeDao -import com.ilein.thecocktailapp.db.DrinkLikeDb -import com.ilein.thecocktailapp.network.DrinksReq -import com.ilein.thecocktailapp.network.NetworkUtil -import com.ilein.thecocktailapp.ui.detail.DrinkItemDetailViewModel -import com.ilein.thecocktailapp.ui.detail.DrinkLikeItemDetailViewModel -import com.ilein.thecocktailapp.ui.liked.LikedDrinksViewModel -import com.ilein.thecocktailapp.ui.search.SearchDrinksViewModel -import org.koin.android.ext.koin.androidContext -import org.koin.androidx.viewmodel.dsl.viewModel -import org.koin.dsl.module - -val databaseModule = module { - single { DrinkLikeDb.createDatabaseInstance(context = androidContext()) } - - single { get().drinkLikeDao() } -} - -@RequiresApi(Build.VERSION_CODES.M) -val networkModule = module { - single { DrinksReq() } - - single { NetworkUtil(context = androidContext()) } - - single { get().isOnline() } -} - -val vmModule = module { - viewModel { - SearchDrinksViewModel(get() as DrinkLikeDao, get() as DrinksReq, get() as NetworkUtil) - } - viewModel { - LikedDrinksViewModel(get() as DrinkLikeDao) - } - viewModel { - DrinkItemDetailViewModel(get() as DrinkLikeDao, get() as DrinksReq, get() as NetworkUtil) - } - viewModel { - DrinkLikeItemDetailViewModel(get() as DrinkLikeDao, get() as DrinksReq, get() as NetworkUtil) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/network/Api.kt b/app/src/main/java/com/ilein/thecocktailapp/network/Api.kt deleted file mode 100644 index 95e9723..0000000 --- a/app/src/main/java/com/ilein/thecocktailapp/network/Api.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.ilein.thecocktailapp.network - -import com.ilein.thecocktailapp.BuildConfig -import com.ilein.thecocktailapp.network.model.Drinks -import io.ktor.client.* -import io.ktor.client.features.json.* -import io.ktor.client.features.json.serializer.* -import io.ktor.client.features.logging.* -import io.ktor.client.request.* - -class Api { - private val baseUrl = BuildConfig.API_BASE_URL - private val drinks = "api/json/${BuildConfig.API_VERSION}/${BuildConfig.API_KEY}/search.php" - private val drink = "api/json/${BuildConfig.API_VERSION}/${BuildConfig.API_KEY}/lookup.php" - - - private val ktorClient = HttpClient() { - install(Logging) { - level = LogLevel.BODY - } - - install(JsonFeature) { - serializer = KotlinxSerializer(kotlinx.serialization.json.Json { - prettyPrint = true - isLenient = true - ignoreUnknownKeys = true - }) - } - } - - suspend fun getDrink(drinkId: Int): Drinks = ktorClient.use { - it.get( - host = baseUrl, - path = buildPath(drink, mapOf(Pair("i", drinkId))) - ) - } - - suspend fun getDrinks(searchText: String): Drinks = ktorClient.use { - it.get( - host = baseUrl, - path = buildPath(drinks, mapOf(Pair("s", searchText))) - ) - } - - private fun buildPath( - endpoint: String, - query: Map = emptyMap(), - ): String = - buildString { - append("$endpoint?") - append( - query.entries.joinToString( - separator = "&" - ) { (key, value) -> - "$key=$value" - } - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/network/model/Drinks.kt b/app/src/main/java/com/ilein/thecocktailapp/network/model/Drinks.kt deleted file mode 100644 index 7f38e5f..0000000 --- a/app/src/main/java/com/ilein/thecocktailapp/network/model/Drinks.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.ilein.thecocktailapp.network.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.builtins.ListSerializer -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonTransformingSerializer - -@Serializable -data class Drinks( - @SerialName("drinks") - @Serializable(with = DrinkListSerializer::class) - val results: List = ArrayList() -) - -object DrinkListSerializer : JsonTransformingSerializer>(ListSerializer(Drink.serializer())) { - // If response is not an array, then it is an empty array - override fun transformDeserialize(element: JsonElement): JsonElement = - if (element !is JsonArray) JsonArray(listOf()) else element -} \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/MainActivity.kt b/app/src/main/java/com/ilein/thecocktailapp/presentation/MainActivity.kt similarity index 82% rename from app/src/main/java/com/ilein/thecocktailapp/MainActivity.kt rename to app/src/main/java/com/ilein/thecocktailapp/presentation/MainActivity.kt index 84f11f4..e729c6a 100644 --- a/app/src/main/java/com/ilein/thecocktailapp/MainActivity.kt +++ b/app/src/main/java/com/ilein/thecocktailapp/presentation/MainActivity.kt @@ -1,8 +1,9 @@ -package com.ilein.thecocktailapp +package com.ilein.thecocktailapp.presentation import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.view.Window +import com.ilein.thecocktailapp.R class MainActivity : AppCompatActivity() { diff --git a/app/src/main/java/com/ilein/thecocktailapp/ui/detail/DrinkItemDetailFragment.kt b/app/src/main/java/com/ilein/thecocktailapp/presentation/ui/detail/DrinkItemDetailFragment.kt similarity index 89% rename from app/src/main/java/com/ilein/thecocktailapp/ui/detail/DrinkItemDetailFragment.kt rename to app/src/main/java/com/ilein/thecocktailapp/presentation/ui/detail/DrinkItemDetailFragment.kt index 69f79e7..7a259f1 100644 --- a/app/src/main/java/com/ilein/thecocktailapp/ui/detail/DrinkItemDetailFragment.kt +++ b/app/src/main/java/com/ilein/thecocktailapp/presentation/ui/detail/DrinkItemDetailFragment.kt @@ -1,4 +1,4 @@ -package com.ilein.thecocktailapp.ui.detail +package com.ilein.thecocktailapp.presentation.ui.detail import android.content.Intent import android.os.Bundle @@ -13,7 +13,7 @@ import coil.transform.RoundedCornersTransformation import com.google.android.material.button.MaterialButton import com.ilein.thecocktailapp.R import com.ilein.thecocktailapp.databinding.FragmentDrinkItemDetailBinding -import com.ilein.thecocktailapp.ui.state.DrinkItemState +import com.ilein.thecocktailapp.presentation.ui.state.DrinkItemState import org.koin.androidx.viewmodel.ext.android.viewModel /** @@ -89,13 +89,13 @@ class DrinkItemDetailFragment : Fragment() { if (state.isOnline) { state.item?.let { - binding.poster.contentDescription = it.drink.strDrinkThumb - binding.poster.load("${it.drink.strDrinkThumb}") { + binding.poster.contentDescription = it.drink.image + binding.poster.load("${it.drink.image}") { transformations(RoundedCornersTransformation(16f)) } - binding.drinkTitle.text = it.drink.strDrink - binding.instructions.text = it.drink.strInstructions - binding.ingredients.text = it.drink.getIngredientsText() + binding.drinkTitle.text = it.drink.name + binding.instructions.text = it.drink.instruction + binding.ingredients.text = it.drink.ingredients } } else { val toast = Toast.makeText(context, "No Internet Connection!!!", Toast.LENGTH_LONG) diff --git a/app/src/main/java/com/ilein/thecocktailapp/ui/detail/DrinkItemDetailViewModel.kt b/app/src/main/java/com/ilein/thecocktailapp/presentation/ui/detail/DrinkItemDetailViewModel.kt similarity index 64% rename from app/src/main/java/com/ilein/thecocktailapp/ui/detail/DrinkItemDetailViewModel.kt rename to app/src/main/java/com/ilein/thecocktailapp/presentation/ui/detail/DrinkItemDetailViewModel.kt index fff033c..f4be765 100644 --- a/app/src/main/java/com/ilein/thecocktailapp/ui/detail/DrinkItemDetailViewModel.kt +++ b/app/src/main/java/com/ilein/thecocktailapp/presentation/ui/detail/DrinkItemDetailViewModel.kt @@ -1,18 +1,19 @@ -package com.ilein.thecocktailapp.ui.detail +package com.ilein.thecocktailapp.presentation.ui.detail import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.ilein.thecocktailapp.db.DrinkLikeDao -import com.ilein.thecocktailapp.network.DrinksReq -import com.ilein.thecocktailapp.network.NetworkUtil -import com.ilein.thecocktailapp.ui.state.DrinkData -import com.ilein.thecocktailapp.ui.state.DrinkItemState +import com.ilein.thecocktailapp.data.db.DrinkFavoriteDao +import com.ilein.thecocktailapp.data.network.DrinksReq +import com.ilein.thecocktailapp.data.network.NetworkUtil +import com.ilein.thecocktailapp.data.toDomain +import com.ilein.thecocktailapp.presentation.ui.state.DrinkData +import com.ilein.thecocktailapp.presentation.ui.state.DrinkItemState import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart -class DrinkItemDetailViewModel(private val drinkLikeDao: DrinkLikeDao, +class DrinkItemDetailViewModel(private val drinkFavoriteDao: DrinkFavoriteDao, private val drinksReq: DrinksReq, private val networkUtils: NetworkUtil ): ViewModel() { @@ -24,8 +25,8 @@ class DrinkItemDetailViewModel(private val drinkLikeDao: DrinkLikeDao, .onStart { state.postValue(DrinkItemState(null, true, isOnline = true)) } .onEach { drinks -> run { - val drink = drinks.results[0] - state.postValue(DrinkItemState(DrinkData(drink, isLike), false, isOnline = true)) + val drink = drinks.results.first() + state.postValue(DrinkItemState(DrinkData(drink.toDomain(), isLike), false, isOnline = true)) } } .launchIn(viewModelScope) diff --git a/app/src/main/java/com/ilein/thecocktailapp/ui/detail/DrinkLikeItemDetailFragment.kt b/app/src/main/java/com/ilein/thecocktailapp/presentation/ui/detail/DrinkLikeItemDetailFragment.kt similarity index 91% rename from app/src/main/java/com/ilein/thecocktailapp/ui/detail/DrinkLikeItemDetailFragment.kt rename to app/src/main/java/com/ilein/thecocktailapp/presentation/ui/detail/DrinkLikeItemDetailFragment.kt index 21858e7..d34139a 100644 --- a/app/src/main/java/com/ilein/thecocktailapp/ui/detail/DrinkLikeItemDetailFragment.kt +++ b/app/src/main/java/com/ilein/thecocktailapp/presentation/ui/detail/DrinkLikeItemDetailFragment.kt @@ -1,4 +1,4 @@ -package com.ilein.thecocktailapp.ui.detail +package com.ilein.thecocktailapp.presentation.ui.detail import android.content.Intent import android.os.Bundle @@ -14,7 +14,7 @@ import coil.transform.RoundedCornersTransformation import com.google.android.material.button.MaterialButton import com.ilein.thecocktailapp.R import com.ilein.thecocktailapp.databinding.FragmentDrinkLikeItemDetailBinding -import com.ilein.thecocktailapp.ui.state.DrinkItemLikeState +import com.ilein.thecocktailapp.presentation.ui.state.DrinkItemLikeState import org.koin.androidx.viewmodel.ext.android.viewModel @@ -90,13 +90,13 @@ class DrinkLikeItemDetailFragment : Fragment() { if (state.isOnline) { state.drink?.let { - binding.posterLike.contentDescription = state.drink.strDrinkThumb - binding.posterLike.load("${state.drink.strDrinkThumb}") { + binding.posterLike.contentDescription = state.drink.image + binding.posterLike.load("${state.drink.image}") { transformations(RoundedCornersTransformation(16f)) } - binding.drinkTitleLike.text = state.drink.strDrink - binding.instructionsLike.text = state.drink.strInstructions - binding.ingredientsLike.text = state.drink.getIngredientsText() + binding.drinkTitleLike.text = state.drink.name + binding.instructionsLike.text = state.drink.instruction + binding.ingredientsLike.text = state.drink.ingredients } } else { val toast = Toast.makeText(context, "No Internet Connection", Toast.LENGTH_LONG) diff --git a/app/src/main/java/com/ilein/thecocktailapp/presentation/ui/detail/DrinkLikeItemDetailViewModel.kt b/app/src/main/java/com/ilein/thecocktailapp/presentation/ui/detail/DrinkLikeItemDetailViewModel.kt new file mode 100644 index 0000000..37c1dc5 --- /dev/null +++ b/app/src/main/java/com/ilein/thecocktailapp/presentation/ui/detail/DrinkLikeItemDetailViewModel.kt @@ -0,0 +1,73 @@ +package com.ilein.thecocktailapp.presentation.ui.detail + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.ilein.thecocktailapp.data.network.NetworkUtil +import com.ilein.thecocktailapp.domain.base.Result +import com.ilein.thecocktailapp.domain.usecase.GetDrinkFavoriteUseCase +import com.ilein.thecocktailapp.domain.usecase.GetDrinkUseCase +import com.ilein.thecocktailapp.domain.usecase.GetOneDrinkUseCase +import com.ilein.thecocktailapp.domain.usecase.InsertDrinkUseCase +import com.ilein.thecocktailapp.presentation.ui.state.DrinkItemLikeState +import kotlinx.coroutines.launch + +class DrinkLikeItemDetailViewModel(private val insertDrinkUseCase: InsertDrinkUseCase, + private val getDrinkFavoriteUseCase: GetDrinkFavoriteUseCase, + private val getDrinkUseCase: GetDrinkUseCase, + private val getOneDrinkUseCase: GetOneDrinkUseCase, + private val networkUtil: NetworkUtil +): ViewModel() { + val state = MutableLiveData() + + fun init(drinkId: Int) { + if (networkUtil.isOnline()) { + viewModelScope.launch { + when(val result = getDrinkFavoriteUseCase(drinkId)) { + is Result.Success -> { + when(val result2 = getOneDrinkUseCase(drinkId)) { + is Result.Success -> { + state.postValue(DrinkItemLikeState(result2.data, result.data, false)) + } + is Result.Error -> { + state.postValue(DrinkItemLikeState(null, null, false, isOnline = true)) + } + } + } + is Result.Error -> { + state.postValue(DrinkItemLikeState(null, null, false, isOnline = true)) + } + } + } + } else { + state.postValue(DrinkItemLikeState(null, null, false, isOnline = false)) + } + } + + fun onRefresh(drinkId: Int) { + init(drinkId) + } + + fun onNoteChange(note: String) { + if (state.value!!.drinkLike?.note != note) { + state.postValue(state.value!!.copy( + drinkLike = state.value!!.drinkLike?.copy(note = note) + )) + } + } + + fun onSaveClick() { + state.value?.drinkLike?.let { + viewModelScope.launch { + when (val result = insertDrinkUseCase(it)) { + is Result.Success -> { + + } + is Result.Error -> { + result.throwable + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/ui/liked/LikedDrinkItemAdapter.kt b/app/src/main/java/com/ilein/thecocktailapp/presentation/ui/liked/LikedDrinkItemAdapter.kt similarity index 80% rename from app/src/main/java/com/ilein/thecocktailapp/ui/liked/LikedDrinkItemAdapter.kt rename to app/src/main/java/com/ilein/thecocktailapp/presentation/ui/liked/LikedDrinkItemAdapter.kt index 1a21342..43c05e0 100644 --- a/app/src/main/java/com/ilein/thecocktailapp/ui/liked/LikedDrinkItemAdapter.kt +++ b/app/src/main/java/com/ilein/thecocktailapp/presentation/ui/liked/LikedDrinkItemAdapter.kt @@ -1,4 +1,4 @@ -package com.ilein.thecocktailapp.ui.liked +package com.ilein.thecocktailapp.presentation.ui.liked import android.annotation.SuppressLint import android.os.Build @@ -14,17 +14,17 @@ import androidx.recyclerview.widget.RecyclerView import coil.load import coil.transform.RoundedCornersTransformation import com.ilein.thecocktailapp.R -import com.ilein.thecocktailapp.db.DrinkLikeEntity -import com.ilein.thecocktailapp.ui.utils.DiffLikeCallback +import com.ilein.thecocktailapp.domain.FavoriteDrink +import com.ilein.thecocktailapp.presentation.ui.utils.DiffLikeCallback import java.time.format.DateTimeFormatter -internal class LikedDrinkItemAdapter (private val onItemClick: (DrinkLikeEntity) -> Unit, - private val onDelClick: (DrinkLikeEntity) -> Unit) : +internal class LikedDrinkItemAdapter (private val onItemClick: (FavoriteDrink) -> Unit, + private val onDelClick: (FavoriteDrink) -> Unit) : RecyclerView.Adapter() { - private val items: MutableList = mutableListOf() + private val items: MutableList = mutableListOf() - fun setItems(newItems: List) { + fun setItems(newItems: List) { DiffUtil.calculateDiff(DiffLikeCallback(items, newItems)).also { result -> items.clear() items.addAll(newItems) @@ -41,9 +41,9 @@ internal class LikedDrinkItemAdapter (private val onItemClick: (DrinkLikeEntity) private val image: ImageView = view.findViewById(R.id.imageLiked) private val updateDate: TextView = view.findViewById(R.id.tvUpdateDate) private val del: ImageButton = view.findViewById(R.id.ibDelete) + @SuppressLint("SetTextI18n") - @RequiresApi(Build.VERSION_CODES.O) - fun onBind(item: DrinkLikeEntity, onItemClick: (DrinkLikeEntity) -> Unit, onDelClick: (DrinkLikeEntity) -> Unit) { + fun onBind(item: FavoriteDrink, onItemClick: (FavoriteDrink) -> Unit, onDelClick: (FavoriteDrink) -> Unit) { itemView.setOnClickListener { onItemClick(item) } del.setOnClickListener { onDelClick(item) @@ -51,7 +51,6 @@ internal class LikedDrinkItemAdapter (private val onItemClick: (DrinkLikeEntity) title.text = item.name val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss") updateDate.text = "Added: ${item.createDate?.format(formatter) ?: ""}" - //like.isChecked = item.like ?: false image.load("${item.image}/preview") { transformations(RoundedCornersTransformation(16f)) } diff --git a/app/src/main/java/com/ilein/thecocktailapp/ui/liked/LikedDrinksFragment.kt b/app/src/main/java/com/ilein/thecocktailapp/presentation/ui/liked/LikedDrinksFragment.kt similarity index 96% rename from app/src/main/java/com/ilein/thecocktailapp/ui/liked/LikedDrinksFragment.kt rename to app/src/main/java/com/ilein/thecocktailapp/presentation/ui/liked/LikedDrinksFragment.kt index fc3008b..8f7b963 100644 --- a/app/src/main/java/com/ilein/thecocktailapp/ui/liked/LikedDrinksFragment.kt +++ b/app/src/main/java/com/ilein/thecocktailapp/presentation/ui/liked/LikedDrinksFragment.kt @@ -1,4 +1,4 @@ -package com.ilein.thecocktailapp.ui.liked +package com.ilein.thecocktailapp.presentation.ui.liked import android.os.Bundle import androidx.fragment.app.Fragment diff --git a/app/src/main/java/com/ilein/thecocktailapp/presentation/ui/liked/LikedDrinksViewModel.kt b/app/src/main/java/com/ilein/thecocktailapp/presentation/ui/liked/LikedDrinksViewModel.kt new file mode 100644 index 0000000..cc745de --- /dev/null +++ b/app/src/main/java/com/ilein/thecocktailapp/presentation/ui/liked/LikedDrinksViewModel.kt @@ -0,0 +1,57 @@ +package com.ilein.thecocktailapp.presentation.ui.liked + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.ilein.thecocktailapp.domain.FavoriteDrink +import com.ilein.thecocktailapp.domain.base.Result +import com.ilein.thecocktailapp.domain.usecase.DeleteDrinkUseCase +import com.ilein.thecocktailapp.domain.usecase.GetDrinkFavoritesUseCase +import com.ilein.thecocktailapp.presentation.ui.state.DrinksLikeState +import kotlinx.coroutines.launch + +class LikedDrinksViewModel(private val deleteDrinkUseCase: DeleteDrinkUseCase, + private val getDrinkFavoritesUseCase: GetDrinkFavoritesUseCase) : ViewModel() { + + private val _liveData: MutableLiveData = MutableLiveData(DrinksLikeState()) + + val state: LiveData = _liveData + + init { + initData() + } + + private fun initData() { + viewModelScope.launch { + when(val result = getDrinkFavoritesUseCase.invoke("")){ + is Result.Success -> { + _liveData.postValue(DrinksLikeState( + items = result.data, + loading = false + )) + } + is Result.Error -> { + result.throwable + } + } + } + } + + /** + * ACTIONS + */ + + fun onDelClick(drink: FavoriteDrink) { + viewModelScope.launch { + when(val result = deleteDrinkUseCase(drink.id)) { + is Result.Success -> { + initData() + } + is Result.Error -> { + result.throwable + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/ui/main/MainFragment.kt b/app/src/main/java/com/ilein/thecocktailapp/presentation/ui/main/MainFragment.kt similarity index 95% rename from app/src/main/java/com/ilein/thecocktailapp/ui/main/MainFragment.kt rename to app/src/main/java/com/ilein/thecocktailapp/presentation/ui/main/MainFragment.kt index 468eb90..e6da89f 100644 --- a/app/src/main/java/com/ilein/thecocktailapp/ui/main/MainFragment.kt +++ b/app/src/main/java/com/ilein/thecocktailapp/presentation/ui/main/MainFragment.kt @@ -1,4 +1,4 @@ -package com.ilein.thecocktailapp.ui.main +package com.ilein.thecocktailapp.presentation.ui.main import android.os.Bundle import androidx.fragment.app.Fragment diff --git a/app/src/main/java/com/ilein/thecocktailapp/ui/main/MainViewModel.kt b/app/src/main/java/com/ilein/thecocktailapp/presentation/ui/main/MainViewModel.kt similarity index 67% rename from app/src/main/java/com/ilein/thecocktailapp/ui/main/MainViewModel.kt rename to app/src/main/java/com/ilein/thecocktailapp/presentation/ui/main/MainViewModel.kt index 986e6e6..77dc09f 100644 --- a/app/src/main/java/com/ilein/thecocktailapp/ui/main/MainViewModel.kt +++ b/app/src/main/java/com/ilein/thecocktailapp/presentation/ui/main/MainViewModel.kt @@ -1,4 +1,4 @@ -package com.ilein.thecocktailapp.ui.main +package com.ilein.thecocktailapp.presentation.ui.main import androidx.lifecycle.ViewModel diff --git a/app/src/main/java/com/ilein/thecocktailapp/ui/map/MapFragment.kt b/app/src/main/java/com/ilein/thecocktailapp/presentation/ui/map/MapFragment.kt similarity index 98% rename from app/src/main/java/com/ilein/thecocktailapp/ui/map/MapFragment.kt rename to app/src/main/java/com/ilein/thecocktailapp/presentation/ui/map/MapFragment.kt index 4b9a363..4270dc9 100644 --- a/app/src/main/java/com/ilein/thecocktailapp/ui/map/MapFragment.kt +++ b/app/src/main/java/com/ilein/thecocktailapp/presentation/ui/map/MapFragment.kt @@ -1,4 +1,4 @@ -package com.ilein.thecocktailapp.ui.map +package com.ilein.thecocktailapp.presentation.ui.map import android.Manifest diff --git a/app/src/main/java/com/ilein/thecocktailapp/ui/search/DrinkItemAdapter.kt b/app/src/main/java/com/ilein/thecocktailapp/presentation/ui/search/DrinkItemAdapter.kt similarity index 87% rename from app/src/main/java/com/ilein/thecocktailapp/ui/search/DrinkItemAdapter.kt rename to app/src/main/java/com/ilein/thecocktailapp/presentation/ui/search/DrinkItemAdapter.kt index 6322beb..8ec3404 100644 --- a/app/src/main/java/com/ilein/thecocktailapp/ui/search/DrinkItemAdapter.kt +++ b/app/src/main/java/com/ilein/thecocktailapp/presentation/ui/search/DrinkItemAdapter.kt @@ -1,4 +1,4 @@ -package com.ilein.thecocktailapp.ui.search +package com.ilein.thecocktailapp.presentation.ui.search import android.view.LayoutInflater import android.view.View @@ -11,9 +11,9 @@ import androidx.recyclerview.widget.RecyclerView import coil.load import coil.transform.RoundedCornersTransformation import com.ilein.thecocktailapp.R -import com.ilein.thecocktailapp.network.model.Drink -import com.ilein.thecocktailapp.ui.state.DrinkData -import com.ilein.thecocktailapp.ui.utils.DiffCallback +import com.ilein.thecocktailapp.domain.Drink +import com.ilein.thecocktailapp.presentation.ui.state.DrinkData +import com.ilein.thecocktailapp.presentation.ui.utils.DiffCallback internal class DrinkItemAdapter(private val onItemClick: (Drink) -> Unit, private val onLikeClick: (Drink, Boolean) -> Unit) : @@ -43,9 +43,9 @@ internal class DrinkItemAdapter(private val onItemClick: (Drink) -> Unit, like.setOnCheckedChangeListener { _, isChecked -> onLikeClick(item.drink, isChecked) } - title.text = item.drink.strDrink + title.text = item.drink.id.toString() like.isChecked = item.like ?: false - image.load("${item.drink.strDrinkThumb}/preview") { + image.load("${item.drink.image}/preview") { transformations(RoundedCornersTransformation(16f)) } } diff --git a/app/src/main/java/com/ilein/thecocktailapp/ui/search/SearchDrinksFragment.kt b/app/src/main/java/com/ilein/thecocktailapp/presentation/ui/search/SearchDrinksFragment.kt similarity index 86% rename from app/src/main/java/com/ilein/thecocktailapp/ui/search/SearchDrinksFragment.kt rename to app/src/main/java/com/ilein/thecocktailapp/presentation/ui/search/SearchDrinksFragment.kt index ee60969..6c56a89 100644 --- a/app/src/main/java/com/ilein/thecocktailapp/ui/search/SearchDrinksFragment.kt +++ b/app/src/main/java/com/ilein/thecocktailapp/presentation/ui/search/SearchDrinksFragment.kt @@ -1,14 +1,12 @@ -package com.ilein.thecocktailapp.ui.search +package com.ilein.thecocktailapp.presentation.ui.search -import android.os.Build import android.os.Bundle -import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast -import androidx.annotation.RequiresApi import androidx.core.widget.addTextChangedListener +import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager import com.ilein.thecocktailapp.databinding.SearchDrinksFragmentBinding @@ -28,15 +26,13 @@ class SearchDrinksFragment : Fragment() { return binding.root } - @RequiresApi(Build.VERSION_CODES.O) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.lCardDrinksRecycler.visibility = View.VISIBLE - //binding.loadingDrinks.visibility = View.INVISIBLE val drinksAdapter = DrinkItemAdapter ({ - findNavController().navigate(SearchDrinksFragmentDirections.toDrinkDetailWithParam(it.idDrink)) + findNavController().navigate(SearchDrinksFragmentDirections.toDrinkDetailWithParam(it.id)) }, { drink, isLike -> vm.onLikeClick(drink, isLike) @@ -47,8 +43,6 @@ class SearchDrinksFragment : Fragment() { run { binding.lCardDrinksRecycler.visibility = if (state.loading) View.INVISIBLE else View.VISIBLE - //binding.loadingDrinks.visibility = - // if (state.loading) View.VISIBLE else View.INVISIBLE binding.twHint.visibility = if (state.itemsMap.isNotEmpty()) View.GONE else View.VISIBLE diff --git a/app/src/main/java/com/ilein/thecocktailapp/presentation/ui/search/SearchDrinksViewModel.kt b/app/src/main/java/com/ilein/thecocktailapp/presentation/ui/search/SearchDrinksViewModel.kt new file mode 100644 index 0000000..2e9a637 --- /dev/null +++ b/app/src/main/java/com/ilein/thecocktailapp/presentation/ui/search/SearchDrinksViewModel.kt @@ -0,0 +1,201 @@ +package com.ilein.thecocktailapp.presentation.ui.search + +import android.os.Handler +import android.os.Looper +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.ilein.thecocktailapp.data.network.NetworkUtil +import com.ilein.thecocktailapp.domain.Drink +import com.ilein.thecocktailapp.domain.FavoriteDrink +import com.ilein.thecocktailapp.domain.base.Result +import com.ilein.thecocktailapp.domain.usecase.DeleteDrinkUseCase +import com.ilein.thecocktailapp.domain.usecase.GetDrinkFavoritesUseCase +import com.ilein.thecocktailapp.domain.usecase.InsertDrinkUseCase +import com.ilein.thecocktailapp.domain.usecase.SearchDrinkUseCase +import com.ilein.thecocktailapp.presentation.ui.state.DrinkData +import com.ilein.thecocktailapp.presentation.ui.state.DrinksSearchState +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onEmpty +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.launch +import java.time.LocalDateTime +import java.util.Collections.emptyList + +@OptIn(ExperimentalCoroutinesApi::class, kotlinx.coroutines.FlowPreview::class) +class SearchDrinksViewModel(private val searchDrinksUseCase: SearchDrinkUseCase, + private val getDrinkFavoritesUseCase: GetDrinkFavoritesUseCase, + private val insertDrinkUseCase: InsertDrinkUseCase, + private val deleteDrinkUseCase: DeleteDrinkUseCase, + private val networkUtils: NetworkUtil) : ViewModel() { + + private val _liveData: MutableLiveData = MutableLiveData(DrinksSearchState()) + + val state: LiveData = _liveData + + private val sharedFlow = MutableSharedFlow(1, 1) + + private val drinkLikes = mutableListOf() + + init { + init() + } + + private fun init() { + viewModelScope.launch { + when (val result = getDrinkFavoritesUseCase("")) { + is Result.Success -> { + result.data.forEach { + if (it.id !in drinkLikes) { + drinkLikes.add(it.id) + } + } + } + is Result.Error -> { + result.throwable + } + } + } + + viewModelScope.launch { + sharedFlow + .filter { it.isNotBlank() } + .debounce(500) + .flatMapLatest { query -> + if (query.isBlank()) flowOf(emptyList()) else + if (networkUtils.isOnline()) { + + when (val result = searchDrinksUseCase(query)) { + is Result.Success -> { + result.data + .onStart { + updateState { + it.copy(loading = true) + } + } + .onEmpty { + updateState { + it.copy(loading = false) + } + } + .onCompletion { + updateState { + it.copy(loading = false) + } + } + return@flatMapLatest result.data + } + is Result.Error -> { + result.throwable + flowOf(emptyList()) + } + } + } else { + updateState { + it.copy(loading = false, isOnline = false) + } + flowOf(emptyList()) + } + } + .flowOn(Dispatchers.IO) + .onEach(::onResponse) + .launchIn(viewModelScope) + } + } + + + private fun updateState(update: (DrinksSearchState) -> DrinksSearchState) { + Handler(Looper.getMainLooper()).post { + _liveData.value?.also { + _liveData.value = update(it) + } + } + } + + private fun onResponse(drinks: List) { + updateState { it -> + it.copy( + itemsMap = drinks.associate { + it.id to DrinkData( + it, + drinkLikes.contains(it.id) + ) + }, + loading = false + ) + } + } + + /** + * ACTIONS + */ + + fun onTextInput(text: String) { + viewModelScope.launch { + sharedFlow.emit(text) + } + } + + fun onRefresh() { + init() + } + + + fun onLikeClick(drink: Drink, isLike: Boolean) { + if (!isLike) { + viewModelScope.launch { + when(val result = deleteDrinkUseCase(drink.id)) { + is Result.Success -> { + drinkLikes.remove(drink.id) + updateState { + val copyVal = _liveData.value?.copy() + if (copyVal != null) { + copyVal.itemsMap[drink.id]?.like = false + } + return@updateState copyVal!! + } + } + is Result.Error -> { + result.throwable + } + } + } + } else { + viewModelScope.launch { + val favoriteDrink = FavoriteDrink( + id = drink.id, + name = drink.name, + image = drink.image, + createDate = LocalDateTime.now(), + note = null + ) + when(val result = insertDrinkUseCase(favoriteDrink)) { + is Result.Success -> { + if (drink.id !in drinkLikes) {drinkLikes.add(drink.id)} + updateState { + val copyVal = _liveData.value?.copy() + if (copyVal != null) { + copyVal.itemsMap[drink.id]?.like = true + } + return@updateState copyVal!! + } + } + is Result.Error -> { + result.throwable + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/ui/state/DrinksState.kt b/app/src/main/java/com/ilein/thecocktailapp/presentation/ui/state/DrinksState.kt similarity index 70% rename from app/src/main/java/com/ilein/thecocktailapp/ui/state/DrinksState.kt rename to app/src/main/java/com/ilein/thecocktailapp/presentation/ui/state/DrinksState.kt index 0dbccd2..86ffb5c 100644 --- a/app/src/main/java/com/ilein/thecocktailapp/ui/state/DrinksState.kt +++ b/app/src/main/java/com/ilein/thecocktailapp/presentation/ui/state/DrinksState.kt @@ -1,7 +1,7 @@ -package com.ilein.thecocktailapp.ui.state +package com.ilein.thecocktailapp.presentation.ui.state -import com.ilein.thecocktailapp.db.DrinkLikeEntity -import com.ilein.thecocktailapp.network.model.Drink +import com.ilein.thecocktailapp.domain.Drink +import com.ilein.thecocktailapp.domain.FavoriteDrink data class DrinksSearchState ( val input: String = "", @@ -18,7 +18,7 @@ data class DrinkItemState ( data class DrinkItemLikeState ( val drink: Drink?, - val drinkLike: DrinkLikeEntity?, + val drinkLike: FavoriteDrink?, val loading: Boolean = false, val isOnline: Boolean = true ) @@ -29,6 +29,6 @@ data class DrinkData ( ) data class DrinksLikeState ( - val items: List = emptyList(), + val items: List = emptyList(), val loading: Boolean = false ) \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/ui/utils/DiffCallback.kt b/app/src/main/java/com/ilein/thecocktailapp/presentation/ui/utils/DiffCallback.kt similarity index 72% rename from app/src/main/java/com/ilein/thecocktailapp/ui/utils/DiffCallback.kt rename to app/src/main/java/com/ilein/thecocktailapp/presentation/ui/utils/DiffCallback.kt index 045f787..4ae0a01 100644 --- a/app/src/main/java/com/ilein/thecocktailapp/ui/utils/DiffCallback.kt +++ b/app/src/main/java/com/ilein/thecocktailapp/presentation/ui/utils/DiffCallback.kt @@ -1,7 +1,7 @@ -package com.ilein.thecocktailapp.ui.utils +package com.ilein.thecocktailapp.presentation.ui.utils import androidx.recyclerview.widget.DiffUtil -import com.ilein.thecocktailapp.ui.state.DrinkData +import com.ilein.thecocktailapp.presentation.ui.state.DrinkData class DiffCallback(private val oldList: List, private val newList: List) : DiffUtil.Callback() { @@ -10,7 +10,7 @@ class DiffCallback(private val oldList: List, private val newList: Li override fun getNewListSize(): Int = newList.size override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = - oldList[oldItemPosition].drink.idDrink == newList[newItemPosition].drink.idDrink + oldList[oldItemPosition].drink.id == newList[newItemPosition].drink.id override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = oldList[oldItemPosition] == newList[newItemPosition] diff --git a/app/src/main/java/com/ilein/thecocktailapp/ui/utils/DrinkLikeCallback.kt b/app/src/main/java/com/ilein/thecocktailapp/presentation/ui/utils/DrinkLikeCallback.kt similarity index 66% rename from app/src/main/java/com/ilein/thecocktailapp/ui/utils/DrinkLikeCallback.kt rename to app/src/main/java/com/ilein/thecocktailapp/presentation/ui/utils/DrinkLikeCallback.kt index 0e485de..2e29e0a 100644 --- a/app/src/main/java/com/ilein/thecocktailapp/ui/utils/DrinkLikeCallback.kt +++ b/app/src/main/java/com/ilein/thecocktailapp/presentation/ui/utils/DrinkLikeCallback.kt @@ -1,9 +1,9 @@ -package com.ilein.thecocktailapp.ui.utils +package com.ilein.thecocktailapp.presentation.ui.utils import androidx.recyclerview.widget.DiffUtil -import com.ilein.thecocktailapp.db.DrinkLikeEntity +import com.ilein.thecocktailapp.domain.FavoriteDrink -class DiffLikeCallback(private val oldList: List, private val newList: List) : DiffUtil.Callback() { +class DiffLikeCallback(private val oldList: List, private val newList: List) : DiffUtil.Callback() { override fun getOldListSize(): Int = oldList.size diff --git a/app/src/main/java/com/ilein/thecocktailapp/ui/command/CommandsLiveData.kt b/app/src/main/java/com/ilein/thecocktailapp/ui/command/CommandsLiveData.kt deleted file mode 100644 index a093c39..0000000 --- a/app/src/main/java/com/ilein/thecocktailapp/ui/command/CommandsLiveData.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.ilein.thecocktailapp.ui.command - -import androidx.lifecycle.MutableLiveData -import java.util.* -import java.util.concurrent.ConcurrentLinkedQueue - -class CommandsLiveData : MutableLiveData>() { - - fun onNext(value: T) { - val commands = getValue() ?: ConcurrentLinkedQueue() - commands.add(value) - setValue(commands) - } -} diff --git a/app/src/main/java/com/ilein/thecocktailapp/ui/command/LifecycleExt.kt b/app/src/main/java/com/ilein/thecocktailapp/ui/command/LifecycleExt.kt deleted file mode 100644 index 00277b6..0000000 --- a/app/src/main/java/com/ilein/thecocktailapp/ui/command/LifecycleExt.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.ilein.thecocktailapp.ui.command - -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LiveData - -inline fun LifecycleOwner.observe(liveData: LiveData, crossinline block: (T) -> Unit) { - liveData.observe(this) { block.invoke(it) } -} - -inline fun LifecycleOwner.observe(liveData: CommandsLiveData, crossinline block: (T) -> Unit) { - liveData.observe(this) { commands -> - val iterator = commands.iterator() - while (iterator.hasNext()) { - block.invoke(iterator.next()) - iterator.remove() - } - } -} diff --git a/app/src/main/java/com/ilein/thecocktailapp/ui/command/NavigationCommand.kt b/app/src/main/java/com/ilein/thecocktailapp/ui/command/NavigationCommand.kt deleted file mode 100644 index 56eab44..0000000 --- a/app/src/main/java/com/ilein/thecocktailapp/ui/command/NavigationCommand.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.ilein.thecocktailapp.ui.command - -sealed class NavigationCommand { - - object Back: NavigationCommand() - data class ToDrinkLikeDetail(val drinkId: Int?): NavigationCommand() - data class ToDrinkDetail(val drinkId: Int?): NavigationCommand() -} \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/ui/detail/DrinkLikeItemDetailViewModel.kt b/app/src/main/java/com/ilein/thecocktailapp/ui/detail/DrinkLikeItemDetailViewModel.kt deleted file mode 100644 index c9f1d18..0000000 --- a/app/src/main/java/com/ilein/thecocktailapp/ui/detail/DrinkLikeItemDetailViewModel.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.ilein.thecocktailapp.ui.detail - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.ilein.thecocktailapp.db.DrinkLikeDao -import com.ilein.thecocktailapp.network.DrinksReq -import com.ilein.thecocktailapp.network.NetworkUtil -import com.ilein.thecocktailapp.ui.state.DrinkItemLikeState -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.schedulers.Schedulers -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.onStart - -class DrinkLikeItemDetailViewModel(private val drinkLikeDao: DrinkLikeDao, - private val drinksReq: DrinksReq, - private val networkUtil: NetworkUtil -): ViewModel() { - val state = MutableLiveData() - - private val compositeDisposable = CompositeDisposable() - - fun init(drinkId: Int) { - if (networkUtil.isOnline()) { - drinksReq.requestDrink(drinkId) - .onStart { state.postValue(DrinkItemLikeState(null, null, true)) } - .onEach { drinks -> - run { - val drink = drinks.results[0] - drinkLikeDao.getDrinkLike(drinkId) - .subscribeOn(Schedulers.io()) - .observeOn(Schedulers.single()) - .subscribe { drinkLike -> - state.postValue(DrinkItemLikeState(drink, drinkLike, false)) - } - - } - } - .launchIn(viewModelScope) - } else { - state.postValue(DrinkItemLikeState(null, null, false, isOnline = false)) - } - } - - fun onRefresh(drinkId: Int) { - init(drinkId) - } - - fun onNoteChange(note: String) { - if (state.value!!.drinkLike?.note != note) { - state.postValue(state.value!!.copy( - drinkLike = state.value!!.drinkLike?.copy(note = note) - )) - } - } - - fun onSaveClick() { - state.value?.drinkLike?.let { - compositeDisposable.add( - drinkLikeDao.insertDrinkLike(it) - .subscribeOn(Schedulers.io()) - .observeOn(Schedulers.io()) - .subscribe() - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/ui/liked/LikedDrinksViewModel.kt b/app/src/main/java/com/ilein/thecocktailapp/ui/liked/LikedDrinksViewModel.kt deleted file mode 100644 index b1c586e..0000000 --- a/app/src/main/java/com/ilein/thecocktailapp/ui/liked/LikedDrinksViewModel.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.ilein.thecocktailapp.ui.liked - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import com.ilein.thecocktailapp.db.DrinkLikeDao -import com.ilein.thecocktailapp.db.DrinkLikeEntity -import com.ilein.thecocktailapp.ui.state.DrinksLikeState -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.schedulers.Schedulers - -class LikedDrinksViewModel(private val drinkLikeDao: DrinkLikeDao) : ViewModel() { - private val compositeDisposable = CompositeDisposable() - - private val _liveData: MutableLiveData = MutableLiveData(DrinksLikeState()) - - val state: LiveData = _liveData - - init { - initData() - } - - private fun initData() { - compositeDisposable.add( - drinkLikeDao.observeDrinkLikeDesc() - .subscribeOn(Schedulers.io()) - .observeOn(Schedulers.single()) - .doOnSubscribe { _liveData.postValue(DrinksLikeState( - loading = true - )) } - .subscribe { drinksList -> - _liveData.postValue(DrinksLikeState( - items = drinksList, - loading = false - )) - } - ) - } - - override fun onCleared() { - compositeDisposable.dispose() - super.onCleared() - } - - /** - * ACTIONS - */ - - fun onDelClick(drink: DrinkLikeEntity) { - compositeDisposable.add( - drinkLikeDao.deleteDrinkLike(drink.id) - .subscribeOn(Schedulers.io()) - .observeOn(Schedulers.single()) - .subscribe { - initData() - } - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/ilein/thecocktailapp/ui/search/SearchDrinksViewModel.kt b/app/src/main/java/com/ilein/thecocktailapp/ui/search/SearchDrinksViewModel.kt deleted file mode 100644 index db2301a..0000000 --- a/app/src/main/java/com/ilein/thecocktailapp/ui/search/SearchDrinksViewModel.kt +++ /dev/null @@ -1,176 +0,0 @@ -package com.ilein.thecocktailapp.ui.search - -import android.os.Build -import android.os.Handler -import android.os.Looper -import androidx.annotation.RequiresApi -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.ilein.thecocktailapp.db.DrinkLikeDao -import com.ilein.thecocktailapp.db.DrinkLikeEntity -import com.ilein.thecocktailapp.koin.networkModule -import com.ilein.thecocktailapp.network.DrinksReq -import com.ilein.thecocktailapp.network.NetworkUtil -import com.ilein.thecocktailapp.network.model.Drink -import com.ilein.thecocktailapp.network.model.Drinks -import com.ilein.thecocktailapp.ui.state.DrinkData -import com.ilein.thecocktailapp.ui.state.DrinksSearchState -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.schedulers.Schedulers -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import java.time.LocalDateTime - -@OptIn(ExperimentalCoroutinesApi::class, kotlinx.coroutines.FlowPreview::class) -class SearchDrinksViewModel(private val drinkLikeDao: DrinkLikeDao, - private val drinksReq: DrinksReq, - private val networkUtils: NetworkUtil) : ViewModel() { - - private val _liveData: MutableLiveData = MutableLiveData(DrinksSearchState()) - - val state: LiveData = _liveData - - private val sharedFlow = MutableSharedFlow(1, 1) - - private val drinkLikes = mutableListOf() - - private val compositeDisposable = CompositeDisposable() - - init { - init() - } - - private fun init() { - compositeDisposable.add( - drinkLikeDao.observeDrinkLike() - .subscribeOn(Schedulers.io()) - .observeOn(Schedulers.single()) - .subscribe { drinkList -> - drinkList.forEach { if (it.id !in drinkLikes) {drinkLikes.add(it.id)} } - } - ) - sharedFlow - .filter { it.isNotBlank() } - .debounce(500) - .flatMapLatest { query -> - if (query.isBlank()) flowOf(Drinks()) else - if ( networkUtils.isOnline() ) { - drinksReq.requestDrinks(query) - .onStart { - updateState { - it.copy(loading = true) - } - } - .onEmpty { - updateState { - it.copy(loading = false) - } - } - .onCompletion { - updateState { - it.copy(loading = false) - } - } - } else { - updateState { - it.copy(loading = false, isOnline = false) } - flowOf(Drinks()) - } - } - .flowOn(Dispatchers.IO) - .onEach(::onResponse) - .launchIn(viewModelScope) - } - - - private fun updateState(update: (DrinksSearchState) -> DrinksSearchState) { - Handler(Looper.getMainLooper()).post { - _liveData.value?.also { - _liveData.value = update(it) - } - } - } - - private fun onResponse(drinks: Drinks) { - updateState { it -> - it.copy( - itemsMap = drinks.results.associate { - it.idDrink to DrinkData( - it, - drinkLikes.contains(it.idDrink) - ) - }, - loading = false - ) - } - } - - override fun onCleared() { - compositeDisposable.dispose() - super.onCleared() - } - - /** - * ACTIONS - */ - - fun onTextInput(text: String) { - viewModelScope.launch { - sharedFlow.emit(text) - } - } - - fun onRefresh() { - init() - } - - @RequiresApi(Build.VERSION_CODES.O) - fun onLikeClick(drinkLike: Drink, isLike: Boolean) { - if (!isLike) { - compositeDisposable.add( - drinkLikeDao.deleteDrinkLike(drinkLike.idDrink) - .subscribeOn(Schedulers.io()) - .observeOn(Schedulers.single()) - .subscribe { - drinkLikes.remove(drinkLike.idDrink) - updateState { - val copyVal = _liveData.value?.copy() - if (copyVal != null) { - copyVal.itemsMap[drinkLike.idDrink]?.like = false - } - return@updateState copyVal!! - } - } - ) - - } else { - compositeDisposable.add( - drinkLikeDao.insertDrinkLike( - DrinkLikeEntity( - id = drinkLike.idDrink, - name = drinkLike.strDrink, - image = drinkLike.strDrinkThumb, - createDate = LocalDateTime.now(), - note = null - ) - ) - .subscribeOn(Schedulers.io()) - .observeOn(Schedulers.single()) - .subscribe { - if (drinkLike.idDrink !in drinkLikes) {drinkLikes.add(drinkLike.idDrink)} - updateState { - val copyVal = _liveData.value?.copy() - if (copyVal != null) { - copyVal.itemsMap[drinkLike.idDrink]?.like = true - } - return@updateState copyVal!! - } - } - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_drink_item_detail.xml b/app/src/main/res/layout/fragment_drink_item_detail.xml index 6b3a3ae..543ec81 100644 --- a/app/src/main/res/layout/fragment_drink_item_detail.xml +++ b/app/src/main/res/layout/fragment_drink_item_detail.xml @@ -5,7 +5,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="visible" - tools:context=".ui.detail.DrinkLikeItemDetailFragment"> + tools:context=".presentation.ui.detail.DrinkLikeItemDetailFragment"> + tools:context=".presentation.ui.detail.DrinkLikeItemDetailFragment"> + tools:context=".presentation.ui.map.MapFragment"> + tools:context=".presentation.ui.liked.LikedDrinksFragment"> + tools:context=".presentation.MainActivity"> + tools:context=".presentation.ui.main.MainFragment"> + tools:context=".presentation.ui.search.SearchDrinksFragment"> \ No newline at end of file