Implement loading and saving full recipe info
This commit is contained in:
@@ -4,7 +4,8 @@ import androidx.room.Database
|
|||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
import gq.kirmanak.mealie.data.impl.RoomTypeConverters
|
import gq.kirmanak.mealie.data.impl.RoomTypeConverters
|
||||||
import gq.kirmanak.mealie.data.recipes.db.*
|
import gq.kirmanak.mealie.data.recipes.db.RecipeDao
|
||||||
|
import gq.kirmanak.mealie.data.recipes.db.entity.*
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package gq.kirmanak.mealie.data.recipes
|
package gq.kirmanak.mealie.data.recipes
|
||||||
|
|
||||||
import androidx.paging.Pager
|
import androidx.paging.Pager
|
||||||
import gq.kirmanak.mealie.data.recipes.db.RecipeSummaryEntity
|
import gq.kirmanak.mealie.data.recipes.db.entity.RecipeSummaryEntity
|
||||||
|
|
||||||
interface RecipeRepo {
|
interface RecipeRepo {
|
||||||
fun createPager(): Pager<Int, RecipeSummaryEntity>
|
fun createPager(): Pager<Int, RecipeSummaryEntity>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import androidx.room.Dao
|
|||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
|
import gq.kirmanak.mealie.data.recipes.db.entity.*
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface RecipeDao {
|
interface RecipeDao {
|
||||||
@@ -18,7 +19,7 @@ interface RecipeDao {
|
|||||||
fun queryRecipesByPages(): PagingSource<Int, RecipeSummaryEntity>
|
fun queryRecipesByPages(): PagingSource<Int, RecipeSummaryEntity>
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insertRecipe(recipeSummaryEntity: RecipeSummaryEntity): Long
|
suspend fun insertRecipe(recipeSummaryEntity: RecipeSummaryEntity)
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
suspend fun insertTag(tagEntity: TagEntity): Long
|
suspend fun insertTag(tagEntity: TagEntity): Long
|
||||||
@@ -55,4 +56,13 @@ interface RecipeDao {
|
|||||||
|
|
||||||
@Query("SELECT * FROM tag_recipe")
|
@Query("SELECT * FROM tag_recipe")
|
||||||
suspend fun queryAllTagRecipes(): List<TagRecipeEntity>
|
suspend fun queryAllTagRecipes(): List<TagRecipeEntity>
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
suspend fun insertRecipe(recipe: RecipeEntity)
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
suspend fun insertRecipeInstructions(instructions: List<RecipeInstructionEntity>)
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
suspend fun insertRecipeIngredients(ingredients: List<RecipeIngredientEntity>)
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
package gq.kirmanak.mealie.data.recipes.db
|
package gq.kirmanak.mealie.data.recipes.db
|
||||||
|
|
||||||
import androidx.paging.PagingSource
|
import androidx.paging.PagingSource
|
||||||
import gq.kirmanak.mealie.data.recipes.network.GetRecipeSummaryResponse
|
import gq.kirmanak.mealie.data.recipes.db.entity.RecipeSummaryEntity
|
||||||
|
import gq.kirmanak.mealie.data.recipes.network.response.GetRecipeResponse
|
||||||
|
import gq.kirmanak.mealie.data.recipes.network.response.GetRecipeSummaryResponse
|
||||||
|
|
||||||
interface RecipeStorage {
|
interface RecipeStorage {
|
||||||
suspend fun saveRecipes(recipes: List<GetRecipeSummaryResponse>)
|
suspend fun saveRecipes(recipes: List<GetRecipeSummaryResponse>)
|
||||||
@@ -11,4 +13,6 @@ interface RecipeStorage {
|
|||||||
suspend fun refreshAll(recipes: List<GetRecipeSummaryResponse>)
|
suspend fun refreshAll(recipes: List<GetRecipeSummaryResponse>)
|
||||||
|
|
||||||
suspend fun clearAllLocalData()
|
suspend fun clearAllLocalData()
|
||||||
|
|
||||||
|
suspend fun saveRecipeInfo(recipe: GetRecipeResponse)
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,11 @@ package gq.kirmanak.mealie.data.recipes.db
|
|||||||
import androidx.paging.PagingSource
|
import androidx.paging.PagingSource
|
||||||
import androidx.room.withTransaction
|
import androidx.room.withTransaction
|
||||||
import gq.kirmanak.mealie.data.MealieDb
|
import gq.kirmanak.mealie.data.MealieDb
|
||||||
import gq.kirmanak.mealie.data.recipes.network.GetRecipeSummaryResponse
|
import gq.kirmanak.mealie.data.recipes.db.entity.*
|
||||||
|
import gq.kirmanak.mealie.data.recipes.network.response.GetRecipeIngredientResponse
|
||||||
|
import gq.kirmanak.mealie.data.recipes.network.response.GetRecipeInstructionResponse
|
||||||
|
import gq.kirmanak.mealie.data.recipes.network.response.GetRecipeResponse
|
||||||
|
import gq.kirmanak.mealie.data.recipes.network.response.GetRecipeSummaryResponse
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -27,16 +31,20 @@ class RecipeStorageImpl @Inject constructor(
|
|||||||
val categoryRecipeEntities = mutableSetOf<CategoryRecipeEntity>()
|
val categoryRecipeEntities = mutableSetOf<CategoryRecipeEntity>()
|
||||||
|
|
||||||
for (recipe in recipes) {
|
for (recipe in recipes) {
|
||||||
val recipeId = recipeDao.insertRecipe(recipe.recipeEntity())
|
val recipeSummaryEntity = recipe.recipeEntity()
|
||||||
|
recipeDao.insertRecipe(recipeSummaryEntity)
|
||||||
|
|
||||||
for (tag in recipe.tags) {
|
for (tag in recipe.tags) {
|
||||||
val tagId = getIdOrInsert(tagEntities, tag)
|
val tagId = getIdOrInsert(tagEntities, tag)
|
||||||
tagRecipeEntities += TagRecipeEntity(tagId, recipeId)
|
tagRecipeEntities += TagRecipeEntity(tagId, recipeSummaryEntity.remoteId)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (category in recipe.recipeCategories) {
|
for (category in recipe.recipeCategories) {
|
||||||
val categoryId = getOrInsert(categoryEntities, category)
|
val categoryId = getOrInsert(categoryEntities, category)
|
||||||
categoryRecipeEntities += CategoryRecipeEntity(categoryId, recipeId)
|
categoryRecipeEntities += CategoryRecipeEntity(
|
||||||
|
categoryId,
|
||||||
|
recipeSummaryEntity.remoteId
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,4 +116,42 @@ class RecipeStorageImpl @Inject constructor(
|
|||||||
recipeDao.removeAllTags()
|
recipeDao.removeAllTags()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun saveRecipeInfo(recipe: GetRecipeResponse) {
|
||||||
|
Timber.v("saveRecipeInfo() called with: recipe = $recipe")
|
||||||
|
db.withTransaction {
|
||||||
|
recipeDao.insertRecipe(recipe.toRecipeEntity())
|
||||||
|
val ingredients = recipe.recipeIngredients.map {
|
||||||
|
it.toRecipeIngredientEntity(recipe.remoteId)
|
||||||
|
}
|
||||||
|
recipeDao.insertRecipeIngredients(ingredients)
|
||||||
|
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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package gq.kirmanak.mealie.data.recipes.db
|
package gq.kirmanak.mealie.data.recipes.db.entity
|
||||||
|
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package gq.kirmanak.mealie.data.recipes.db
|
package gq.kirmanak.mealie.data.recipes.db.entity
|
||||||
|
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
@@ -17,7 +17,7 @@ import androidx.room.Index
|
|||||||
onUpdate = ForeignKey.CASCADE
|
onUpdate = ForeignKey.CASCADE
|
||||||
), ForeignKey(
|
), ForeignKey(
|
||||||
entity = RecipeSummaryEntity::class,
|
entity = RecipeSummaryEntity::class,
|
||||||
parentColumns = ["local_id"],
|
parentColumns = ["remote_id"],
|
||||||
childColumns = ["recipe_id"],
|
childColumns = ["recipe_id"],
|
||||||
onDelete = ForeignKey.CASCADE,
|
onDelete = ForeignKey.CASCADE,
|
||||||
onUpdate = ForeignKey.CASCADE
|
onUpdate = ForeignKey.CASCADE
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package gq.kirmanak.mealie.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,
|
||||||
|
)
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package gq.kirmanak.mealie.data.recipes.db.entity
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
@Entity(tableName = "recipe_ingredient")
|
||||||
|
data class RecipeIngredientEntity(
|
||||||
|
@PrimaryKey @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,
|
||||||
|
)
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package gq.kirmanak.mealie.data.recipes.db.entity
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
@Entity(tableName = "recipe_instruction")
|
||||||
|
data class RecipeInstructionEntity(
|
||||||
|
@PrimaryKey @ColumnInfo(name = "recipe_id") val recipeId: Long,
|
||||||
|
@ColumnInfo(name = "title") val title: String,
|
||||||
|
@ColumnInfo(name = "text") val text: String,
|
||||||
|
)
|
||||||
@@ -1,16 +1,14 @@
|
|||||||
package gq.kirmanak.mealie.data.recipes.db
|
package gq.kirmanak.mealie.data.recipes.db.entity
|
||||||
|
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.Index
|
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
import kotlinx.datetime.LocalDateTime
|
import kotlinx.datetime.LocalDateTime
|
||||||
|
|
||||||
@Entity(tableName = "recipe_summaries", indices = [Index(value = ["remote_id"], unique = true)])
|
@Entity(tableName = "recipe_summaries")
|
||||||
data class RecipeSummaryEntity(
|
data class RecipeSummaryEntity(
|
||||||
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "local_id") val localId: Long = 0,
|
@PrimaryKey @ColumnInfo(name = "remote_id") val remoteId: Long,
|
||||||
@ColumnInfo(name = "remote_id") val remoteId: Long,
|
|
||||||
@ColumnInfo(name = "name") val name: String,
|
@ColumnInfo(name = "name") val name: String,
|
||||||
@ColumnInfo(name = "slug") val slug: String,
|
@ColumnInfo(name = "slug") val slug: String,
|
||||||
@ColumnInfo(name = "image") val image: String,
|
@ColumnInfo(name = "image") val image: String,
|
||||||
@@ -20,6 +18,6 @@ data class RecipeSummaryEntity(
|
|||||||
@ColumnInfo(name = "date_updated") val dateUpdated: LocalDateTime
|
@ColumnInfo(name = "date_updated") val dateUpdated: LocalDateTime
|
||||||
) {
|
) {
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return "RecipeEntity(localId=$localId, remoteId=$remoteId, name='$name')"
|
return "RecipeEntity(remoteId=$remoteId, name='$name')"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package gq.kirmanak.mealie.data.recipes.db
|
package gq.kirmanak.mealie.data.recipes.db.entity
|
||||||
|
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package gq.kirmanak.mealie.data.recipes.db
|
package gq.kirmanak.mealie.data.recipes.db.entity
|
||||||
|
|
||||||
import androidx.room.ColumnInfo
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
@@ -15,7 +15,7 @@ import androidx.room.ForeignKey
|
|||||||
onUpdate = ForeignKey.CASCADE
|
onUpdate = ForeignKey.CASCADE
|
||||||
), ForeignKey(
|
), ForeignKey(
|
||||||
entity = RecipeSummaryEntity::class,
|
entity = RecipeSummaryEntity::class,
|
||||||
parentColumns = ["local_id"],
|
parentColumns = ["remote_id"],
|
||||||
childColumns = ["recipe_id"],
|
childColumns = ["recipe_id"],
|
||||||
onDelete = ForeignKey.CASCADE,
|
onDelete = ForeignKey.CASCADE,
|
||||||
onUpdate = ForeignKey.CASCADE
|
onUpdate = ForeignKey.CASCADE
|
||||||
@@ -2,7 +2,7 @@ package gq.kirmanak.mealie.data.recipes.impl
|
|||||||
|
|
||||||
import androidx.paging.PagingSource
|
import androidx.paging.PagingSource
|
||||||
import gq.kirmanak.mealie.data.recipes.db.RecipeStorage
|
import gq.kirmanak.mealie.data.recipes.db.RecipeStorage
|
||||||
import gq.kirmanak.mealie.data.recipes.db.RecipeSummaryEntity
|
import gq.kirmanak.mealie.data.recipes.db.entity.RecipeSummaryEntity
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import androidx.paging.Pager
|
|||||||
import androidx.paging.PagingConfig
|
import androidx.paging.PagingConfig
|
||||||
import gq.kirmanak.mealie.data.recipes.RecipeRepo
|
import gq.kirmanak.mealie.data.recipes.RecipeRepo
|
||||||
import gq.kirmanak.mealie.data.recipes.db.RecipeStorage
|
import gq.kirmanak.mealie.data.recipes.db.RecipeStorage
|
||||||
import gq.kirmanak.mealie.data.recipes.db.RecipeSummaryEntity
|
import gq.kirmanak.mealie.data.recipes.db.entity.RecipeSummaryEntity
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import androidx.paging.LoadType.REFRESH
|
|||||||
import androidx.paging.PagingState
|
import androidx.paging.PagingState
|
||||||
import androidx.paging.RemoteMediator
|
import androidx.paging.RemoteMediator
|
||||||
import gq.kirmanak.mealie.data.recipes.db.RecipeStorage
|
import gq.kirmanak.mealie.data.recipes.db.RecipeStorage
|
||||||
import gq.kirmanak.mealie.data.recipes.db.RecipeSummaryEntity
|
import gq.kirmanak.mealie.data.recipes.db.entity.RecipeSummaryEntity
|
||||||
import gq.kirmanak.mealie.data.recipes.network.RecipeDataSource
|
import gq.kirmanak.mealie.data.recipes.network.RecipeDataSource
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
package gq.kirmanak.mealie.data.recipes.network
|
package gq.kirmanak.mealie.data.recipes.network
|
||||||
|
|
||||||
|
import gq.kirmanak.mealie.data.recipes.network.response.GetRecipeResponse
|
||||||
|
import gq.kirmanak.mealie.data.recipes.network.response.GetRecipeSummaryResponse
|
||||||
|
|
||||||
interface RecipeDataSource {
|
interface RecipeDataSource {
|
||||||
suspend fun requestRecipes(start: Int = 0, limit: Int = 9999): List<GetRecipeSummaryResponse>
|
suspend fun requestRecipes(start: Int = 0, limit: Int = 9999): List<GetRecipeSummaryResponse>
|
||||||
|
|
||||||
|
suspend fun requestRecipeInfo(slug: String): GetRecipeResponse
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,8 @@ package gq.kirmanak.mealie.data.recipes.network
|
|||||||
|
|
||||||
import gq.kirmanak.mealie.data.auth.AuthRepo
|
import gq.kirmanak.mealie.data.auth.AuthRepo
|
||||||
import gq.kirmanak.mealie.data.impl.RetrofitBuilder
|
import gq.kirmanak.mealie.data.impl.RetrofitBuilder
|
||||||
|
import gq.kirmanak.mealie.data.recipes.network.response.GetRecipeResponse
|
||||||
|
import gq.kirmanak.mealie.data.recipes.network.response.GetRecipeSummaryResponse
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -21,6 +23,14 @@ class RecipeDataSourceImpl @Inject constructor(
|
|||||||
return 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 {
|
private suspend fun getRecipeService(): RecipeService {
|
||||||
Timber.v("getRecipeService() called")
|
Timber.v("getRecipeService() called")
|
||||||
val cachedService: RecipeService? = _recipeService
|
val cachedService: RecipeService? = _recipeService
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package gq.kirmanak.mealie.data.recipes.network
|
package gq.kirmanak.mealie.data.recipes.network
|
||||||
|
|
||||||
|
import gq.kirmanak.mealie.data.recipes.network.response.GetRecipeResponse
|
||||||
|
import gq.kirmanak.mealie.data.recipes.network.response.GetRecipeSummaryResponse
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Path
|
||||||
import retrofit2.http.Query
|
import retrofit2.http.Query
|
||||||
|
|
||||||
interface RecipeService {
|
interface RecipeService {
|
||||||
@@ -9,4 +12,9 @@ interface RecipeService {
|
|||||||
@Query("start") start: Int,
|
@Query("start") start: Int,
|
||||||
@Query("limit") limit: Int
|
@Query("limit") limit: Int
|
||||||
): List<GetRecipeSummaryResponse>
|
): List<GetRecipeSummaryResponse>
|
||||||
|
|
||||||
|
@GET("/api/recipes/:recipe_slug")
|
||||||
|
suspend fun getRecipe(
|
||||||
|
@Path("recipe_slug") recipeSlug: String
|
||||||
|
): GetRecipeResponse
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package gq.kirmanak.mealie.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,
|
||||||
|
)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package gq.kirmanak.mealie.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,
|
||||||
|
)
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package gq.kirmanak.mealie.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>,
|
||||||
|
)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package gq.kirmanak.mealie.data.recipes.network
|
package gq.kirmanak.mealie.data.recipes.network.response
|
||||||
|
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
import kotlinx.datetime.LocalDateTime
|
import kotlinx.datetime.LocalDateTime
|
||||||
@@ -2,7 +2,7 @@ package gq.kirmanak.mealie.ui.recipes
|
|||||||
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import gq.kirmanak.mealie.R
|
import gq.kirmanak.mealie.R
|
||||||
import gq.kirmanak.mealie.data.recipes.db.RecipeSummaryEntity
|
import gq.kirmanak.mealie.data.recipes.db.entity.RecipeSummaryEntity
|
||||||
import gq.kirmanak.mealie.databinding.ViewHolderRecipeBinding
|
import gq.kirmanak.mealie.databinding.ViewHolderRecipeBinding
|
||||||
|
|
||||||
class RecipeViewHolder(
|
class RecipeViewHolder(
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import gq.kirmanak.mealie.data.recipes.RecipeImageLoader
|
import gq.kirmanak.mealie.data.recipes.RecipeImageLoader
|
||||||
import gq.kirmanak.mealie.data.recipes.RecipeRepo
|
import gq.kirmanak.mealie.data.recipes.RecipeRepo
|
||||||
import gq.kirmanak.mealie.data.recipes.db.RecipeSummaryEntity
|
import gq.kirmanak.mealie.data.recipes.db.entity.RecipeSummaryEntity
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import android.view.LayoutInflater
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.paging.PagingDataAdapter
|
import androidx.paging.PagingDataAdapter
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import gq.kirmanak.mealie.data.recipes.db.RecipeSummaryEntity
|
import gq.kirmanak.mealie.data.recipes.db.entity.RecipeSummaryEntity
|
||||||
import gq.kirmanak.mealie.databinding.ViewHolderRecipeBinding
|
import gq.kirmanak.mealie.databinding.ViewHolderRecipeBinding
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package gq.kirmanak.mealie.ui.recipes.info
|
||||||
|
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class RecipeInfoFragment : Fragment() {
|
||||||
|
}
|
||||||
89
app/src/main/res/layout/fragment_recipe_info.xml
Normal file
89
app/src/main/res/layout/fragment_recipe_info.xml
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="@dimen/fragment_recipe_info_image_height"
|
||||||
|
android:layout_marginBottom="@dimen/margin_small"
|
||||||
|
android:contentDescription="@string/content_description_fragment_recipe_info_image"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:srcCompat="@tools:sample/backgrounds/scenic" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="@dimen/margin_small"
|
||||||
|
android:textAppearance="?textAppearanceHeadline6"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/image"
|
||||||
|
tools:text="Best-Ever Beef Stew" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/description"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="@dimen/margin_small"
|
||||||
|
android:textAppearance="?textAppearanceBody2"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/title"
|
||||||
|
tools:text="Stay warm all winter with this classic Beef Stew made with red wine and beef stock from Delish.com." />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/ingredients_header"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="@dimen/margin_small"
|
||||||
|
android:text="@string/fragment_recipe_info_ingredients_header"
|
||||||
|
android:textAppearance="?textAppearanceHeadline6"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/description" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/ingredients_list"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="@dimen/margin_small"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/ingredients_header"
|
||||||
|
tools:itemCount="3"
|
||||||
|
tools:listitem="@layout/view_holder_ingredient" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/instructions_header"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="@dimen/margin_small"
|
||||||
|
android:text="@string/fragment_recipe_info_instructions_header"
|
||||||
|
android:textAppearance="?textAppearanceHeadline6"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/ingredients_list" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/instructions_list"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="@dimen/margin_small"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/instructions_header"
|
||||||
|
tools:itemCount="2"
|
||||||
|
tools:listitem="@layout/view_holder_instruction" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</androidx.core.widget.NestedScrollView>
|
||||||
17
app/src/main/res/layout/view_holder_ingredient.xml
Normal file
17
app/src/main/res/layout/view_holder_ingredient.xml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/checkBox"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="@dimen/margin_small"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="900 g braising steak/stew meat, cubed into 2.5cm pieces" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
30
app/src/main/res/layout/view_holder_instruction.xml
Normal file
30
app/src/main/res/layout/view_holder_instruction.xml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/step"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="@dimen/margin_small"
|
||||||
|
android:textAppearance="?textAppearanceHeadline6"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="Step: 1" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/instruction"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="@dimen/margin_small"
|
||||||
|
android:textAppearance="?textAppearanceBody2"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/step"
|
||||||
|
tools:text="In a large dutch oven or heavy-bottomed pot over medium heat, heat oil. Add beef and cook until seared on all sides, 10 minutes, working in batches if necessary. Transfer beef to a plate." />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -3,4 +3,5 @@
|
|||||||
<dimen name="margin_small">8dp</dimen>
|
<dimen name="margin_small">8dp</dimen>
|
||||||
<dimen name="width_view_holder_recipe_image">360dp</dimen>
|
<dimen name="width_view_holder_recipe_image">360dp</dimen>
|
||||||
<dimen name="height_view_holder_recipe_image">180dp</dimen>
|
<dimen name="height_view_holder_recipe_image">180dp</dimen>
|
||||||
|
<dimen name="fragment_recipe_info_image_height">@dimen/height_view_holder_recipe_image</dimen>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -8,4 +8,7 @@
|
|||||||
<string name="menu_main_toolbar_content_description_logout">@string/menu_main_toolbar_logout</string>
|
<string name="menu_main_toolbar_content_description_logout">@string/menu_main_toolbar_logout</string>
|
||||||
<string name="menu_main_toolbar_logout">Logout</string>
|
<string name="menu_main_toolbar_logout">Logout</string>
|
||||||
<string name="view_holder_recipe_text_placeholder">Loading…</string>
|
<string name="view_holder_recipe_text_placeholder">Loading…</string>
|
||||||
|
<string name="content_description_fragment_recipe_info_image">@string/content_description_view_holder_recipe_image</string>
|
||||||
|
<string name="fragment_recipe_info_ingredients_header">Ingredients</string>
|
||||||
|
<string name="fragment_recipe_info_instructions_header">Instructions</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package gq.kirmanak.mealie.data.recipes
|
package gq.kirmanak.mealie.data.recipes
|
||||||
|
|
||||||
import gq.kirmanak.mealie.data.recipes.db.RecipeSummaryEntity
|
import gq.kirmanak.mealie.data.recipes.db.entity.RecipeSummaryEntity
|
||||||
import gq.kirmanak.mealie.data.recipes.network.GetRecipeSummaryResponse
|
import gq.kirmanak.mealie.data.recipes.network.response.GetRecipeSummaryResponse
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
import kotlinx.datetime.LocalDateTime
|
import kotlinx.datetime.LocalDateTime
|
||||||
import okhttp3.mockwebserver.MockResponse
|
import okhttp3.mockwebserver.MockResponse
|
||||||
@@ -68,7 +68,6 @@ object RecipeImplTestData {
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
val CAKE_RECIPE_ENTITY = RecipeSummaryEntity(
|
val CAKE_RECIPE_ENTITY = RecipeSummaryEntity(
|
||||||
localId = 1,
|
|
||||||
remoteId = 1,
|
remoteId = 1,
|
||||||
name = "Cake",
|
name = "Cake",
|
||||||
slug = "cake",
|
slug = "cake",
|
||||||
@@ -80,7 +79,6 @@ object RecipeImplTestData {
|
|||||||
)
|
)
|
||||||
|
|
||||||
val PORRIDGE_RECIPE_ENTITY = RecipeSummaryEntity(
|
val PORRIDGE_RECIPE_ENTITY = RecipeSummaryEntity(
|
||||||
localId = 2,
|
|
||||||
remoteId = 2,
|
remoteId = 2,
|
||||||
name = "Porridge",
|
name = "Porridge",
|
||||||
slug = "porridge",
|
slug = "porridge",
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ import gq.kirmanak.mealie.data.recipes.RecipeImplTestData.CAKE_RECIPE_ENTITY
|
|||||||
import gq.kirmanak.mealie.data.recipes.RecipeImplTestData.PORRIDGE_RECIPE_ENTITY
|
import gq.kirmanak.mealie.data.recipes.RecipeImplTestData.PORRIDGE_RECIPE_ENTITY
|
||||||
import gq.kirmanak.mealie.data.recipes.RecipeImplTestData.RECIPE_SUMMARY_CAKE
|
import gq.kirmanak.mealie.data.recipes.RecipeImplTestData.RECIPE_SUMMARY_CAKE
|
||||||
import gq.kirmanak.mealie.data.recipes.RecipeImplTestData.TEST_RECIPE_SUMMARIES
|
import gq.kirmanak.mealie.data.recipes.RecipeImplTestData.TEST_RECIPE_SUMMARIES
|
||||||
|
import gq.kirmanak.mealie.data.recipes.db.entity.CategoryEntity
|
||||||
|
import gq.kirmanak.mealie.data.recipes.db.entity.CategoryRecipeEntity
|
||||||
|
import gq.kirmanak.mealie.data.recipes.db.entity.TagEntity
|
||||||
|
import gq.kirmanak.mealie.data.recipes.db.entity.TagRecipeEntity
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -79,9 +83,7 @@ class RecipeStorageImplTest : HiltRobolectricTest() {
|
|||||||
subject.saveRecipes(TEST_RECIPE_SUMMARIES)
|
subject.saveRecipes(TEST_RECIPE_SUMMARIES)
|
||||||
subject.refreshAll(listOf(RECIPE_SUMMARY_CAKE))
|
subject.refreshAll(listOf(RECIPE_SUMMARY_CAKE))
|
||||||
val actual = mealieDb.recipeDao().queryAllRecipes()
|
val actual = mealieDb.recipeDao().queryAllRecipes()
|
||||||
assertThat(actual).containsExactly(
|
assertThat(actual).containsExactly(CAKE_RECIPE_ENTITY)
|
||||||
CAKE_RECIPE_ENTITY.copy(localId = 3),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -90,8 +92,8 @@ class RecipeStorageImplTest : HiltRobolectricTest() {
|
|||||||
subject.refreshAll(listOf(RECIPE_SUMMARY_CAKE))
|
subject.refreshAll(listOf(RECIPE_SUMMARY_CAKE))
|
||||||
val actual = mealieDb.recipeDao().queryAllCategoryRecipes()
|
val actual = mealieDb.recipeDao().queryAllCategoryRecipes()
|
||||||
assertThat(actual).containsExactly(
|
assertThat(actual).containsExactly(
|
||||||
CategoryRecipeEntity(categoryId = 1, recipeId = 3),
|
CategoryRecipeEntity(categoryId = 1, recipeId = 1),
|
||||||
CategoryRecipeEntity(categoryId = 2, recipeId = 3),
|
CategoryRecipeEntity(categoryId = 2, recipeId = 1),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,8 +103,8 @@ class RecipeStorageImplTest : HiltRobolectricTest() {
|
|||||||
subject.refreshAll(listOf(RECIPE_SUMMARY_CAKE))
|
subject.refreshAll(listOf(RECIPE_SUMMARY_CAKE))
|
||||||
val actual = mealieDb.recipeDao().queryAllTagRecipes()
|
val actual = mealieDb.recipeDao().queryAllTagRecipes()
|
||||||
assertThat(actual).containsExactly(
|
assertThat(actual).containsExactly(
|
||||||
TagRecipeEntity(tagId = 1, recipeId = 3),
|
TagRecipeEntity(tagId = 1, recipeId = 1),
|
||||||
TagRecipeEntity(tagId = 2, recipeId = 3),
|
TagRecipeEntity(tagId = 2, recipeId = 1),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import gq.kirmanak.mealie.data.recipes.RecipeImplTestData.PORRIDGE_RECIPE_ENTITY
|
|||||||
import gq.kirmanak.mealie.data.recipes.RecipeImplTestData.TEST_RECIPE_ENTITIES
|
import gq.kirmanak.mealie.data.recipes.RecipeImplTestData.TEST_RECIPE_ENTITIES
|
||||||
import gq.kirmanak.mealie.data.recipes.RecipeImplTestData.enqueueSuccessfulRecipeSummaryResponse
|
import gq.kirmanak.mealie.data.recipes.RecipeImplTestData.enqueueSuccessfulRecipeSummaryResponse
|
||||||
import gq.kirmanak.mealie.data.recipes.RecipeImplTestData.enqueueUnsuccessfulRecipeSummaryResponse
|
import gq.kirmanak.mealie.data.recipes.RecipeImplTestData.enqueueUnsuccessfulRecipeSummaryResponse
|
||||||
import gq.kirmanak.mealie.data.recipes.db.RecipeSummaryEntity
|
import gq.kirmanak.mealie.data.recipes.db.entity.RecipeSummaryEntity
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|||||||
Reference in New Issue
Block a user