Replace "Mealie" with "Mealient" everywhere

This commit is contained in:
Kirill Kamakin
2021-11-20 13:41:47 +03:00
parent d789bfcf97
commit 5866584d14
81 changed files with 283 additions and 284 deletions

View File

@@ -0,0 +1,7 @@
package gq.kirmanak.mealient.data.recipes
import android.widget.ImageView
interface RecipeImageLoader {
suspend fun loadRecipeImage(view: ImageView, slug: String?)
}

View File

@@ -0,0 +1,32 @@
package gq.kirmanak.mealient.data.recipes
import androidx.paging.ExperimentalPagingApi
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import gq.kirmanak.mealient.data.recipes.db.RecipeStorage
import gq.kirmanak.mealient.data.recipes.db.RecipeStorageImpl
import gq.kirmanak.mealient.data.recipes.impl.RecipeImageLoaderImpl
import gq.kirmanak.mealient.data.recipes.impl.RecipeRepoImpl
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSourceImpl
import kotlinx.serialization.ExperimentalSerializationApi
@ExperimentalPagingApi
@ExperimentalSerializationApi
@Module
@InstallIn(SingletonComponent::class)
interface RecipeModule {
@Binds
fun provideRecipeDataSource(recipeDataSourceImpl: RecipeDataSourceImpl): RecipeDataSource
@Binds
fun provideRecipeStorage(recipeStorageImpl: RecipeStorageImpl): RecipeStorage
@Binds
fun provideRecipeRepo(recipeRepoImpl: RecipeRepoImpl): RecipeRepo
@Binds
fun provideRecipeImageLoader(recipeImageLoaderImpl: RecipeImageLoaderImpl): RecipeImageLoader
}

View File

@@ -0,0 +1,13 @@
package gq.kirmanak.mealient.data.recipes
import androidx.paging.Pager
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.data.recipes.impl.FullRecipeInfo
interface RecipeRepo {
fun createPager(): Pager<Int, RecipeSummaryEntity>
suspend fun clearLocalData()
suspend fun loadRecipeInfo(recipeId: Long, recipeSlug: String): FullRecipeInfo
}

View File

@@ -0,0 +1,76 @@
package gq.kirmanak.mealient.data.recipes.db
import androidx.paging.PagingSource
import androidx.room.*
import gq.kirmanak.mealient.data.recipes.db.entity.*
import gq.kirmanak.mealient.data.recipes.impl.FullRecipeInfo
@Dao
interface RecipeDao {
@Query("SELECT * FROM tags")
suspend fun queryAllTags(): List<TagEntity>
@Query("SELECT * FROM categories")
suspend fun queryAllCategories(): List<CategoryEntity>
@Query("SELECT * FROM recipe_summaries ORDER BY date_added DESC")
fun queryRecipesByPages(): PagingSource<Int, RecipeSummaryEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertRecipe(recipeSummaryEntity: RecipeSummaryEntity)
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertTag(tagEntity: TagEntity): Long
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertTagRecipeEntity(tagRecipeEntity: TagRecipeEntity)
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertCategory(categoryEntity: CategoryEntity): Long
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertCategoryRecipeEntity(categoryRecipeEntity: CategoryRecipeEntity)
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertTagRecipeEntities(tagRecipeEntities: Set<TagRecipeEntity>)
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertCategoryRecipeEntities(categoryRecipeEntities: Set<CategoryRecipeEntity>)
@Query("DELETE FROM recipe_summaries")
suspend fun removeAllRecipes()
@Query("DELETE FROM tags")
suspend fun removeAllTags()
@Query("DELETE FROM categories")
suspend fun removeAllCategories()
@Query("SELECT * FROM recipe_summaries ORDER BY date_updated DESC")
suspend fun queryAllRecipes(): List<RecipeSummaryEntity>
@Query("SELECT * FROM category_recipe")
suspend fun queryAllCategoryRecipes(): List<CategoryRecipeEntity>
@Query("SELECT * FROM tag_recipe")
suspend fun queryAllTagRecipes(): List<TagRecipeEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertRecipe(recipe: RecipeEntity)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertRecipeInstructions(instructions: List<RecipeInstructionEntity>)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertRecipeIngredients(ingredients: List<RecipeIngredientEntity>)
@Transaction
@Query("SELECT * FROM recipe JOIN recipe_summaries ON recipe.remote_id = recipe_summaries.remote_id JOIN recipe_ingredient ON recipe_ingredient.recipe_id = recipe.remote_id JOIN recipe_instruction ON recipe_instruction.recipe_id = recipe.remote_id WHERE recipe.remote_id = :recipeId")
suspend fun queryFullRecipeInfo(recipeId: Long): FullRecipeInfo?
@Query("DELETE FROM recipe_ingredient WHERE recipe_id = :recipeId")
suspend fun deleteRecipeIngredients(recipeId: Long)
@Query("DELETE FROM recipe_instruction WHERE recipe_id = :recipeId")
suspend fun deleteRecipeInstructions(recipeId: Long)
}

