chore(mvvm): CalendarActivity ViewModel
This commit is contained in:
parent
6832d25163
commit
851b3c1f38
|
@ -5,35 +5,27 @@ import android.os.Bundle
|
|||
import android.widget.ImageButton
|
||||
import android.widget.TextView
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.p_vacho.neat_calendar.MyApplication
|
||||
import com.p_vacho.neat_calendar.R
|
||||
import com.p_vacho.neat_calendar.adapters.CalendarDayItemAdapter
|
||||
import com.p_vacho.neat_calendar.api.RetrofitClient
|
||||
import com.p_vacho.neat_calendar.api.models.EventResponse
|
||||
import com.p_vacho.neat_calendar.models.CalendarDay
|
||||
import kotlinx.coroutines.launch
|
||||
import com.p_vacho.neat_calendar.viewmodels.CalendarViewModel
|
||||
import java.time.YearMonth
|
||||
import java.time.ZoneId
|
||||
import java.time.format.TextStyle
|
||||
import java.util.Locale
|
||||
|
||||
|
||||
class CalendarActivity : AppCompatActivity() {
|
||||
private val viewModel: CalendarViewModel by viewModels()
|
||||
private lateinit var adapter: CalendarDayItemAdapter
|
||||
|
||||
private lateinit var tvMonthYear: TextView
|
||||
private lateinit var rvCalendar: RecyclerView
|
||||
private lateinit var btnPreviousMonth: ImageButton
|
||||
private lateinit var btnNextMonth: ImageButton
|
||||
|
||||
private val tokenManager by lazy { (application as MyApplication).tokenManager }
|
||||
|
||||
private var currentYearMonth: YearMonth = YearMonth.now()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
|
@ -44,125 +36,31 @@ class CalendarActivity : AppCompatActivity() {
|
|||
insets
|
||||
}
|
||||
|
||||
initializeViews()
|
||||
|
||||
// Observers
|
||||
viewModel.monthYearText.observe(this) { text -> tvMonthYear.text = text }
|
||||
viewModel.calendarDays.observe(this) {days -> adapter.updateDays(days) }
|
||||
|
||||
// Listeners
|
||||
btnPreviousMonth.setOnClickListener { viewModel.prevMonth() }
|
||||
btnNextMonth.setOnClickListener { viewModel.nextMonth() }
|
||||
|
||||
// Setup for current month
|
||||
viewModel.setCalendarMonth(YearMonth.now())
|
||||
}
|
||||
|
||||
private fun initializeViews() {
|
||||
tvMonthYear = findViewById(R.id.tvMonthYear)
|
||||
rvCalendar = findViewById(R.id.rvCalendar)
|
||||
btnPreviousMonth = findViewById(R.id.btnPreviousMonth)
|
||||
btnNextMonth = findViewById(R.id.btnNextMonth)
|
||||
|
||||
setCalendarForMonth(currentYearMonth)
|
||||
|
||||
btnPreviousMonth.setOnClickListener {
|
||||
currentYearMonth = currentYearMonth.minusMonths(1)
|
||||
setCalendarForMonth(currentYearMonth)
|
||||
}
|
||||
|
||||
btnNextMonth.setOnClickListener {
|
||||
currentYearMonth = currentYearMonth.plusMonths(1)
|
||||
setCalendarForMonth(currentYearMonth)
|
||||
}
|
||||
adapter = CalendarDayItemAdapter(listOf(), ::navigateToDayActivity)
|
||||
rvCalendar.layoutManager = GridLayoutManager(this@CalendarActivity, 7) // 7 columns for days of the week
|
||||
rvCalendar.adapter = adapter
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain all of the events that occur for each day in the given month from the API.
|
||||
*
|
||||
* The returned structure is a hashmap with int keys, being the day numbers in the month.
|
||||
* If an event spans multiple days, it will be added for each of those days.
|
||||
*
|
||||
* Note that this also includes the events this user was invited into.
|
||||
*/
|
||||
private suspend fun fetchEventsForMonth(yearMonth: YearMonth): Map<Int, List<EventResponse>> {
|
||||
// Use the system's default time zone
|
||||
val zoneId = ZoneId.systemDefault()
|
||||
|
||||
// Get the start and end of the month in the local time zone
|
||||
val startFrom = yearMonth.atDay(1).atStartOfDay(zoneId).toOffsetDateTime()
|
||||
val endTo = yearMonth.atEndOfMonth().atTime(23, 59, 59).atZone(zoneId).toOffsetDateTime()
|
||||
|
||||
val userId = tokenManager.userId
|
||||
|
||||
if (userId.isNullOrEmpty()) {
|
||||
navigateToMainActivity()
|
||||
return emptyMap()
|
||||
}
|
||||
|
||||
// BUG: Events that last longer than a month will not show
|
||||
// up in the following month due to the startFrom filter here.
|
||||
val userEvents = RetrofitClient.eventsService.userEvents(
|
||||
userId = userId,
|
||||
startFrom = startFrom,
|
||||
startTo = endTo
|
||||
)
|
||||
val invitedEvents = RetrofitClient.eventsService.userEventsInvited(
|
||||
userId = userId,
|
||||
startFrom = startFrom,
|
||||
startTo = endTo,
|
||||
inviteStatus = "accepted"
|
||||
)
|
||||
|
||||
val allEvents = userEvents + invitedEvents
|
||||
|
||||
// Create a mapping of days to events
|
||||
val eventsByDay = mutableMapOf<Int, MutableList<EventResponse>>()
|
||||
|
||||
for (event in allEvents) {
|
||||
val startDay = maxOf(event.start_time.dayOfMonth, 1)
|
||||
val endDay = minOf(event.end_time.dayOfMonth, yearMonth.lengthOfMonth())
|
||||
|
||||
for (day in startDay..endDay) {
|
||||
eventsByDay.computeIfAbsent(day) { mutableListOf() }.add(event)
|
||||
}
|
||||
}
|
||||
|
||||
return eventsByDay
|
||||
}
|
||||
|
||||
private fun setCalendarForMonth(yearMonth: YearMonth) {
|
||||
lifecycleScope.launch {
|
||||
val firstDayOfMonth = yearMonth.atDay(1)
|
||||
val daysInMonth = yearMonth.lengthOfMonth()
|
||||
|
||||
val eventsByDay = fetchEventsForMonth(yearMonth)
|
||||
|
||||
// Get localized month name and year
|
||||
val monthYearText = "${
|
||||
yearMonth.month.getDisplayName(
|
||||
TextStyle.FULL,
|
||||
Locale.getDefault()
|
||||
)
|
||||
} ${yearMonth.year}"
|
||||
tvMonthYear.text = monthYearText
|
||||
|
||||
// Calculate the amount of placeholder slots until the first day in the month
|
||||
// (Our calendar starts from Monday, but not all months start on monday, so we need fillers)
|
||||
val startOffset = firstDayOfMonth.dayOfWeek.ordinal // Gives 0 (Monday) to 6 (Sunday)
|
||||
|
||||
// Generate calendar days
|
||||
val days = mutableListOf<CalendarDay?>()
|
||||
|
||||
// Add padding (placeholder items) for days of the previous month
|
||||
for (i in 1..startOffset) {
|
||||
days.add(null)
|
||||
}
|
||||
|
||||
// Add the actual days of the current month
|
||||
for (i in 1..daysInMonth) {
|
||||
val fullDate = yearMonth.atDay(i)
|
||||
val eventsForDay = eventsByDay[i] ?: emptyList()
|
||||
days.add(CalendarDay(date = fullDate, events = eventsForDay))
|
||||
}
|
||||
|
||||
// Set up RecyclerView
|
||||
rvCalendar.layoutManager = GridLayoutManager(this@CalendarActivity, 7) // 7 columns for days of the week
|
||||
rvCalendar.adapter = CalendarDayItemAdapter(days) { day -> navigateToDayActivity(day) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToMainActivity() {
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
startActivity(intent)
|
||||
finish() // Close the login screen
|
||||
}
|
||||
|
||||
private fun navigateToDayActivity(day: CalendarDay) {
|
||||
var intent = Intent(this, DayViewActivity::class.java)
|
||||
|
@ -177,6 +75,6 @@ class CalendarActivity : AppCompatActivity() {
|
|||
// Events could've been modified or even deleted, so make sure we have a fresh copy
|
||||
// it would probably be better to pass these around through extras, this extra lookup
|
||||
// isn't a huge deal, it's a potential future optimization though
|
||||
setCalendarForMonth(currentYearMonth)
|
||||
viewModel.refetch()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import com.p_vacho.neat_calendar.R
|
||||
import com.p_vacho.neat_calendar.models.CalendarDay
|
||||
|
||||
class CalendarDayItemAdapter(private val days: List<CalendarDay?>, private val onDayClicked: (CalendarDay) -> Unit) :
|
||||
class CalendarDayItemAdapter(private var days: List<CalendarDay?>, private val onDayClicked: (CalendarDay) -> Unit) :
|
||||
RecyclerView.Adapter<CalendarDayItemAdapter.DayViewHolder>() {
|
||||
|
||||
class DayViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
|
@ -49,4 +49,17 @@ class CalendarDayItemAdapter(private val days: List<CalendarDay?>, private val o
|
|||
}
|
||||
|
||||
override fun getItemCount(): Int = days.size
|
||||
|
||||
/**
|
||||
* This function allows updating the day list.
|
||||
*
|
||||
* This will entirely overwrite the original data.
|
||||
*/
|
||||
fun updateDays(newDays: List<CalendarDay?>) {
|
||||
days = newDays
|
||||
|
||||
// We're resetting to an entirely new list, this is needed
|
||||
@Suppress("NotifyDataSetChanged")
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
package com.p_vacho.neat_calendar.viewmodels
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.p_vacho.neat_calendar.MyApplication
|
||||
import com.p_vacho.neat_calendar.api.RetrofitClient
|
||||
import com.p_vacho.neat_calendar.api.models.EventResponse
|
||||
import com.p_vacho.neat_calendar.models.CalendarDay
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.time.YearMonth
|
||||
import java.time.ZoneId
|
||||
import java.time.format.TextStyle
|
||||
import java.util.Locale
|
||||
|
||||
class CalendarViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private var currentYearMonth: YearMonth? = null
|
||||
|
||||
private val _calendarDays = MutableLiveData<List<CalendarDay?>>()
|
||||
val calendarDays: LiveData<List<CalendarDay?>> = _calendarDays
|
||||
|
||||
private val _monthYearText = MutableLiveData<String>()
|
||||
val monthYearText: LiveData<String> = _monthYearText
|
||||
|
||||
/**
|
||||
* Move to the next month.
|
||||
*
|
||||
* Can only be used after the initial setCalendarMonth.
|
||||
*/
|
||||
fun prevMonth() {
|
||||
setCalendarMonth(currentYearMonth!!.minusMonths(1))
|
||||
}
|
||||
|
||||
/**
|
||||
* Move to the next month.
|
||||
*
|
||||
* Can only be used after the initial setCalendarMonth.
|
||||
*/
|
||||
fun nextMonth() {
|
||||
setCalendarMonth(currentYearMonth!!.plusMonths(1))
|
||||
}
|
||||
|
||||
/**
|
||||
* Refetch the data for the currently selected month.
|
||||
*
|
||||
* Can only be used after the initial setCalendarMonth.
|
||||
*/
|
||||
fun refetch() {
|
||||
setCalendarMonth(currentYearMonth!!)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the view model for a specific month.
|
||||
*/
|
||||
fun setCalendarMonth(yearMonth: YearMonth) {
|
||||
currentYearMonth = yearMonth
|
||||
|
||||
val firstDayOfMonth = yearMonth.atDay(1)
|
||||
val daysInMonth = yearMonth.lengthOfMonth()
|
||||
|
||||
// Get localized month name and year name
|
||||
val monthYearText = "${
|
||||
yearMonth.month.getDisplayName(
|
||||
TextStyle.FULL,
|
||||
Locale.getDefault()
|
||||
)
|
||||
} ${yearMonth.year}"
|
||||
_monthYearText.value = monthYearText
|
||||
|
||||
viewModelScope.launch {
|
||||
val eventsByDay = withContext(Dispatchers.IO) { fetchEventsForMonthInternal(yearMonth) }
|
||||
|
||||
val startOffset = firstDayOfMonth.dayOfWeek.ordinal
|
||||
val days = mutableListOf<CalendarDay?>()
|
||||
|
||||
for (i in 1..startOffset) {
|
||||
days.add(null)
|
||||
}
|
||||
|
||||
for (i in 1..daysInMonth) {
|
||||
val fullDate = yearMonth.atDay(i)
|
||||
val eventsForDay = eventsByDay[i] ?: emptyList()
|
||||
days.add(CalendarDay(date = fullDate, events = eventsForDay))
|
||||
}
|
||||
|
||||
_calendarDays.value = days
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain all of the events that occur for each day in the given month from the API.
|
||||
*
|
||||
* The returned structure is a hashmap with int keys, being the day numbers in the month.
|
||||
* If an event spans multiple days, it will be added for each of those days.
|
||||
*
|
||||
* Note that this also includes the events this user was invited into.
|
||||
*/
|
||||
private suspend fun fetchEventsForMonthInternal(yearMonth: YearMonth): Map<Int, List<EventResponse>> {
|
||||
// Use the system's default time zone
|
||||
val zoneId = ZoneId.systemDefault()
|
||||
|
||||
// Get the start and end of the month in the local time zone
|
||||
val startFrom = yearMonth.atDay(1).atStartOfDay(zoneId).toOffsetDateTime()
|
||||
val endTo = yearMonth.atEndOfMonth().atTime(23, 59, 59).atZone(zoneId).toOffsetDateTime()
|
||||
|
||||
// Obtain current (logged in) user ID
|
||||
val userId = (getApplication<MyApplication>()).tokenManager.userId ?: throw IllegalStateException("User ID is not set")
|
||||
|
||||
// BUG: Events that last longer than a month will not show
|
||||
// up in the following month due to the startFrom filter here.
|
||||
val userEvents = RetrofitClient.eventsService.userEvents(
|
||||
userId = userId,
|
||||
startFrom = startFrom,
|
||||
startTo = endTo
|
||||
)
|
||||
val invitedEvents = RetrofitClient.eventsService.userEventsInvited(
|
||||
userId = userId,
|
||||
startFrom = startFrom,
|
||||
startTo = endTo,
|
||||
inviteStatus = "accepted"
|
||||
)
|
||||
|
||||
val allEvents = userEvents + invitedEvents
|
||||
|
||||
// Create a mapping of days to events
|
||||
val eventsByDay = mutableMapOf<Int, MutableList<EventResponse>>()
|
||||
for (event in allEvents) {
|
||||
val startDay = maxOf(event.start_time.dayOfMonth, 1)
|
||||
val endDay = minOf(event.end_time.dayOfMonth, yearMonth.lengthOfMonth())
|
||||
for (day in startDay..endDay) {
|
||||
eventsByDay.computeIfAbsent(day) { mutableListOf() }.add(event)
|
||||
}
|
||||
}
|
||||
|
||||
return eventsByDay
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue