Stack Navigator + Paper’s Appbar
Stack Navigator でツイートのフィードを表示するスクリーン ( Feed.js
) とツイートの詳細を表示するスクリーン ( Details.js
) を作成します。
各スクリーンのヘッダーは Paper の Appbar
を利用します。
まず、シンプル版に変更するので、StackNavigator.js
を下記のように変更します。
src/StackNavigator.js
// StackNavigator.js import React from "react"; import { createStackNavigator } from "@react-navigation/stack"; import { Feed } from "./Feed"; import { Details } from "./Details"; const Stack = createStackNavigator(); export const StackNavigator = () => { return ( <Stack.Navigator initialRouteName="Feed"> <Stack.Screen name="Feed" component={Feed} options={{ headerTitle: "Twitter" }} /> <Stack.Screen name="Details" component={Details} options={{ headerTitle: "Tweet" }} /> </Stack.Navigator> ); };
※ Error: undefined Unable to resolve module ./Feed from src/StackNavigator.js:
のようなエラーが表示されますが、これから Feed.js
と Details.js
ファイルを作成しますので、そのままで良いです。
src/Feed.js
と src/Details.js
ファイルを作成します。( VSCode で imrnfc
実行 )
try🐶everything twitter-clone-example$ touch ./src/Feed.js try🐶everything twitter-clone-example$ touch ./src/Details.js
src/Feed.js
// Feed.js import React from "react"; import { View, Text, StyleSheet } from "react-native"; export const Feed = props => { return ( <View style={styles.container}> <Text>Feed</Text> </View> ); }; const styles = StyleSheet.create({ container: { flex: 1, alignItems: "center", justifyContent: "center" } });
src/Details.js
//Detais.js import React from "react"; import { View, Text, StyleSheet } from "react-native"; export const Details = props => { return ( <View style={styles.container}> <Text>Details</Text> </View> ); }; const styles = StyleSheet.create({ container: { flex: 1, alignItems: "center", justifyContent: "center" } });
下記のようになります。
次は、Stack Navigator の ヘッダー を作成します。
- Paper の
Appbar.Header
コンポーネントを Stack のscreenOptions
のヘッダーとして渡して作成します。 - また、
screen
の値を持つheaderMode
プロパティを渡し、見栄えの良いフェードイン/アウトアニメーションを作成します。 - Paper のテーマも適用し始めます。
既存のファイルを下記のように変更してください。
src/StackNavigator.js
screenOptions
の header
プロパティに渡す関数は、3つのプロパティ ( scene
、previous
、navigation
) にアクセスできます。
- screen : Stack の一番上の画面のタイトルにアクセスし、ヘッダーに表示できます
- previous : Stack の他の画面があるかどうかを示します。
- navigation : さまざまな画面に移動できます。( e.g. Drawer の開閉など )
// StackNavigator.js import React from "react"; import { TouchableOpacity } from "react-native"; import { createStackNavigator } from "@react-navigation/stack"; import { Appbar, Avatar, useTheme } from "react-native-paper"; import { MaterialCommunityIcons } from "@expo/vector-icons"; import { Feed } from "./Feed"; import { Details } from "./Details"; const Stack = createStackNavigator(); const Header = ({ scene, previous, navigation }) => { const { options } = scene.descriptor; const theme = useTheme(); const title = options.headerTitle !== undefined ? options.headerTitle : options.title !== undefined ? options.title : scene.route.name; return ( <Appbar.Header theme={{ colors: { primary: theme.colors.surface } }}> {previous ? ( <Appbar.BackAction onPress={navigation.pop} color={theme.colors.primary} /> ) : ( <TouchableOpacity onPress={() => { navigation.openDrawer(); }} > <Avatar.Image size={40} source={{ uri: "https://pbs.twimg.com/profile_images/952545910990495744/b59hSXUd_400x400.jpg" }} /> </TouchableOpacity> )} <Appbar.Content title={ previous ? title : <MaterialCommunityIcons name="twitter" size={40} /> } /> </Appbar.Header> ); }; export const StackNavigator = () => { return ( <Stack.Navigator initialRouteName="Feed" headerMode="screen" screenOptions={{ header: ({ scene, previous, navigation }) => ( <Header scene={scene} previous={previous} navigation={navigation} /> ) }} > <Stack.Screen name="Feed" component={Feed} options={{ headerTitle: "Twitter" }} /> <Stack.Screen name="Details" component={Details} options={{ headerTitle: "Tweet" }} /> </Stack.Navigator> ); };
すると、下記のように変わります。
Tab と Drawer ナビケーターはデフォルトでスクリーン間の移動ができますが、Stack では別途実装する必要があるため、まだ、Feed
から Details
スクリーンまでは移動できない状態です。
▲ ヘッダーの「戻る」ボタンを押すたびに navigation.pop
関数を呼び出します。Details
から Feed
スクリーンに戻ることもできます。
次は、Details.js
ファイルから設定して行きます。
必要なファイルを作成します。
try🐶everything twitter-clone-example$ touch ./src/Details.js try🐶everything twitter-clone-example$ mkdir ./src/components try🐶everything twitter-clone-example$ touch ./src/components/DetailedTweet.js
Details.js
ファイルを下記のように変更します。
( ※シミュレータにエラーメッセージが出ますがこの設定がおわるまでそのままにしておきます。)
- 詳細内容を
DetailedTweet.js
に分離します。
src/Details.js
//Detais.js import React from "react"; import { DetailedTweet } from "./components/DetailedTweet"; export const Details = props => { return <DetailedTweet {...props.route.params} />; };
src/components/DetailedTweet.js
ファイルを作成します。
src/components/DetailedTweet.js
// DetailedTweet.js import React from "react"; import { StyleSheet, View, Image } from "react-native"; import { Surface, Title, Caption, Avatar, Subheading, useTheme } from "react-native-paper"; import color from "color"; export const DetailedTweet = props => { const theme = useTheme(); const contentColor = color(theme.colors.text) .alpha(0.8) .rgb() .string(); const imageBorderColor = color(theme.colors.text) .alpha(0.15) .rgb() .string(); return ( <Surface style={styles.container}> <View style={styles.topRow}> <Avatar.Image style={styles.avatar} source={{ uri: props.avatar }} size={60} /> <View> <Title>{props.name}</Title> <Caption style={styles.handle}>{props.handle}</Caption> </View> </View> <Subheading style={(styles.content, { color: contentColor })}> {props.content} </Subheading> <Image source={{ uri: props.image }} style={[ styles.image, { borderColor: imageBorderColor } ]} /> </Surface> ); }; const styles = StyleSheet.create({ container: { flex: 1, padding: 20 }, avatar: { marginRight: 20 }, topRow: { flexDirection: "row", alignItems: "center" }, handle: { marginRight: 3, lineHeight: 12 }, content: { marginTop: 25, fontSize: 20, lineHeight: 30 }, image: { borderWidth: StyleSheet.hairlineWidth, marginTop: 25, borderRadius: 20, width: "100%", height: 280 } });
Feed.js
ファイルを変更します
Paper 側で既に用意されてある仮の Tweet データを FlatList
を使用してレンタリングするように作成します。
必要なファイルを作成します。
try🐶everything twitter-clone-example$ touch ./src/components/Tweet.js try🐶everything twitter-clone-example$ mkdir ./src/data try🐶everything twitter-clone-example$ touch ./src/data/index.js
src/Feed.js
// Feed.js import React from "react"; import { FlatList, View, StyleSheet } from "react-native"; import { useTheme } from "react-native-paper"; import { Tweet } from "./components/Tweet"; import { tweets } from "./data"; function renderItem({ item }) { return <Tweet {...item} />; } function keyExtractor(item) { return item.id.toString(); } export const Feed = props => { const theme = useTheme(); const data = tweets.map(tweetsProps => ({ ...tweetsProps, onPress: () => props.navigation && props.navigation.push("Details", { ...tweetsProps }) })); return ( <FlatList contentContainerStyle={{ backgroundColor: theme.colors.primary }} style={{ backgroundColor: theme.colors.background }} data={data} renderItem={renderItem} keyExtractor={keyExtractor} ItemSeparatorComponent={() => ( <View style={{ height: StyleSheet.hairlineWidth }} /> )} /> ); };
src/components/Tweet.js
と src/data/index.js
ファイルを作成します。
src/components/Tweet.js
// Tweet.js import React from "react"; import { StyleSheet, View, Image, TouchableOpacity } from "react-native"; import { Surface, Title, Caption, Text, Avatar, TouchableRipple, useTheme } from "react-native-paper"; import { MaterialCommunityIcons } from "@expo/vector-icons"; import color from "color"; export const Tweet = props => { const theme = useTheme(); const iconColor = color(theme.colors.text) .alpha(0.54) .rgb() .string(); const contentColor = color(theme.colors.text) .alpha(0.58) .rgb() .string(); const imageBorderColor = color(theme.colors.text) .alpha(0.15) .rgb() .string(); return ( <TouchableRipple onPress={() => props.onPress(props.id)}> <Surface style={styles.container}> <View style={styles.leftColum}> <Avatar.Image source={{ uri: props.avatar }} size={60} /> </View> <View style={styles.rightColum}> <View style={styles.topRow}> <Title>{props.name}</Title> <Caption style={styles.handle}>{props.handle}</Caption> <Caption style={(styles.handle, styles.dot)}>{"\u2824"}</Caption> <Caption>{props.date}</Caption> </View> <Text style={{ color: contentColor }}>{props.content}</Text> <Image source={{ uri: props.image }} style={[styles.image, { borderColor: imageBorderColor }]} /> <View style={styles.bottomRow}> <TouchableOpacity onPress={() => {}} hitSlop={{ top: 10, bottom: 10 }} > <View style={styles.iconContainer}> <MaterialCommunityIcons name="comment-outline" size={12} color={iconColor} /> <Caption style={styles.iconDescription}> {props.comments} </Caption> </View> </TouchableOpacity> <TouchableOpacity onPress={() => {}} hitSlop={{ top: 10, bottom: 10 }} > <View style={styles.iconContainer}> <MaterialCommunityIcons name="share-outline" size={14} color={iconColor} /> <Caption style={styles.iconDescription}> {props.retweets} </Caption> </View> </TouchableOpacity> <TouchableOpacity onPress={() => {}} hitSlop={{ top: 10, bottom: 10 }} > <View style={styles.iconContainer}> <MaterialCommunityIcons name="heart-outline" size={12} color={iconColor} /> <Caption style={styles.iconDescription}>{props.hearts}</Caption> </View> </TouchableOpacity> </View> </View> </Surface> </TouchableRipple> ); }; const styles = StyleSheet.create({ container: { flex: 1, flexDirection: "row", paddingTop: 10, paddingHorizontal: 10 }, leftColumn: { width: 100, alignItems: "center" }, rightColum: { flex: 1, marginHorizontal: 10 }, topRow: { flexDirection: "row", alignItems: "baseline" }, handle: { marginRight: 3 }, dot: { fontSize: 3 }, image: { borderWidth: StyleSheet.hairlineWidth, marginTop: 10, borderRadius: 20, width: "100%", height: 150 }, bottomRow: { paddingVertical: 10, flexDirection: "row", alignItems: "center", justifyContent: "space-between" }, iconContainer: { flexDirection: "row", alignItems: "center" }, iconDescription: { marginLeft: 2, lineHeight: 12 } });
src/data/index.js
// data/index.js export const tweets = [ { id: 1, name: "🌈 Josh", handle: "@JoshWComeau", date: "10h", content: '🔥 Automatically use "smart" directional curly quotes with the `quotes` CSS property! Even handles nested quotes with the <q> tag :o', image: "https://pbs.twimg.com/media/EOUrCOcWAAA71rA?format=png&name=small", avatar: "https://pbs.twimg.com/profile_images/461190672117035010/0kJ4pynr_400x400.jpeg", comments: 12, retweets: 36, hearts: 175 }, { id: 2, name: "Satyajit Sahoo", handle: "@satya164", date: "20h", content: "Not sure if I should be proud or ashamed of this piece of art 😅\n\n#Typescript", image: "https://pbs.twimg.com/media/EONH4KWX4AEV-JP?format=jpg&name=medium", avatar: "https://pbs.twimg.com/profile_images/1203032057875771393/x0nVAZPL_400x400.jpg", comments: 64, retweets: 87, hearts: 400 }, { id: 3, name: "Elvin", handle: "@elvin_not_11", date: "14h", content: "Hid the home indicator from the app so the device resembles an actual iPod even more. Thanks @flipeesposito for the suggestion!", image: "https://static.antyweb.pl/uploads/2014/09/IPod_classic_6G_80GB_packaging-2007-09-22-1420x670.jpg", avatar: "https://pbs.twimg.com/profile_images/1203624639538302976/h-rvrjWy_400x400.jpg", comments: 23, retweets: 21, hearts: 300 }, { id: 4, name: "🌈 Josh", handle: "@JoshWComeau", date: "10h", content: '🔥 Automatically use "smart" directional curly quotes with the `quotes` CSS property! Even handles nested quotes with the <q> tag :o', image: "https://pbs.twimg.com/media/EOUrCOcWAAA71rA?format=png&name=small", avatar: "https://pbs.twimg.com/profile_images/461190672117035010/0kJ4pynr_400x400.jpeg", comments: 12, retweets: 36, hearts: 175 }, { id: 5, name: "Satyajit Sahoo", handle: "@satya164", date: "20h", content: "Not sure if I should be proud or ashamed of this piece of art 😅\n\n#Typescript", image: "https://pbs.twimg.com/media/EONH4KWX4AEV-JP?format=jpg&name=medium", avatar: "https://pbs.twimg.com/profile_images/1203032057875771393/x0nVAZPL_400x400.jpg", comments: 64, retweets: 87, hearts: 400 }, { id: 6, name: "Elvin", handle: "@elvin_not_11", date: "14h", content: "Hid the home indicator from the app so the device resembles an actual iPod even more. Thanks @flipeesposito for the suggestion!", image: "https://static.antyweb.pl/uploads/2014/09/IPod_classic_6G_80GB_packaging-2007-09-22-1420x670.jpg", avatar: "https://pbs.twimg.com/profile_images/1203624639538302976/h-rvrjWy_400x400.jpg", comments: 23, retweets: 21, hearts: 300 } ];
※ TypeError: undefined is not an object (evaluating 'state.routes[state.index].key')
のようなエラーが表示されます。
対処:src/StackNavigator.js
ファイルを下記のように修正します。▼
return (
<Appbar.Header theme={{ colors: { primary: theme.colors.surface } }}>
{previous ? (
<Appbar.BackAction
– onPress={navigation.pop}
+ onPress={navigation.goBack}
color={theme.colors.primary}
/>
) : (
…
iOS シミュレータを再起動 ( ⌘ R ) し、下記のように動作すれば OK です。
▲これで、Feed
と Details
間の移動ができました。次は、Tab Navigator を実装する番です。
コメント
参考になりました。ありがとうございます。
React NativeはネイティブアプリらしいUIが作りづらく苦労していたので参考になりました。
技術革新も早くコピペしてもまともに動かないサイトも多く非常に参考になりました。
動かない部分もありますが自分で調べてみようと思います。