View File

@@ -0,0 +1,21 @@
package gq.kirmanak.mealient.data.recipes.db
import androidx.paging.PagingSource
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.data.recipes.impl.FullRecipeInfo
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeResponse
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeSummaryResponse
interface RecipeStorage {
suspend fun saveRecipes(recipes: List<GetRecipeSummaryResponse>)
fun queryRecipes(): PagingSource<Int, RecipeSummaryEntity>
suspend fun refreshAll(recipes: List<GetRecipeSummaryResponse>)
suspend fun clearAllLocalData()
suspend fun saveRecipeInfo(recipe: GetRecipeResponse)
suspend fun queryRecipeInfo(recipeId: Long): FullRecipeInfo
}

View File

@@ -0,0 +1,171 @@
package gq.kirmanak.mealient.data.recipes.db
import androidx.paging.PagingSource
import androidx.room.withTransaction
import gq.kirmanak.mealient.data.AppDb
import gq.kirmanak.mealient.data.recipes.db.entity.*
import gq.kirmanak.mealient.data.recipes.impl.FullRecipeInfo
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeIngredientResponse
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeInstructionResponse
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeResponse
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeSummaryResponse
import timber.log.Timber
import javax.inject.Inject
class RecipeStorageImpl @Inject constructor(
private val db: AppDb
) : RecipeStorage {
private val recipeDao: RecipeDao by lazy { db.recipeDao() }
override suspend fun saveRecipes(
recipes: List<GetRecipeSummaryResponse>
) = db.withTransaction {
Timber.v("saveRecipes() called with $recipes")
val tagEntities = mutableSetOf<TagEntity>()
tagEntities.addAll(recipeDao.queryAllTags())
val categoryEntities = mutableSetOf<CategoryEntity>()
categoryEntities.addAll(recipeDao.queryAllCategories())
val tagRecipeEntities = mutableSetOf<TagRecipeEntity>()
val categoryRecipeEntities = mutableSetOf<CategoryRecipeEntity>()
for (recipe in recipes) {
val recipeSummaryEntity = recipe.recipeEntity()
recipeDao.insertRecipe(recipeSummaryEntity)
for (tag in recipe.tags) {
val tagId = getIdOrInsert(tagEntities, tag)
tagRecipeEntities += TagRecipeEntity(tagId, recipeSummaryEntity.remoteId)
}
for (category in recipe.recipeCategories) {
val categoryId = getOrInsert(categoryEntities, category)
categoryRecipeEntities += CategoryRecipeEntity(
categoryId,
recipeSummaryEntity.remoteId
)
}
}
recipeDao.insertTagRecipeEntities(tagRecipeEntities)
recipeDao.insertCategoryRecipeEntities(categoryRecipeEntities)
}
private suspend fun getOrInsert(
categoryEntities: MutableSet<CategoryEntity>,
category: String
): Long {
val existingCategory = categoryEntities.find { it.name == category }
val categoryId = if (existingCategory == null) {
val categoryEntity = CategoryEntity(name = category)
val newId = recipeDao.insertCategory(categoryEntity)
categoryEntities.add(categoryEntity.copy(localId = newId))
newId
} else {
existingCategory.localId
}
return categoryId
}
private suspend fun getIdOrInsert(
tagEntities: MutableSet<TagEntity>,
tag: String
): Long {
val existingTag = tagEntities.find { it.name == tag }
val tagId = if (existingTag == null) {
val tagEntity = TagEntity(name = tag)
val newId = recipeDao.insertTag(tagEntity)
tagEntities.add(tagEntity.copy(localId = newId))
newId
} else {
existingTag.localId
}
return tagId
}
private fun GetRecipeSummaryResponse.recipeEntity() = RecipeSummaryEntity(
remoteId = remoteId,
name = name,
slug = slug,
image = image,
description = description,
rating = rating,
dateAdded = dateAdded,
dateUpdated = dateUpdated,
)
override fun queryRecipes(): PagingSource<Int, RecipeSummaryEntity> {
Timber.v("queryRecipes() called")
return recipeDao.queryRecipesByPages()
}
override suspend fun refreshAll(recipes: List<GetRecipeSummaryResponse>) {
Timber.v("refreshAll() called with: recipes = $recipes")
db.withTransaction {
recipeDao.removeAllRecipes()
saveRecipes(recipes)
}
}
override suspend fun clearAllLocalData() {
Timber.v("clearAllLocalData() called")
db.withTransaction {
recipeDao.removeAllRecipes()
recipeDao.removeAllCategories()
recipeDao.removeAllTags()
}
}
override suspend fun saveRecipeInfo(recipe: GetRecipeResponse) {
Timber.v("saveRecipeInfo() called with: recipe = $recipe")
db.withTransaction {
recipeDao.insertRecipe(recipe.toRecipeEntity())
recipeDao.deleteRecipeIngredients(recipe.remoteId)
val ingredients = recipe.recipeIngredients.map {
it.toRecipeIngredientEntity(recipe.remoteId)
}
recipeDao.insertRecipeIngredients(ingredients)
recipeDao.deleteRecipeInstructions(recipe.remoteId)
val instructions = recipe.recipeInstructions.map {
it.toRecipeInstructionEntity(recipe.remoteId)
}
recipeDao.insertRecipeInstructions(instructions)
}
}
private fun GetRecipeResponse.toRecipeEntity() = RecipeEntity(
remoteId = remoteId,
recipeYield = recipeYield
)
private fun GetRecipeIngredientResponse.toRecipeIngredientEntity(remoteId: Long) =
RecipeIngredientEntity(
recipeId = remoteId,
title = title,
note = note,
unit = unit,
food = food,
disableAmount = disableAmount,
quantity = quantity
)
private fun GetRecipeInstructionResponse.toRecipeInstructionEntity(remoteId: Long) =
RecipeInstructionEntity(
recipeId = remoteId,
title = title,
text = text
)
override suspend fun queryRecipeInfo(recipeId: Long): FullRecipeInfo {
Timber.v("queryRecipeInfo() called with: recipeId = $recipeId")
val fullRecipeInfo = checkNotNull(recipeDao.queryFullRecipeInfo(recipeId)) {
"Can't find recipe by id $recipeId in DB"
}
Timber.v("queryRecipeInfo() returned: $fullRecipeInfo")
return fullRecipeInfo
}
}

