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
10 changes: 10 additions & 0 deletions app/src/main/java/otus/gpb/recyclerview/Chat.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package otus.gpb.recyclerview

data class Chat(
val id: Int,
val title: String,
val author: String,
val message: String,
val time: String,
val icon: Int
)
88 changes: 88 additions & 0 deletions app/src/main/java/otus/gpb/recyclerview/DataSource.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package otus.gpb.recyclerview

class DataSource {
private val messagesList = listOf(
Chat(
id = 1,
title = "Title1",
author = "User1",
message = "Message1",
time = "12:00",
icon = R.drawable.cat
),
Chat(
id = 2,
title = "Title2",
author = "User2",
message = "Message2",
time = "13:05",
icon = R.drawable.dog
),
Chat(
id = 3,
title = "Title3",
author = "User3",
message = "Message3",
time = "9:13",
icon = R.drawable.fox
),
Chat(
id = 4,
title = "Title4",
author = "User4",
message = "Message4",
time = "4:17",
icon = R.drawable.giraffe
),
Chat(
id = 5,
title = "Title5",
author = "User5",
message = "Message5",
time = "10:00",
icon = R.drawable.koala
),
Chat(
id = 6,
title = "Title6",
author = "User6",
message = "Message6",
time = "10:35",
icon = R.drawable.lion
),
Chat(
id = 7,
title = "Title7",
author = "User7",
message = "Message7",
time = "15:16",
icon = R.drawable.owl
),
Chat(
id = 8,
title = "Title8",
author = "User8",
message = "Message8",
time = "16:22",
icon = R.drawable.panda
),
Chat(
id = 9,
title = "Title9",
author = "User9",
message = "Message9",
time = "21:18",
icon = R.drawable.rabbit
),
Chat(
id = 10,
title = "Title10",
author = "User10",
message = "Message10",
time = "22:45",
icon = R.drawable.wolf
)
)

fun getMessages(): List<Chat> = messagesList
}
31 changes: 31 additions & 0 deletions app/src/main/java/otus/gpb/recyclerview/ItemTouchHelperCallback.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package otus.gpb.recyclerview

import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView

class ItemTouchHelperCallback(private val listener: MessagesAdapterListener) : ItemTouchHelper.Callback() {
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
return makeMovementFlags(
0,
ItemTouchHelper.LEFT
)
}

override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false
}

override fun onSwiped(
viewHolder: RecyclerView.ViewHolder,
direction: Int
) {
listener.onItemSwipe(viewHolder.adapterPosition)
}
}
49 changes: 48 additions & 1 deletion app/src/main/java/otus/gpb/recyclerview/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,58 @@ package otus.gpb.recyclerview

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView

class MainActivity : AppCompatActivity() {
class MainActivity : AppCompatActivity(), MessagesAdapterListener {

private lateinit var recycleView: RecyclerView
private lateinit var layoutManager: LinearLayoutManager

private val messagesAdapter by lazy { MessagesAdapter() }
private val messagesListDataSource = DataSource().getMessages()

private val messagesList = mutableListOf<Chat>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

recycleView = findViewById<RecyclerView>(R.id.recyclerView)
layoutManager = LinearLayoutManager(this)
recycleView.layoutManager = layoutManager
recycleView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
ItemTouchHelper(ItemTouchHelperCallback(this)).attachToRecyclerView(recycleView)

messagesList.addAll(messagesListDataSource)

updateRecyclerView(messagesList)

val scrollListener = ScrollListener(layoutManager) {
loadMoreMessages()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onLoadFinished() вызывается сразу после создания listener, хотя загрузка еще не началась. Логичнее вызывать его после завершения загрузки данных в loadMoreMessages(), чтобы правильно управлять флагом isLoading

}
recycleView.addOnScrollListener(scrollListener)
scrollListener.onLoadFinished()
}

override fun onItemSwipe(position: Int) {
val newList = messagesList.toMutableList()
newList.removeAt(position)
messagesList.clear()
messagesList.addAll(newList)

updateRecyclerView(messagesList)
}

private fun updateRecyclerView(messagesList: List<Chat>) {
messagesAdapter.submitList(messagesList)
recycleView.adapter = messagesAdapter

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Адаптер нужно устанавливать один раз в onCreate(), а не при каждом обновлении списка. submitList() уже обновляет данные в адаптере, повторное присваивание не требуется

}

private fun loadMoreMessages() {
messagesList.addAll(messagesListDataSource)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Каждый раз добавляются все элементы из messagesListDataSource, что приводит к дублированию при повторных вызовах

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Сейчас в onCreate загружаются все 10 элементов сразу, а при пагинации добавляются те же элементы снова, что приводит к дублированию. Для правильной пагинации нужно загружать данные порциями: сначала загрузить часть данных (например, первые 5 элементов), а при достижении конца списка загружать следующие порции. Так каждый раз будут добавляться новые элементы, которые еще не были загружены.

updateRecyclerView(messagesList)
}
}
26 changes: 26 additions & 0 deletions app/src/main/java/otus/gpb/recyclerview/MessageViewHolder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package otus.gpb.recyclerview

import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.res.ResourcesCompat
import androidx.recyclerview.widget.RecyclerView

class MessageViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
private val title: TextView by lazy { view.findViewById(R.id.title) }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lazy здесь не нужен. View уже создан в конструкторе, поэтому findViewById можно вызвать сразу. lazy добавляет лишние накладные расходы и вызывается при каждом обращении

