React Nativeでアプリ作ってみたよパート3

React Nativeでアプリ作ってみたよパート3

パート3は前回から2年ほどが経っております。前回のはJavaのコードを移したせいかなんか変なJSで、今エディターで見ると「.TSほにゃほにゃなエラー」の数がすごい。今回も元々はNuxt.jsでなんかやろ的なアプリで、Templateタグは使わないでRender関数で書きたかったんで〜、そんな感じで書いてるうちに、なんかこれってReactNativeでいいやん。というふうになって、そんなこんなで今に至ります。

今回はBltblogさんの【Nuxt.jsで近くのお店を探すアプリを作成】#1 開発準備を参考に書いていたため、【近くで済ますアプリ】というアプリにしました。そのままやん的な。
仕様の方ですが、ホットペッパさんのAPIクライアントはBltblogさんが公開されているものをそのまま使わせていただいて、キーワードを含めるように変数を1つ追加しました。プロジェクト作成にはExpoを使っています。
使った機能は以下。

axiosaxios
react-navigation/nativeNavigationContainer
react-navigation/stackcreateStackNavigator
reactReact, useState, userEffect
react-nativeView, Text, StyleSheet, Image, ScrollView, TouchableOpacity, TextInput
react-native-elementsCatd, Button, Overlay

公式に書いてあるUSAGEのほぼそのままで実装して、こんな雰囲気になっています。

そもそもの始まりはRender関数を使おうと思ったと言いながらもw、すべて関数コンポーネントで仕上げて(Render使わない)、ナビ、タイトル、お店の一覧と詳細の計4枚。
NavigationContainer、createStackNavigatorを使ってヘッダー表示とページ遷移なんかをやってます。コンポーネントをスタック(積み重ねる)していく感じに感じます。

app.js

export default function App() {
  return (
    <NavigationContainer>
      <Navi />
    </NavigationContainer>
  );
}

components/Navi.js

const Stack = createStackNavigator();

const Navi = () => (
  <Stack.Navigator
    screenOptions={{
      headerTitle: "近くで済ますアプリ"
    }}
  >
    <Stack.Screen
      options={{
        headerShown: false
      }}
      name="title"
      component={title}
    />
    <Stack.Screen name="menu" component={menu} />
    <Stack.Screen name="shop" component={shop} />
  </Stack.Navigator>
);

export default Navi;
自己満でタイトル画面を作って、お店一覧でキーワードなしで検索。ヘッダーのボタンからオーバーレイで検索枠が出ます。ReactNativeではReact-Native-Elementsというモジュールで簡単にコンポーネントやオーバーレイが実装できてしまいます。

タイトルは最近たまにやっているゲームアプリっぽ風に、TauchableOpacityを使って画面全体をタップしてメイン画面に遷移します。

components/title.js

export default function Title({ navigation }) {
  return (
    <TouchableOpacity
      style={{ flex: 1 }}
      onPress={() => navigation.navigate("menu")}
    >
      <View
        style={{
          backgroundColor: "lightyellow",
          flex: 1,
          alignItems: "center",
          justifyContent: "center"
        }}
      >
        <Image
          resizeMode="contain"
          style={{ width: 300, height: 58 }}
          source={require("../assets/title.png")}
        />
        <Text color="#841584">TAP TO START</Text>
      </View>
    </TouchableOpacity>
  );
}

components/menu.js


const getShops = keyword => {
  return api.getShops(keyword).then(res => {
    return res.data.results;
  });
};

