Fully documented all Kotlin files and added TODOs to functions that need to be updated in some way

This commit is contained in:
denizk0461
2023-04-23 11:57:03 +02:00
parent 2534849329
commit fbad84aa09
33 changed files with 1220 additions and 571 deletions
@@ -7,29 +7,44 @@ import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveData
import com.denizk0461.studip.adapter.CanteenOfferPageAdapter
import com.denizk0461.studip.databinding.FragmentCanteenBinding
import com.denizk0461.studip.model.*
import com.denizk0461.studip.viewmodel.CanteenViewModel
import com.google.android.material.tabs.TabLayoutMediator
/**
* User-facing fragment view that displays the canteen offers from the website of the
* Studierendenwerk Bremen.
*/
class CanteenFragment : Fragment() {
// Nullable view binding reference
private var _binding: FragmentCanteenBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
// BUG: if the fragment is changed before the refresh is finished, the app crashes with a NPE
/*
* Non-null reference to the view binding. This property is only valid between onCreateView and
* onDestroyView.
* BUG if the fragment is changed before the refresh is finished, the app crashes with a NPE
*/
private val binding get() = _binding!!
// Adapter for the view pager displaying all offers
private lateinit var viewPagerAdapter: CanteenOfferPageAdapter
// View model reference for providing access to the database
private val viewModel: CanteenViewModel by viewModels()
// Elements in the canteen plan
private var elements: List<CanteenOffer> = listOf()
private var dateSize = 0
// Count of days displayed in the canteen plan
private var dateSize: Int = 0
// Used if no preference has been set or none can be found
private val emptyPreferenceRegex: String = ".........."
// Instantiate the view binding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentCanteenBinding.inflate(inflater, container, false)
return binding.root
@@ -38,6 +53,7 @@ class CanteenFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Assign a preference value to every button to filter for dietary preferences
val chipMap = mapOf(
binding.chipPrefFair to DietaryPreferences.FAIR,
binding.chipPrefFish to DietaryPreferences.FISH,
@@ -51,15 +67,21 @@ class CanteenFragment : Fragment() {
binding.chipPrefGame to DietaryPreferences.GAME,
)
// Set up every chip
chipMap.forEach { (chip, pref) ->
// Set chip to be checked if user has set this preference before
chip.isChecked = getPreference(pref)
chip.setOnCheckedChangeListener { buttonView, newValue ->
setPreference(pref, newValue)
Log.d("eek!4", "object as : ${viewModel.getDietaryPrefs().deconstruct()}")
/* Since this removes the view before immediately adding it back, this method causes a
* flicker. Note for the future: implement a more efficient / better-looking method.
*
// Set a listener for when the user clicks the chip
chip.setOnCheckedChangeListener { buttonView, newValue ->
// Save the preference change to persistent storage
setPreference(pref, newValue)
/*
* Force an animation on click. This is a subpar method from StackOverflow. Since
* it removes the view before immediately adding it back, this method causes a
* flicker.
* TODO implement a more efficient / better-looking method
* TODO order the chips alphabetically
*/
val index = binding.chipsPreference.indexOfChild(buttonView)
@@ -68,29 +90,42 @@ class CanteenFragment : Fragment() {
}
}
viewPagerAdapter = CanteenOfferPageAdapter(listOf(), 0, getPrefRegex())
// Set up the view pager's adapter
viewPagerAdapter = CanteenOfferPageAdapter(listOf(), 0)
// Assign the adapter to the view pager
binding.viewPager.adapter = viewPagerAdapter
// Create and attach the tab mediator for the view pager
createTabLayoutMediator()
// Set up LiveData observer to refresh the view on update
viewModel.allOffers.observe(viewLifecycleOwner) { offers ->
// Update the element list stored in this fragment
elements = offers
// Group elements by category
val groupedElements = offers.groupElements().distinct()
val newDates = groupedElements.map { it.date }.distinct()//.map { item -> OfferDate(item.) }
// val newDates = viewModel.getDates()
// Find all dates stored in the database
val newDates = groupedElements.map { it.date }.distinct() // TODO viewModel.getDates()
// Update the date count stored in this fragment
dateSize = newDates.size
// Update the item list in the view pager's adapter
viewPagerAdapter.setNewItems(groupedElements, dateSize)
// createTabLayoutMediator(newDates)
// TODO this must be changed. if an exception is raised, then this will not fire
/*
* Tell the swipe refresh layout to stop refreshing.
* TODO if an exception is raised, this will not be fired. This must be changed
*/
binding.swipeRefreshLayout.isRefreshing = false
}
// Set up functions for when the user swipes to refresh the view
binding.swipeRefreshLayout.setOnRefreshListener {
// Retrieve new offers from the website(s)
viewModel.fetchOffers(onRefreshUpdate = { status ->
// TODO refresh updates
}, onFinish = {
@@ -100,7 +135,11 @@ class CanteenFragment : Fragment() {
}
}
private fun createTabLayoutMediator(){//List<OfferDate>) {
/**
* Create and attach the object mediating the tabs for the view pager.
*/
private fun createTabLayoutMediator() {
// Fetch all dates from the database
val dates = viewModel.getDates()
if (dates.isNotEmpty()) {
Log.d("eek!7", "dates: ${dates}, viewpager pages: ${binding.viewPager.adapter?.itemCount}")
@@ -111,57 +150,106 @@ class CanteenFragment : Fragment() {
}
}
// Invalidate the view binding
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
/**
* Updates a given preference and refreshes the view.
*
* @param pref preference to be updated
* @param newValue value to set the preference to
*/
private fun setPreference(pref: DietaryPreferences, newValue: Boolean) {
// Update the preference
viewModel.setPreference(pref, newValue)
// Update the view
viewPagerAdapter.setNewItems(elements.groupElements().distinct(), dateSize)
}
/**
* Retrieves a single user-specified dietary preference.
*
* @param pref the dietary preference that should be retrieved
* @return whether the preference needs to be met
*/
private fun getPreference(pref: DietaryPreferences): Boolean =
viewModel.getPreference(pref)
/**
* Retrieves the user's dietary preferences and compiles them into a regular expression that can
* be used to filter out items that don't fulfil the preferences. Example:
* .......t..|........t.
* This will retrieve all items that marked as either vegetarian or vegan
* *
* @return regular expression object reflecting the user's preferences
*/
private fun getPrefRegex(): Regex {
val prefs = viewModel.getDietaryPrefs().deconstruct()//.replace('f', '.')
// Retrieve the user's dietary preference as a string
val prefs = viewModel.getDietaryPrefs().deconstruct()
val template = ".........."
return if (prefs == "..........") {
Regex(template)
// Return the 'empty' template string if no preference has been set or found
return if (prefs == emptyPreferenceRegex) {
Regex(emptyPreferenceRegex)
} else {
// Create the variable to insert the regular expression into
var regexString = ""
// Create the object to store which preferences need to be met
val indices = mutableListOf<Int>()
// Find all preferences that need to be met
prefs.forEachIndexed { index, c ->
if (c == 't') indices.add(index)
if (c == DietaryPrefObject.C_TRUE) indices.add(index)
}
// Needs to be set to correctly assemble the regular expression
var isFirst = true
// Iterate through every preference that has been set
indices.forEach { index ->
// Set an OR if more than one preference needs to be met
if (!isFirst) regexString += '|'
regexString += template.substring(0 until index) + 't' + template.substring(index+1)
// Add the expression looking for the single preference to the entire string
regexString += emptyPreferenceRegex.substring(0 until index) +
DietaryPrefObject.C_TRUE +
emptyPreferenceRegex.substring(index+1)
// Ensure that OR statements will be placed between the individual expressions
isFirst = false
}
// Compile the string to a regular expression object
Regex(regexString)
}
}
/**
* Groups [CanteenOffer] elements by their categories into [CanteenOfferGroup] elements and
* filters for the user's dietary preferences.
*
* @return the grouped and filtered elements
*/
private fun List<CanteenOffer>.groupElements(): List<CanteenOfferGroup> {
// Get dietary preference regex
val prefsRegex = getPrefRegex()
val filteredForPreferences = if (prefsRegex.toString() == "ffffffffff") {
// show all elements and skip filtering
val filteredForPreferences = if (prefsRegex.toString() == emptyPreferenceRegex) {
// Show all elements and skip filtering if no preference is set
this
} else {
// Filter for dietary preferences
this.filter {
prefsRegex.matches(it.dietaryPreferences)
}
}
val b = filteredForPreferences.map { offer ->
return filteredForPreferences.map { offer ->
// Create new group elements to group the already filtered elements by their categories
CanteenOfferGroup(
offer.date,
offer.dateId,
@@ -170,6 +258,7 @@ class CanteenFragment : Fragment() {
filteredForPreferences.filter {
it.category == offer.category && it.date == offer.date && it.canteen == offer.canteen
}.map {
// Map the individual items to their respective groups
CanteenOfferGroupElement(
it.title,
it.price,
@@ -178,6 +267,5 @@ class CanteenFragment : Fragment() {
}
)
}
return b
}
}