Initialize RecipesFragment
This commit is contained in:
18
app/src/main/java/gq/kirmanak/mealie/data/MealieDb.kt
Normal file
18
app/src/main/java/gq/kirmanak/mealie/data/MealieDb.kt
Normal file
@@ -0,0 +1,18 @@
|
||||
package gq.kirmanak.mealie.data
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import gq.kirmanak.mealie.data.recipes.db.*
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Database(
|
||||
version = 1,
|
||||
entities = [CategoryEntity::class, CategoryRecipeEntity::class, TagEntity::class, TagRecipeEntity::class, RecipeEntity::class],
|
||||
exportSchema = false
|
||||
)
|
||||
@TypeConverters(RoomTypeConverters::class)
|
||||
@Singleton
|
||||
abstract class MealieDb : RoomDatabase() {
|
||||
abstract fun recipeDao(): RecipeDao
|
||||
}
|
||||
20
app/src/main/java/gq/kirmanak/mealie/data/MealieModule.kt
Normal file
20
app/src/main/java/gq/kirmanak/mealie/data/MealieModule.kt
Normal file
@@ -0,0 +1,20 @@
|
||||
package gq.kirmanak.mealie.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class MealieModule {
|
||||
companion object {
|
||||
@Provides
|
||||
fun createDb(@ApplicationContext context: Context): MealieDb {
|
||||
return Room.databaseBuilder(context, MealieDb::class.java, "mealie.db").build()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package gq.kirmanak.mealie.data
|
||||
|
||||
import gq.kirmanak.mealie.data.auth.AuthOkHttpInterceptor
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
@@ -7,13 +8,15 @@ import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class OkHttpBuilder @Inject constructor() {
|
||||
fun buildOkHttp(): OkHttpClient {
|
||||
return OkHttpClient.Builder()
|
||||
fun buildOkHttp(token: String?): OkHttpClient {
|
||||
Timber.v("buildOkHttp() called with: token = $token")
|
||||
val builder = OkHttpClient.Builder()
|
||||
.addNetworkInterceptor(buildLoggingInterceptor())
|
||||
.build()
|
||||
if (token != null) builder.addNetworkInterceptor(AuthOkHttpInterceptor(token))
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
private fun buildLoggingInterceptor() : Interceptor {
|
||||
private fun buildLoggingInterceptor(): Interceptor {
|
||||
val interceptor = HttpLoggingInterceptor { message -> Timber.tag("OkHttp").v(message) }
|
||||
interceptor.level = HttpLoggingInterceptor.Level.BODY
|
||||
return interceptor
|
||||
|
||||
@@ -10,13 +10,13 @@ import javax.inject.Inject
|
||||
|
||||
@ExperimentalSerializationApi
|
||||
class RetrofitBuilder @Inject constructor(private val okHttpBuilder: OkHttpBuilder) {
|
||||
fun buildRetrofit(baseUrl: String): Retrofit {
|
||||
Timber.v("buildRetrofit() called with: baseUrl = $baseUrl")
|
||||
fun buildRetrofit(baseUrl: String, token: String? = null): Retrofit {
|
||||
Timber.v("buildRetrofit() called with: baseUrl = $baseUrl, token = $token")
|
||||
val url = if (baseUrl.startsWith("http")) baseUrl else "https://$baseUrl"
|
||||
val contentType = "application/json".toMediaType()
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(url)
|
||||
.client(okHttpBuilder.buildOkHttp())
|
||||
.client(okHttpBuilder.buildOkHttp(token))
|
||||
.addConverterFactory(Json.asConverterFactory(contentType))
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package gq.kirmanak.mealie.data
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import kotlinx.datetime.Instant
|
||||
|
||||
object RoomTypeConverters {
|
||||
@TypeConverter
|
||||
fun instantToTimestamp(instant: Instant) = instant.toEpochMilliseconds()
|
||||
|
||||
@TypeConverter
|
||||
fun timestampToInstant(timestamp: Long) = Instant.fromEpochMilliseconds(timestamp)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package gq.kirmanak.mealie.data.auth
|
||||
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
class AuthOkHttpInterceptor(token: String) : Interceptor {
|
||||
private val headerValue = "Bearer $token"
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val newRequest = chain.request()
|
||||
.newBuilder()
|
||||
.addHeader("Authorization", headerValue)
|
||||
.build()
|
||||
return chain.proceed(newRequest)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package gq.kirmanak.mealie.data.recipes
|
||||
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ViewModelComponent
|
||||
import gq.kirmanak.mealie.data.recipes.db.RecipeStorage
|
||||
import gq.kirmanak.mealie.data.recipes.db.RecipeStorageImpl
|
||||
import gq.kirmanak.mealie.data.recipes.network.RecipeDataSource
|
||||
import gq.kirmanak.mealie.data.recipes.network.RecipeDataSourceImpl
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
|
||||
@ExperimentalPagingApi
|
||||
@ExperimentalSerializationApi
|
||||
@Module
|
||||
@InstallIn(ViewModelComponent::class)
|
||||
interface RecipeModule {
|
||||
@Binds
|
||||
fun provideRecipeDataSource(recipeDataSourceImpl: RecipeDataSourceImpl): RecipeDataSource
|
||||
|
||||
@Binds
|
||||
fun provideRecipeStorage(recipeStorageImpl: RecipeStorageImpl): RecipeStorage
|
||||
|
||||
@Binds
|
||||
fun provideRecipeRepo(recipeRepoImpl: RecipeRepoImpl): RecipeRepo
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package gq.kirmanak.mealie.data.recipes
|
||||
|
||||
import androidx.paging.Pager
|
||||
import gq.kirmanak.mealie.data.recipes.db.RecipeEntity
|
||||
|
||||
interface RecipeRepo {
|
||||
fun createPager(): Pager<Int, RecipeEntity>
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package gq.kirmanak.mealie.data.recipes
|
||||
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import gq.kirmanak.mealie.data.recipes.db.RecipeEntity
|
||||
import gq.kirmanak.mealie.data.recipes.db.RecipeStorage
|
||||
import javax.inject.Inject
|
||||
|
||||
@ExperimentalPagingApi
|
||||
class RecipeRepoImpl @Inject constructor(
|
||||
private val mediator: RecipesRemoteMediator,
|
||||
private val storage: RecipeStorage
|
||||
) : RecipeRepo {
|
||||
override fun createPager(): Pager<Int, RecipeEntity> {
|
||||
val pagingConfig = PagingConfig(pageSize = 30)
|
||||
return Pager(pagingConfig, 0, mediator) {
|
||||
storage.queryRecipes()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package gq.kirmanak.mealie.data.recipes
|
||||
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.LoadType
|
||||
import androidx.paging.LoadType.*
|
||||
import androidx.paging.PagingState
|
||||
import androidx.paging.RemoteMediator
|
||||
import gq.kirmanak.mealie.data.recipes.db.RecipeEntity
|
||||
import gq.kirmanak.mealie.data.recipes.db.RecipeStorage
|
||||
import gq.kirmanak.mealie.data.recipes.network.RecipeDataSource
|
||||
import javax.inject.Inject
|
||||
|
||||
@ExperimentalPagingApi
|
||||
class RecipesRemoteMediator @Inject constructor(
|
||||
private val storage: RecipeStorage,
|
||||
private val network: RecipeDataSource
|
||||
) : RemoteMediator<Int, RecipeEntity>() {
|
||||
|
||||
override suspend fun load(
|
||||
loadType: LoadType,
|
||||
state: PagingState<Int, RecipeEntity>
|
||||
): MediatorResult {
|
||||
val pageSize = state.config.pageSize
|
||||
val closestPage = state.anchorPosition?.let { state.closestPageToPosition(it) }
|
||||
val start = when (loadType) {
|
||||
REFRESH -> 0
|
||||
PREPEND -> closestPage?.prevKey ?: 0
|
||||
APPEND -> closestPage?.nextKey ?: 0
|
||||
}
|
||||
val end = when (loadType) {
|
||||
REFRESH -> pageSize
|
||||
PREPEND, APPEND -> start + pageSize
|
||||
}
|
||||
|
||||
val recipes = try {
|
||||
network.requestRecipes(start = start, end = end)
|
||||
} catch (e: Exception) {
|
||||
return MediatorResult.Error(e)
|
||||
}
|
||||
|
||||
try {
|
||||
when (loadType) {
|
||||
REFRESH -> storage.refreshAll(recipes)
|
||||
PREPEND, APPEND -> storage.saveRecipes(recipes)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return MediatorResult.Error(e)
|
||||
}
|
||||
val expectedCount = end - start
|
||||
val isEndReached = recipes.size < expectedCount
|
||||
return MediatorResult.Success(isEndReached)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package gq.kirmanak.mealie.data.recipes.db
|
||||
|
||||
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,
|
||||
)
|
||||
@@ -0,0 +1,29 @@
|
||||
package gq.kirmanak.mealie.data.recipes.db
|
||||
|
||||
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 = RecipeEntity::class,
|
||||
parentColumns = ["local_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
|
||||
)
|
||||
@@ -0,0 +1,43 @@
|
||||
package gq.kirmanak.mealie.data.recipes.db
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
|
||||
@Dao
|
||||
interface RecipeDao {
|
||||
@Query("SELECT * FROM tags")
|
||||
suspend fun queryAllTags(): List<TagEntity>
|
||||
|
||||
@Query("SELECT * FROM categories")
|
||||
suspend fun queryAllCategories(): List<CategoryEntity>
|
||||
|
||||
@Query("SELECT * FROM recipes")
|
||||
fun queryRecipesByPages(): PagingSource<Int, RecipeEntity>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertRecipe(recipeEntity: RecipeEntity): Long
|
||||
|
||||
@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 recipes")
|
||||
suspend fun removeAllRecipes()
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package gq.kirmanak.mealie.data.recipes.db
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import kotlinx.datetime.Instant
|
||||
|
||||
@Entity(tableName = "recipes", indices = [Index(value = ["remote_id"], unique = true)])
|
||||
data class RecipeEntity(
|
||||
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "local_id") val localId: Long = 0,
|
||||
@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: Instant,
|
||||
@ColumnInfo(name = "date_updated") val dateUpdated: Instant
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
package gq.kirmanak.mealie.data.recipes.db
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import gq.kirmanak.mealie.data.recipes.network.GetRecipeSummaryResponse
|
||||
|
||||
interface RecipeStorage {
|
||||
suspend fun saveRecipes(recipes: List<GetRecipeSummaryResponse>)
|
||||
|
||||
fun queryRecipes(): PagingSource<Int, RecipeEntity>
|
||||
|
||||
suspend fun refreshAll(recipes: List<GetRecipeSummaryResponse>)
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package gq.kirmanak.mealie.data.recipes.db
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.room.withTransaction
|
||||
import gq.kirmanak.mealie.data.MealieDb
|
||||
import gq.kirmanak.mealie.data.recipes.network.GetRecipeSummaryResponse
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class RecipeStorageImpl @Inject constructor(
|
||||
private val db: MealieDb
|
||||
) : 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 recipeId = recipeDao.insertRecipe(recipe.recipeEntity())
|
||||
|
||||
for (tag in recipe.tags) {
|
||||
val tagId = getIdOrInsert(tagEntities, tag)
|
||||
tagRecipeEntities += TagRecipeEntity(tagId, recipeId)
|
||||
}
|
||||
|
||||
for (category in recipe.recipeCategories) {
|
||||
val categoryId = getOrInsert(categoryEntities, category)
|
||||
categoryRecipeEntities += CategoryRecipeEntity(categoryId, recipeId)
|
||||
}
|
||||
}
|
||||
|
||||
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() = RecipeEntity(
|
||||
remoteId = remoteId,
|
||||
name = name,
|
||||
slug = slug,
|
||||
image = image,
|
||||
description = description,
|
||||
rating = rating,
|
||||
dateAdded = dateAdded,
|
||||
dateUpdated = dateUpdated,
|
||||
)
|
||||
|
||||
override fun queryRecipes(): PagingSource<Int, RecipeEntity> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package gq.kirmanak.mealie.data.recipes.db
|
||||
|
||||
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
|
||||
)
|
||||
@@ -0,0 +1,27 @@
|
||||
package gq.kirmanak.mealie.data.recipes.db
|
||||
|
||||
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 = RecipeEntity::class,
|
||||
parentColumns = ["local_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
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
package gq.kirmanak.mealie.data.recipes.network
|
||||
|
||||
import kotlinx.datetime.Instant
|
||||
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: Instant,
|
||||
@SerialName("dateUpdated") val dateUpdated: Instant
|
||||
)
|
||||
@@ -0,0 +1,5 @@
|
||||
package gq.kirmanak.mealie.data.recipes.network
|
||||
|
||||
interface RecipeDataSource {
|
||||
suspend fun requestRecipes(start: Int = 0, end: Int = 9999): List<GetRecipeSummaryResponse>
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package gq.kirmanak.mealie.data.recipes.network
|
||||
|
||||
import gq.kirmanak.mealie.data.RetrofitBuilder
|
||||
import gq.kirmanak.mealie.data.auth.AuthRepo
|
||||
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, end: Int): List<GetRecipeSummaryResponse> {
|
||||
Timber.v("requestRecipes() called")
|
||||
val service: RecipeService = getRecipeService()
|
||||
return service.getRecipeSummary(start, end)
|
||||
}
|
||||
|
||||
private suspend fun getRecipeService(): RecipeService {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package gq.kirmanak.mealie.data.recipes.network
|
||||
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface RecipeService {
|
||||
@GET("/api/recipes/summary")
|
||||
suspend fun getRecipeSummary(
|
||||
@Query("start") start: Int,
|
||||
@Query("end") end: Int
|
||||
): List<GetRecipeSummaryResponse>
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import gq.kirmanak.mealie.databinding.FragmentAuthenticationBinding
|
||||
@@ -46,6 +47,7 @@ class AuthenticationFragment : Fragment() {
|
||||
private fun checkIfAuthenticatedAlready() {
|
||||
Timber.v("checkIfAuthenticatedAlready() called")
|
||||
lifecycleScope.launchWhenCreated {
|
||||
if (viewModel.isAuthenticated()) navigateToRecipes()
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
if (viewModel.isAuthenticated()) "User is authenticated"
|
||||
@@ -55,6 +57,10 @@ class AuthenticationFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToRecipes() {
|
||||
findNavController().navigate(AuthenticationFragmentDirections.actionAuthenticationFragmentToRecipesFragment())
|
||||
}
|
||||
|
||||
private fun onLoginClicked() {
|
||||
Timber.v("onLoginClicked() called")
|
||||
val email: String
|
||||
@@ -73,6 +79,7 @@ class AuthenticationFragment : Fragment() {
|
||||
}
|
||||
lifecycleScope.launchWhenResumed {
|
||||
val exception = viewModel.authenticate(email, pass, url)
|
||||
if (exception == null) navigateToRecipes()
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
"Exception is ${exception?.message ?: "null"}",
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package gq.kirmanak.mealie.ui.recipes
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.cachedIn
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import gq.kirmanak.mealie.data.recipes.RecipeRepo
|
||||
import gq.kirmanak.mealie.data.recipes.db.RecipeEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class RecipeViewModel @Inject constructor(private val recipeRepo: RecipeRepo) : ViewModel() {
|
||||
private val pager: Pager<Int, RecipeEntity> by lazy { recipeRepo.createPager() }
|
||||
val recipeFlow: Flow<PagingData<RecipeEntity>> by lazy { pager.flow.cachedIn(viewModelScope) }
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package gq.kirmanak.mealie.ui.recipes
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import gq.kirmanak.mealie.databinding.FragmentRecipesBinding
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
|
||||
@AndroidEntryPoint
|
||||
class RecipesFragment : Fragment() {
|
||||
private var _binding: FragmentRecipesBinding? = null
|
||||
private val binding: FragmentRecipesBinding
|
||||
get() = checkNotNull(_binding) { "Binding requested when fragment is off screen" }
|
||||
private val viewModel by viewModels<RecipeViewModel>()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = FragmentRecipesBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.recipes.layoutManager = LinearLayoutManager(requireContext())
|
||||
val recipesPagingAdapter = RecipesPagingAdapter()
|
||||
binding.recipes.adapter = recipesPagingAdapter
|
||||
lifecycleScope.launchWhenResumed {
|
||||
viewModel.recipeFlow.collectLatest {
|
||||
recipesPagingAdapter.submitData(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package gq.kirmanak.mealie.ui.recipes
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import gq.kirmanak.mealie.data.recipes.db.RecipeEntity
|
||||
import gq.kirmanak.mealie.databinding.ViewHolderRecipeBinding
|
||||
import timber.log.Timber
|
||||
|
||||
class RecipesPagingAdapter : PagingDataAdapter<RecipeEntity, RecipeViewHolder>(RecipeDiffCallback) {
|
||||
override fun onBindViewHolder(holder: RecipeViewHolder, position: Int) {
|
||||
val item = getItem(position)
|
||||
holder.bind(item)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecipeViewHolder {
|
||||
Timber.v("onCreateViewHolder() called with: parent = $parent, viewType = $viewType")
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val binding = ViewHolderRecipeBinding.inflate(inflater, parent, false)
|
||||
return RecipeViewHolder(binding)
|
||||
}
|
||||
}
|
||||
|
||||
class RecipeViewHolder(private val binding: ViewHolderRecipeBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(item: RecipeEntity?) {
|
||||
binding.name.text = item?.name
|
||||
}
|
||||
}
|
||||
|
||||
private object RecipeDiffCallback : DiffUtil.ItemCallback<RecipeEntity>() {
|
||||
override fun areItemsTheSame(oldItem: RecipeEntity, newItem: RecipeEntity): Boolean {
|
||||
return oldItem.localId == newItem.localId
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: RecipeEntity, newItem: RecipeEntity): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
17
app/src/main/res/layout/fragment_recipes.xml
Normal file
17
app/src/main/res/layout/fragment_recipes.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="match_parent"
|
||||
tools:context=".ui.recipes.RecipesFragment">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recipes"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
25
app/src/main/res/layout/view_holder_recipe.xml
Normal file
25
app/src/main/res/layout/view_holder_recipe.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?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/name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/image" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image"
|
||||
android:layout_width="340dp"
|
||||
android:layout_height="224dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:srcCompat="@tools:sample/backgrounds/scenic" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -1,11 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<navigation 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:id="@+id/nav_graph"
|
||||
app:startDestination="@id/authenticationFragment">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/authenticationFragment"
|
||||
android:name="gq.kirmanak.mealie.ui.auth.AuthenticationFragment"
|
||||
android:label="AuthenticationFragment" />
|
||||
android:label="AuthenticationFragment" >
|
||||
<action
|
||||
android:id="@+id/action_authenticationFragment_to_recipesFragment"
|
||||
app:destination="@id/recipesFragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/recipesFragment"
|
||||
android:name="gq.kirmanak.mealie.ui.recipes.RecipesFragment"
|
||||
android:label="fragment_recipes"
|
||||
tools:layout="@layout/fragment_recipes" />
|
||||
</navigation>
|
||||
Reference in New Issue
Block a user