iT邦幫忙

2024 iThome 鐵人賽

DAY 18
0

前言

昨天我們提到,我們的APP原本要更新資料需要重新啟動APP才能獲取新的資料,因為我們將請求資料的動作放在 onCreate裡面,今天我打算在原有的APP裡面添加了下拉刷新功能,讓我們使用起來更方便。

下拉刷新

下拉刷新在現在的APP中已經是很常見的功能了
https://ithelp.ithome.com.tw/upload/images/20241002/201626492mtrS8RnR5.png
(就是畫面上方的那個重整箭頭)
為了實踐這個功能,我們需要使用使用了SwipeRefresh來實現下拉刷新的功能,雖然該功能在Compose Material 1.3.0中,Accompanist SwipeRefresh 已被 PullRefresh 取代,但她還是可以在我們的專案中使用(升級版本問題太多,先不管他)
先附上今天的程式碼

  • kotlin
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 } // 讓子組件更新事件列表
            )
        }
    }
}

1.SwipeRefresh

SwipeRefresh是一個提供下拉刷新功能的組件,當用戶下拉時,它會觸發 onRefresh 回調,然後呼叫 API 獲取新的數據並更新 UI。state參數用來表示當前是否處於刷新狀態。

2.onRefresh

當觸發下拉刷新時,這個回調會執行,首先會將 isRefreshing.value 設為 true,表示正在刷新。
然後通過 googleCalendarAuth?.fetchEvents 來從 Google Calendar 獲取新的日曆事件。
獲取事件成功後,將事件存儲在 calendarEvents 中。
如果當前正在顯示單日行事曆(showDayView 為 true),並且 selectedDate 不為空,那麼會更新 selectedEvents 以顯示當天的事件。
刷新結束時,將 isRefreshing.value 設為 false,這樣 UI 會停止顯示刷新狀態。

3.verticalScroll(scrollState)

同時由與我們的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

  • kotlin
@Composable
fun CalendarPager(
    events: List<Event>,
    showDayView: Boolean,
    selectedDate: String,
    selectedEvents: List<Event>,
    onShowDayViewChanged: (Boolean) -> Unit,
    onSelectedDateChanged: (String) -> Unit,
    onSelectedEventsChanged: (List<Event>) -> Unit
) {
    // 其他代碼...
}

然後修改CalendarPager

  • kotlin
@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裡都各新增了一個事件https://ithelp.ithome.com.tw/upload/images/20241002/20162649qLl1s5WiPa.pnghttps://ithelp.ithome.com.tw/upload/images/20241002/20162649TlxAzASR5K.png
呈現效果如下
https://ithelp.ithome.com.tw/upload/images/20241002/20162649hBsmamq3m9.png
然後我們再新增其他事件
https://ithelp.ithome.com.tw/upload/images/20241002/20162649UrcNZVfGUT.pnghttps://ithelp.ithome.com.tw/upload/images/20241002/20162649rIJQZC5tIL.png
然後下拉刷新,呈現效果如下,可以看到我們新的事件已經成功刷新到畫面上了
https://ithelp.ithome.com.tw/upload/images/20241002/20162649Ose6sHKH7Z.png

後話

今天的內容就到這邊了,本來以為是簡單的功能,但因為要實現在單日行事曆裡也能同步查了好久(看看那越來越多的log),程式碼越寫越多,想要更新一個新的功能時,都會遇到越來越多的問題,這是我一開始考慮不周到的關係,到時候在整個專案告一段落時,可能需要重新盤過一遍才行,感謝你今天的觀看,我們明天再見(我還沒想好明天寫啥)。


上一篇
Day17:連結GOOGLE Task到代辦事項
下一篇
Day19:自動刷新APP獲取新的資料
系列文
github裡永遠有一個還沒做的SideProject :用Kotlin來開發點沒用的酷東西30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言