0%

(2022.08.22 更新) 從 Expo SDK 45 之後 expo-app-loading 已經 deprecated,只能使用 expo-splash-screen

介紹


Expo 提供了兩個 API
讓你能夠修改 app 的讀取畫面
分別是 AppLoadingSplashScreen

AppLoading 是一個元件,當整個app渲染的只有AppLoading而沒有別的元件時
它就會讓 splash screen 持續顯示
所以我們就可以在 app 剛打開正在進行 API call 時先渲染 AppLoading
等資料都得到了再恢復顯示主畫面

另一個 SplashScreen 就不是元件了
但他提供兩個函數

  1. SplashScreen.preventAutoHideAsyce():防止 splash screen 在程式讀取完後關閉
  2. SplashScreen.hideAsyce(): 觸發時,關閉 spalsh screen

簡單來說他讓我們能自由控制 splash screen 消失的時機
除了可以達到跟單獨使用 AppLoading 一樣的效果外
更厲害的是我們也能藉此讓App初始化完成後
由顯示靜態 splash screen 切換成動畫(看你要放 gif 或寫 animation 都可以)

使用方式


首先下載並引入

1
2
expo install expo-app-loading
expo install expo-splash-screen
1
2
import AppLoading from 'expo-app-loading';
import * as SplashScreen from 'expo-splash-screen';

再來,若你只想單獨使用 AppLoading 可以這樣做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const [isLoading, setIsLoading] = useState(true)

const fetchData = async () => {
try{
await fetch('...')
} catch(e){
console.log(e)
}finally{
setIsLoading(false)
}
}

useEffect(()=>{
fetchData()
},[])

if (isLoading) {
return <AppLoading/>
} else return <App/>

其實這可以應付大部分的狀況
例如在進入app前先讀取字型等等的
然而使用 SplashScreen 的話靈活度更高

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 const [isLoading, setIsLoading] = useState(true)

const fetchData = async () => {
try{
await SplashScreen.preventAutoHideAsync() //等等!先別關!
await fetch('...') // 做一些事
} catch(e){
console.log(e)
}finally{
SplashScreen.preventAutoHideAsync() //好可以關了
}
}

useEffect(()=>{
fetchData()
},[])

if (isLoading) {
return null //不渲染東西 但因為 splash screen 還蓋在畫面上,所以不會沒畫面
} else return <App/>

(2022.08.22 更新) 從 Expo SDK 45 之後 expo-app-loading 已經 deprecated,只能使用 expo-splash-screen

這篇會示範如何實作購物車app常有的全選功能

之前曾經用錯誤的方式做導致卡住
而且腦筋轉不過來
一時想不到其他變通方式
只好到社團問
一問才發現其實很容易
不過應該很多事情都是這樣啦
不懂的時候就好像很難
會了之後就覺得自己好蠢XD

首先做出選單
假設 MOCK 是我們資料庫的資料,或是從api獲取的資料
data state 是我們的資料變數
之所以先把資料放在 react state 中
是因為傳入 FlatList 的資料必須是能夠變動的
這樣我們只要改變資料
就能動態渲染在FlatList中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

const MOCK = [
{ name: 'Andy' },
{ name: 'Beck' },
{ name: 'Carl' },
]

const RenderData = ({ item }) => {

return (
<TouchableOpacity
onPress={() => { }}
style={[styles.item_btn]}>
<Text>{item.name}</Text>
</TouchableOpacity>
)
}

export default function App() {
return (
<View style={styles.container}>

<View>
<TouchableOpacity
onPress={()=>{}}
style={[styles.selectAll_btn]}>
<Text>全選</Text>
</TouchableOpacity>
</View>

<View style={styles.flatlist_container}>
<FlatList
data={MOCK}
renderItem={(cases) => <RenderData item={cases.item} />}
keyExtractor={(cases, index) => index.toString()}
style={{ width: '100%', }}
/>
</View>

</View >
);
}

左圖是 app 剛啟動的畫面
中間圖是選取單個項目時的畫面
右圖是點擊全選按鈕時的畫面

以下是實作全選功能的流程圖

虛線部分是 app 剛啟動時的狀態,這時data也還是初始值
接下來我會宣告一個selectedItem陣列來放我所選的項目物件

1
const [selectedItems, setSelectedItems] = useState([]);

每當項目被點擊時,用some()檢查
如果selectedItem中有這個物件,就用filter()把那個項目從中清除;
反之如果沒有的話,就複製一份丟進去
這個函數會被丟進每個項目中

1
2
3
4
5
6
const onItemSelect = (item) => {
setSelectedItems(
prev => prev.some((prevItem) => prevItem === item)
? prev.filter((prevItem) => prevItem !== item)
: [...prev, item])
}

接著記得把selectedItem傳入各個項目中
以便讓每個項目判斷自己是否在其中,改變isSelected state,藉此渲染自己的樣式

1
2
3
4
5
6
7
// in <RenderData/>
const [isSelected, setIsSelected] = useState(false)
useEffect(() => {
if (selectedItems.some(selectedItem => selectedItem === item)) {
setIsSelected(true)
} else { setIsSelected(false) }
}, [selectedItems])

選取後可能還會編輯項目,這時只要更新data並清空selectedItem就ok了
我這邊就先不做編輯的部分

接著回到app()
別忘了還沒做全選按鈕的功能
策略很簡單,宣告一個isAllSelected state
如果還沒全選,就把data中的所有項目丟進selectedItems
如果已經全選了就把selectedItems清空
我們就叫他selectAll

1
2
3
4
5
const [isAllSelected, setIsAllSelected] = useState(false); //現在有沒有全選?
const selectAll = () => {
if (isAllSelected) setSelectedItems([])
else setSelectedItems([...MOCK])
}

在useEffect中判斷有沒有全選

1
2
3
4
5
useEffect(() => {
if (selectedItems.length === MOCK.length) {
setIsAllSelected(true)
} else { setIsAllSelected(false) }
}, [selectedItems]) // 每當selectedItems改變,就檢查一次

把前面提到的onItemSelect和剛剛的selectAll畫成流程圖:

最後的成果:

完整程式碼放在這邊:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103

import React, { useEffect, useState } from 'react';
import { FlatList, StyleSheet, Text, TouchableOpacity, View } from 'react-native';

const MOCK = [
{ name: 'Andy' },
{ name: 'Beck' },
{ name: 'Carl' },
]

const RenderData = ({ item, selectedItems, onItemSelect }) => {

const [isSelected, setIsSelected] = useState(false);
useEffect(() => {

if (selectedItems.some(selectedItem => selectedItem === item)) {
setIsSelected(true)
} else { setIsSelected(false) }
}, [selectedItems])

return (
<TouchableOpacity
onPress={() => { onItemSelect() }}
style={[styles.item_btn, { backgroundColor: isSelected ? 'gray' : 'transparent' }]}>
<Text>{item.name}</Text>
</TouchableOpacity>
)
}

export default function App() {
const [selectedItems, setSelectedItems] = useState([]);
const [isAllSelected, setIsAllSelected] = useState(false);
const onItemSelect = (item) => {
setSelectedItems(prev => prev.some((prevItem) => prevItem === item) ? prev.filter((prevItem) => prevItem !== item) : [...prev, item])
}
const selectAll = () => {
if (isAllSelected) {
setSelectedItems([])
} else {
setSelectedItems([...MOCK])
}
}


useEffect(() => {
if (selectedItems.length === MOCK.length) {
setIsAllSelected(true)
} else { setIsAllSelected(false) }
}, [selectedItems])

return (
<View style={styles.container}>

<View>
<TouchableOpacity
onPress={selectAll}
style={[styles.selectAll_btn, { backgroundColor: isAllSelected ? 'gray' : 'transparent' }]}>
<Text>全選</Text>
</TouchableOpacity>
</View>

<View style={styles.flatlist_container}>
<FlatList
data={MOCK}
renderItem={(cases) => <RenderData item={cases.item} selectedItems={selectedItems} onItemSelect={() => { onItemSelect(cases.item) }} />}
keyExtractor={(cases, index) => index.toString()}
style={{ width: '100%', }}
/>
</View>

</View >
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
selectAll_btn: {
borderWidth: 1,
width: 80,
height: 40,
justifyContent: 'center',
alignItems: 'center',
},
flatlist_container: {
borderWidth: 1,
width: 200,
height: 150,
alignItems: 'center',
marginTop: 20,
},
item_btn: {
height: 40,
justifyContent: 'center',
alignItems: 'center',
},
});


延續 上一集
確定元件的輸入輸出介面後
(回顧四個基本屬性: isVisible, displayDate, mode, onConfirm)
現在要開始實作功能啦

我們可以把實作步驟切分成幾個部分:

  1. 排版: 如何動態顯示每個月的日期數以及星期
  2. 單日日期操作: 每次點選都會更新 data.date
  3. 多日日期操作: 有時點選是設定開始日date.startDate,有時是結束日endDate,需設計操控流程
  4. 效能問題: 目前為止每次當元件內有一堆按鈕的時候都會有不可忽略的卡頓(因為每次點擊都會讓每個按鈕重新渲染),希望我能研究出如何減少這個卡頓時間

今天這章節就來看看如何做日期的動態顯示吧!

動態顯示日期

首先我們來想一想
一般的日曆app都怎麼排版?
每個月第一天的星期都不一樣
並且前後分別會補上個月最後幾天和下個月前幾天
直到補滿整行,像這樣(網路找的圖):

要達到這個目標,必須先有三個資訊:

  1. 本月有幾天? days
  2. 本月第一天是星期幾? firstDay
  3. 上個月有幾天? prevMonthDays

所以我打算做一個useDaysOfMonthcustom hook 來提供這些訊息

  • Custom Hook 是一組包裝起來的邏輯,有自己的 life cycle,你可以在其中使用useStateuseEffect之類的 React hook。如果你還記得,hook 不是有很多使用上的限制嗎? 例如只能存在於元件內的第一層而不能被包在元件內的函數內,這項限制讓你沒辦法把重複用的邏輯包起來。而 custom hook 就是為此而生。

Custom Hook: useDaysOfMonth

custom hook 有個條件,就是一定要是use開頭,如此一來 React 運行時才能判斷元件內的 hook 是不是都在最上層,所以別忘了這步。
除此要求之外,一個 custom hook 看起來就像是不做渲染的 React 元件,或是可以放 hook 的一般函數。

那就開始吧,首先我們建立一個檔案叫 useDaysOfMonth.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// in useDaysOfMonth.jsx
import { useState } from 'react'

const useDaysOfMonth =(inputTime)=>{ //輸入一個時間,這會是來自元件的 displayDate 屬性
const [days, setDays] = useState(0);
const [firstDay, setFirstDay] = useState(0)
const [prevMonthDays, setPrevMonthDays] = useState(0);

// 這裡要做點事

return {days, firstDay, prevMonthDays} //輸出三個我們要的資訊,皆為 number type
}

export default useDaysOfMonth

接下來我們需要拆解輸入進來的日期物件,得到年份,月份

1
2
const year = inputTime.getFullYear()
const month = inputTime.getMonth()

然後我們就要依照年月來獲得 days, firstDay, prevMonthDays 三個資訊
*注意不要用日期物件的setDate來做這些事,因為Date()的 method 都會 mutate(會改變物件自身的值,而不會另外建立一個物件)

1
2
3
4
5
6
7
8
9
10
11
12
setDays( new Date(year,month+1,0).getDate() )
// 在 Date constructor 中,如果日期填0,則會視為上個月的最後一天
// new Date(2021,2,1) => 三月一號
// new Date(2021,2,0) => 二月二十八號
// new Date(2021,2,-1) => 二月二十七號
// 所以若要獲得本月最後一天的日期,只要把月份+1,然後日期設為0即可

setFirstDay(new Date(year,month,1).getDay())
// 很直觀,取本月第一天的星期

setPrevMonthDays(new Date(year, month, 0).getDate())
// 同 setDays() 不過月份不用+1

過程很簡單,都是基本的Date物件取值
再來我們希望在任何使用這個 hook 的元件中,每當 input 值改變時,輸出值也要跟著更新
這不就是useEffect的功能嗎!
所以我們將以上運算放進useEffect中,並將inputTime設為 dependency

1
2
3
4
5
6
7
useEffect(()=>{
let year = inputTime.getFullYear()
let month = inputTime.getMonth()
setDays(new Date(year, month + 1, 0).getDate())
setFirstDay(new Date(year, month, 1).getDay())
setPrevMonthDays(new Date(year, month, 0).getDate())
},[inputTime])

完成! 完整的code如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { useState, useEffect } from 'react'

const useDaysOfMonth = (inputTime) => {
const [days, setDays] = useState(0);
const [firstDay, setFirstDay] = useState(0)
const [prevMonthDays, setPrevMonthDays] = useState(0);

useEffect(() => {
let year = inputTime.getFullYear()
let month = inputTime.getMonth()
setDays(new Date(year, month + 1, 0).getDate())
setFirstDay(new Date(year, month, 1).getDay())
setPrevMonthDays(new Date(year, month, 0).getDate())
}, [inputTime])
return { days, firstDay, prevMonthDays }
}

export default useDaysOfMonth

一個小細節,我把輸出的參數用大括號(object literal)包起來,這樣在存取參數時就不需考慮順序
什麼意思呢? 看以下的使用方式就知道了

1
const {firstDay, prevMonthDays, days} = useDaysOfMonth(new Date())

可以看到我取得三個參數的順序不同,但仍然會work
缺點是這樣就一定要把參數名稱打對了,想要更改參數名就要打{firstDay: xxx}
反之,如果你回傳的是用中括號(array literal)包起來[firstDay, prevMonthDays, days]
取得參數時的順序就很重要了,不過就可以直接改名