引き続き、React Native + Expoによる記録アプリ開発での実装例紹介 。
今回はReact Navigation (v5) のお話です。Stack, Bottom tab, Drawerなど、基本的なUIとして非常に多くのアプリで取り入れられているはず。
reactnavigation.org
本記事のコードのjsファイルなどはこちら。
Life-Report/Router.js at master · tkd708/Life-Report · GitHub
解説や実装例の記事
自分が今回のアプリに取り掛かり始めたころはまだReact Navigation v4だったのですが、ゆーーっくり開発している間にv5が出てしまいました。
これまた日本語で解説してくださっている素晴らしい記事がいくつかありますので、ぜひ参考に。
【v4 -> v5変更点、v5使い方】react-navigationV5でちゃった・・・// 【react-native】 - Qiita
React Navigation v5 傾向と対策 - Qiita
英語での例もいくつか貼っときます。
React Navigation V5 news and examples - dooboolab - Medium
React Navigation Version 5.0 – Personal Blog
App.jsでのラップ
多少書き方が変わっただけで、最上層のNavigatorをラップする、というのは変わりませんね。
注意点としては、一番最初にimport 'react-native-gesture-handler'を持ってくるように、とのこと。
下記でRouter.jsに書かれているNavigatorの中身を解説していきます。
import 'react-native-gesture-handler';
import React from 'react';
import { Navigator } from './Router';
import { NavigationContainer } from '@react-navigation/native';
import { Provider } from 'unstated'
//Provider must wrap all the components to have share the states
export default class App extends React.Component {
render() {
return (
<Provider>
<NavigationContainer>
<Navigator />
</NavigationContainer>
</Provider>
);
}
}
Router.jsでのImport
基本的な使い方として、import { creatXNavigator } from @react-navigation/X というようにスタックやタブやドロワーや、Xにあたる部分を書き換えてそれぞれインポートし、それらのインスタンスを指定することになります。
import { createStackNavigator } from '@react-navigation/stack'
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { createDrawerNavigator } from '@react-navigation/drawer';
const Stack = createStackNavigator()
const Tab = createBottomTabNavigator()
const Drawer = createDrawerNavigator()
Stack
X.Screenでスクリーンを指定し、さらにその中でoptionsとしてヘッダーなど指定できます。
自分は後に設定するDrawerを開くいわゆるハンバーガーアイコンをヘッダーに置きたかったので、それぞれのスタックで指定しました。
このStackは下記にあるようにBottom tabに渡されるんですが、そちらのoptionでまとめてヘッダー指定...とはできず。
const RecordStack = () => {
return (
<Stack.Navigator>
<Stack.Screen
name="Record expenses"
component={RecordWrapper}
options={({ navigation }) => ({
headerLeft: () => (<FontAwesome name="bars" size={24} onPress={() => { navigation.openDrawer() }} style={{ paddingLeft: 20 }} />)
})}
/>
</Stack.Navigator>
)
}
Tab
で、作ったそれぞれのStackを下記のようにBottom tabに渡しました。
ここのscreenOptionsではBottom tabのアイコンをroute.nameによって指定しつつactiveかどうかで色を分けてる感じです。
見直したらfocusedのとこ使ってないし、if文も三項演算子でやればよかったな。
const RecordBottomTab = () => {
return (
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
let iconName;
if (route.name === 'Record') {
iconName = 'pluscircleo'
} else if (route.name === 'Report') {
iconName = 'piechart';
}
return <AntDesign name={iconName} size={size} color={color} />;
},
})}
tabBarOptions={{
activeTintColor: 'dodgerblue',
inactiveTintColor: 'gray',
}}
>
<Tab.Screen name="Record" component={RecordStack} />
<Tab.Screen name="Report" component={ReportStack} />
</Tab.Navigator>
)
};
ここまでのとこのUI、ヘッダーとBottom tabはこんな感じになります。
Drawer
特にアプリらしい挙動のDrawer。
自分の場合これが最上層のNavigatorになってるので、最初のApp.jsでラップするためにexportされてます。
Bottom tabの時と同じ要領で、アイコンを指定しています。
export const Navigator = () => {
return (
<Drawer.Navigator
initialRouteName="Record expenses"
screenOptions={({ route }) => ({
drawerIcon: ({ focused, color, size }) => {
let iconName;
if (route.name === 'Record expenses') {
iconName = 'addfile'
} else if (route.name === 'Daily chart') {
iconName = 'linechart';
} else if (route.name === 'Setting') {
iconName = 'setting';
}
return <AntDesign name={iconName} size={size} color={color} />;
},
})}
tabBarOptions={{
activeTintColor: 'dodgerblue',
inactiveTintColor: 'gray',
}}
>
<Tab.Screen name="Record expenses" component={RecordBottomTab} />
<Tab.Screen name="Daily chart" component={DailyChartBottomTab} />
<Tab.Screen name="Setting" component={SettingStack} />
</Drawer.Navigator>
)
};
こんな感じのDrawerができあがります。
useIsFocused
Routerでの諸々とは別に取り上げたかったのが、このuseIsFocused。
Bottom Tabなんかでスクリーンを移動したときに、その挙動に合わせて関数を実行したい時なんかに便利。
reactnavigation.org
自分の場合、記録しているお金と時間のDaily chartをBottom Tabそれぞれ配置していて、けどChartそのものは同じコンポーネントで、Bottom TabのFocusしてるスクリーンによってChartに入るデータを切り替える必要があった。
このためにわざわざv5に書き換えたといっても過言ではない(同様の機能のものがv4以前でもあったかもしれないけど知らなかった)。
こちら、前回記事で載せたのと同じコードなんですが改めて。インポートしたuseIsFocusedのインスタンスを作ると、それがそのままそのスクリーン(タブ)を開いているかどうかのBoolean型の変数になるので、それをuseEffectの配列に入れつつ、内部の関数実行の条件にもしています。これが反対側のタブ(このタブはMoneyの方なので、反対はTime)にも書かれていて、タブを移るたびにそれぞれで指定された関数が実行される、というもの。
import { useIsFocused } from '@react-navigation/native';
const DailyReportScreenMoney = ({ container }) => {
const isFocused = useIsFocused();
const [categorySelected, setCategory] = useState('Daily total')
const [lowerDate, setLowerDate] = useState(moment(new Date()).add(-28, 'days').format('YYYY-MM-DD'))
const [upperDate, setUpperDate] = useState(moment(new Date()).add(0, 'days').format('YYYY-MM-DD'))
// initial trigger [] is included
useEffect( () => {
isFocused && container.getDailyExpenses("Money", categorySelected, lowerDate, upperDate)
},
[isFocused, categorySelected, lowerDate, upperDate])
この挙動、あるいはUnstatedのcontainerにRouter.jsの方からアクセスできれば各Navigationのoptionsからアイコンを操作したみたいにしてcontainerのmethodを実行できたのかも。でも最上層のNavigatorをラップしても、該当するNavigatorをラップしても上手くいかなかった...単に書き方がどこかおかしかったのかもしれない。実例あれば見てみたいし、どなたかご教授いただければ幸いです。
おわりに
これまたささやかな実装例にとどまってますが、v5ではHooksを全面的に導入してさまざまな動きが可能となっているみたいです。
今後のアプリ開発でも間違いなくお世話になるReact Navigation、まだまだ開拓していきたいですね。