commit f8436373490c6f0214c780d997c880d5e0dd1598
parent 85ae8d23b9312908e10f6fa503909bb07a21dd47
Author: MTRNord <MTRNord@users.noreply.github.com>
Date: Fri, 1 Aug 2025 22:12:51 +0200
Fix performance problems
Diffstat:
7 files changed, 161 insertions(+), 168 deletions(-)
diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/CalendarScreen.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/CalendarScreen.kt
@@ -15,6 +15,7 @@ import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -30,7 +31,11 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import space.midnightthoughts.nordiccalendar.R
import space.midnightthoughts.nordiccalendar.components.DateRangeHeader
+import space.midnightthoughts.nordiccalendar.util.Event
import space.midnightthoughts.nordiccalendar.viewmodels.CalendarViewModel
+import space.midnightthoughts.nordiccalendar.viewmodels.DayViewModel
+import space.midnightthoughts.nordiccalendar.viewmodels.MonthViewModel
+import space.midnightthoughts.nordiccalendar.viewmodels.WeekViewModel
/**
* CalendarScreen is the main composable for displaying the calendar view.
@@ -55,54 +60,65 @@ fun CalendarScreen(
/**
* State holding the currently selected tab (0=month, 1=week, 2=day).
*/
- val selectedTab = remember(calendarViewModel) {
- calendarViewModel.selectedTab
- }.collectAsState(initial = 0)
+ val selectedTab by calendarViewModel.selectedTab.collectAsState()
/**
* Get the appropriate specialized view model based on selected tab.
*/
- val activeViewModel = when (selectedTab.value) {
- 0 -> calendarViewModel.monthViewModel
- 1 -> calendarViewModel.weekViewModel
- 2 -> calendarViewModel.dayViewModel
- else -> calendarViewModel.monthViewModel
+ val activeViewModel = remember(selectedTab) {
+ when (selectedTab) {
+ 0 -> calendarViewModel.monthViewModel
+ 1 -> calendarViewModel.weekViewModel
+ 2 -> calendarViewModel.dayViewModel
+ else -> calendarViewModel.monthViewModel
+ }
}
/**
- * State holding the list of events for the current view.
+ * Collect events state from the main CalendarViewModel
*/
- val events = remember(selectedTab.value, calendarViewModel) {
- when (selectedTab.value) {
- 0 -> calendarViewModel.monthViewModel.events
- 1 -> calendarViewModel.weekViewModel.events
- 2 -> calendarViewModel.dayViewModel.events
- else -> calendarViewModel.monthViewModel.events
- }
- }.collectAsState(initial = emptyList())
+ val eventsList by calendarViewModel.events.collectAsState()
/**
* State indicating whether a refresh is in progress.
*/
- val isRefreshing = remember(activeViewModel) {
- activeViewModel.isRefreshing
- }.collectAsState(initial = false)
+ val isRefreshing by calendarViewModel.isRefreshing.collectAsState()
/**
* State for pull-to-refresh gesture.
*/
val pullToRefreshState = rememberPullToRefreshState()
+
+ // Memoized callbacks to prevent recomposition
+ val onRefresh = remember(calendarViewModel) {
+ { calendarViewModel.refreshEvents() }
+ }
+
+ val onTabSelected = remember(calendarViewModel) {
+ { tab: Int -> calendarViewModel.setTab(tab) }
+ }
+
+ val onPrevious = remember(activeViewModel) {
+ { activeViewModel.navigateToPrevious() }
+ }
+
+ val onNext = remember(activeViewModel) {
+ { activeViewModel.navigateToNext() }
+ }
+
+ val onToday = remember(activeViewModel) {
+ { activeViewModel.navigateToToday() }
+ }
+
PullToRefreshBox(
state = pullToRefreshState,
modifier = modifier,
- onRefresh = {
- activeViewModel.refreshEvents()
- },
- isRefreshing = isRefreshing.value,
+ onRefresh = onRefresh,
+ isRefreshing = isRefreshing,
indicator = {
Indicator(
modifier = Modifier.align(Alignment.TopCenter),
- isRefreshing = isRefreshing.value,
+ isRefreshing = isRefreshing,
containerColor = MaterialTheme.colorScheme.primaryContainer,
color = MaterialTheme.colorScheme.onPrimaryContainer,
state = pullToRefreshState
@@ -111,42 +127,71 @@ fun CalendarScreen(
) {
Column(modifier = Modifier.fillMaxSize()) {
CalendarTabBar(
- selectedTab = selectedTab.value,
- onTabSelected = { calendarViewModel.setTab(it) })
+ selectedTab = selectedTab,
+ onTabSelected = onTabSelected
+ )
DateRangeHeader(
- selectedTab = selectedTab.value,
+ selectedTab = selectedTab,
calendarViewModel = activeViewModel,
- onPrev = { activeViewModel.navigateToPrevious() },
- onNext = { activeViewModel.navigateToNext() },
- onToday = { activeViewModel.navigateToToday() }
+ onPrev = onPrevious,
+ onNext = onNext,
+ onToday = onToday
+ )
+
+ CalendarContent(
+ selectedTab = selectedTab,
+ eventsList = eventsList,
+ navController = navController,
+ monthViewModel = calendarViewModel.monthViewModel,
+ weekViewModel = calendarViewModel.weekViewModel,
+ dayViewModel = calendarViewModel.dayViewModel
+ )
+ }
+ }
+}
+
+@Composable
+private fun CalendarContent(
+ selectedTab: Int,
+ eventsList: List<Event>,
+ navController: NavController,
+ monthViewModel: MonthViewModel,
+ weekViewModel: WeekViewModel,
+ dayViewModel: DayViewModel
+) {
+ if (eventsList.isEmpty()) {
+ Text(
+ text = stringResource(R.string.no_events_found),
+ modifier = Modifier.padding(16.dp)
+ )
+ } else {
+ when (selectedTab) {
+ 0 -> MonthView(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp),
+ navController = navController,
+ monthViewModel = monthViewModel,
+ events = eventsList
+ )
+
+ 1 -> WeekView(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp),
+ navController = navController,
+ weekViewModel = weekViewModel,
+ events = eventsList
+ )
+
+ 2 -> DayView(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp),
+ navController = navController,
+ dayViewModel = dayViewModel,
+ events = eventsList
)
- if (events.value.isEmpty()) {
- Text(stringResource(R.string.no_events_found), modifier = Modifier.padding(16.dp))
- } else when (selectedTab.value) {
- 0 -> MonthView(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 8.dp),
- navController,
- calendarViewModel.monthViewModel,
- )
-
- 1 -> WeekView(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 8.dp),
- navController,
- calendarViewModel.weekViewModel,
- )
-
- 2 -> DayView(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 8.dp),
- navController,
- calendarViewModel.dayViewModel,
- )
- }
}
}
}
diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/DayView.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/DayView.kt
@@ -204,11 +204,13 @@ fun DayView(
modifier: Modifier = Modifier,
navController: NavController,
dayViewModel: space.midnightthoughts.nordiccalendar.viewmodels.DayViewModel,
+ events: List<space.midnightthoughts.nordiccalendar.util.Event> = emptyList()
) {
val hourHeightDp = 64.dp
val timeColumnWidth = 64.dp
val density = LocalDensity.current
val hourHeightPx = with(density) { hourHeightDp.toPx() }
+
val dayStart = remember(dayViewModel) { dayViewModel.startMillis }.collectAsState()
val dayEnd = remember(dayViewModel) { dayViewModel.endMillis }.collectAsState()
val appLocale = getCurrentAppLocale(LocalContext.current)
@@ -229,15 +231,13 @@ fun DayView(
val scrollTo = (nowOffsetY - visibleHeightPx / 2).toInt().coerceAtLeast(0)
val scrollState = rememberScrollState(initial = scrollTo)
- val events = remember(dayViewModel) { dayViewModel.events }.collectAsState()
-
BoxWithConstraints(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.semantics {
collectionInfo = CollectionInfo(
- rowCount = events.value.size,
+ rowCount = events.size,
columnCount = 1,
)
}
@@ -253,8 +253,8 @@ fun DayView(
)
// Events (optimized as before)
- val eventColumns = remember(events.value, maxWidthPx) { assignColumns(events.value) }
- events.value.forEach { event ->
+ val eventColumns = remember(events, maxWidthPx) { assignColumns(events) }
+ events.forEach { event ->
val triple =
eventColumns.find { it.first.eventId == event.eventId && it.first.calendar.id == event.calendar.id }
if (triple != null) {
diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/MonthView.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/MonthView.kt
@@ -41,13 +41,13 @@ import java.time.format.TextStyle
import java.util.Locale
/**
- * MonthView displays a monthly calendar grid with days and events.
- * It highlights the current day, shows a grid of days for the month, and displays up to three events per day as compact chips.
- * If there are more than three events, a "+N more" chip is shown.
+ * MonthView displays the calendar in a monthly format, showing all days of the month
+ * with events and provides navigation to view event details.
*
* @param modifier Modifier for styling and layout.
* @param navController NavController for navigation actions.
* @param monthViewModel Specialized ViewModel for month view operations.
+ * @param events List of events to display.
*/
@SuppressLint("UnusedBoxWithConstraintsScope")
@OptIn(ExperimentalMaterial3Api::class)
@@ -56,8 +56,8 @@ fun MonthView(
modifier: Modifier = Modifier,
navController: NavController,
monthViewModel: space.midnightthoughts.nordiccalendar.viewmodels.MonthViewModel,
+ events: List<space.midnightthoughts.nordiccalendar.util.Event> = emptyList()
) {
- val events by monthViewModel.events.collectAsState()
val startMillis by monthViewModel.startMillis.collectAsState()
val today = LocalDate.now()
val zoneId = ZoneId.systemDefault()
diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/WeekView.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/screens/WeekView.kt
@@ -7,34 +7,30 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
+import space.midnightthoughts.nordiccalendar.util.Event
import space.midnightthoughts.nordiccalendar.viewmodels.WeekViewModel
/**
- * WeekView displays a list of events for the current week.
- * Each event is shown as a clickable text item that navigates to the event details view.
+ * WeekView displays the calendar in a weekly format, showing events for the current week.
*
* @param modifier Modifier for styling and layout.
* @param navController NavController for navigation actions.
* @param weekViewModel Specialized ViewModel for week view operations.
+ * @param events List of events to display.
*/
@Composable
fun WeekView(
modifier: Modifier = Modifier,
navController: NavController,
weekViewModel: WeekViewModel,
+ events: List<Event> = emptyList()
) {
// TODO: Implement WeekView
- val events = remember(weekViewModel) {
- weekViewModel.events
- }.collectAsState(initial = emptyList())
-
LazyColumn(modifier = Modifier.fillMaxSize()) {
- items(events.value) { event ->
+ items(events) { event ->
Text(
event.title, modifier = Modifier
.padding(8.dp)
diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/BaseCalendarViewModel.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/BaseCalendarViewModel.kt
@@ -1,18 +1,11 @@
package space.midnightthoughts.nordiccalendar.viewmodels
-import android.content.Context
import android.util.Log
import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.launch
import space.midnightthoughts.nordiccalendar.data.CalendarRepository
-import space.midnightthoughts.nordiccalendar.util.Event
/**
* Base class for calendar view models providing common functionality for event management,
@@ -51,23 +44,6 @@ abstract class BaseCalendarViewModel(
val isRefreshing: StateFlow<Boolean> = _isRefreshing.asStateFlow()
/**
- * StateFlow of the current list of events for this view's time range.
- * The context is passed as a parameter to avoid holding a reference to it.
- */
- fun getEventsFlow(context: Context): StateFlow<List<Event>> = combine(
- repository.calendarsFlow,
- _startMillis,
- _endMillis
- ) { calendars, start, end ->
- val selectedIds = calendars.filter { it.selected }.map { it.id }
- repository.getEventsForCalendars(context, selectedIds, start, end)
- }.stateIn(
- viewModelScope,
- SharingStarted.WhileSubscribed(5000),
- emptyList()
- )
-
- /**
* Sets the time range for this view and validates it.
* Subclasses should override this to enforce their specific time range requirements.
*
@@ -100,17 +76,6 @@ abstract class BaseCalendarViewModel(
}
/**
- * Schedules reminders for the given events.
- * The context is passed as a parameter to avoid holding a reference to it.
- */
- fun scheduleRemindersForEvents(context: Context, events: List<Event>) {
- viewModelScope.launch {
- Log.d("BaseCalendarViewModel", "Scheduling reminders for ${events.size} events")
- repository.scheduleRemindersForEvents(context, events)
- }
- }
-
- /**
* Navigates to the current time period (today) for this view type.
* Subclasses must implement this to define what "today" means for their view.
*/
diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/CalendarViewModel.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/CalendarViewModel.kt
@@ -3,12 +3,19 @@ package space.midnightthoughts.nordiccalendar.viewmodels
import android.content.Context
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
import space.midnightthoughts.nordiccalendar.data.CalendarRepository
+import space.midnightthoughts.nordiccalendar.util.Event
import javax.inject.Inject
/**
@@ -34,21 +41,21 @@ class CalendarViewModel @Inject constructor(
* Specialized ViewModel for month view with month-specific time range management.
*/
val monthViewModel: MonthViewModel by lazy {
- MonthViewModel(context, repository, savedStateHandle)
+ MonthViewModel(repository, savedStateHandle)
}
/**
* Specialized ViewModel for week view with week-specific time range management.
*/
val weekViewModel: WeekViewModel by lazy {
- WeekViewModel(context, repository, savedStateHandle)
+ WeekViewModel(repository, savedStateHandle)
}
/**
* Specialized ViewModel for day view with day-specific time range management.
*/
val dayViewModel: DayViewModel by lazy {
- DayViewModel(context, repository, savedStateHandle)
+ DayViewModel(repository, savedStateHandle)
}
/**
@@ -73,25 +80,45 @@ class CalendarViewModel @Inject constructor(
}
/**
- * Convenience properties that delegate to the current view model.
+ * Centralized events flow that handles all event loading and reminder scheduling.
*/
- val events
- get() = when (_selectedTab.value) {
- 0 -> monthViewModel.events
- 1 -> weekViewModel.events
- 2 -> dayViewModel.events
- else -> monthViewModel.events
- }
-
- val startMillis get() = currentViewModel.startMillis
- val endMillis get() = currentViewModel.endMillis
- val isRefreshing get() = currentViewModel.isRefreshing
+ val events: StateFlow<List<Event>> = combine(
+ repository.calendarsFlow,
+ currentViewModel.startMillis,
+ currentViewModel.endMillis
+ ) { calendars, start, end ->
+ val selectedIds = calendars.filter { it.selected }.map { it.id }
+ repository.getEventsForCalendars(context, selectedIds, start, end)
+ }.stateIn(
+ viewModelScope,
+ SharingStarted.WhileSubscribed(5000),
+ emptyList()
+ )
+
+ /**
+ * StateFlow indicating if a refresh is in progress.
+ */
+ private val _isRefreshing = MutableStateFlow(false)
+ val isRefreshing: StateFlow<Boolean> = _isRefreshing.asStateFlow()
init {
val tab = savedStateHandle.get<Int?>("tab")
if (tab != null) {
setTab(tab)
}
+
+ // Schedule reminders in background when events change
+ viewModelScope.launch(Dispatchers.IO) {
+ events.collect { eventList ->
+ if (eventList.isNotEmpty()) {
+ try {
+ repository.scheduleRemindersForEvents(context, eventList)
+ } catch (e: Exception) {
+ // Handle exception
+ }
+ }
+ }
+ }
}
/**
@@ -130,6 +157,8 @@ class CalendarViewModel @Inject constructor(
* Delegates refresh to the current view model.
*/
fun refreshEvents() {
+ _isRefreshing.value = true
currentViewModel.refreshEvents()
+ _isRefreshing.value = false
}
}
diff --git a/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/SpecializedCalendarViewModels.kt b/app/src/main/java/space/midnightthoughts/nordiccalendar/viewmodels/SpecializedCalendarViewModels.kt
@@ -1,15 +1,9 @@
package space.midnightthoughts.nordiccalendar.viewmodels
-import android.content.Context
import android.util.Log
import androidx.lifecycle.SavedStateHandle
-import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
-import dagger.hilt.android.qualifiers.ApplicationContext
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.launch
import space.midnightthoughts.nordiccalendar.data.CalendarRepository
-import space.midnightthoughts.nordiccalendar.util.Event
import java.time.LocalDate
import javax.inject.Inject
@@ -19,24 +13,12 @@ import javax.inject.Inject
*/
@HiltViewModel
class MonthViewModel @Inject constructor(
- @param:ApplicationContext private val context: Context,
repository: CalendarRepository,
savedStateHandle: SavedStateHandle
) : BaseCalendarViewModel(repository) {
- /**
- * StateFlow of events for the current month.
- */
- val events: StateFlow<List<Event>> by lazy { getEventsFlow(context) }
-
init {
initializeDefaultTimeRange()
- // Schedule reminders when events change
- viewModelScope.launch {
- events.collect { eventList ->
- scheduleRemindersForEvents(context, eventList)
- }
- }
}
override fun initializeDefaultTimeRange() {
@@ -112,24 +94,12 @@ class MonthViewModel @Inject constructor(
*/
@HiltViewModel
class WeekViewModel @Inject constructor(
- @param:ApplicationContext private val context: Context,
repository: CalendarRepository,
savedStateHandle: SavedStateHandle
) : BaseCalendarViewModel(repository) {
- /**
- * StateFlow of events for the current week.
- */
- val events: StateFlow<List<Event>> by lazy { getEventsFlow(context) }
-
init {
initializeDefaultTimeRange()
- // Schedule reminders when events change
- viewModelScope.launch {
- events.collect { eventList ->
- scheduleRemindersForEvents(context, eventList)
- }
- }
}
override fun initializeDefaultTimeRange() {
@@ -203,16 +173,10 @@ class WeekViewModel @Inject constructor(
*/
@HiltViewModel
class DayViewModel @Inject constructor(
- @param:ApplicationContext private val context: Context,
repository: CalendarRepository,
savedStateHandle: SavedStateHandle
) : BaseCalendarViewModel(repository) {
- /**
- * StateFlow of events for the current day.
- */
- val events: StateFlow<List<Event>> by lazy { getEventsFlow(context) }
-
init {
// Check for date argument from navigation
val dateArg = savedStateHandle.get<String?>("date")
@@ -221,12 +185,6 @@ class DayViewModel @Inject constructor(
} else {
initializeDefaultTimeRange()
}
- // Schedule reminders when events change
- viewModelScope.launch {
- events.collect { eventList ->
- scheduleRemindersForEvents(context, eventList)
- }
- }
}
override fun initializeDefaultTimeRange() {