From dc0842e87c4319a7f26d66b38f2a2d7e6b8c656d Mon Sep 17 00:00:00 2001 From: Adel Khaziakhmetov Date: Mon, 2 Jun 2025 15:09:38 +0300 Subject: [PATCH 1/2] added recycle view for chat --- app/build.gradle | 7 ++ .../java/otus/gpb/recyclerview/ChatAdapter.kt | 58 ++++++++++++ .../otus/gpb/recyclerview/ChatFragment.kt | 88 +++++++++++++++++++ .../java/otus/gpb/recyclerview/ChatItem.kt | 25 ++++++ .../java/otus/gpb/recyclerview/ChatMessage.kt | 24 +++++ .../otus/gpb/recyclerview/ChatViewHolder.kt | 32 +++++++ .../otus/gpb/recyclerview/ChatViewModel.kt | 44 ++++++++++ .../java/otus/gpb/recyclerview/Listener.kt | 5 ++ .../otus/gpb/recyclerview/LoadViewHolder.kt | 10 +++ .../otus/gpb/recyclerview/MainActivity.kt | 4 + .../otus/gpb/recyclerview/WithLayoutId.kt | 8 ++ app/src/main/res/drawable/divider.xml | 6 ++ app/src/main/res/drawable/rounded_corners.xml | 5 ++ app/src/main/res/layout/activity_main.xml | 10 +-- app/src/main/res/layout/chat_item.xml | 66 ++++++++++++++ app/src/main/res/layout/fragment_chat.xml | 15 ++++ app/src/main/res/layout/load_item.xml | 30 +++++++ app/src/main/res/values/strings.xml | 3 +- 18 files changed, 432 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/otus/gpb/recyclerview/ChatAdapter.kt create mode 100644 app/src/main/java/otus/gpb/recyclerview/ChatFragment.kt create mode 100644 app/src/main/java/otus/gpb/recyclerview/ChatItem.kt create mode 100644 app/src/main/java/otus/gpb/recyclerview/ChatMessage.kt create mode 100644 app/src/main/java/otus/gpb/recyclerview/ChatViewHolder.kt create mode 100644 app/src/main/java/otus/gpb/recyclerview/ChatViewModel.kt create mode 100644 app/src/main/java/otus/gpb/recyclerview/Listener.kt create mode 100644 app/src/main/java/otus/gpb/recyclerview/LoadViewHolder.kt create mode 100644 app/src/main/java/otus/gpb/recyclerview/WithLayoutId.kt create mode 100644 app/src/main/res/drawable/divider.xml create mode 100644 app/src/main/res/drawable/rounded_corners.xml create mode 100644 app/src/main/res/layout/chat_item.xml create mode 100644 app/src/main/res/layout/fragment_chat.xml create mode 100644 app/src/main/res/layout/load_item.xml diff --git a/app/build.gradle b/app/build.gradle index 54e4eac..2e46370 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -30,6 +30,9 @@ android { kotlinOptions { jvmTarget = '1.8' } + buildFeatures { + viewBinding = true + } } dependencies { @@ -38,6 +41,10 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.5.1' implementation 'com.google.android.material:material:1.7.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.7' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7' + implementation 'androidx.fragment:fragment-ktx:1.8.6' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/app/src/main/java/otus/gpb/recyclerview/ChatAdapter.kt b/app/src/main/java/otus/gpb/recyclerview/ChatAdapter.kt new file mode 100644 index 0000000..2215f3c --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/ChatAdapter.kt @@ -0,0 +1,58 @@ +package otus.gpb.recyclerview + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView + +class ChatAdapter(private val listener: Listener): RecyclerView.Adapter() { + private var list = listOf() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return when (viewType) { + ViewTypes.CHAT.id -> { + val view = LayoutInflater.from(parent.context).inflate(R.layout.chat_item, parent, false) + + ChatViewHolder(view, listener) + } + ViewTypes.LOAD.id -> { + val view = LayoutInflater.from(parent.context).inflate(R.layout.load_item, parent, false) + + LoadViewHolder(view) + } + else -> throw IllegalArgumentException("Not found view type for chat adapter") + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val item = list.getOrNull(position) ?: return + + when (item) { + is ChatListItem.ChatItem -> { + (holder as? ChatViewHolder)?.bind(item) + } + is ChatListItem.LoadItem -> { + return + } + } + } + + override fun getItemViewType(position: Int): Int { + return when (list[position]) { + is ChatListItem.ChatItem -> ViewTypes.CHAT.id + is ChatListItem.LoadItem -> ViewTypes.LOAD.id + } + } + + override fun getItemCount(): Int = list.size + + fun setItems(items: List) { + list = items + notifyDataSetChanged() + } + + enum class ViewTypes(val id: Int) { + CHAT(R.layout.chat_item), + LOAD(R.layout.load_item) + } +} \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/ChatFragment.kt b/app/src/main/java/otus/gpb/recyclerview/ChatFragment.kt new file mode 100644 index 0000000..adf2a60 --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/ChatFragment.kt @@ -0,0 +1,88 @@ +package otus.gpb.recyclerview + +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import androidx.fragment.app.viewModels +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView + +class ChatFragment : Fragment() { + companion object { + fun newInstance() = ChatFragment() + } + + private lateinit var recyclerView: RecyclerView + private val viewModel: ChatViewModel by viewModels() + + private val itemTouchHelperCallback = object : ItemTouchHelper.SimpleCallback( + 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) { + viewModel.removeItem(viewHolder.adapterPosition) + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return inflater.inflate(R.layout.fragment_chat, container, false) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupRecyclerView(view) + } + + private fun setupRecyclerView(view: View) { + recyclerView = view.findViewById(R.id.recyclerView) + + val dividerItemDecoration = DividerItemDecoration(context, LinearLayoutManager.VERTICAL) + recyclerView.addItemDecoration(dividerItemDecoration) + recyclerView.adapter = viewModel.chatAdapter + + val itemTouchHelper = ItemTouchHelper(itemTouchHelperCallback) + itemTouchHelper.attachToRecyclerView(recyclerView) + + recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + + val layoutManager = recyclerView.layoutManager as LinearLayoutManager + val visibleItemCount = layoutManager.childCount + val totalItemCount = layoutManager.itemCount + val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() + + if ((visibleItemCount + firstVisibleItemPosition) >= totalItemCount + && firstVisibleItemPosition >= 0) { + viewModel.loadNewData() + } + } + }) + + viewModel.setupData() + } +} \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/ChatItem.kt b/app/src/main/java/otus/gpb/recyclerview/ChatItem.kt new file mode 100644 index 0000000..1ea7bc3 --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/ChatItem.kt @@ -0,0 +1,25 @@ +package otus.gpb.recyclerview + +import androidx.annotation.LayoutRes +import java.time.LocalDate + +sealed class ChatListItem: WithLayoutId { + data class ChatItem(private val chatMessage: ChatMessage) : ChatListItem(), WithLayoutId by ChatItem { + companion object : WithLayoutId { + @get:LayoutRes + override val layoutId: Int = R.layout.chat_item + } + + val user: String get() = chatMessage.user + val message: String get() = chatMessage.message + val date: LocalDate get() = chatMessage.date + } + + data class LoadItem(private val id: String = "1") : ChatListItem(), WithLayoutId by LoadItem { + companion object : WithLayoutId { + @get:LayoutRes + override val layoutId: Int = R.layout.load_item + } + } +} + diff --git a/app/src/main/java/otus/gpb/recyclerview/ChatMessage.kt b/app/src/main/java/otus/gpb/recyclerview/ChatMessage.kt new file mode 100644 index 0000000..6611a96 --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/ChatMessage.kt @@ -0,0 +1,24 @@ +package otus.gpb.recyclerview + +import java.sql.Date +import java.time.LocalDate + +data class ChatMessage( + val id: Int, + val user: String, + val message: String, + val date: LocalDate +) + +val chatMessages = listOf( + ChatListItem.ChatItem(ChatMessage(0, "Dave", message = "Hello everyone!", date = LocalDate.now())), + ChatListItem.ChatItem(ChatMessage(1, "Ana", message = "I want pizza for lunch", date = LocalDate.now().plusDays(1))), + ChatListItem.ChatItem(ChatMessage(2, "Micky", message = "TDS helps you create your own concepts and learn new things", date = LocalDate.now().plusDays(2))), + ChatListItem.ChatItem(ChatMessage(3, "Sarah", message = "Has anyone finished the project yet?", date = LocalDate.now().minusDays(1))), + ChatListItem.ChatItem(ChatMessage(4, "John", message = "Meeting at 3 PM tomorrow", date = LocalDate.now().plusDays(3))), + ChatListItem.ChatItem(ChatMessage(5, "Emma", message = "Don't forget to submit your reports", date = LocalDate.now().plusDays(1))), + ChatListItem.ChatItem(ChatMessage(6, "Mike", message = "The new update is amazing!", date = LocalDate.now().minusDays(2))), + ChatListItem.ChatItem(ChatMessage(7, "Lisa", message = "Can someone help me with Kotlin?", date = LocalDate.now())), + ChatListItem.ChatItem(ChatMessage(8, "Alex", message = "Let's schedule a team building event", date = LocalDate.now().plusDays(5))), + ChatListItem.ChatItem(ChatMessage(9, "Tina", message = "Happy Friday everyone!", date = LocalDate.now().plusDays(4))) +) \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/ChatViewHolder.kt b/app/src/main/java/otus/gpb/recyclerview/ChatViewHolder.kt new file mode 100644 index 0000000..6b78d74 --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/ChatViewHolder.kt @@ -0,0 +1,32 @@ +package otus.gpb.recyclerview + +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import java.util.Locale + +class ChatViewHolder(private val view: View, private val listener: Listener) : RecyclerView.ViewHolder(view) { + private val nameTV: TextView by lazy { view.findViewById(R.id.nameTV) } + private val userTV: TextView by lazy { view.findViewById(R.id.userTV) } + private val messageTV: TextView by lazy { view.findViewById(R.id.messageTV) } + private val dateTV: TextView by lazy { view.findViewById(R.id.dateTV) } + + fun bind(item: ChatListItem.ChatItem) { + nameTV.text = item.user.firstOrNull().toString() + userTV.text = item.user + messageTV.text = item.message + dateTV.text = formatDate(item.date) + + view.setOnClickListener { + listener.onItemClicked(layoutPosition) + } + } + + private fun formatDate(date: LocalDate): String { + val formatter = DateTimeFormatter.ofPattern("EEE d", Locale.getDefault()) + return date.format(formatter) + } +} \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/ChatViewModel.kt b/app/src/main/java/otus/gpb/recyclerview/ChatViewModel.kt new file mode 100644 index 0000000..b01124e --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/ChatViewModel.kt @@ -0,0 +1,44 @@ +package otus.gpb.recyclerview + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +class ChatViewModel : ViewModel(), Listener { + val chatAdapter = ChatAdapter(this) + + private var items: MutableList = chatMessages.toMutableList() + private var isDataLoading: Boolean = false + + fun setupData() { + chatAdapter.setItems(items) + } + + fun removeItem(id: Int) { + items.removeAt(id) + setupData() + } + + fun loadNewData() { + if (isDataLoading) { return } + + isDataLoading = true + + viewModelScope.launch { + items.add(ChatListItem.LoadItem()) + setupData() + + delay(3000L) + + items.removeLast() + items.addAll(chatMessages) + setupData() + isDataLoading = false + } + } + + override fun onItemClicked(id: Int) { + removeItem(id) + } +} \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/Listener.kt b/app/src/main/java/otus/gpb/recyclerview/Listener.kt new file mode 100644 index 0000000..039d52d --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/Listener.kt @@ -0,0 +1,5 @@ +package otus.gpb.recyclerview + +interface Listener { + fun onItemClicked(id: Int) +} \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/LoadViewHolder.kt b/app/src/main/java/otus/gpb/recyclerview/LoadViewHolder.kt new file mode 100644 index 0000000..d535b4c --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/LoadViewHolder.kt @@ -0,0 +1,10 @@ +package otus.gpb.recyclerview + +import android.view.View +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import java.util.Locale + +class LoadViewHolder(private val view: View) : RecyclerView.ViewHolder(view) { } \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt b/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt index e2cdca7..bf27e21 100644 --- a/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt +++ b/app/src/main/java/otus/gpb/recyclerview/MainActivity.kt @@ -8,5 +8,9 @@ class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + + supportFragmentManager.beginTransaction() + .replace(R.id.container, ChatFragment.newInstance()) + .commit() } } \ No newline at end of file diff --git a/app/src/main/java/otus/gpb/recyclerview/WithLayoutId.kt b/app/src/main/java/otus/gpb/recyclerview/WithLayoutId.kt new file mode 100644 index 0000000..7794157 --- /dev/null +++ b/app/src/main/java/otus/gpb/recyclerview/WithLayoutId.kt @@ -0,0 +1,8 @@ +package otus.gpb.recyclerview + +import androidx.annotation.LayoutRes + +interface WithLayoutId { + @get:LayoutRes + val layoutId: Int +} \ No newline at end of file diff --git a/app/src/main/res/drawable/divider.xml b/app/src/main/res/drawable/divider.xml new file mode 100644 index 0000000..44e6f26 --- /dev/null +++ b/app/src/main/res/drawable/divider.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/rounded_corners.xml b/app/src/main/res/drawable/rounded_corners.xml new file mode 100644 index 0000000..3375984 --- /dev/null +++ b/app/src/main/res/drawable/rounded_corners.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 2d026df..61ae785 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,13 +1,9 @@ - - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/chat_item.xml b/app/src/main/res/layout/chat_item.xml new file mode 100644 index 0000000..ec9892b --- /dev/null +++ b/app/src/main/res/layout/chat_item.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_chat.xml b/app/src/main/res/layout/fragment_chat.xml new file mode 100644 index 0000000..d0170af --- /dev/null +++ b/app/src/main/res/layout/fragment_chat.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/load_item.xml b/app/src/main/res/layout/load_item.xml new file mode 100644 index 0000000..2c37e66 --- /dev/null +++ b/app/src/main/res/layout/load_item.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3d78b1f..a6a71e1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,4 @@ - RecyclerView + Chat + Загрузка... \ No newline at end of file From 51f7a65dcd3543dbf2d86831c37d6f81f8cb3b1a Mon Sep 17 00:00:00 2001 From: Adel Khaziakhmetov Date: Wed, 18 Jun 2025 10:55:05 +0300 Subject: [PATCH 2/2] added recycle view for chat --- app/src/main/java/otus/gpb/recyclerview/ChatFragment.kt | 4 ---- app/src/main/java/otus/gpb/recyclerview/ChatViewModel.kt | 4 +--- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/app/src/main/java/otus/gpb/recyclerview/ChatFragment.kt b/app/src/main/java/otus/gpb/recyclerview/ChatFragment.kt index adf2a60..8e20b1c 100644 --- a/app/src/main/java/otus/gpb/recyclerview/ChatFragment.kt +++ b/app/src/main/java/otus/gpb/recyclerview/ChatFragment.kt @@ -48,10 +48,6 @@ class ChatFragment : Fragment() { return inflater.inflate(R.layout.fragment_chat, container, false) } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupRecyclerView(view) diff --git a/app/src/main/java/otus/gpb/recyclerview/ChatViewModel.kt b/app/src/main/java/otus/gpb/recyclerview/ChatViewModel.kt index b01124e..ba054f7 100644 --- a/app/src/main/java/otus/gpb/recyclerview/ChatViewModel.kt +++ b/app/src/main/java/otus/gpb/recyclerview/ChatViewModel.kt @@ -38,7 +38,5 @@ class ChatViewModel : ViewModel(), Listener { } } - override fun onItemClicked(id: Int) { - removeItem(id) - } + override fun onItemClicked(id: Int) { } } \ No newline at end of file