Updated EventPageFragment.kt to have access to its own ViewModel and, by extension, its own reference to the database, which forgoes the need to transport data from EventFragment.kt through StudIPEventPageAdapter.kt to EventPageFragment.kt and allows for filtering for a specific day's items through the database rather than needing to iterate through the list via an extension function manually. Also implemented DiffUtil to perform individual updates rather than calling notifyDataSetChanged() every time a single item has been changed.

This commit is contained in:
denizk0461
2023-04-28 11:18:05 +02:00
parent ee0934f29b
commit aa5d32e64a
11 changed files with 200 additions and 142 deletions
@@ -20,7 +20,7 @@ import com.denizk0461.studip.model.CanteenOfferGroup
* @param displayAllergens whether the user wants allergens to be marked
*/
class CanteenOfferPageAdapter(
val fragmentActivity: FragmentActivity,
fragmentActivity: FragmentActivity,
private var offers: List<CanteenOfferGroup>,
private var daysCovered: Int,
private val onClickListener: CanteenOfferItemAdapter.OnClickListener,
@@ -3,7 +3,9 @@ package com.denizk0461.studip.adapter
import android.content.res.ColorStateList
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.denizk0461.studip.data.AppDiffUtilCallback
import com.google.android.material.R
import com.denizk0461.studip.data.getThemedColor
import com.denizk0461.studip.data.parseToMinutes
@@ -12,29 +14,30 @@ import com.denizk0461.studip.databinding.ItemEventBinding
import java.util.*
/**
* Custom RecyclerView adapter that lays out the Stud.IP events of a given day.
* Custom RecyclerView adapter that lays out the Stud.IP events for a given day.
*
* @param events list of all events (still not filtered by day at this point)
* @param currentDay current day that is used to show only events of a given day (0 = Monday,
* @param currentDay current day that is used to show only a given day's events (0 = Monday,
* 4 = Friday)
* @param onClickListener used for listening to clicks and long presses
*/
class StudIPEventItemAdapter(
events: List<StudIPEvent>,
private val currentDay: Int,
private val onClickListener: OnClickListener,
) : RecyclerView.Adapter<StudIPEventItemAdapter.EventViewHolder>() {
// Filter events to only show those of a given day
private val filteredEvents: List<StudIPEvent> = events.filter { it.day == currentDay }
/**
* List of all events (still not filtered by day at this point)
*/
private var events: MutableList<StudIPEvent> = mutableListOf()
/*
/**
* Retrieve an instance of Calendar to check whether the adapter's day matches the current day.
* TODO this can be optimised by checking in StudIPEventPageAdapter.kt and delivering a boolean
* TODO this can be optimised by checking in EventPageFragment.kt and delivering a boolean
*/
private val currentCalendar = Calendar.getInstance()
/*
/**
* Used to disallow highlighting more than one course at a time, since only a single course
* could be coming up at a given time.
*/
@@ -58,12 +61,11 @@ class StudIPEventItemAdapter(
}
// Set the amount of items to the size of the filtered list, so only events of the current day
override fun getItemCount(): Int = filteredEvents.size
override fun getItemCount(): Int = events.size
// TODO inflate a view saying "no events" if none are available for a given day
override fun onBindViewHolder(holder: EventViewHolder, position: Int) {
// Retrieve item for current position
val currentItem = filteredEvents[position]
val currentItem = events[position]
// Set up text fields with corresponding values
holder.binding.textTitle.text = currentItem.title
@@ -117,6 +119,26 @@ class StudIPEventItemAdapter(
}
}
/**
* Updates the data and calculates the difference between the old dataset and the newly provided
* dataset.
*
* @param newData new dataset to be displayed
*/
fun setNewData(newData: List<StudIPEvent>) {
// Calculate the difference between the old list and the new list
val diffResult = DiffUtil.calculateDiff(AppDiffUtilCallback(events, newData))
// Remove all items from the list
events.clear()
// Add all items from the new list
events.addAll(newData)
// Tell the DiffUtil which items have changed between the two lists
diffResult.dispatchUpdatesTo(this)
}
/**
* Checks if a given course is the next course coming up. This is done by checking whether the
* day of the course matches the current real world day, as well as by evaluating if the end
@@ -131,7 +153,6 @@ class StudIPEventItemAdapter(
(calendar.get(Calendar.DAY_OF_WEEK) == this.day.toCalendarDay()) &&
((calendar.get(Calendar.MINUTE) + calendar.get(Calendar.HOUR_OF_DAY) * 60) < this.timeslotEnd.parseToMinutes())
// Converts day as defined in StudIPEvent.kt to day from Calendar
/**
* Converts a numeric value as defined in StudIPEvent.kt to a calendar day. Used to convert from
* American convention (where Sunday is the first day, 1 = Sunday, 2 = Monday, 7 = Saturday) to
@@ -3,20 +3,15 @@ package com.denizk0461.studip.adapter
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.denizk0461.studip.model.StudIPEvent
import com.denizk0461.studip.fragment.EventPageFragment
/**
* Custom ViewPager adapter for managing multiple pages of Stud.IP events.
*
* @param fragmentActivity parent fragment activity
* @param events all events (not filtered by day at this point)
* @param onClickListener for managing click and long press events
*/
class StudIPEventPageAdapter(
fragmentActivity: FragmentActivity,
private var events: List<StudIPEvent>,
private val onClickListener: StudIPEventItemAdapter.OnClickListener,
) : FragmentStateAdapter(fragmentActivity) {
/*
@@ -25,18 +20,5 @@ class StudIPEventPageAdapter(
override fun getItemCount(): Int = 7
override fun createFragment(position: Int): Fragment =
EventPageFragment(events, position, onClickListener)
/**
* Update the entire list of items
*
* @param items the new set of items
*/
fun setNewItems(items: List<StudIPEvent>) {
// Update items
events = items
// Force an update of the view. TODO replace with individual updates, as this is inefficient
notifyDataSetChanged()
}
EventPageFragment(position)
}
@@ -0,0 +1,50 @@
package com.denizk0461.studip.data
import androidx.recyclerview.widget.DiffUtil
/**
* Utility class used to calculate the difference between two lists to provide the adapter with
* means to perform operations only on items that have been newly added/modified/deleted.
*
* @param oldList old list
* @param newList new list
*/
class AppDiffUtilCallback(
private val oldList: List<Any>,
private val newList: List<Any>,
) : DiffUtil.Callback() {
/**
* Get the old list's size count.
*
* @return size of the old list
*/
override fun getOldListSize(): Int = oldList.size
/**
* Get the new list's size count.
*
* @return size of the new list
*/
override fun getNewListSize(): Int = newList.size
/**
* Check whether two items in the lists contain items of the same class.
*
* @param oldItemPosition position of the item in the old list to be checked
* @param newItemPosition position of the item in the new list to be checked
* @return whether the two items are of the same class
*/
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
oldList[oldItemPosition].javaClass == newList[newItemPosition].javaClass
/**
* Check whether two items in the lists have the same contents.
*
* @param oldItemPosition position of the item in the old list to be checked
* @param newItemPosition position of the item in the new list to be checked
* @return whether the two items are the same (content is equal)
*/
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
oldList[oldItemPosition].hashCode() == newList[newItemPosition].hashCode()
}
@@ -21,6 +21,14 @@ interface AppDAO {
@get:Query("SELECT * FROM studipevents ORDER BY timeslotId, id")
val allEvents: LiveData<List<StudIPEvent>>
/**
* Retrieves Stud.IP events for a specific day, ordered by their timeslots, then their IDs.
*
* @return Stud.IP events for a certain day exposed through a LiveData object
*/
@Query("SELECT * FROM studipevents WHERE day = :day ORDER BY timeslotId, id")
fun getEventsForDay(day: Int): LiveData<List<StudIPEvent>>
/**
* Updates a given Stud.IP element.
*
@@ -26,11 +26,11 @@ class AppRepository(app: Application) {
private val blankDietaryPrefs: String = DietaryPreferences.C_FALSE.toString().repeat(10)
/**
* Retrieves all Stud.IP events.
* Retrieves Stud.IP events for a specific day, ordered by their timeslots, then their IDs.
*
* @return all Stud.IP events exposed through a LiveData object
* @return Stud.IP events for a certain day exposed through a LiveData object
*/
val allEvents: LiveData<List<StudIPEvent>> = dao.allEvents
fun getEventsForDay(day: Int): LiveData<List<StudIPEvent>> = dao.getEventsForDay(day)
/**
* Inserts a list of Stud.IP events into the database.
@@ -166,13 +166,6 @@ class AppRepository(app: Application) {
*/
fun nukeOfferDates() { dao.nukeOfferDates() }
/**
* Inserts a date into the database.
*
* @param date object to be saved to the database
*/
fun insert(date: OfferDate) { dao.insert(date) }
/**
* Inserts a list of dates into the database.
*
@@ -180,13 +173,6 @@ class AppRepository(app: Application) {
*/
fun insertDates(dates: List<OfferDate>) { dao.insertDates(dates) }
/**
* Inserts a canteen into the database.
*
* @param canteen object to be saved to the database
*/
fun insert(canteen: OfferCanteen) { dao.insert(canteen) }
/**
* Inserts a list of canteens into the database.
*
@@ -194,13 +180,6 @@ class AppRepository(app: Application) {
*/
fun insertCanteens(canteens: List<OfferCanteen>) { dao.insertCanteens(canteens) }
/**
* Inserts a canteen offer category into the database.
*
* @param category object to be saved to the database
*/
fun insert(category: OfferCategory) { dao.insert(category) }
/**
* Inserts a list of categories into the database.
*
@@ -208,13 +187,6 @@ class AppRepository(app: Application) {
*/
fun insertCategories(categories: List<OfferCategory>) { dao.insertCategories(categories) }
/**
* Inserts a canteen offer item into the database.
*
* @param item object to be saved to the database
*/
fun insert(item: OfferItem) { dao.insert(item) }
/**
* Inserts a list of items into the database.
*
@@ -1,7 +1,6 @@
package com.denizk0461.studip.fragment
import android.os.Bundle
import android.os.Parcelable
import android.util.Log
import android.view.LayoutInflater
import android.view.View
@@ -9,11 +8,8 @@ import android.view.ViewGroup
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.viewModels
import com.denizk0461.studip.R
import com.denizk0461.studip.adapter.StudIPEventItemAdapter
import com.denizk0461.studip.adapter.StudIPEventPageAdapter
import com.denizk0461.studip.databinding.FragmentEventBinding
import com.denizk0461.studip.model.StudIPEvent
import com.denizk0461.studip.sheet.ScheduleUpdateSheet
import com.denizk0461.studip.viewmodel.EventViewModel
import com.google.android.material.tabs.TabLayoutMediator
import java.util.*
@@ -73,24 +69,7 @@ class EventFragment : AppFragment() {
}
// Set up the view pager's adapter
viewPagerAdapter =
StudIPEventPageAdapter(activity as FragmentActivity, listOf(), object : StudIPEventItemAdapter.OnClickListener {
override fun onClick(event: StudIPEvent) {
// TODO implement functionality or delete
}
override fun onLongClick(event: StudIPEvent): Boolean {
// Save the current page of the ViewPager
viewPagerPosition = binding.viewPager.currentItem
// Open a bottom sheet to edit the event
openBottomSheet(ScheduleUpdateSheet(event, onUpdate = { eventToUpdate ->
viewModel.update(eventToUpdate)
}, onDelete = { eventToDelete ->
viewModel.delete(eventToDelete)
}))
return true
}
})
viewPagerAdapter = StudIPEventPageAdapter(activity as FragmentActivity)
// Assign the adapter to the view pager
binding.viewPager.adapter = viewPagerAdapter
@@ -101,25 +80,25 @@ class EventFragment : AppFragment() {
}.attach()
// Set up LiveData observer to refresh the view on update
viewModel.allEvents.observe(viewLifecycleOwner) { events ->
Log.d("AAAbA?", events.toString())
// Update the item list in the view pager's adapter
viewPagerAdapter.setNewItems(events)
if (!hasFragmentStarted) {
// Scroll to the current day, if no page has been stored to be scrolled to
switchToCurrentDayView()
hasFragmentStarted = true
// } else {
// /*
// * If a page was previously saved, scroll to that one instead. This is meant to
// * prevent jumping from
// */
// binding.viewPager.currentItem = viewPagerPosition
}
}
// viewModel.allEvents.observe(viewLifecycleOwner) { events ->
//
// Log.d("AAAbA?", events.toString())
//
// // Update the item list in the view pager's adapter
// viewPagerAdapter.setNewItems(events)
//
// if (!hasFragmentStarted) {
// // Scroll to the current day, if no page has been stored to be scrolled to
// switchToCurrentDayView()
// hasFragmentStarted = true
//// } else {
//// /*
//// * If a page was previously saved, scroll to that one instead. This is meant to
//// * prevent jumping from
//// */
//// binding.viewPager.currentItem = viewPagerPosition
// }
// }
}
override fun onPause() {
@@ -4,26 +4,25 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.denizk0461.studip.adapter.StudIPEventPageAdapter
import com.denizk0461.studip.adapter.StudIPEventItemAdapter
import com.denizk0461.studip.databinding.RecyclerViewBinding
import com.denizk0461.studip.model.StudIPEvent
import com.denizk0461.studip.sheet.ScheduleUpdateSheet
import com.denizk0461.studip.viewmodel.EventPageViewModel
/**
* Fragment that is instantiated by [StudIPEventPageAdapter] to display individual days' pages and
* their events.
*
* @param events list of all events (still not filtered by day at this point)
* @param currentDay current day that is used to show only events of a given day (0 = Monday,
* 4 = Friday)
* @param onClickListener used for listening to clicks and long presses
* @param currentDay current day that is used to show only events of a given day (0 = Monday,
* 4 = Friday)
*/
class EventPageFragment(
private val events: List<StudIPEvent>,
private val currentDay: Int,
private val onClickListener: StudIPEventItemAdapter.OnClickListener,
) : AppFragment() {
) : AppFragment(), StudIPEventItemAdapter.OnClickListener {
// Nullable view binding reference
private var _binding: RecyclerViewBinding? = null
@@ -34,6 +33,14 @@ class EventPageFragment(
*/
private val binding get() = _binding!!
// View model reference for providing access to the database
private val viewModel: EventPageViewModel by viewModels()
/**
* Adapter that manages the page's items
*/
private val eventAdapter = StudIPEventItemAdapter(currentDay, this)
// Instantiate the view binding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = RecyclerViewBinding.inflate(inflater, container, false)
@@ -49,12 +56,32 @@ class EventPageFragment(
/*
* Create new adapter for every page. Attribute position denotes day that will be set up
* by the newly created adapter (0 = Monday, 4 = Friday)
* by the newly created adapter (0 = Monday, 4 = Friday, 6 = Sunday)
*/
adapter = StudIPEventItemAdapter(events, currentDay, onClickListener) // TODO check if empty
adapter = eventAdapter
// Animate creation of new page
scheduleLayoutAnimation()
}
// Set up LiveData observer to refresh the view on update
viewModel.getEventsForDay(currentDay).observe(viewLifecycleOwner) { events ->
// eventAdapter.setNewItems(events)
eventAdapter.setNewData(events)
}
}
override fun onClick(event: StudIPEvent) {
// TODO implement functionality or delete
}
override fun onLongClick(event: StudIPEvent): Boolean {
// Open a bottom sheet to edit the event
openBottomSheet(ScheduleUpdateSheet(event, onUpdate = { eventToUpdate ->
viewModel.update(eventToUpdate)
}, onDelete = { eventToDelete ->
viewModel.delete(eventToDelete)
}))
return true
}
}
@@ -37,7 +37,7 @@ data class StudIPEvent(
*/
fun timeslot(): String = "$timeslotStart $timeslotEnd"
// Auto-generated methods
// Auto-generated methods. Necessary for [AppDiffUtilCallback]
override fun equals(other: Any?): Boolean {
if (this === other) return true
@@ -45,11 +45,19 @@ data class StudIPEvent(
other as StudIPEvent
if (id != other.id) return false
if (title != other.title) return false
if (!lecturer.contentEquals(other.lecturer)) return false
if (lecturer != other.lecturer) return false
if (room != other.room) return false
if (day != other.day) return false
if (timeslotStart != other.timeslotStart) return false
if (timeslotEnd != other.timeslotEnd) return false
if (timeslotId != other.timeslotId) return false
if (colour != other.colour) return false
return true
}
override fun hashCode(): Int {
var result = id
result = 31 * result + title.hashCode()
@@ -58,7 +66,8 @@ data class StudIPEvent(
result = 31 * result + day
result = 31 * result + timeslotStart.hashCode()
result = 31 * result + timeslotEnd.hashCode()
result = 31 * result + colour.hashCode()
result = 31 * result + timeslotId
result = 31 * result + colour
return result
}
}
@@ -0,0 +1,34 @@
package com.denizk0461.studip.viewmodel
import android.app.Application
import androidx.lifecycle.LiveData
import com.denizk0461.studip.model.StudIPEvent
/**
* View model for [com.denizk0461.studip.fragment.EventPageFragment]
*
* @param app reference to the app
*/
class EventPageViewModel(app: Application) : AppViewModel(app) {
/**
* Retrieves Stud.IP events for a specific day, ordered by their timeslots, then their IDs.
*
* @return Stud.IP events for a certain day exposed through a LiveData object
*/
fun getEventsForDay(day: Int): LiveData<List<StudIPEvent>> = repo.getEventsForDay(day)
/**
* Updates a schedule element.
*
* @param event the event to update
*/
fun update(event: StudIPEvent) { doAsync { repo.update(event) } }
/**
* Deletes a schedule element.
*
* @param event the event to delete
*/
fun delete(event: StudIPEvent) { doAsync { repo.delete(event) } }
}
@@ -1,34 +1,10 @@
package com.denizk0461.studip.viewmodel
import android.app.Application
import androidx.lifecycle.LiveData
import com.denizk0461.studip.model.StudIPEvent
/**
* View model for [com.denizk0461.studip.fragment.EventFragment]
*
* @param app reference to the app
*/
class EventViewModel(app: Application) : AppViewModel(app) {
/**
* Retrieves all Stud.IP events.
*
* @return all Stud.IP events exposed through a LiveData object
*/
val allEvents: LiveData<List<StudIPEvent>> = repo.allEvents
/**
* Updates a schedule element.
*
* @param event the event to update
*/
fun update(event: StudIPEvent) { doAsync { repo.update(event) } }
/**
* Deletes a schedule element.
*
* @param event the event to delete
*/
fun delete(event: StudIPEvent) { doAsync { repo.delete(event) } }
}
class EventViewModel(app: Application) : AppViewModel(app)