-
Notifications
You must be signed in to change notification settings - Fork 63
ДЗ RecyclerView #2 #57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
878ec22
c90a990
fc45e09
18c3853
aca4be9
589b163
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| ) |
| 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 | ||
| } |
| 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) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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() | ||
| } | ||
| 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Адаптер нужно устанавливать один раз в onCreate(), а не при каждом обновлении списка. submitList() уже обновляет данные в адаптере, повторное присваивание не требуется |
||
| } | ||
|
|
||
| private fun loadMoreMessages() { | ||
| messagesList.addAll(messagesListDataSource) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Каждый раз добавляются все элементы из messagesListDataSource, что приводит к дублированию при повторных вызовах There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Сейчас в onCreate загружаются все 10 элементов сразу, а при пагинации добавляются те же элементы снова, что приводит к дублированию. Для правильной пагинации нужно загружать данные порциями: сначала загрузить часть данных (например, первые 5 элементов), а при достижении конца списка загружать следующие порции. Так каждый раз будут добавляться новые элементы, которые еще не были загружены. |
||
| updateRecyclerView(messagesList) | ||
| } | ||
| } | ||
| 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) } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| } | ||
| } | ||
| 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) | ||
| } |
| 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( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Нужно сравнивать по уникальному идентификатору (id), чтобы DiffUtil понимал, что это один и тот же элемент. А в areContentsTheSame сравнивайте все поля, чтобы понять, изменилось ли содержимое элемента. Сейчас логика перепутана местами: |
||
| oldItem: Chat, | ||
| newItem: Chat | ||
| ): Boolean { | ||
| return oldItem == newItem | ||
| } | ||
|
|
||
| override fun areContentsTheSame( | ||
| oldItem: Chat, | ||
| newItem: Chat | ||
| ): Boolean { | ||
| return oldItem.id == newItem.id | ||
| } | ||
| } | ||
| 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Может срабатывать слишком рано. Обычно пагинацию запускают при достижении последнего видимого элемента, поэтому лучше использовать >= totalItemCount - 1 |
||
|
|
||
| if (!isLoading && isAtEnd) { | ||
| isLoading = true | ||
| loadMore() | ||
| } | ||
| } | ||
|
|
||
| fun onLoadFinished() { | ||
| isLoading = false | ||
| } | ||
| } | ||
| 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> |
| 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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
onLoadFinished() вызывается сразу после создания listener, хотя загрузка еще не началась. Логичнее вызывать его после завершения загрузки данных в loadMoreMessages(), чтобы правильно управлять флагом isLoading