private val author: TextView by lazy { view.findViewById(R.id.author) }
private val message: TextView by lazy { view.findViewById(R.id.message) }
private val time: TextView by lazy { view.findViewById(R.id.time) }
private val icon: ImageView by lazy { view.findViewById(R.id.icon) }

fun bind(chat: Chat) {
val drawableImage = ResourcesCompat
.getDrawable(view.resources, chat.icon, null)
icon.setImageDrawable(drawableImage)

title.text = chat.title
author.text = chat.author
message.text = chat.message
time.text = chat.time
}
}
25 changes: 25 additions & 0 deletions app/src/main/java/otus/gpb/recyclerview/MessagesAdapter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package otus.gpb.recyclerview

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.ListAdapter

class MessagesAdapter() :
ListAdapter<Chat, MessageViewHolder>(MessagesDiffCallback()) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): MessageViewHolder {
val view = LayoutInflater
.from(parent.context)
.inflate(R.layout.chat_item, parent, false)
return MessageViewHolder(view)
}

override fun onBindViewHolder(
holder: MessageViewHolder,
position: Int
) {
holder.bind(getItem(position))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package otus.gpb.recyclerview

interface MessagesAdapterListener {
fun onItemSwipe(position: Int)
}
19 changes: 19 additions & 0 deletions app/src/main/java/otus/gpb/recyclerview/MessagesDiffCallback.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package otus.gpb.recyclerview

import androidx.recyclerview.widget.DiffUtil

class MessagesDiffCallback: DiffUtil.ItemCallback<Chat>() {
override fun areItemsTheSame(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Нужно сравнивать по уникальному идентификатору (id), чтобы DiffUtil понимал, что это один и тот же элемент. А в areContentsTheSame сравнивайте все поля, чтобы понять, изменилось ли содержимое элемента.

Сейчас логика перепутана местами:
если два элемента с разными id имеют одинаковые поля, areItemsTheSame вернет false, и DiffUtil решит, что это разные элементы
если у элемента изменилось содержимое, но id тот же, areContentsTheSame вернет true, и DiffUtil не обновит ViewHolder

oldItem: Chat,
newItem: Chat
): Boolean {
return oldItem == newItem
}

override fun areContentsTheSame(
oldItem: Chat,
newItem: Chat
): Boolean {
return oldItem.id == newItem.id
}
}
30 changes: 30 additions & 0 deletions app/src/main/java/otus/gpb/recyclerview/ScrollListener.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package otus.gpb.recyclerview

import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView

class ScrollListener(
private val layoutManager: LinearLayoutManager,
private val loadMore: () -> Unit
) : RecyclerView.OnScrollListener() {

private var isLoading = false

override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (dy <= 0) return

val totalItemCount = layoutManager.itemCount
val lastVisibleItem = layoutManager.findLastVisibleItemPosition()

val isAtEnd = lastVisibleItem >= totalItemCount - 3

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Может срабатывать слишком рано. Обычно пагинацию запускают при достижении последнего видимого элемента, поэтому лучше использовать >= totalItemCount - 1


if (!isLoading && isAtEnd) {
isLoading = true
loadMore()
}
}

fun onLoadFinished() {
isLoading = false
}
}
Binary file added app/src/main/res/drawable/cat.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/src/main/res/drawable/dog.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/src/main/res/drawable/fox.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/src/main/res/drawable/giraffe.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/src/main/res/drawable/koala.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/src/main/res/drawable/lion.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/src/main/res/drawable/owl.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/src/main/res/drawable/panda.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/src/main/res/drawable/rabbit.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/src/main/res/drawable/wolf.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".MainActivity">

<androidx.recyclerview.widget.RecyclerView
Expand Down
55 changes: 55 additions & 0 deletions app/src/main/res/layout/chat_item.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp">

<androidx.cardview.widget.CardView
android:id="@+id/card"
android:layout_width="100dp"
android:layout_height="100dp"
app:cardCornerRadius="50dp">

<ImageView
android:id="@+id/icon"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.cardview.widget.CardView>

<TextView
android:id="@+id/title"
android:layout_width="200dp"
android:layout_height="35dp"
android:layout_marginStart="20dp"
app:layout_constraintStart_toEndOf="@id/card"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/author"
android:layout_width="200dp"
android:layout_height="35dp"
android:layout_marginStart="20dp"
app:layout_constraintStart_toEndOf="@id/card"
app:layout_constraintTop_toBottomOf="@id/title" />

<TextView
android:id="@+id/message"
android:layout_width="200dp"
android:layout_height="35dp"
android:layout_marginStart="20dp"
app:layout_constraintStart_toEndOf="@id/card"
app:layout_constraintTop_toBottomOf="@id/author" />

<TextView
android:id="@+id/time"
android:layout_width="40dp"
android:layout_height="35dp"
android:layout_marginStart="20dp"
app:layout_constraintStart_toEndOf="@id/title"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
3 changes: 3 additions & 0 deletions app/src/main/res/values/colors.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="neutral_blue">#517DA2</color>
<color name="dark_blue">#416482</color>
<color name="light_blue">#66A9E0</color>
</resources>
3 changes: 2 additions & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<resources>
<string name="app_name">RecyclerView</string>
<string name="app_name">Telegram</string>
<!-- TODO: Remove or change this placeholder text -->
</resources>
Loading