View File

@@ -0,0 +1,12 @@
package gq.kirmanak.mealient.data.recipes.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(tableName = "categories", indices = [Index(value = ["name"], unique = true)])
data class CategoryEntity(
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "local_id") val localId: Long = 0,
@ColumnInfo(name = "name") val name: String,
)

View File

@@ -0,0 +1,29 @@
package gq.kirmanak.mealient.data.recipes.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
@Entity(
tableName = "category_recipe",
primaryKeys = ["category_id", "recipe_id"],
indices = [Index(value = ["category_id", "recipe_id"], unique = true)],
foreignKeys = [ForeignKey(
entity = CategoryEntity::class,
parentColumns = ["local_id"],
childColumns = ["category_id"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
), ForeignKey(
entity = RecipeSummaryEntity::class,
parentColumns = ["remote_id"],
childColumns = ["recipe_id"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)]
)
data class CategoryRecipeEntity(
@ColumnInfo(name = "category_id") val categoryId: Long,
@ColumnInfo(name = "recipe_id", index = true) val recipeId: Long
)

View File

@@ -0,0 +1,11 @@
package gq.kirmanak.mealient.data.recipes.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "recipe")
data class RecipeEntity(
@PrimaryKey @ColumnInfo(name = "remote_id") val remoteId: Long,
@ColumnInfo(name = "recipe_yield") val recipeYield: String,
)

View File

@@ -0,0 +1,17 @@
package gq.kirmanak.mealient.data.recipes.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "recipe_ingredient")
data class RecipeIngredientEntity(
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "local_id") val localId: Long = 0,
@ColumnInfo(name = "recipe_id") val recipeId: Long,
@ColumnInfo(name = "title") val title: String,
@ColumnInfo(name = "note") val note: String,
@ColumnInfo(name = "unit") val unit: String,
@ColumnInfo(name = "food") val food: String,
@ColumnInfo(name = "disable_amount") val disableAmount: Boolean,
@ColumnInfo(name = "quantity") val quantity: Int,
)

