原來這就是「刷過去」的效果。
置頂說明
後來我發現原來 Stack 有分兩種:
簡單來說一個是用 JS 去寫的,可以客製化的程度比較高,但可能也會有比較多 bug。後者的話則是用原生的 navigation,所以限制會變多,但 bug 會少一點。
這篇文章用的是 Native 的 Stack,不過大致上沒什麼問題,還是可以參考看看。
自我檢測
- 我知道什麼是 Navigation Stack
- 我知道什麼是 Drawer navigation
- 我知道怎麼用 Navigate 來切換 screen
- 我知道怎麼在 Screen 之間傳遞與接收 props
簡介
首先要特別講一下,在 App 裡面的導覽跟 Web 裡的導覽思路是不同的。
我們在 Web 中可以透過「URL」來判斷目前要顯示哪個元件,這很合理沒有問題,但 App 呢?仔細想想看一件事情:
App 有 URL 這玩意兒嗎?
當然沒有。
所以這邊就要介紹一下 App 的導覽思路。
首先核心觀念是「Stack Navigate」,還有一個叫做「Drawer navigation」的觀念,也是蠻重要的,但這之後才會介紹,這一篇還不會提到。總之,值得關注的是 Stack 這個關鍵字(是一種資料型態)。
簡單來說,假設我們有 Home 頁面跟 Another 頁面,那當我們第一次進入 App 時,我們的 Stack Navigate 就會長這樣:
接著當我們進入到 Another 頁面時,就會把他「疊上去 Stack」,變成這樣:
所以現在 Home 還是放在那邊,只是我們用 Another 把他蓋住了而已。
接著當我們再去下一個 Another 頁面時,就會變成這樣:
好,接著我們想回到 Home 頁面的話怎麼做?如果你知道 Stack 的運作的話(不懂的話可以複習 資料結構-Stack 與 Queue),就會知道我們要:
- 把第一個 Another pop 掉
- 把第二個 Another pop 掉
- 回到 Home 頁面
這個就是在 App 裡實作導覽的方式,我覺得還蠻特別的,不知道你怎麼想?
需要的套件
這邊會用 React Navigation 來做示範,詳細可以參考 官方文件
主要會裝的套件有這幾個:
- @react-navigation/native (npm)
- @react-navigation/native-stack (npm)
- react-native-screens (expo)
- react-native-safe-area-context (expo)
用法
基本頁面
這邊為了方便閱讀我就全部寫一起了,程式碼也不多:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import Detail from './screens/Detail'; import About from './screens/About'; import Home from './screens/Home'; import { NavigationContainer } from "@react-navigation/native"; import { createNativeStackNavigator } from "@react-navigation/native-stack";
const Stack = createNativeStackNavigator();
export default function App() { return ( <NavigationContainer> <Stack.Navigator> <Stack.Screen name='Home' component={Home} options={{ title: 'PeaNu' }} /> <Stack.Screen name='About' component={About} /> <Stack.Screen name='Detail' component={Detail} /> </Stack.Navigator> </NavigationContainer> ); }
|
看起來有一種 React-router-dom 的既視感吧?總之重點有兩個地方:
NavigationContainer
負責包住所有 Screnn,就跟 React-router-dom 裡的 HashRouter
是差不多的概念
createNativeStackNavigator
建立 Stack,所有 Screnn 都會疊在這裡面
他的機制是這樣,在 Stack.Navigator
裡面的第一個 Screen
會被放入 Navigation Stack,以上面來說的話就是 Home
這個頁面。
這個頁面會是我們進入 App 時的「預設頁面」,如果要用圖來表示目前的 Navigation Stack 的話,會像這樣:
假設進來後,我們又進入到 Detail 的話就會變這樣:
這個就是他背後運作的方式,也是前面介紹的 Navigation Stack 的運作流程。
切換到其他頁面
這邊只要知道一件事就夠了,官方文件中有提到:
Each screen component in your app is provided with the navigation prop automatically.
附註:想知道有哪些相關的 API 可以到 這邊 來看。
意思就是我們放在 Stack 裡面的頁面都會自動被傳入 navigation
這個 props,所以只要拿進來用就好了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import { View, Text, Button } from "react-native"; import { globalStyles } from "../style/globalStyle";
type HomeProps = { navigation: any }
const Home: React.FC<HomeProps> = ({ navigation }) => { return ( <View style={globalStyles.container}> <Text style={globalStyles.titleText}>Home screen</Text> {/* 加上一個跳轉的按鈕 */} <Button title="Go to Detail" onPress={() => navigation.navigate('Detail')} /> </View> ) }
export default Home;
|
附註:其實寫成 navigation.push('Detaik')
也行,不過比較少人這樣寫就是了。
只要這樣子做,我們就有一個可以跳轉的按鈕了:
當我們按下按鈕後,就會跳轉到我們指定的頁面(會對應到 name
):
這邊的 <
(返回按鈕)是自動產生的,如果你也想要自製一個類似的按鈕,可以用 navigation.goBack()
的方式來實作。
在不同的 Screen 之間傳遞 props
這應該是個很常見的需求,假設我有 Home
跟 Detail
兩個頁面,Home
負責顯示列表的標題,Detail
顯示列表的內容,那我該怎麼把放在 Home
這邊的資料傳給 Detail
來顯示?
首先,navigation.navigate()
是有第二個參數的,這個參數就是用來讓我們傳遞 props 的,所以 Home 就可以這樣子寫:
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
| import { useState } from "react"; import { View, FlatList, TouchableOpacity, Text } from "react-native"; import { globalStyles } from "../style/globalStyle";
interface Reviews { name: string, body: string, rating: number, key: string }
type HomeProps = { navigation: any }
const Home: React.FC<HomeProps> = ({ navigation }) => {
const [reviews, setReviews] = useState<Reviews[]>([ { name: 'Zelda, Breath of Fresh Air', rating: 5, body: 'lorem ipsum', key: '1' }, { name: 'Gotta Catch Them All (again)', rating: 4, body: 'lorem ipsum', key: '2' }, { name: 'Not so "Final" Fantasy', rating: 3, body: 'lorem ipsum', key: '3' }, ])
return ( <View style={globalStyles.container}> <FlatList contentContainerStyle={{ paddingVertical: 20, justifyContent: 'center', alignItems: 'center' }} data={reviews} renderItem={({ item }) => ( // 把要傳的 props 放在第二個參數 <TouchableOpacity onPress={() => navigation.navigate('Detail', item)}> <Text>{item.name}</Text> </TouchableOpacity> )} /> </View> ) }
export default Home;
|
這樣就成功傳進去了,接下來的第二個問題是:要怎麼接收?
這時候 Detail
裡會自動接收到一個 props,叫做 route
,所以可以這樣取得:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import { View, Text } from "react-native"; import { globalStyles } from "../style/globalStyle";
type DetailProps = { route: any }
const Detail: React.FC<DetailProps> = ({ route }) => { const { name, body, rating } = route.params return ( <View style={globalStyles.container}> <Text>{name}</Text> <Text>{body}</Text> <Text>{rating}</Text> </View> ) }
export default Detail;
|
在 Title 顯示 props 傳進來的參數
只是想特別寫一下,這功能應該蠻常用到的。
附註:有需要的話可以參考官方文件。
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
| const HomeStack: React.FC = () => { return ( <Stack.Navigator screenOptions={{ headerStyle: { backgroundColor: '#eee' }, headerTintColor: '#333', }} > <Stack.Screen name='Home' component={Home} options={{ title: 'GameZone', }} /> <Stack.Screen name='Detail' component={Detail} // 重點是這一行 options={({ route }: any) => ({ title: route.params.name }) } /> </Stack.Navigator> ) }
export default HomeStack;
|
設定 navbar 的樣式
如果想改變預設的樣式,可以用 options
或 screenOptions
這個 props 來更新:
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
| export default function App() { return ( <NavigationContainer> {/* 寫在這邊就是 global style */} <Stack.Navigator screenOptions={{ headerStyle: { backgroundColor: '#eee' } }} > {/* local style */} <Stack.Screen name='Home' component={Home} options={{ title: 'GameZone', headerTintColor: '#333', }} /> <Stack.Screen name='Detail' component={Detail} options={{ headerBackTitleStyle: { fontSize: 20 } }}/> </Stack.Navigator> </NavigationContainer> ); }
|
想知道有哪些東西可以調的話,可以參考這裡