Open7

Expo勉強日記

あさかいあさかい

1. Expo Router

リンク:https://docs.expo.dev/develop/file-based-routing/#what-is-expo-router

1-1. 概要

  • ファイルベースのルーティングであるので、Next.jsとかに慣れていると、そのままapp routerのような感じで開発ができる
  • app ディレクトリ以下がそのままroutingになっている
  • app 以下の index.tsx ファイルがページを表している
  • これらは必ず .js , .jsx , .ts , .tsx にすること

1-2. 例:/settings というページを作成したい場合

  • TL;DR
    • /app/settings/index.tsx を作成して、このindexファイルにページを作成すれば良い

1-3. _layout.tsx

  • Next.jsと同様にして、ディレクトリ以下に共通のレイアウトを適用することができる
  • app/_layout.tsx にて定義したレイアウトはどのroutesにも適用されるので、あまり思い処理などは書かない方が良い

1-4. Stack navigator

  • アプリでの異なるroutes間のナビゲーションパターンを表す

  • <Stack.Screen> コンポーネントでルートを追加

    import { Stack } from 'expo-router';
    
    export default function RootLayout() {
      return (
        <Stack
          screenOptions={{
            headerStyle: {
              backgroundColor: '#f4511e',
            },
            headerTintColor: '#fff',
            headerTitleStyle: {
              fontWeight: 'bold',
            },
          }}>
          <Stack.Screen name="index" /> <-- /
          <Stack.Screen name="details" /> <-- /details
        </Stack>
      );
    }
    
  • 上を、 <Link> コンポーネントや useRouter を用いて遷移できる

    import { Link } from 'expo-router';
    import { View, Text, StyleSheet } from 'react-native';
    
    export default function HomeScreen() {
      return (
        <View style={styles.container}>
          <Text>Home</Text>
          <Link href="/details">View details</Link> <-- ここを押すと遷移できる
        </View>
      );
    }
    
    ...
    
    • Link は基本 <Text> を wrap している
    • <Button> コンポーネントを利用したい場合には、expo-router由来の useRouter を用いて onPress 部分で関数を差し込めば良い

1-5. Group

  • 関連するルートをまとめることができるもの
  • 名前の規則として、 (name) のように名前を丸括弧で括ったディレクトリを利用する
// app/(home)/index.tsx
import { Link, useRouter } from "expo-router";
import { Button, Text, View } from "react-native";

export default function HomePage() {
  const router = useRouter()

  return (
    <View
      style={{
        flex: 1,
        justifyContent: "center",
        alignItems: "center",
      }}
    >
      <Text>Home</Text>
        <Button title="View Details" onPress={() => router.push('/details')} />
    </View>
  );
}

// app/(home)/_layout.tsx
import { Stack } from "expo-router";

export default function HomeLayout() {
  return (
    <Stack
      screenOptions={{
        headerStyle: {
          backgroundColor: '#f4511e',
        },
        headerTintColor: '#fff',
        headerTitleStyle: {
          fontWeight: 'bold',
        },
      }}
    >
      <Stack.Screen name="index" />
      <Stack.Screen name="details" />
    </Stack>
  );
}

// app/(home)/_details.tsx
import { StyleSheet, Text, View } from "react-native";

export default function DetailsPage() {
  return (
    <View style={styles.containers}>
      <Text>Details</Text>
    </View>
  )
}

const styles = StyleSheet.create({
  containers: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center'
  }
})
  • トップのルートレイアウト部分に <Stack.Screen name="(home)" /> のようにして明示的にスクリーンの宣言をする
// app/_layout.tsx
import { Stack } from 'expo-router';

export default function RootLayout() {
  return (
    <Stack>
      <Stack.Screen name="(home)" />
    </Stack>
  );
}
あさかいあさかい

Next.jsに慣れていると、Expoでのモバイル開発はかなり簡単で良い。

その反面、学びは少し少ないかもなので、Flutterなどもキャッチアップしたい。

あさかいあさかい

1-6. Tab Navigator

  • 特にモバイルアプリであると、指での操作などの観点から、タブでのナビゲーションが結構必要だったりする
  • Expoでは、タブでのナビゲーションが簡単に実装できる
    • Groupsで (tabs) とつくったからといってタブでのナビゲーションができるというわけではなくて、わかりやすいようにこのグループを作っている

Example

※ツリー構造は以下のようにする。(tabs)でグループを分けて、その下にタブでナビゲーションをしたい項目を並べるみたいな感じ。

-> % tree app
app
├── (tabs)├── (home)
│   │   ├── _layout.tsx
│   │   ├── details.tsx
│   │   └── index.tsx
│   ├── _layout.tsx
│   └── settings.tsx
└── _layout.tsx
  • app/(tabs)/_layout.tsx

    • Stack コンポーネントと同じ感じで、 Tabs コンポーネントを利用してタブを作成していく(とても簡単)
    import { Tabs } from "expo-router";
    
    export default function TabLayout() {
      return (
        <Tabs>
          <Tabs.Screen name="(home)" />
          <Tabs.Screen name="settings" />
        </Tabs>
      )
    }
    
  • app/_layout.tsx

    • 先ほどまでは、ここに直接 Stack にてスタックする画面を明示的に記載していたが、(tabs)配下に集約させたので、ここは(tabs)だけスタックさせれば良い
    import { Stack } from "expo-router";
    
    export default function RootLayout() {
      return (
        <Stack>
          <Stack.Screen name=**"(tabs)"** />
        </Stack>
      );
    }
    
    

※実際の動作

あさかいあさかい

