Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ dependencies {
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-compiler:$hilt_version"
implementation "androidx.paging:paging-runtime-ktx:$paging_version"
implementation "androidx.room:room-paging:$room_version"

testImplementation 'junit:junit:4.13.2'
implementation "androidx.arch.core:core-testing:$arch_version"
Expand Down
16 changes: 16 additions & 0 deletions app/src/main/java/ru/netology/nmedia/adapter/AdViewHolder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package ru.netology.nmedia.adapter

import androidx.recyclerview.widget.RecyclerView
import ru.netology.nmedia.BuildConfig
import ru.netology.nmedia.databinding.CardAdBinding
import ru.netology.nmedia.model.Ad
import ru.netology.nmedia.view.load

class AdViewHolder(
private val binding: CardAdBinding
) : RecyclerView.ViewHolder(binding.root) {

fun bind(ad: Ad) {
binding.image.load("${BuildConfig.BASE_URL}/media/${ad.image}")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package ru.netology.nmedia.adapter

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.paging.LoadState
import androidx.paging.LoadStateAdapter
import androidx.recyclerview.widget.RecyclerView
import ru.netology.nmedia.databinding.LoadStateBinding

class PagingLoadStateAdapter(
private val onInteractionListener: OnInteractionListener
) : LoadStateAdapter<PagingLoadStateAdapter.LoadStateViewHolder>() {

interface OnInteractionListener {
fun onRetry() {}
}

override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): LoadStateViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
return LoadStateViewHolder(
LoadStateBinding.inflate(layoutInflater, parent, false),
onInteractionListener
)
}

override fun onBindViewHolder(holder: LoadStateViewHolder, loadState: LoadState) {
holder.bind(loadState)
}

class LoadStateViewHolder(
private val binding: LoadStateBinding,
private val onInteractionListener: OnInteractionListener
) : RecyclerView.ViewHolder(binding.root) {
fun bind(loadState: LoadState) {
binding.apply {
progress.isVisible = loadState is LoadState.Loading
retry.isVisible = loadState is LoadState.Error

retry.setOnClickListener {
onInteractionListener.onRetry()
}
}
}
}

}
25 changes: 25 additions & 0 deletions app/src/main/java/ru/netology/nmedia/adapter/PostDiffCallback.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package ru.netology.nmedia.adapter

import androidx.recyclerview.widget.DiffUtil
import ru.netology.nmedia.model.FeedItem
import ru.netology.nmedia.model.Post

class PostDiffCallback : DiffUtil.ItemCallback<FeedItem>(){
override fun areItemsTheSame(oldItem: FeedItem, newItem: FeedItem): Boolean {
if (oldItem::class != newItem::class) {
return false
}
return oldItem.id == newItem.id
}

override fun areContentsTheSame(oldItem: FeedItem, newItem: FeedItem): Boolean {
return oldItem == newItem
}

override fun getChangePayload(oldItem: FeedItem, newItem: FeedItem): Any =
Payload(
likedByMe = (newItem as Post).likedByMe.takeIf { it != (oldItem as Post).likedByMe },
likes = newItem.likes.takeIf { it != (oldItem as Post).likes },
content = newItem.content.takeIf { it != (oldItem as Post).content }
)
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package ru.netology.nmedia.view
package ru.netology.nmedia.adapter

import android.animation.ObjectAnimator
import android.animation.PropertyValuesHolder
import android.view.View
import android.widget.PopupMenu
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import ru.netology.nmedia.R
import ru.netology.nmedia.databinding.CardPostBinding
import ru.netology.nmedia.model.Post
import ru.netology.nmedia.util.ViewUtils
import ru.netology.nmedia.view.OnInteractionListener
import ru.netology.nmedia.view.load
import ru.netology.nmedia.view.loadCircleCrop

class PostViewHolder(
private val binding: CardPostBinding,
Expand All @@ -21,7 +25,7 @@ class PostViewHolder(
fun bind(post: Post) {
binding.apply {
tvAuthor.text = post.author
tvPublished.text = post.published
tvPublished.text = post.published.toString()
tvContent.text = post.content
mbLike.isChecked = post.likedByMe
mbLike.setOnClickListener { onInteractionListener.onLike(post) }
Expand Down Expand Up @@ -54,22 +58,11 @@ class PostViewHolder(
}

val url = "$G_BASE_URL/avatars/${post.authorAvatar}"
Glide.with(ivAvatar)
.load(url)
.timeout(10_000)
.placeholder(R.drawable.ic_baseline_image_24)
.error(R.drawable.ic_baseline_cancel_24)
.circleCrop()
.into(ivAvatar)
ivAvatar.loadCircleCrop(url)

if (post.attachment != null) {
ivContent.visibility = View.VISIBLE
Glide.with(ivContent)
.load("$G_BASE_URL/media/${post.attachment.url}")
.timeout(10_000)
.placeholder(R.drawable.ic_baseline_image_24)
.error(R.drawable.ic_baseline_cancel_24)
.into(ivContent)
ivContent.load("$G_BASE_URL/media/${post.attachment.url}")
} else {
ivContent.visibility = View.GONE
}
Expand All @@ -82,12 +75,24 @@ class PostViewHolder(
}
}

fun bindOnLikeChanged(post: Post) {
with(binding.mbLike) {
isChecked = post.likedByMe
text = ViewUtils.formattedNumber(post.likes)
setOnClickListener { onInteractionListener.onLike(post) }
fun bind(payload: Payload) {
payload.likedByMe?.also {
binding.mbLike.isChecked = it
if (it) {
ObjectAnimator.ofPropertyValuesHolder(
binding.mbLike,
PropertyValuesHolder.ofFloat(View.SCALE_X, 1.0F, 1.2F, 1.0F),
PropertyValuesHolder.ofFloat(View.SCALE_Y, 1.0F, 1.2F, 1.0F)
)
} else {
ObjectAnimator.ofFloat(binding.mbLike, View.ROTATION, 0F, 360F)
}.start()
}
payload.likes?.also {
binding.mbLike.text = it.toString()
}
payload.content?.also {
binding.tvContent.text = it
}

}
}
103 changes: 103 additions & 0 deletions app/src/main/java/ru/netology/nmedia/adapter/PostsAdapter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package ru.netology.nmedia.adapter

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.RecyclerView
import ru.netology.nmedia.R
import ru.netology.nmedia.databinding.CardAdBinding
import ru.netology.nmedia.databinding.CardPostBinding
import ru.netology.nmedia.databinding.CardTimingSeparatorBinding
import ru.netology.nmedia.model.Ad
import ru.netology.nmedia.model.FeedItem
import ru.netology.nmedia.model.Post
import ru.netology.nmedia.model.TimingSeparator
import ru.netology.nmedia.view.OnInteractionListener

class PostsAdapter(
private val onInteractionListener: OnInteractionListener
) : PagingDataAdapter<FeedItem, RecyclerView.ViewHolder>(PostDiffCallback()) {

override fun getItemViewType(position: Int): Int =
when (getItem(position)) {
is Ad -> R.layout.card_ad
is Post -> R.layout.card_post
is TimingSeparator -> R.layout.card_timing_separator
null -> error("unknown item type")
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
when (viewType) {
R.layout.card_post -> {
val binding = CardPostBinding.inflate(LayoutInflater.from(parent.context), parent, false)
PostViewHolder(binding, onInteractionListener)
}
R.layout.card_ad -> {
val binding = CardAdBinding.inflate(LayoutInflater.from(parent.context), parent, false)
AdViewHolder(binding)
}
R.layout.card_timing_separator -> {
val binding = CardTimingSeparatorBinding.inflate(LayoutInflater.from(parent.context), parent, false)
TimingSeparatorViewHolder(binding)
}
else -> error("unknown view type: $viewType")
}


override fun onBindViewHolder(
holder: RecyclerView.ViewHolder,
position: Int
) {
when (val item = getItem(position)) {
is Ad -> (holder as? AdViewHolder)?.bind(item)
is Post -> (holder as? PostViewHolder)?.bind(item)
is TimingSeparator -> (holder as? TimingSeparatorViewHolder)?.bind(item)
null -> error("unknown item type")
}
}

override fun onBindViewHolder(
holder: RecyclerView.ViewHolder,
position: Int,
payloads: List<Any>
) {
if (payloads.isEmpty()) {
onBindViewHolder(holder, position)
} else {
payloads.forEach {
(it as? Payload)?.let { payloads ->
(holder as PostViewHolder).bind(payloads)
}
}
}
}
}

data class Payload(
val likedByMe: Boolean? = null,
val likes: Int? = null,
val content: String? = null,

)






















Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package ru.netology.nmedia.adapter

import androidx.recyclerview.widget.RecyclerView
import ru.netology.nmedia.R
import ru.netology.nmedia.databinding.CardTimingSeparatorBinding
import ru.netology.nmedia.model.TimingSeparator

class TimingSeparatorViewHolder(
private val binding: CardTimingSeparatorBinding
) : RecyclerView.ViewHolder(binding.root) {

fun bind(timingSeparator: TimingSeparator) {
binding.tvSeparator.setText(
when(timingSeparator.title) {
TimingSeparator.Period.TODAY -> R.string.today
TimingSeparator.Period.YESTERDAY -> R.string.yesterday
TimingSeparator.Period.LAST_WEEK -> R.string.lastWeek
}
)
}
}
2 changes: 1 addition & 1 deletion app/src/main/java/ru/netology/nmedia/api/ApiModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import javax.inject.Singleton
class ApiModule {

companion object {
private const val BASE_URL = "${BuildConfig.BASE_URL}/api/"
private const val BASE_URL = "${BuildConfig.BASE_URL}/api/slow/"
}

@Provides
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/java/ru/netology/nmedia/dao/PostDao.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package ru.netology.nmedia.dao

import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
Expand All @@ -13,6 +14,9 @@ interface PostDao {
@Query("SELECT * FROM PostEntity WHERE shown = 1 ORDER BY id DESC")
fun getAll(): Flow<List<PostEntity>>

@Query("SELECT * FROM PostEntity ORDER BY id DESC")
fun getPagingSource(): PagingSource<Int, PostEntity>

@Query("SELECT COUNT(*) FROM PostEntity WHERE shown = 0")
fun getInvisibleAmount(): Int

Expand Down Expand Up @@ -45,6 +49,10 @@ interface PostDao {
@Query("DELETE FROM PostEntity WHERE id = :id")
suspend fun removeById(id: Long)

@Query("DELETE FROM PostEntity")
suspend fun clear()


@Query("UPDATE PostEntity SET shown = 1 WHERE shown = 0")
suspend fun showAll()
}
27 changes: 27 additions & 0 deletions app/src/main/java/ru/netology/nmedia/dao/PostRemoteKeyDao.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package ru.netology.nmedia.dao

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import ru.netology.nmedia.entity.PostRemoteKeyEntity

@Dao
interface PostRemoteKeyDao {

@Query("SELECT max(key) FROM PostRemoteKeyEntity")
suspend fun max(): Long?

@Query("SELECT min(key) FROM PostRemoteKeyEntity")
suspend fun min(): Long?

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(postRemoteKeyEntity: PostRemoteKeyEntity)

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(postRemoteKeyEntity: List<PostRemoteKeyEntity>)

@Query("DELETE FROM PostRemoteKeyEntity")
suspend fun clear()

}
9 changes: 7 additions & 2 deletions app/src/main/java/ru/netology/nmedia/db/AppDb.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@ import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import ru.netology.nmedia.dao.PostDao
import ru.netology.nmedia.dao.PostRemoteKeyDao
import ru.netology.nmedia.entity.Converters
import ru.netology.nmedia.entity.PostEntity
import ru.netology.nmedia.entity.PostRemoteKeyEntity

@Database(entities = [PostEntity::class], version = 1, exportSchema = false)
@Database(entities = [PostEntity::class, PostRemoteKeyEntity::class],
version = 1,
exportSchema = false
)
@TypeConverters(Converters::class)
abstract class AppDb : RoomDatabase() {
abstract fun postDao(): PostDao

abstract fun postRemoteKeyDao(): PostRemoteKeyDao
}
Loading