View File

@@ -0,0 +1,13 @@
package gq.kirmanak.mealient.data.recipes.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "recipe_instruction")
data class RecipeInstructionEntity(
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "local_id") val localId: Long = 0,
@ColumnInfo(name = "recipe_id") val recipeId: Long,
@ColumnInfo(name = "title") val title: String,
@ColumnInfo(name = "text") val text: String,
)

View File

@@ -0,0 +1,23 @@
package gq.kirmanak.mealient.data.recipes.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
@Entity(tableName = "recipe_summaries")
data class RecipeSummaryEntity(
@PrimaryKey @ColumnInfo(name = "remote_id") val remoteId: Long,
@ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "slug") val slug: String,
@ColumnInfo(name = "image") val image: String,
@ColumnInfo(name = "description") val description: String,
@ColumnInfo(name = "rating") val rating: Int?,
@ColumnInfo(name = "date_added") val dateAdded: LocalDate,
@ColumnInfo(name = "date_updated") val dateUpdated: LocalDateTime
) {
override fun toString(): String {
return "RecipeEntity(remoteId=$remoteId, name='$name')"
}
}

View File

@@ -0,0 +1,12 @@
package gq.kirmanak.mealient.data.recipes.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(tableName = "tags", indices = [Index(value = ["name"], unique = true)])
data class TagEntity(
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "local_id") val localId: Long = 0,
@ColumnInfo(name = "name") val name: String
)

View File

