My Recycler View is unable to properly display items. However, the items are already in place, as I can click on them and enter the details fragment. When I load the page, I can only view 5 items, and scrolling down does not reveal any other items outside the final one at position 11. Additionally, when I search for items on the page, I am able to receive any item, even if they are not visible on the page. Filtering causes the correct items to show, but outside of the initial 5 displayed items, the rest are not visible (but are indeed there). Thanks in advance for reading.
ExercisesFragment
package com.example.workouttracker.ui.exercises
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.SearchView
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.workouttracker.R
import com.example.workouttracker.databinding.FragmentExercisesBinding
class ExercisesFragment : Fragment() {
private var _binding: FragmentExercisesBinding? = null
private val binding get() = _binding!!
private lateinit var viewModel: ExercisesViewModel
private lateinit var adapter: ExerciseAdapter
private val args: ExercisesFragmentArgs by navArgs()
private var selectedTypes: List<String> = emptyList()
private var selectedBodyParts: List<String> = emptyList()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentExercisesBinding.inflate(inflater, container, false)
val root: View = binding.root
setupRecyclerView()
setupViewModel()
setupObservers()
viewModel.clearExercises()
viewModel.fetchExercises()
// Apply filters if any
selectedTypes = args.selectedTypes?.toList() ?: emptyList()
selectedBodyParts = args.selectedBodyParts?.toList() ?: emptyList()
Log.d("Type Filter Selections", selectedTypes.toString())
Log.d("BodyPart Filter Selections", selectedBodyParts.toString())
return root
}
private fun setupRecyclerView() {
adapter = ExerciseAdapter(mutableListOf()) { exercise ->
val action = ExercisesFragmentDirections.actionNavigationExercisesToNavigationExerciseDetails(exercise)
findNavController().navigate(action)
}
binding.recyclerViewExercises.layoutManager = LinearLayoutManager(requireContext())
binding.recyclerViewExercises.adapter = adapter
binding.recyclerViewExercises.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()
val LastVisibleItemPosition = layoutManager.findLastVisibleItemPosition()
Log.d("RecyclerView", "LastVisibleItemPosition: $LastVisibleItemPosition")
if (!viewModel.isLoading.value!! && (visibleItemCount + firstVisibleItemPosition) >= totalItemCount && firstVisibleItemPosition >= 0) {
viewModel.loadNextChunk()
}
}
})
}
private fun setupViewModel() {
viewModel = ViewModelProvider(this)[ExercisesViewModel::class.java]
}
private fun setupObservers() {
viewModel.exercises.observe(viewLifecycleOwner) { exercises ->
Log.d("Exercises", "${exercises.size}")
if (selectedTypes.isNotEmpty() || selectedBodyParts.isNotEmpty()) {
filterExercises(selectedTypes, selectedBodyParts)
}
else
Handler(Looper.getMainLooper()).post {
adapter.updateExercises(exercises)
}
}
viewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
binding.progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE
}
viewModel.error.observe(viewLifecycleOwner) { error ->
error?.let {
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val createButton = binding.addExerciseButton
createButton.setOnClickListener {
findNavController().navigate(R.id.action_navigation_exercises_to_navigation_create_exercise)
}
setupSearchBar()
setupFilterButton()
}
private fun filterExercises(selectedTypes: List<String>, selectedBodyParts: List<String>) {
viewModel.exercises.value?.let { exercises ->
val filteredExercises = exercises.filter { exercise ->
(selectedTypes.isEmpty() || exercise.type in selectedTypes) &&
(selectedBodyParts.isEmpty() || exercise.bodypart in selectedBodyParts)
}
Handler(Looper.getMainLooper()).post {
adapter.updateExercises(filteredExercises)
}
}
}
private fun setupFilterButton() {
val filterButton = binding.filterExercise
filterButton.setOnClickListener {
val bundle = Bundle().apply {
putStringArray("selectedTypes", selectedTypes.toTypedArray())
putStringArray("selectedBodyParts", selectedBodyParts.toTypedArray())
}
findNavController().navigate(R.id.action_navigation_exercise_to_navigation_filter_exercises, bundle)
}
}
private fun setupSearchBar() {
val searchBar: SearchView = binding.searchBar
searchBar.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
// Handle the query text submission
query?.let {
Toast.makeText(context, "Searching for: $it", Toast.LENGTH_SHORT).show()
}
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
// Handle the query text change
newText?.let {
val filteredExercises = viewModel.filterExercises(it, selectedTypes, selectedBodyParts)
adapter.updateExercises(filteredExercises)
}
return false
}
})
}
override fun onResume() {
super.onResume()
clearSearchView()
}
private fun clearSearchView() {
binding.searchBar.setQuery("", false)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
ExercisesViewModel
package com.example.workouttracker.ui.exercises
import android.app.Application
import android.util.Log
import androidx.lifecycle.*
import com.example.workouttracker.ui.data.Exercise
import com.example.workouttracker.ui.data.ExerciseDatabase
import com.example.workouttracker.ui.data.ExerciseRepository
import com.google.firebase.auth.ktx.auth
import com.google.firebase.ktx.Firebase
import kotlinx.coroutines.launch
class ExercisesViewModel(application: Application) : AndroidViewModel(application) {
private val exerciseRepository: ExerciseRepository
private val _exercises = MutableLiveData<List<Exercise>>()
val exercises: LiveData<List<Exercise>> get() = _exercises
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> get() = _isLoading
private val _error = MutableLiveData<String?>()
val error: LiveData<String?> get() = _error
private var allExercises = listOf<Exercise>()
private var currentChunkIndex = 0
private val chunkSize = 7
private var isFirstFetch = true
init {
val exerciseDao = ExerciseDatabase.getDatabase(application).exerciseDao()
exerciseRepository = ExerciseRepository(exerciseDao)
}
fun fetchExercises() {
viewModelScope.launch {
_isLoading.value = true
try {
val userId = Firebase.auth.currentUser?.uid
if (userId != null) {
exerciseRepository.getAllExercises(userId,
onSuccess = { exercisesList ->
allExercises = exercisesList
currentChunkIndex = 0
loadNextChunk()
_isLoading.value = false
},
onFailure = {
fetchExercisesFromCache()
}
)
} else {
fetchExercisesFromCache()
}
} catch (e: Exception) {
fetchExercisesFromCache()
}
}
}
private fun fetchExercisesFromCache() {
viewModelScope.launch {
Log.d("isfirstfetch", "$isFirstFetch")
allExercises = exerciseRepository.getAllExercisesFromCache()
if (isFirstFetch) {
currentChunkIndex = 0
isFirstFetch = false
loadNextChunk()
}
_isLoading.postValue(false)
}
}
fun loadNextChunk() {
if (currentChunkIndex * chunkSize >= allExercises.size) return
val nextChunk = allExercises.drop(currentChunkIndex * chunkSize).take(chunkSize)
val currentExercises = _exercises.value ?: emptyList()
val uniqueExercises = (currentExercises + nextChunk).distinct()
viewModelScope.launch {
_exercises.value = uniqueExercises
}
currentChunkIndex++
}
fun filterExercises(query: String, selectedTypes: List<String>, selectedBodyParts: List<String>): List<Exercise> {
return exercises.value?.filter { exercise ->
(selectedTypes.isEmpty() || exercise.type in selectedTypes) &&
(selectedBodyParts.isEmpty() || exercise.bodypart in selectedBodyParts) &&
(exercise.title.contains(query, ignoreCase = true) ||
exercise.description.contains(query, ignoreCase = true))
} ?: emptyList()
}
fun clearExercises() {
_exercises.value = emptyList()
}
}
ExerciseRepository
package com.example.workouttracker.ui.data
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.PropertyName
import android.util.Log
import android.os.Parcel
import android.os.Parcelable
import com.google.firebase.firestore.Exclude
import kotlinx.coroutines.tasks.await
class ExerciseRepository(private val exerciseDao: ExerciseDao) {
private val firestore = FirebaseFirestore.getInstance()
private val defaultExercisesCollection = firestore.collection("exercises")
// Add an exercise to a user's collection in Firestore
fun addExercise(userId: String, exercise: Exercise) {
val userExercisesCollection = firestore.collection("users").document(userId).collection("exercises")
userExercisesCollection.add(exercise)
.addOnSuccessListener { documentReference ->
// Exercise added successfully
val exerciseId = documentReference.id
Log.d("ExerciseRepository", "Exercise added with ID: $exerciseId")
// You can handle success as needed
}
.addOnFailureListener { e ->
// Failed to add exercise
Log.e("ExerciseRepository", "Error adding exercise", e)
}
}
// Update an existing exercise in a user's collection in Firestore
fun updateExercise(userId: String, exerciseId: String, updatedExercise: Exercise) {
val exerciseRef = firestore.collection("users").document(userId).collection("exercises").document(exerciseId)
exerciseRef.set(updatedExercise)
.addOnSuccessListener {
// Exercise updated successfully
Log.d("ExerciseRepository", "Exercise updated with ID: $exerciseId")
// You can handle success as needed
}
.addOnFailureListener { e ->
// Failed to update exercise
Log.e("ExerciseRepository", "Error updating exercise", e)
}
}
// Delete an exercise from a user's collection in Firestore
fun deleteExercise(userId: String, exerciseId: String) {
val exerciseRef = firestore.collection("users").document(userId).collection("exercises").document(exerciseId)
exerciseRef.delete()
.addOnSuccessListener {
// Exercise deleted successfully
Log.d("ExerciseRepository", "Exercise deleted with ID: $exerciseId")
// You can handle success as needed
}
.addOnFailureListener { e ->
// Failed to delete exercise
Log.e("ExerciseRepository", "Error deleting exercise", e)
}
}
private fun combineExercises(defaultExercises: List<Exercise>, userExercises: List<Exercise>): List<Exercise> {
val allExercises = defaultExercises.toMutableList()
allExercises.addAll(userExercises)
return allExercises
}
suspend fun getAllExercises(userId: String, onSuccess: (List<Exercise>) -> Unit, onFailure: (Exception) -> Unit) {
val userExercisesCollection = firestore.collection("users").document(userId).collection("exercises")
try {
// Fetch from Firestore
val userDocuments = userExercisesCollection.get().await()
val userExercises = userDocuments.toObjects(Exercise::class.java)
// Fetch default exercises from Firestore
val defaultDocuments = defaultExercisesCollection.get().await()
val defaultExercises = defaultDocuments.toObjects(Exercise::class.java)
// Combine both lists
val allExercises = combineExercises(defaultExercises, userExercises)
// Clear cache and insert new exercises
exerciseDao.clearExercises()
exerciseDao.insertExercises(allExercises)
Log.d("ExerciseRepository", "Fetched exercises from Firestore: $allExercises")
onSuccess(allExercises)
} catch (e: Exception) {
onFailure(e)
}
}
suspend fun getAllExercisesFromCache(): List<Exercise> {
Log.d("ExerciseRepository", "Getting all exercises from cache")
return exerciseDao.getAllExercises()
}
}
fragment_exercises.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".ui.exercises.ExercisesFragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/nav_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?android:attr/windowBackground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:menu="@menu/bottom_nav_menu" />
<SearchView
android:id="@+id/search_bar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:iconifiedByDefault="false"
android:queryHint="Search Exercises"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintWidth_percent="0.9"
app:layout_constraintHorizontal_bias="0.5" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view_exercises"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:nestedScrollingEnabled="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintTop_toBottomOf="@id/search_bar"
app:layout_constraintBottom_toTopOf="@id/nav_view"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<ImageView
android:id="@+id/filter_exercise"
android:layout_width="40dp"
android:layout_height="40dp"
android:contentDescription="@string/filter_exercises"
android:src="@drawable/ic_filter_funnel"
android:elevation="4dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintVertical_bias="0.0" />
<Button
android:id="@+id/add_exercise_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginBottom="12dp"
android:text="@string/create_exercise"
app:layout_constraintBottom_toTopOf="@id/nav_view"
app:layout_constraintEnd_toEndOf="parent" />
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
item_exercise.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="exercise"
type="com.example.workouttracker.ui.data.Exercise" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{exercise.title}"
android:textSize="18dp"
android:textStyle="bold"
android:layout_marginBottom="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:id="@+id/description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{exercise.description}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintEnd_toEndOf="parent" />
<!-- Add other views for animation and default if needed -->
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
I have spent days prompting GPT to solve my issue, introduced features to stop the adapter from binding items if they have been binded before (causes the items to not display completely, and my search exercises completely ceases to work.) I’m new to Kotlin, and the bulk of the code is from AI sources or online. I have followed the logic of the code various times but I cannot seem to find the root cause of the issue. I am very tired from coding for days on end and I really need help from someone. Thank you for reading.
Ethan Lee is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.