react-navigation 5.x 最佳實踐

文章示例源碼: github.com/youngjuning…react

安裝依賴

$ yarn add @react-navigation/native @react-navigation/stack @react-navigation/bottom-tabs react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context @react-native-community/masked-view
複製代碼

配置

爲了完成 react-native-screens 的安裝,添加下面兩行代碼到 android/app/build.gradle 文件的 dependencies 部分中:android

implementation 'androidx.appcompat:appcompat:1.1.0-rc01'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha02'
複製代碼

爲了完成 react-native-gesture-handler 的安裝, 在入口文件的頂部添加下面的代碼, 好比 index.jsApp.js:git

import 'react-native-gesture-handler';
複製代碼

如今,咱們須要把整個 App用 NavigationContainer包裹:github

import React from 'react';
import { NavigationContainer } from '@react-navigation/native';

const App = () => {
  return (
    <NavigationContainer> {/* Rest of your app code */} </NavigationContainer>
  );
};

export default App;
複製代碼

App.js

import React from 'react';
import {
  View,
  Text,
  StyleSheet,
  SafeAreaView,
  StatusBar,
  BackHandler,
} from 'react-native';
import {NavigationContainer, useFocusEffect} from '@react-navigation/native';
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
import {createStackNavigator, HeaderBackButton} from '@react-navigation/stack';
import {IconOutline} from '@ant-design/icons-react-native';
import {Button} from '@ant-design/react-native';
import IconWithBadge from './IconWithBadge';
import HeaderButtons from './HeaderButtons';
import getActiveRouteName from './getActiveRouteName';
import getScreenOptions from './getScreenOptions';
import {navigationRef} from './NavigationService';

const HomeScreen = ({navigation, route}) => {
  navigation.setOptions({
    headerLeft: props => (
      <HeaderBackButton {...props} onPress={() => { console.log('不能再返回了!'); }} />
    ),
    headerRight: () => (
      <HeaderButtons> {/* title、iconName、onPress、IconComponent、iconSize、color */} <HeaderButtons.Item title="添加" iconName="plus" onPress={() => console.log('點擊了添加按鈕')} iconSize={24} color="#ffffff" /> </HeaderButtons>
    ),
  });

  useFocusEffect(
    React.useCallback(() => {
      // Do something when the screen is focused
      return () => {
        // Do something when the screen is unfocused
        // Useful for cleanup functions
      };
    }, []),
  );
  const {author} = route.params || {};
  return (
    <> <StatusBar barStyle="dark-content" /> <View style={styles.container}> <Text>Home Screen</Text> <Text>{author}</Text> <Button type="warning" // 使用 setOptions 更新標題 onPress={() => navigation.setOptions({headerTitle: 'Updated!'})}> Update the title </Button> <Button type="primary" onPress={() => // 跳轉到指定頁面,並傳遞兩個參數 navigation.navigate('DetailsScreen', { otherParam: 'anything you want here', }) }> Go to DetailsScreen </Button> <Button type="warning" onPress={() => navigation.navigate('SafeAreaViewScreen')}> Go SafeAreaViewScreen </Button> <Button type="primary" onPress={() => navigation.navigate('CustomAndroidBackButtonBehaviorScreen') }> Go CustomAndroidBackButtonBehavior </Button> </View> </>
  );
};

const DetailsScreen = ({navigation, route}) => {
  // 經過 props.route.params 接收參數
  const {itemId, otherParam} = route.params;
  return (
    <View style={styles.container}> <Text>Details Screen</Text> <Text>itemId: {itemId}</Text> <Text>otherParam: {otherParam}</Text> <Button type="primary" // 返回上一頁 onPress={() => navigation.goBack()}> Go back </Button> <Button type="primary" // 若是返回上一個頁面須要傳遞參數,請使用 navigate 方法 onPress={() => navigation.navigate('HomeScreen', {author: '楊俊寧'})}> Go back with Params </Button> </View>
  );
};