@@ -0,0 +1,27 @@
package gq.kirmanak.mealient.data.recipes.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
@Entity(
tableName = "tag_recipe",
primaryKeys = ["tag_id", "recipe_id"],
foreignKeys = [ForeignKey(
entity = TagEntity::class,
parentColumns = ["local_id"],
childColumns = ["tag_id"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
), ForeignKey(
entity = RecipeSummaryEntity::class,
parentColumns = ["remote_id"],
childColumns = ["recipe_id"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)]
)
data class TagRecipeEntity(
@ColumnInfo(name = "tag_id") val tagId: Long,
@ColumnInfo(name = "recipe_id", index = true) val recipeId: Long
)

View File

@@ -0,0 +1,27 @@
package gq.kirmanak.mealient.data.recipes.impl
import androidx.room.Embedded
import androidx.room.Relation
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeEntity
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeIngredientEntity
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeInstructionEntity
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
data class FullRecipeInfo(
@Embedded val recipeEntity: RecipeEntity,
@Relation(
parentColumn = "remote_id",
entityColumn = "remote_id"
)
val recipeSummaryEntity: RecipeSummaryEntity,
@Relation(
parentColumn = "remote_id",
entityColumn = "recipe_id"
)
val recipeIngredients: List<RecipeIngredientEntity>,
@Relation(
parentColumn = "remote_id",
entityColumn = "recipe_id"
)
val recipeInstructions: List<RecipeInstructionEntity>,
)

View File

@@ -0,0 +1,21 @@
package gq.kirmanak.mealient.data.recipes.impl
import android.widget.ImageView
import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.data.auth.AuthRepo
import gq.kirmanak.mealient.data.recipes.RecipeImageLoader
import gq.kirmanak.mealient.ui.ImageLoader
import javax.inject.Inject
class RecipeImageLoaderImpl @Inject constructor(
private val imageLoader: ImageLoader,
private val authRepo: AuthRepo
): RecipeImageLoader {
override suspend fun loadRecipeImage(view: ImageView, slug: String?) {
val baseUrl = authRepo.getBaseUrl()
val recipeImageUrl =
if (baseUrl.isNullOrBlank() || slug.isNullOrBlank()) null
else "$baseUrl/api/media/recipes/$slug/images/original.webp"
imageLoader.loadImage(recipeImageUrl, R.drawable.placeholder_recipe, view)
}
}

View File

@@ -0,0 +1,32 @@
package gq.kirmanak.mealient.data.recipes.impl
import androidx.paging.PagingSource
import gq.kirmanak.mealient.data.recipes.db.RecipeStorage
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class RecipePagingSourceFactory @Inject constructor(
private val recipeStorage: RecipeStorage
) : () -> PagingSource<Int, RecipeSummaryEntity> {
private val sources: MutableList<PagingSource<Int, RecipeSummaryEntity>> = mutableListOf()
override fun invoke(): PagingSource<Int, RecipeSummaryEntity> {
Timber.v("invoke() called")
val newSource = recipeStorage.queryRecipes()
sources.add(newSource)
return newSource
}
fun invalidate() {
Timber.v("invalidate() called")
for (source in sources) {
if (!source.invalid) {
source.invalidate()
}
}
sources.removeAll { it.invalid }
}
}

View File

@@ -0,0 +1,49 @@
package gq.kirmanak.mealient.data.recipes.impl
import androidx.paging.ExperimentalPagingApi
import androidx.paging.Pager
import androidx.paging.PagingConfig
import gq.kirmanak.mealient.data.recipes.RecipeRepo
import gq.kirmanak.mealient.data.recipes.db.RecipeStorage
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
import kotlinx.coroutines.CancellationException
import timber.log.Timber
import javax.inject.Inject
@ExperimentalPagingApi
class RecipeRepoImpl @Inject constructor(
private val mediator: RecipesRemoteMediator,
private val storage: RecipeStorage,
private val pagingSourceFactory: RecipePagingSourceFactory,
private val dataSource: RecipeDataSource,
) : RecipeRepo {
override fun createPager(): Pager<Int, RecipeSummaryEntity> {
Timber.v("createPager() called")
val pagingConfig = PagingConfig(pageSize = 30, enablePlaceholders = true)
return Pager(
config = pagingConfig,
remoteMediator = mediator,
pagingSourceFactory = pagingSourceFactory
)
}
override suspend fun clearLocalData() {
Timber.v("clearLocalData() called")
storage.clearAllLocalData()
}
override suspend fun loadRecipeInfo(recipeId: Long, recipeSlug: String): FullRecipeInfo {
Timber.v("loadRecipeInfo() called with: recipeId = $recipeId, recipeSlug = $recipeSlug")
try {
storage.saveRecipeInfo(dataSource.requestRecipeInfo(recipeSlug))
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
Timber.e(e, "loadRecipeInfo: can't update full recipe info")
}
return storage.queryRecipeInfo(recipeId)
}
}

View File

@@ -0,0 +1,63 @@
package gq.kirmanak.mealient.data.recipes.impl
import androidx.annotation.VisibleForTesting
import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadType
import androidx.paging.LoadType.PREPEND
import androidx.paging.LoadType.REFRESH
import androidx.paging.PagingState
import androidx.paging.RemoteMediator
import gq.kirmanak.mealient.data.recipes.db.RecipeStorage
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
import kotlinx.coroutines.CancellationException
import timber.log.Timber
import javax.inject.Inject
@ExperimentalPagingApi
class RecipesRemoteMediator @Inject constructor(
private val storage: RecipeStorage,
private val network: RecipeDataSource,
private val pagingSourceFactory: RecipePagingSourceFactory,
) : RemoteMediator<Int, RecipeSummaryEntity>() {
@VisibleForTesting
var lastRequestEnd: Int = 0
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, RecipeSummaryEntity>
): MediatorResult {
Timber.v("load() called with: lastRequestEnd = $lastRequestEnd, loadType = $loadType, state = $state")
if (loadType == PREPEND) {
Timber.i("load: early exit, PREPEND isn't supported")
return MediatorResult.Success(endOfPaginationReached = true)
}
val start = if (loadType == REFRESH) 0 else lastRequestEnd
val limit = if (loadType == REFRESH) state.config.initialLoadSize else state.config.pageSize
val count: Int = try {
val recipes = network.requestRecipes(start, limit)
if (loadType == REFRESH) storage.refreshAll(recipes)
else storage.saveRecipes(recipes)
recipes.size
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
Timber.e(e, "Can't load recipes")
return MediatorResult.Error(e)
}
// After something is inserted into DB the paging sources have to be invalidated
// But for some reason Room/Paging library don't do it automatically
// Here we invalidate them manually.
// Read that trick here https://github.com/android/architecture-components-samples/issues/889#issuecomment-880847858
pagingSourceFactory.invalidate()
Timber.d("load: expectedCount = $limit, received $count")
lastRequestEnd = start + count
return MediatorResult.Success(endOfPaginationReached = count < limit)
}
}

View File

@@ -0,0 +1,10 @@
package gq.kirmanak.mealient.data.recipes.network
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeResponse
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeSummaryResponse
interface RecipeDataSource {
suspend fun requestRecipes(start: Int = 0, limit: Int = 9999): List<GetRecipeSummaryResponse>
suspend fun requestRecipeInfo(slug: String): GetRecipeResponse
}

View File

@@ -0,0 +1,50 @@
package gq.kirmanak.mealient.data.recipes.network
import gq.kirmanak.mealient.data.auth.AuthRepo
import gq.kirmanak.mealient.data.impl.RetrofitBuilder
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeResponse
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeSummaryResponse
import kotlinx.serialization.ExperimentalSerializationApi
import timber.log.Timber
import javax.inject.Inject
@ExperimentalSerializationApi
class RecipeDataSourceImpl @Inject constructor(
private val authRepo: AuthRepo,
private val retrofitBuilder: RetrofitBuilder
) : RecipeDataSource {
private var _recipeService: RecipeService? = null
override suspend fun requestRecipes(start: Int, limit: Int): List<GetRecipeSummaryResponse> {
Timber.v("requestRecipes() called with: start = $start, limit = $limit")
val service: RecipeService = getRecipeService()
val recipeSummary = service.getRecipeSummary(start, limit)
Timber.v("requestRecipes() returned: $recipeSummary")
return recipeSummary
}
override suspend fun requestRecipeInfo(slug: String): GetRecipeResponse {
Timber.v("requestRecipeInfo() called with: slug = $slug")
val service: RecipeService = getRecipeService()
val recipeInfo = service.getRecipe(slug)
Timber.v("requestRecipeInfo() returned: $recipeInfo")
return recipeInfo
}
private suspend fun getRecipeService(): RecipeService {
Timber.v("getRecipeService() called")
val cachedService: RecipeService? = _recipeService
val service: RecipeService = if (cachedService == null) {
val baseUrl = checkNotNull(authRepo.getBaseUrl()) { "Base url is null" }
val token = checkNotNull(authRepo.getToken()) { "Token is null" }
Timber.d("requestRecipes: baseUrl = $baseUrl, token = $token")
val retrofit = retrofitBuilder.buildRetrofit(baseUrl, token)
val createdService = retrofit.create(RecipeService::class.java)
_recipeService = createdService
createdService
} else {
cachedService
}
return service
}
}

View File

@@ -0,0 +1,20 @@
package gq.kirmanak.mealient.data.recipes.network
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeResponse
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeSummaryResponse
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
interface RecipeService {
@GET("/api/recipes/summary")
suspend fun getRecipeSummary(
@Query("start") start: Int,
@Query("limit") limit: Int
): List<GetRecipeSummaryResponse>
@GET("/api/recipes/{recipe_slug}")
suspend fun getRecipe(
@Path("recipe_slug") recipeSlug: String
): GetRecipeResponse
}

View File

@@ -0,0 +1,14 @@
package gq.kirmanak.mealient.data.recipes.network.response
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GetRecipeIngredientResponse(
@SerialName("title") val title: String = "",
@SerialName("note") val note: String = "",
@SerialName("unit") val unit: String = "",
@SerialName("food") val food: String = "",
@SerialName("disableAmount") val disableAmount: Boolean,
@SerialName("quantity") val quantity: Int,
)

View File

@@ -0,0 +1,10 @@
package gq.kirmanak.mealient.data.recipes.network.response
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GetRecipeInstructionResponse(
@SerialName("title") val title: String = "",
@SerialName("text") val text: String,
)

View File

@@ -0,0 +1,23 @@
package gq.kirmanak.mealient.data.recipes.network.response
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GetRecipeResponse(
@SerialName("id") val remoteId: Long,
@SerialName("name") val name: String,
@SerialName("slug") val slug: String,
@SerialName("image") val image: String,
@SerialName("description") val description: String = "",
@SerialName("recipeCategory") val recipeCategories: List<String>,
@SerialName("tags") val tags: List<String>,
@SerialName("rating") val rating: Int?,
@SerialName("dateAdded") val dateAdded: LocalDate,
@SerialName("dateUpdated") val dateUpdated: LocalDateTime,
@SerialName("recipeYield") val recipeYield: String = "",
@SerialName("recipeIngredient") val recipeIngredients: List<GetRecipeIngredientResponse>,
@SerialName("recipeInstructions") val recipeInstructions: List<GetRecipeInstructionResponse>,
)

View File

@@ -0,0 +1,24 @@
package gq.kirmanak.mealient.data.recipes.network.response
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GetRecipeSummaryResponse(
@SerialName("id") val remoteId: Long,
@SerialName("name") val name: String,
@SerialName("slug") val slug: String,
@SerialName("image") val image: String,
@SerialName("description") val description: String = "",
@SerialName("recipeCategory") val recipeCategories: List<String>,
@SerialName("tags") val tags: List<String>,
@SerialName("rating") val rating: Int?,
@SerialName("dateAdded") val dateAdded: LocalDate,
@SerialName("dateUpdated") val dateUpdated: LocalDateTime
) {
override fun toString(): String {
return "GetRecipeSummaryResponse(remoteId=$remoteId, name='$name')"
}
}