From 961f67c6ab557697c205ec26fc3bd4209af605b6 Mon Sep 17 00:00:00 2001 From: Idan Tepper Date: Sat, 14 Feb 2026 17:48:31 +0200 Subject: [PATCH] feat: added basic pagination --- .../travelmeet/app/ui/feed/FeedFragment.kt | 80 +++++++++++++++++-- .../app/ui/profile/MySpotsFragment.kt | 79 +++++++++++++++++- app/src/main/res/layout/fragment_feed.xml | 44 +++++++++- app/src/main/res/layout/fragment_my_spots.xml | 46 ++++++++++- app/src/main/res/values/strings.xml | 4 + 5 files changed, 242 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/travelmeet/app/ui/feed/FeedFragment.kt b/app/src/main/java/com/travelmeet/app/ui/feed/FeedFragment.kt index 29e6184..a1a228d 100644 --- a/app/src/main/java/com/travelmeet/app/ui/feed/FeedFragment.kt +++ b/app/src/main/java/com/travelmeet/app/ui/feed/FeedFragment.kt @@ -10,16 +10,25 @@ import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager import com.travelmeet.app.R +import com.travelmeet.app.data.local.entity.SpotEntity import com.travelmeet.app.databinding.FragmentFeedBinding import com.travelmeet.app.ui.viewmodel.SpotViewModel import com.travelmeet.app.util.Resource +import kotlin.math.min class FeedFragment : Fragment() { + companion object { + private const val PAGE_SIZE = 5 + } + private var _binding: FragmentFeedBinding? = null private val binding get() = _binding!! private val spotViewModel: SpotViewModel by activityViewModels() private lateinit var spotAdapter: SpotAdapter + private lateinit var feedLayoutManager: LinearLayoutManager + private var pagedSpots: List = emptyList() + private var currentPage = 0 override fun onCreateView( inflater: LayoutInflater, @@ -50,9 +59,28 @@ class FeedFragment : Fragment() { spotViewModel.toggleLike(spot.id) } ) + feedLayoutManager = LinearLayoutManager(requireContext()) binding.rvSpots.apply { adapter = spotAdapter - layoutManager = LinearLayoutManager(requireContext()) + layoutManager = feedLayoutManager + } + setupPaginationControls() + } + + private fun setupPaginationControls() { + binding.btnNextPage.setOnClickListener { + if (canGoNext()) { + currentPage++ + submitFeedPage() + binding.rvSpots.scrollToPosition(0) + } + } + binding.btnPrevPage.setOnClickListener { + if (currentPage > 0) { + currentPage-- + submitFeedPage() + binding.rvSpots.scrollToPosition(0) + } } } @@ -65,11 +93,18 @@ class FeedFragment : Fragment() { } private fun observeSpots() { - // Observe cached spots from Room spotViewModel.allSpots.observe(viewLifecycleOwner) { spots -> - spotAdapter.submitList(spots) - binding.emptyState.visibility = if (spots.isEmpty()) View.VISIBLE else View.GONE - binding.rvSpots.visibility = if (spots.isEmpty()) View.GONE else View.VISIBLE + pagedSpots = spots + currentPage = 0 + val isEmpty = spots.isEmpty() + binding.emptyState.visibility = if (isEmpty) View.VISIBLE else View.GONE + binding.rvSpots.visibility = if (isEmpty) View.GONE else View.VISIBLE + if (isEmpty) { + spotAdapter.submitList(emptyList()) + updatePaginationUi(0) + } else { + submitFeedPage() + } } // Observe sync state @@ -100,6 +135,41 @@ class FeedFragment : Fragment() { } } + private fun canGoNext(): Boolean { + val pages = totalPages() + return pages > 0 && currentPage < pages - 1 + } + + private fun submitFeedPage() { + val pages = totalPages() + if (pages == 0) { + spotAdapter.submitList(emptyList()) + updatePaginationUi(pages) + return + } + if (currentPage >= pages) currentPage = pages - 1 + val startIndex = currentPage * PAGE_SIZE + val endIndex = min(startIndex + PAGE_SIZE, pagedSpots.size) + val subList = pagedSpots.subList(startIndex, endIndex) + spotAdapter.submitList(ArrayList(subList)) + updatePaginationUi(pages) + } + + private fun totalPages(): Int = + if (pagedSpots.isEmpty()) 0 else ((pagedSpots.size - 1) / PAGE_SIZE) + 1 + + private fun updatePaginationUi(pages: Int) { + val showPagination = pagedSpots.size > PAGE_SIZE + binding.paginationContainer.visibility = if (showPagination) View.VISIBLE else View.GONE + binding.btnPrevPage.isEnabled = showPagination && currentPage > 0 + binding.btnNextPage.isEnabled = showPagination && currentPage < pages - 1 + binding.tvPageIndicator.text = if (pages == 0) { + getString(R.string.page_indicator, 0, 0) + } else { + getString(R.string.page_indicator, currentPage + 1, pages) + } + } + override fun onDestroyView() { super.onDestroyView() _binding = null diff --git a/app/src/main/java/com/travelmeet/app/ui/profile/MySpotsFragment.kt b/app/src/main/java/com/travelmeet/app/ui/profile/MySpotsFragment.kt index 3995b9d..a48784b 100644 --- a/app/src/main/java/com/travelmeet/app/ui/profile/MySpotsFragment.kt +++ b/app/src/main/java/com/travelmeet/app/ui/profile/MySpotsFragment.kt @@ -8,6 +8,8 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager +import com.travelmeet.app.R +import com.travelmeet.app.data.local.entity.SpotEntity import com.travelmeet.app.databinding.FragmentMySpotsBinding import com.travelmeet.app.ui.feed.SpotAdapter import com.travelmeet.app.ui.viewmodel.AuthViewModel @@ -15,11 +17,18 @@ import com.travelmeet.app.ui.viewmodel.SpotViewModel class MySpotsFragment : Fragment() { + companion object { + private const val PAGE_SIZE = 5 + } + private var _binding: FragmentMySpotsBinding? = null private val binding get() = _binding!! private val authViewModel: AuthViewModel by activityViewModels() private val spotViewModel: SpotViewModel by activityViewModels() private lateinit var spotAdapter: SpotAdapter + private lateinit var spotsLayoutManager: LinearLayoutManager + private var pagedSpots: List = emptyList() + private var currentPage = 0 override fun onCreateView( inflater: LayoutInflater, @@ -46,9 +55,63 @@ class MySpotsFragment : Fragment() { spotViewModel.toggleLike(spot.id) } ) + spotsLayoutManager = LinearLayoutManager(requireContext()) binding.rvMySpots.apply { adapter = spotAdapter - layoutManager = LinearLayoutManager(requireContext()) + layoutManager = spotsLayoutManager + } + setupPaginationControls() + } + + private fun setupPaginationControls() { + binding.btnNextPage.setOnClickListener { + if (canGoNext()) { + currentPage++ + submitMySpotsPage() + binding.rvMySpots.scrollToPosition(0) + } + } + binding.btnPrevPage.setOnClickListener { + if (currentPage > 0) { + currentPage-- + submitMySpotsPage() + binding.rvMySpots.scrollToPosition(0) + } + } + } + + private fun canGoNext(): Boolean { + val pages = totalPages() + return pages > 0 && currentPage < pages - 1 + } + + private fun submitMySpotsPage() { + val pages = totalPages() + if (pages == 0) { + spotAdapter.submitList(emptyList()) + updatePaginationUi(pages) + return + } + if (currentPage >= pages) currentPage = pages - 1 + val start = currentPage * PAGE_SIZE + val end = minOf(start + PAGE_SIZE, pagedSpots.size) + val newSpots = pagedSpots.subList(start, end) + spotAdapter.submitList(ArrayList(newSpots)) + updatePaginationUi(pages) + } + + private fun totalPages(): Int = + if (pagedSpots.isEmpty()) 0 else ((pagedSpots.size - 1) / PAGE_SIZE) + 1 + + private fun updatePaginationUi(pages: Int) { + val showPagination = pagedSpots.size > PAGE_SIZE + binding.paginationContainer.visibility = if (showPagination) View.VISIBLE else View.GONE + binding.btnPrevPage.isEnabled = showPagination && currentPage > 0 + binding.btnNextPage.isEnabled = showPagination && currentPage < pages - 1 + binding.tvPageIndicator.text = if (pages == 0) { + getString(R.string.page_indicator, 0, 0) + } else { + getString(R.string.page_indicator, currentPage + 1, pages) } } @@ -56,9 +119,17 @@ class MySpotsFragment : Fragment() { val userId = authViewModel.currentUserId if (userId != null) { spotViewModel.getSpotsByUser(userId).observe(viewLifecycleOwner) { spots -> - spotAdapter.submitList(spots) - binding.emptyState.visibility = if (spots.isEmpty()) View.VISIBLE else View.GONE - binding.rvMySpots.visibility = if (spots.isEmpty()) View.GONE else View.VISIBLE + pagedSpots = spots + currentPage = 0 + val isEmpty = spots.isEmpty() + binding.emptyState.visibility = if (isEmpty) View.VISIBLE else View.GONE + binding.rvMySpots.visibility = if (isEmpty) View.GONE else View.VISIBLE + if (isEmpty) { + spotAdapter.submitList(emptyList()) + updatePaginationUi(0) + } else { + submitMySpotsPage() + } } } } diff --git a/app/src/main/res/layout/fragment_feed.xml b/app/src/main/res/layout/fragment_feed.xml index caf0230..f5161f4 100644 --- a/app/src/main/res/layout/fragment_feed.xml +++ b/app/src/main/res/layout/fragment_feed.xml @@ -24,7 +24,8 @@ android:id="@+id/swipe_refresh" android:layout_width="match_parent" android:layout_height="match_parent" - app:layout_behavior="@string/appbar_scrolling_view_behavior"> + app:layout_behavior="@string/appbar_scrolling_view_behavior" + android:layout_marginBottom="72dp"> + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_my_spots.xml b/app/src/main/res/layout/fragment_my_spots.xml index 2f1a866..3e09c4b 100644 --- a/app/src/main/res/layout/fragment_my_spots.xml +++ b/app/src/main/res/layout/fragment_my_spots.xml @@ -11,7 +11,7 @@ android:id="@+id/rv_my_spots" android:layout_width="0dp" android:layout_height="0dp" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@+id/pagination_container" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> @@ -29,4 +29,48 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4091a84..33ff03c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -61,6 +61,10 @@ Change Photo Updating profile… Total likes: %d + Previous + Next + Page %1$d of %2$d + Page 0 of 0 Map