const SettingsScreen = ({navigation, route}) => {
  return (
    <SafeAreaView style={{flex: 1, justifyContent: 'space-between', alignItems: 'center'}}> <Text>This is top text.</Text> <Text>This is bottom text.</Text> </SafeAreaView>
  );
};

const SafeAreaViewScreen = () => {
  return (
    <SafeAreaView style={{flex: 1, justifyContent: 'space-between', alignItems: 'center'}}> <Text>This is top text.</Text> <Text>This is bottom text.</Text> </SafeAreaView>
  );
};

const CustomAndroidBackButtonBehaviorScreen = ({navigation, route}) => {
  useFocusEffect(
    React.useCallback(() => {
      const onBackPress = () => {
        alert('物理返回鍵被攔截了!');
        return true;
      };

      BackHandler.addEventListener('hardwareBackPress', onBackPress);

      return () =>
        BackHandler.removeEventListener('hardwareBackPress', onBackPress);
    }, []),
  );
  return (
    <View style={styles.container}> <Text>AndroidBackHandlerScreen</Text> </View>
  );
};

const Stack = createStackNavigator();
const BottomTab = createBottomTabNavigator();
const BottomTabScreen = () => (
  <BottomTab.Navigator screenOptions={({route}) => ({ tabBarIcon: ({focused, color, size}) => { let iconName; if (route.name === 'HomeScreen') { iconName = focused ? 'apple' : 'apple'; return ( <IconWithBadge badgeCount={90}> <IconOutline name={iconName} size={size} color={color} /> </IconWithBadge> ); } else if (route.name === 'SettingsScreen') { iconName = focused ? 'twitter' : 'twitter'; } return <IconOutline name={iconName} size={size} color={color} />; }, })} tabBarOptions={{ activeTintColor: 'tomato', inactiveTintColor: 'gray', }}> <Stack.Screen name="HomeScreen" component={HomeScreen} options={{tabBarLabel: '首頁'}} /> <Stack.Screen name="SettingsScreen" component={SettingsScreen} options={{tabBarLabel: '設置'}} /> </BottomTab.Navigator>
);
const App = () => {
  const routeNameRef = React.useRef();
  return (
    <> <NavigationContainer ref={navigationRef} onStateChange={state => { const previousRouteName = routeNameRef.current; const currentRouteName = getActiveRouteName(state); if (previousRouteName !== currentRouteName) { console.log('[onStateChange]', currentRouteName); if (currentRouteName === 'HomeScreen') { StatusBar.setBarStyle('dark-content'); // 修改 StatusBar } else { StatusBar.setBarStyle('dark-content'); // 修改 StatusBar } } // Save the current route name for later comparision routeNameRef.current = currentRouteName; }}> <Stack.Navigator initialRouteName="HomeScreen" // 頁面共享的配置 screenOptions={getScreenOptions()}> <Stack.Screen name="BottomTabScreen" component={BottomTabScreen} options={{headerShown: false}} /> <Stack.Screen name="DetailsScreen" component={DetailsScreen} options={{headerTitle: '詳情'}} // headerTitle 用來設置標題欄 initialParams={{itemId: 42}} // 默認參數 /> <Stack.Screen name="SafeAreaViewScreen" component={SafeAreaViewScreen} options={{headerTitle: 'SafeAreaView'}} /> <Stack.Screen name="CustomAndroidBackButtonBehaviorScreen" component={CustomAndroidBackButtonBehaviorScreen} options={{headerTitle: '攔截安卓物理返回鍵'}} /> </Stack.Navigator> </NavigationContainer> </>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
});

export default App;
複製代碼

路由名稱的大小寫可有可無 -- 你可使用小寫字母home或大寫字母Home,這取決於你的喜愛。 咱們更喜歡將路由名稱大寫。 咱們更喜歡利用咱們的路由名稱。react-native

跳轉方法有 navigatepushgoBackpopToTopmarkdown

能夠用 navigation.setParams 方法更新頁面的參數app

咱們能夠經過 options={({ route, navigation }) => ({ headerTitle: route.params.name })} 的方式在標題中使用參數ide

咱們能夠用 navigation.setOptions 更新頁面配置oop

  • Stack.Navigator
    • initialRouteName : 用來配置 Stack.Navigator 的初始路由
    • screenOptions: 頁面共享配置對象
  • Stack.Screen
    • name: 頁面名
    • component: 頁面對應組件
    • options: 頁面配置對象
    • initialParams: 默認參數

HeaderButtons.js

使用 react-navigation-header-buttons 組件搭配任意 Icon 組件能夠自定義本身的 Header Button 組件,我這裏爲了演示方便,使用了 @ant-design/icons-react-nativeflex

import React from 'react';
import {
  HeaderButtons as RNHeaderButtons,
  HeaderButton as RNHeaderButton,
  Item,
} from 'react-navigation-header-buttons';
import {IconOutline} from '@ant-design/icons-react-native';

const HeaderButton = props => {
  return (
    <RNHeaderButton {...props} IconComponent={IconOutline} iconSize={props.iconSize || 23} color={props.color || '#000000'} />
  );
};

const HeaderButtons = props => {
  return <RNHeaderButtons HeaderButtonComponent={HeaderButton} {...props} />;
};

HeaderButtons.Item = Item;

export default HeaderButtons;
複製代碼

IconWithBadge.js

import React from 'react';
import {View} from 'react-native';
import {Badge} from '@ant-design/react-native';

const IconWithBadge = ({children, badgeCount, ...props}) => {
  return (
    <View style={{width: 24, height: 24, margin: 5}}> {children} <Badge {...props} style={{position: 'absolute', right: -6, top: -3}} text={badgeCount} /> </View>
  );
};

export default IconWithBadge;
複製代碼

getActiveRouteName.js

/** * Gets the current screen from navigation state * @param state */
const getActiveRouteName = state => {
  const route = state.routes[state.index];

  if (route.state) {
    // Dive into nested navigators
    return getActiveRouteName(route.state);
  }

  return route.name;
};

export default getActiveRouteName;

複製代碼

getScreenOptions.js

import {TransitionPresets} from '@react-navigation/stack';

const getScreenOptions = () => {
  return {
    headerStyle: {
      backgroundColor: '#ffffff',
    }, // 一個應用於 header 的最外層 View 的 樣式對象
    headerTintColor: '#000000', // 返回按鈕和標題都使用這個屬性做爲它們的顏色
    headerTitleStyle: {
      fontWeight: 'bold',
    },
    headerBackTitleVisible: false,
    headerTitleAlign: 'center',
    cardStyle: {
      flex: 1,
      backgroundColor: '#f5f5f9',
    },
    ...TransitionPresets.SlideFromRightIOS,
  };
};

export default getScreenOptions;

複製代碼

NavigationService.js

import React from 'react';

export const navigationRef = React.createRef();

const navigate = (name, params) => {
  navigationRef.current && navigationRef.current.navigate(name, params);
};

const getNavigation = () => {
  return navigationRef.current && navigationRef.current;
};

export default {
  navigate,
  getNavigation,
};
複製代碼

頁面生命週期與React Navigation

一個包含 頁面 A 和 B 的 StackNavigator ,當跳轉到 A 時,componentDidMount 方法會被調用; 當跳轉到 B 時,componentDidMount 方法也會被調用,可是 A 依然在堆棧中保持 被加載狀態,他的 componentWillUnMount 也不會被調用。

當從 B 跳轉到 A,B的 componentWillUnmount 方法會被調用,可是 A 的 componentDidMount方法不會被調用,應爲此時 A 依然是被加載狀態。

React Navigation 生命週期事件

addListener

function Profile({ navigation }) {
  React.useEffect(() => {
    const unsubscribe = navigation.addListener('focus', () => {
      // Screen was focused
      // Do something
    });

    return unsubscribe;
  }, [navigation]);

  return <ProfileContent />;
}
複製代碼

useFocusEffect

useFocusEffect(
    React.useCallback(() => {
      // Do something when the screen is focused
      return () => {
        // Do something when the screen is unfocused
        // Useful for cleanup functions
      };
    }, []),
  );
複製代碼

隱藏 Header/TabBar

  • headerMode:"none": hide Header for Stack.Navigator
  • headerShown:false: hide Header for Stack.Screen
  • tabBar={() => null}: hide TabBar for BottomTab.Navigator
import {NavigationContainer, useFocusEffect} from '@react-navigation/native';
import {createStackNavigator, TransitionPresets, HeaderBackButton} from '@react-navigation/stack';
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';

const Stack = createStackNavigator();
const BottomTab = createBottomTabNavigator();

export default App = () => {
  <NavigationContainer> <Stack.Navigator headerMode="none"> <Stack.Screen ... options={{ headerShown: false }} /> <Stack.Screen ...> {() => ( <BottomTab.Navigator ... tabBar={() => null} > ... </BottomTab.Navigator> )} </Stack.Screen> </Stack.Navigator> </NavigationContainer>
}
複製代碼

TabBar 的 StatusBar 不一樣

通常咱們會對特殊的那個TabBar進行處理。

const getActiveRouteName = state => {
  const route = state.routes[state.index];

  if (route.state) {
    // Dive into nested navigators
    return getActiveRouteName(route.state);
  }

  return route.name;
};

const App = () => {
  const ref = React.useRef(null);
	return (
    <> {/* 訪問 ref.current?.navigate */} <NavigationContainer ref={ref} onStateChange={state => { const previousRouteName = ref.current; const currentRouteName = getActiveRouteName(state); if (previousRouteName !== currentRouteName) { console.log('[onStateChange]', currentRouteName); if (currentRouteName === 'HomeScreen') { StatusBar.setBarStyle('dark-content'); // 修改 StatusBar } else { StatusBar.setBarStyle('dark-content'); // 修改 StatusBar } } }} > </NavigationContainer> </>
	)
}
複製代碼

監聽安卓物理返回鍵

import {View, Text, BackHandler} from 'react-native';
const CustomAndroidBackButtonBehaviorScreen = ({navigation, route}) => {
  useFocusEffect(
    React.useCallback(() => {
      const onBackPress = () => {
        alert('物理返回鍵被攔截了!');
        return true;
      };
      BackHandler.addEventListener('hardwareBackPress', onBackPress);
      return () =>
        BackHandler.removeEventListener('hardwareBackPress', onBackPress);
    }, []),
  );
  return (
    <View style={styles.container}> <Text>AndroidBackHandlerScreen</Text> </View>
  );
};
複製代碼

在子組件中訪問 navigation

咱們能夠經過 useNavigation() hook 來訪問 navigation,不再用傳遞多層 navigation

import React from 'react';
import { Button } from 'react-native';
import { useNavigation } from '@react-navigation/native';

function GoToButton({ screenName }) {
  const navigation = useNavigation();

  return (
    <Button title={`Go to ${screenName}`} onPress={() => navigation.navigate(screenName)} />
  );
}
複製代碼

給頁面傳遞額外的屬性

<Stack.Screen
  name="HomeScreen"
  options={{headerTitle: '首頁'}}>
  {props => <HomeScreen {...props} extraData={{author: '楊俊寧'}} />}
</Stack.Screen>
複製代碼

獲取 Header Height

import { useHeaderHeight } from '@react-navigation/stack'

const App = () => {
    const HeaderHeight = useHeaderHeight() // 獲取Header Height
    return(...)
}

export default App
複製代碼

繼續使用類組件

考慮到不適應 Hooks 的可是業務又很緊急的場景,咱們能夠再類組件之上封裝一層來支持 React Navigation 的 Hooks 組件,之因此這麼作,原由是由於 React Navigation 5 中咱們只能經過 useHeaderHeight() 方法獲取標題欄高度。

class Albums extends React.Component {
  render() {
    return <ScrollView ref={this.props.scrollRef}>{/* content */}</ScrollView>;
  }
}
// 封裝並導出
export default function(props) {
  const ref = React.useRef(null);
  useScrollToTop(ref);
  return <Albums {...props} scrollRef={ref} />;
}
複製代碼
相關文章
相關標籤/搜索