From aa5d32e64a14670adedf3b089c3c465649dae482 Mon Sep 17 00:00:00 2001 From: denizk0461 Date: Fri, 28 Apr 2023 11:18:05 +0200 Subject: [PATCH] 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. --- .../studip/adapter/CanteenOfferPageAdapter.kt | 2 +- .../studip/adapter/StudIPEventItemAdapter.kt | 47 ++++++++++---- .../studip/adapter/StudIPEventPageAdapter.kt | 20 +----- .../studip/data/AppDiffUtilCallback.kt | 50 +++++++++++++++ .../java/com/denizk0461/studip/db/AppDAO.kt | 8 +++ .../com/denizk0461/studip/db/AppRepository.kt | 34 +---------- .../studip/fragment/EventFragment.kt | 61 ++++++------------- .../studip/fragment/EventPageFragment.kt | 45 +++++++++++--- .../denizk0461/studip/model/StudIPEvent.kt | 15 ++++- .../studip/viewmodel/EventPageViewModel.kt | 34 +++++++++++ .../studip/viewmodel/EventViewModel.kt | 26 +------- 11 files changed, 200 insertions(+), 142 deletions(-) create mode 100644 app/src/main/java/com/denizk0461/studip/data/AppDiffUtilCallback.kt create mode 100644 app/src/main/java/com/denizk0461/studip/viewmodel/EventPageViewModel.kt diff --git a/app/src/main/java/com/denizk0461/studip/adapter/CanteenOfferPageAdapter.kt b/app/src/main/java/com/denizk0461/studip/adapter/CanteenOfferPageAdapter.kt index 0d9029f..d8d7b24 100644 --- a/app/src/main/java/com/denizk0461/studip/adapter/CanteenOfferPageAdapter.kt +++ b/app/src/main/java/com/denizk0461/studip/adapter/CanteenOfferPageAdapter.kt @@ -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, private var daysCovered: Int, private val onClickListener: CanteenOfferItemAdapter.OnClickListener, diff --git a/app/src/main/java/com/denizk0461/studip/adapter/StudIPEventItemAdapter.kt b/app/src/main/java/com/denizk0461/studip/adapter/StudIPEventItemAdapter.kt index f71a422..d54122f 100644 --- a/app/src/main/java/com/denizk0461/studip/adapter/StudIPEventItemAdapter.kt +++ b/app/src/main/java/com/denizk0461/studip/adapter/StudIPEventItemAdapter.kt @@ -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, private val currentDay: Int, private val onClickListener: OnClickListener, ) : RecyclerView.Adapter() { - // Filter events to only show those of a given day - private val filteredEvents: List = events.filter { it.day == currentDay } + /** + * List of all events (still not filtered by day at this point) + */ + private var events: MutableList = 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) { + // 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 diff --git a/app/src/main/java/com/denizk0461/studip/adapter/StudIPEventPageAdapter.kt b/app/src/main/java/com/denizk0461/studip/adapter/StudIPEventPageAdapter.kt index ab13c8d..aa95431 100644 --- a/app/src/main/java/com/denizk0461/studip/adapter/StudIPEventPageAdapter.kt +++ b/app/src/main/java/com/denizk0461/studip/adapter/StudIPEventPageAdapter.kt @@ -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, - 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) { - // Update items - events = items - - // Force an update of the view. TODO replace with individual updates, as this is inefficient - notifyDataSetChanged() - } + EventPageFragment(position) } \ No newline at end of file diff --git a/app/src/main/java/com/denizk0461/studip/data/AppDiffUtilCallback.kt b/app/src/main/java/com/denizk0461/studip/data/AppDiffUtilCallback.kt new file mode 100644 index 0000000..e0c519b --- /dev/null +++ b/app/src/main/java/com/denizk0461/studip/data/AppDiffUtilCallback.kt @@ -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, + private val newList: List, +) : 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() +} \ No newline at end of file diff --git a/app/src/main/java/com/denizk0461/studip/db/AppDAO.kt b/app/src/main/java/com/denizk0461/studip/db/AppDAO.kt index fcdd77b..3b54b69 100644 --- a/app/src/main/java/com/denizk0461/studip/db/AppDAO.kt +++ b/app/src/main/java/com/denizk0461/studip/db/AppDAO.kt @@ -21,6 +21,14 @@ interface AppDAO { @get:Query("SELECT * FROM studipevents ORDER BY timeslotId, id") val allEvents: LiveData> + /** + * 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> + /** * Updates a given Stud.IP element. * diff --git a/app/src/main/java/com/denizk0461/studip/db/AppRepository.kt b/app/src/main/java/com/denizk0461/studip/db/AppRepository.kt index 983496e..a7d88be 100644 --- a/app/src/main/java/com/denizk0461/studip/db/AppRepository.kt +++ b/app/src/main/java/com/denizk0461/studip/db/AppRepository.kt @@ -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> = dao.allEvents + fun getEventsForDay(day: Int): LiveData> = 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) { 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) { 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) { 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. * diff --git a/app/src/main/java/com/denizk0461/studip/fragment/EventFragment.kt b/app/src/main/java/com/denizk0461/studip/fragment/EventFragment.kt index 5f06ff2..24287e1 100644 --- a/app/src/main/java/com/denizk0461/studip/fragment/EventFragment.kt +++ b/app/src/main/java/com/denizk0461/studip/fragment/EventFragment.kt @@ -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() { diff --git a/app/src/main/java/com/denizk0461/studip/fragment/EventPageFragment.kt b/app/src/main/java/com/denizk0461/studip/fragment/EventPageFragment.kt index a0faecf..a3bd2c4 100644 --- a/app/src/main/java/com/denizk0461/studip/fragment/EventPageFragment.kt +++ b/app/src/main/java/com/denizk0461/studip/fragment/EventPageFragment.kt @@ -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, 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 } } \ No newline at end of file diff --git a/app/src/main/java/com/denizk0461/studip/model/StudIPEvent.kt b/app/src/main/java/com/denizk0461/studip/model/StudIPEvent.kt index 31fa6a4..37c8fea 100644 --- a/app/src/main/java/com/denizk0461/studip/model/StudIPEvent.kt +++ b/app/src/main/java/com/denizk0461/studip/model/StudIPEvent.kt @@ -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 } } diff --git a/app/src/main/java/com/denizk0461/studip/viewmodel/EventPageViewModel.kt b/app/src/main/java/com/denizk0461/studip/viewmodel/EventPageViewModel.kt new file mode 100644 index 0000000..7ad7d07 --- /dev/null +++ b/app/src/main/java/com/denizk0461/studip/viewmodel/EventPageViewModel.kt @@ -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> = 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) } } +} \ No newline at end of file diff --git a/app/src/main/java/com/denizk0461/studip/viewmodel/EventViewModel.kt b/app/src/main/java/com/denizk0461/studip/viewmodel/EventViewModel.kt index 5637f58..405204a 100644 --- a/app/src/main/java/com/denizk0461/studip/viewmodel/EventViewModel.kt +++ b/app/src/main/java/com/denizk0461/studip/viewmodel/EventViewModel.kt @@ -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> = 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) } } -} \ No newline at end of file +class EventViewModel(app: Application) : AppViewModel(app) \ No newline at end of file