const Menu = ({ navigation }) => {
  navigation.setOptions({
    headerRight: () => (
      <Button
        type="clear"
        onPress={() => setIsVisible(true)}
        title="Keyword"
        color="#fff"
      />
    )
  });
  const [shops, setShops] = useState([]);
  const [isVisible, setIsVisible] = useState(false);
  const [keyword, setKeyword] = useState("");

  const setKeywordSearch = keyword => {
    (async () => {
      api.getShops(keyword).then(res => {
        const data = res.data.results;
        setShops({
          data
        });
      });
    })();
    setIsVisible(false);
  };

  useEffect(() => {
    (async () => {
      const data = await getShops();
      await setShops({
        data
      });
    })();
  }, []);

  return (
    <ScrollView style={{ flex: 1 }}>
      <Overlay
        height="auto"
        isVisible={isVisible}
        onBackdropPress={() => setIsVisible(false)}
      >
        <View>
          <Text>検索に含ませたいキーワードがあれば入力してください。</Text>
          <TextInput
            style={{
              marginTop: 10,
              marginBottom: 10,
              height: 30,
              borderColor: "black",
              borderWidth: 1
            }}
            onChangeText={keyword => setKeyword(keyword)}
            value={keyword}
          />
          <Button
            type="clear"
            color="#009488"
            title="検索する"
            onPress={() => setKeywordSearch(keyword)}
          />
        </View>
      </Overlay>
      <View style={styles.container}>
        {shops.data &&
          shops.data.shop.map((data, i) => (
            <Card key={i}>
              <View style={{ width: 280, flex: 1 }}>
                <View style={{ flex: 1, flexDirection: "row" }}>
                  <View style={{ flex: 3, flexDirection: "column" }}>
                    <Text>{data.name}</Text>
                    <Text>{data.genre.name}</Text>
                  </View>
                  <View
                    style={{
                      flex: 2,
                      justifyContent: "flex-end",
                      alignItems: "flex-end"
                    }}
                  >
                    <TouchableOpacity
                      onPress={() =>
                        navigation.navigate("shop", {
                          Data: { shop: data.id }
                        })
                      }
                    >
                      <Image
                        style={{ width: 100, height: 100 }}
                        source={{ uri: data.photo.mobile.l }}
                      />
                    </TouchableOpacity>
                  </View>
                </View>
              </View>
            </Card>
          ))}
      </View>
    </ScrollView>
  );
};
const styles = StyleSheet.create({});

export default Menu;
お店一覧では画像をタップするとお店の詳細へ移動します。検索に含ませたいキーワードがあれば右上のKeywordから窓を出して入力する感じですね。
何れもこんな画面にしたいな〜っていうよりかこれ書きたいなに近い画面(Usageの説明が分かりやすかったのもあるけれども)です。

前回やったときはReactNativeがちょっと書き辛いように感じるところが多かったけどもいろいろと進歩したのかシンプルにまとめて書けるような気がしました。
お店の詳細画面のリスト表示のところだけゴチャっとしたのでStyleSheet.createで分けました。小さいものだとスタイルはまとめて書いちゃってサクッと書けてしまうのでサクッとやっちゃうんだな。これが。

components/Shop.js


const getDetail = id => {
  return api.detailGet(id).then(res => {
    return res.data.results;
  });
};

const ListView = ({ subtitle, data }) => {
  return (
    <View style={styles.container}>
      <View style={styles.subtitle}>
        <Text>{subtitle}</Text>
      </View>
      <View style={styles.information}>
        <Text>{data}</Text>
      </View>
    </View>
  );
};

const Shop = props => {
  const [shop, setShop] = useState([]);
  useEffect(() => {
    (async () => {
      const data = await getDetail(props.route.params.Data.shop);
      await setShop({ data });
    })();
  }, []);

  return (
    <ScrollView style={{ flex: 1 }}>
      {shop.data &&
        shop.data.shop.map((data, i) => (
          <Card key={i} title={data.name} image={{ uri: data.photo.mobile.l }}>
            <View style={{ flex: 1 }}>
              <ListView subtitle="住所" data={data.address} />
              <ListView subtitle="交通アクセス" data={data.access} />
              <ListView subtitle="営業時間" data={data.open} />
              <ListView subtitle="定休日" data={data.close} />
              <ListView
                subtitle="平均予算"
                data={data.budget && data.budget.average}
              />
              <ListView subtitle="備考" data={data.budget_memo} />
              <ListView subtitle="席数" data={data.capacity} />
              <ListView subtitle="メモ" data={data.shop_detail_memo} />
            </View>
          </Card>
        ))}
    </ScrollView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    flexDirection: "row"
  },
  subtitle: {
    flex: 2,
    padding: 10
  },
  information: {
    flex: 15,
    padding: 10
  }
});

export default Shop;
パート3はこんな風になりました。関数コンポーネント推奨ということで関数コンポーネント中心になっていきそうですが、以前のよりもスッキリとしている印象ですね。なんか同じことを2回書いたような気もするけど、今回はそんな感じです。
では次回は前に書いてたNuxt.jsの方もせっかくやからアップしようかと思います。

Related Posts


投稿者: Takeken

インターネット利用者のITリテラシーを向上したいという設定の2次元キャラです。 サーバー弄りからプログラミングまで手を付けた自称エッセイストなたけけんの物語。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です