Expo.ioMaterial DesignReactReact Native React Native PaperReact Navigation

[ReactNative/Expo] Paper と Navigation v5 で Twitter Clone アプリを作成する〜初心者向け〜

scarf-4849441_1920 Expo.io
スポンサーリンク
スポンサーリンク

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.jsDetails.js ファイルを作成しますので、そのままで良いです。

src/Feed.jssrc/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"
  }
});

下記のようになります。

twitterClone-app-04
twitterClone-app-05

次は、Stack Navigator の ヘッダー を作成します。

  • PaperAppbar.Header コンポーネントを Stack の screenOptions のヘッダーとして渡して作成します。
  • また、screen の値を持つ headerMode プロパティを渡し、見栄えの良いフェードイン/アウトアニメーションを作成します。
  • Paper のテーマも適用し始めます。

既存のファイルを下記のように変更してください。

スポンサーリンク

src/StackNavigator.js

screenOptionsheader プロパティに渡す関数は、3つのプロパティ ( scenepreviousnavigation ) にアクセスできます。

  • 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 です。

▲これで、FeedDetails 間の移動ができました。次は、Tab Navigator を実装する番です。


お名前.com

https://www.xserver.ne.jp/lp/service01/

コメント

  1. ふぁびょん より:

    参考になりました。ありがとうございます。
    React NativeはネイティブアプリらしいUIが作りづらく苦労していたので参考になりました。
    技術革新も早くコピペしてもまともに動かないサイトも多く非常に参考になりました。

    動かない部分もありますが自分で調べてみようと思います。

タイトルとURLをコピーしました