1-7. Not found routes

  • 何も見つからない場合、この Not found routesを用いる
  • エラーレスポンス 404 の際に用いる
  • ファイル名は特別で、 +not-found.tsx とする
  • app ディレクトリ直下に配置する

Example

  • app/+not-found.tsx

    • Link コンポーネントですぐにホーム画面に戻れるようにしている
    import React from "react";
    import { Link, Stack } from "expo-router";
    import { StyleSheet, View } from "react-native";
    
    export default function NotFoundScreen() {
      return (
        <>
          <Stack.Screen options={{ title: 'Not Found' }} />
          <View>
            <Link href="/">Go to Home</Link>
          </View>
        </>
      );
    }
    
    const styles = StyleSheet.create({
      container: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center'
      }
    })
    
  • 試しに、(home)の部分の index.tsx に、存在しないページへ移動するためのボタンを配置する

    • app/(home)/index.tsx

      import { useRouter } from "expo-router";
      import { Button, Text, View } from "react-native";
      
      export default function HomePage() {
        const router = useRouter()
      
        return (
          <View
            style={{
              flex: 1,
              justifyContent: "center",
              alignItems: "center",
            }}
          >
            <Text>Home</Text>
            <Button title="View Details" onPress={() => router.push('/details')} />
            <Button title="?" onPress={() => router.push('/aaaaa')} />
          </View>
        );
      }
      
      
    • app/_layout.tsx

      import { Stack } from "expo-router";
      
      export default function RootLayout() {
        return (
          <Stack>
            <Stack.Screen name="(tabs)" />
            <Stack.Screen name="+not-found" />
          </Stack>
        );
      }
      
      
    • “?” を押すことで、404のページに移動できた

あさかいあさかい

公式チュートリアルには、ここの実際にnot foundに行く部分のコードなかったけど、普通に試せた方が良いと思う。

あさかいあさかい

2. Dynamic routes

リンク:https://docs.expo.dev/develop/dynamic-routes/

2-1. Why Dynamic routes?

  • ユーザー別の画面を出したいときに、 user_id などで分けるためにこのルーティングは利用される
    • 例えば、 user_id=1 のための詳細ページであれば、 app/(home)/details/[user_id].tsx のように [user_id] 部分が動的に変わってルーティングできる

2-2. Dynamic routeを作成し、id別で遷移する

2-2-1. 準備

  • 今回は、 app/(tabs)/(home)/details 配下にDynamic routeを配置する

    -> % tree app
    app
    ├── (tabs)
    │   ├── (home)
    │   │   ├── _layout.tsx
    │   │   ├── details
    │   │   │   └── [id].tsx. <-- Dynamic route
    │   │   └── index.tsx
    │   ├── _layout.tsx
    │   └── settings.tsx
    ├── +not-found.tsx
    └── _layout.tsx
    
    • app/(tabs)/(home)/details/[id].tsx (後ほどsearch params別での処理を追加)

      import { StyleSheet, Text, View } from "react-native";
      
      export default function DetailsPage() {
        return (
          <View style={styles.containers}>
            <Text>Details</Text>
          </View>
        )
      }
      
      const styles = StyleSheet.create({
        containers: {
          flex: 1,
          justifyContent: 'center',
          alignItems: 'center'
        }
      })
      

2-2-2. (home)の index.tsx にルーティングを追加する

  • Button コンポーネントにて、それぞれ id=1 または id=2 であるときのページへ遷移するボタンを設ける

    • app/(tabs)/(home)/index.tsx

      import { useRouter } from "expo-router";
      import { Button, Text, View } from "react-native";
      
      export default function HomePage() {
        const router = useRouter()
      
        return (
          <View
            style={{
              flex: 1,
              justifyContent: "center",
              alignItems: "center",
            }}
          >
            <Text>Home</Text>
            <Button title="View Details 1" onPress={() => router.push('/details/1')} />
            <Button title="View Details 2" onPress={() => router.push('/details/2')} />
          </View>
        );
      }
      
      
    • ただ、このままでは id が異なっていても同じ内容を返す

2-2-3. id 別にページの内容を変える

  • このままであると、 id 別にページのコンテンツを変更できないので、 app/(tabs)/(home)/details/[id].tsx に search params を受け取る処理を追加する

    • app/(tabs)/(home)/details/[id].tsx

      import { useLocalSearchParams } from "expo-router";
      import { StyleSheet, Text, View } from "react-native";
      
      export default function DetailsPage() {
        const { id } = useLocalSearchParams()
      
        return (
          <View style={styles.containers}>
            <Text>Details {id}</Text>
          </View>
        )
      }
      
      const styles = StyleSheet.create({
        containers: {
          flex: 1,
          justifyContent: 'center',
          alignItems: 'center'
        }
      })
      
    • id 別に表示された

      • id=1

      • id=2

2-2-4. 遷移するためのボタンは統一する

  • 今では、 search params毎にボタンが異なっているので、これを統一する

  • search paramsのinputを動的にすれば良い

    • app/(tabs)/(home)/index.tsx

      import { useRouter } from "expo-router";
      import { Button, Text, View } from "react-native";
      
      export default function HomePage() {
        const router = useRouter()
        const id = mockUseFetchId()
      
        return (
          <View
            style={{
              flex: 1,
              justifyContent: "center",
              alignItems: "center",
            }}
          >
            <Text>Home</Text>
            <Button title="View Details" onPress={() => router.push(`/details/${id}`)} />
          </View>
        );
      }
      
      const mockUseFetchId = () => {
        return Math.random().toString(36).substring(7)
      }
      
  • 動的にルーティングできている