Initialize RecipesFragment

This commit is contained in:
Kirill Kamakin
2021-11-07 20:56:58 +03:00
parent 39bac99e16
commit 470d5a4aef
32 changed files with 703 additions and 10 deletions

View 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
}

View 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()
}
}
}

View File

@@ -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

View File

@@ -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()
}

View File

@@ -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)
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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>
}

View File

@@ -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()
}
}
}

View File

@@ -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)
}
}

View File

@@ -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,
)

View File

@@ -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
)

View File

@@ -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()
}

View File

@@ -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
)

View File

@@ -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>)
}

View File

@@ -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)
}
}
}

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -0,0 +1,5 @@
package gq.kirmanak.mealie.data.recipes.network
interface RecipeDataSource {
suspend fun requestRecipes(start: Int = 0, end: Int = 9999): List<GetRecipeSummaryResponse>
}

View File

@@ -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
}
}

View File

@@ -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>
}

View File

@@ -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"}",

View File

@@ -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) }
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View 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>

View 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>

View File

@@ -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>