昨天我們提到,我們的APP原本要更新資料需要重新啟動APP才能獲取新的資料,因為我們將請求資料的動作放在 onCreate
裡面,今天我打算在原有的APP裡面添加了下拉刷新功能,讓我們使用起來更方便。
下拉刷新在現在的APP中已經是很常見的功能了
(就是畫面上方的那個重整箭頭)
為了實踐這個功能,我們需要使用使用了SwipeRefresh
來實現下拉刷新的功能,雖然該功能在Compose Material 1.3.0中,Accompanist SwipeRefresh 已被 PullRefresh 取代,但她還是可以在我們的專案中使用(升級版本問題太多,先不管他)
先附上今天的程式碼
SwipeRefresh(
state = swipeRefreshState, // SwipeRefresh 的狀態控制,確保可以根據刷新進行 UI 變化
onRefresh = {
Log.d("onRefresh", "Refresh started")
isRefreshing.value = true // 開始刷新,將刷新狀態設為 true
// 呼叫 Google Calendar API 來獲取最新事件
googleCalendarAuth?.fetchEvents(
onEventsFetched = { events ->
Log.d("onRefresh1", "Fetched events: $events")
calendarEvents = events // 更新全局的日曆事件列表
// 打印 selectedDate 和 showDayView 狀態,確認刷新時是否顯示單日行事曆
Log.d("onRefresh", "selectedDate: $selectedDate, showDayView: $showDayView")
// 如果當前是單日行事曆視圖,並且有選中的日期,則更新單日事件列表
if (selectedDate.isNotEmpty() && showDayView) {
Log.d("onRefresh3", "selectedDate is not empty, updating selectedEvents")
Log.d("onRefresh4", "selectedDate: $selectedDate, showDayView: $showDayView")
// 篩選出符合選中日期的事件
val newSelectedEvents = events.filter { event ->
val eventDate = event.start.date?.toString() ?: ""
eventDate == selectedDate
}
// 更新單日事件列表
selectedEvents = newSelectedEvents.toList()
Log.d("onRefresh", "Updated selectedEvents: $selectedEvents")
}
// 刷新完成,將刷新狀態設為 false
isRefreshing.value = false
},
onError = { error ->
// 當發生錯誤時,打印錯誤訊息並結束刷新
Log.e("MainActivity", "Error fetching calendar events: ${error.message}")
isRefreshing.value = false
}
)
}
) {
// SwipeRefresh 內部的內容佈局
// 當下拉刷新時,這些內容會被重新載入
Row(
modifier = Modifier
.fillMaxSize()
.padding(0.dp)
.background(MaterialTheme.colorScheme.background)
) {
// 左側:顯示時間與代辦事項
Column(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.verticalScroll(scrollState)
.padding(4.dp)
.background(MaterialTheme.colorScheme.background)
) {
// 顯示時間區塊
Box(
modifier = Modifier
.weight(2f)
.fillMaxWidth()
.padding(4.dp)
.background(MaterialTheme.colorScheme.background),
contentAlignment = Alignment.Center
) {
FlipClock() // 顯示時鐘
}
Spacer(modifier = Modifier.height(2.dp))
// 顯示代辦事項區塊
Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.padding(4.dp)
.background(MaterialTheme.colorScheme.background),
contentAlignment = Alignment.Center
) {
ToDoList(tasks = tasks) // 顯示代辦事項
}
}
Spacer(modifier = Modifier.width(2.dp))
// 右側:顯示日曆
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.padding(4.dp)
.background(MaterialTheme.colorScheme.background),
contentAlignment = Alignment.Center
) {
// CalendarPager 是自定義的日曆元件,負責顯示月曆或單日行事曆
CalendarPager(
events = calendarEvents, // 全局事件列表
showDayView = showDayView, // 當前是否顯示單日行事曆
selectedDate = selectedDate, // 選中的日期
selectedEvents = selectedEvents, // 選中日期的事件列表
onShowDayViewChanged = { showDayView = it }, // 讓子組件改變顯示狀態
onSelectedDateChanged = { selectedDate = it }, // 讓子組件改變選中日期
onSelectedEventsChanged = { selectedEvents = it } // 讓子組件更新事件列表
)
}
}
}
SwipeRefresh
是一個提供下拉刷新功能的組件,當用戶下拉時,它會觸發 onRefresh 回調,然後呼叫 API 獲取新的數據並更新 UI。state
參數用來表示當前是否處於刷新狀態。
當觸發下拉刷新時,這個回調會執行,首先會將 isRefreshing.value 設為 true,表示正在刷新。
然後通過 googleCalendarAuth?.fetchEvents 來從 Google Calendar 獲取新的日曆事件。
獲取事件成功後,將事件存儲在 calendarEvents 中。
如果當前正在顯示單日行事曆(showDayView 為 true),並且 selectedDate 不為空,那麼會更新 selectedEvents 以顯示當天的事件。
刷新結束時,將 isRefreshing.value 設為 false,這樣 UI 會停止顯示刷新狀態。
同時由與我們的UI布局都是當好符合螢幕的基本上是不會觸發滾動行為,因此我們需要使用 .verticalScroll(scrollState)
這會使內容具備可滾動性,這樣當用戶從頂部向下拖動時,如果內容已經滾動到了頂部,則會觸發下拉刷新。如果沒有設置滾動行為(例如,移除 verticalScroll),內容不會滾動,當用戶下拉時,系統無法判斷是否已經滾動到頂部,導致無法正確觸發 SwipeRefresh 的下拉刷新功能。
如果仔細看會發現上述的程式碼有一些部份和之前的不同,這是因為原先showDayView、selectedDate、selectedEvents
等變數都是在各自獨立的kt檔中,並不共通,這會導致刷新的過程會出現不同步的問題,例如我在修改的過程中就發生,在單日行事曆刷新時,並不會更新頁面,一定給回月曆葉面重新點近來才會刷新,因此我們需要調整一下變數的層級
@Composable
fun CalendarLayout() {
// 其他代碼...
// 在此處定義狀態變數
var showDayView by rememberSaveable { mutableStateOf(false) }
var selectedDate by rememberSaveable { mutableStateOf("") }
var selectedEvents by remember { mutableStateOf<List<Event>>(emptyList()) }
// 其他代碼...
}
然後修改會用到這些參數的程式,如CalendarPager
@Composable
fun CalendarPager(
events: List<Event>,
showDayView: Boolean,
selectedDate: String,
selectedEvents: List<Event>,
onShowDayViewChanged: (Boolean) -> Unit,
onSelectedDateChanged: (String) -> Unit,
onSelectedEventsChanged: (List<Event>) -> Unit
) {
// 其他代碼...
}
然後修改CalendarPager
@Composable
fun CalendarPager(
// 參數列表...
) {
val pagerState = rememberPagerState()
if (showDayView) {
// 顯示單日行事曆
DayCalendar(
selectedDate = selectedDate,
events = selectedEvents,
onBack = { onShowDayViewChanged(false) }
)
} else {
// 顯示月曆
HorizontalPager(
count = 12,
state = pagerState,
modifier = Modifier.fillMaxSize()
) { page ->
// 計算當前月份等信息...
MonthlyCalendar(
page = page,
todayDay = todayDay,
isCurrentMonth = isCurrentMonth,
events = events,
onDayClick = { date, dayEvents ->
onSelectedDateChanged(date)
onSelectedEventsChanged(dayEvents)
onShowDayViewChanged(true)
}
)
}
}
}
最後在在MonthlyCalendar
中添加onDayClick
回調函數
@Composable
fun MonthlyCalendar(
// 其他參數...
onDayClick: (String, List<Event>) -> Unit
) {
// 其他代碼...
Box(
modifier = Modifier
// 其他修飾符
.clickable {
if (dayOfMonth in 1..daysInMonth) {
val selectedCalendar = Calendar.getInstance()
selectedCalendar.set(Calendar.YEAR, currentYear)
selectedCalendar.set(Calendar.MONTH, calendar.get(Calendar.MONTH))
selectedCalendar.set(Calendar.DAY_OF_MONTH, dayOfMonth)
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
val dateString = dateFormat.format(selectedCalendar.time)
onDayClick(dateString, eventsForDay)
}
},
contentAlignment = Alignment.Center
) {
// 組件內容
}
// 其他代碼...
}
同時為了SwipeRefresh
功能中的判斷式能正確觸發,我們需要統一selectedDate
回傳的日期格式,我們將所有日期的格式都統一為yyyy-MM-dd
最後呈現的效果如下
為了測試,我在我的google行事曆和task裡都各新增了一個事件
呈現效果如下
然後我們再新增其他事件
然後下拉刷新,呈現效果如下,可以看到我們新的事件已經成功刷新到畫面上了
今天的內容就到這邊了,本來以為是簡單的功能,但因為要實現在單日行事曆裡也能同步查了好久(看看那越來越多的log),程式碼越寫越多,想要更新一個新的功能時,都會遇到越來越多的問題,這是我一開始考慮不周到的關係,到時候在整個專案告一段落時,可能需要重新盤過一遍才行,感謝你今天的觀看,我們明天再見(我還沒想好明天寫啥)。