Open25

ReactNative + expoでのアプリ開発してみる

oo

前提条件

•	Node.jsがインストールされていること(公式サイトからダウンロードできます)
•	npmまたはyarnが使用可能であること
oo

Expo CLIのインストール

まず、Expo CLIをグローバルにインストールします。

# npmを使用する場合
npm install -g expo-cli

# yarnを使用する場合
yarn global add expo-cli
oo

新しいプロジェクトの作成

ターミナルで作業ディレクトリに移動し、以下のコマンドで新しいExpoプロジェクトを作成します。

expo init my-first-app
oo

ここでエラー


expo init my-first-app
WARNING: The legacy expo-cli does not support Node +17. Migrate to the new local Expo CLI: https://blog.expo.dev/the-new-expo-cli-f4250d8e3421.
┌───────────────────────────────────────────────────────────────────────────┐
│                                                                           │
│   The global expo-cli package has been deprecated.                        │
│                                                                           │
│   The new Expo CLI is now bundled in your project in the expo package.    │
│   Learn more: https://blog.expo.dev/the-new-expo-cli-f4250d8e3421.        │
│                                                                           │
│   To use the local CLI instead (recommended in SDK 46 and higher), run:   │
│   › npx expo <command>                                                    │
│                                                                           │
└───────────────────────────────────────────────────────────────────────────┘

Migrate to using:
› npx create-expo-app --template

? Choose a template: › - Use arrow-keys. Return to submit.
    ----- Managed workflow -----
❯   blank               a minimal app as clean as an empty canvas
    blank (TypeScript)  same as blank but with TypeScript configuration
    tabs (TypeScript)   several example screens and tabs using react-navigation and TypeScript
    ----- Bare workflow -----
    minimal             bare and minimal, just the essentials to get you started
oo

このエラーは、あなたが使用している expo-cli が非推奨となっており、新しいExpo CLIに移行する必要があることを示しています。

Expoの開発チームは、グローバルにインストールするexpo-cliのサポートを終了し、各プロジェクトにローカルでCLIをバンドルする形(expoパッケージ経由)に変更しました。この移行は、Node.jsの新しいバージョン(特にNode.js 17以降)をサポートし、より柔軟で一貫性のある開発環境を提供するためのものです。

oo

既存のグローバルexpo-cliを削除する

npm uninstall -g expo-cli
oo

新しいCLI(create-expo-app)を使ってプロジェクトを作成

任意のディレクトリで以下のコマンドを実行。

npx create-expo-app my-first-app

インストールが始まる。

oo

立ち上げ

教材がnpmではなくpnpmだったので従う。

pnpm create expo-app@latest react-native-expo-tutorial --template blank-typescript

TypeScript を利用した空プロジェクトというテンプレートを使用する。

出来上がったプロジェクト

react-native-expo-tutorial % tree -I 'node_modules'
.
├── App.tsx
├── app.json
├── assets
│   ├── adaptive-icon.png
│   ├── favicon.png
│   ├── icon.png
│   └── splash-icon.png
├── index.ts
├── package.json
├── pnpm-lock.yaml
└── tsconfig.json
oo

いちおうGit連携させておく。

user_name@device_name react-native-expo-tutorial % git init 
Initialized empty Git repository in /Users/oomori/Developer/_TUTORIALS/EXPO/react-native-expo-tutorial/.git/
user_name@device_name react-native-expo-tutorial % git add .
user_name@device_name react-native-expo-tutorial % git commit - m 'initial commit'
error: pathspec '-' did not match any file(s) known to git
error: pathspec 'm' did not match any file(s) known to git
error: pathspec 'initial commit' did not match any file(s) known to git
user_name@device_name react-native-expo-tutorial % git commit -m 'initial commit' 
[main (root-commit) 2ad1e04] initial commit
 12 files changed, 7357 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 .npmrc
 create mode 100644 App.tsx
 create mode 100644 app.json
 create mode 100644 assets/adaptive-icon.png
 create mode 100644 assets/favicon.png
 create mode 100644 assets/icon.png
 create mode 100644 assets/splash-icon.png
 create mode 100644 index.ts
 create mode 100644 package.json
 create mode 100644 pnpm-lock.yaml
 create mode 100644 tsconfig.json
user_name@device_name react-native-expo-tutorial % git remote add origin https://github.com/Omori0219/react-native-expo-tutorial.git
user_name@device_name react-native-expo-tutorial % git push origin main -f
Enumerating objects: 14, done.
Counting objects: 100% (14/14), done.
Delta compression using up to 8 threads
Compressing objects: 100% (13/13), done.
Writing objects: 100% (14/14), 117.84 KiB | 14.73 MiB/s, done.
Total 14 (delta 0), reused 0 (delta 0), pack-reused 0
To https://github.com/Omori0219/react-native-expo-tutorial.git
 + 2674e14...2ad1e04 main -> main (forced update)
user_name@device_name react-native-expo-tutorial % 
oo

依存パッケージのインストール

Web ブラウザで動作確認するために依存するパッケージをインストールします。

npx expo install を利用するメリットは、インストール済みの React Native のバージョンに合わせて適切なバージョンのパッケージをインストールできることです。


npx expo install react-dom react-native-web @expo/metro-runtime
oo

モバイルと Web でアプリを実行
それでは iOS,Android,Web でアプリの動作確認をローカルで行います。
React Native は1つのコードベースで iOS、Android、Web で動作確認できます。特定の環境のみを確認できますし、すべての環境でも動作確認できます。既に実行するスクリプトは package.json に記述されています。
{
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web",
}
}

とのこと。すごそう

oo

ターミナルに表示された QRコードをスマホで読み取るだけで、実機で確認できた。ローカルのコードいじるとリロードなしに反映。何これ?ヤバ過ぎ

oo

参考書ではエラー対応していたけど、特にエラーが出ていないので進める。

oo

やる必要があるか分からないけどとりあえず

クリーンアップスクリプトの追加

"clean": "git clean -xdf .cache .expo android ios node_modules",

これをpackage.jsonのscript部分に加える

キャッシュクリア的な、flutterでいうとpub消したりする時に使うらしい。

  1. 依存関係の更新後:新しいパッケージを追加したり、既存のパッケージを更新した後に、古いキャッシュやビルドデータが残っていると、予期しない動作やエラーが発生する可能性があります。これらを削除することで、環境をリフレッシュできます。
    2. ビルドや実行時の問題が発生した場合:アプリが正しく動作しない、ビルドが失敗するなどの問題が生じた際、キャッシュや一時ファイルが原因であることがあります。クリーンアップを行うことで、これらの問題を解決できる場合があります。
    3. 開発環境の初期化時:新しい機能の追加や大規模な変更を行う前に、環境をクリーンな状態に戻すことで、不要な影響を避けることができます。
oo

Chapter 05 srcフォルダを作成

多くのプロジェクトでは、コンポーネントや画面などのファイルを src フォルダーにまとめることが一般的です。本プロジェクトでも同様に src フォルダーを作成します。

src フォルダーを作成します。

パス 説明
src/assets 画像やフォントなどのリソースファイルを格納するフォルダー
src/screens 画面コンポーネントを格納するフォルダー

$ mkdir -p  src/assets \
            src/screens
oo

エントリーポイントを変更

package.json の更新
エントリーポイントのパスは package.json の main に指定されています。Expo ではデフォルトで expo/AppEntry.js がエントリーポイントとなっています。これを src/app.tsx に変更します。

と書かれているが、現状は"index.ts"が指定されている。
とりあえずここを"main": "src/screens/app.tsx",に変えてみておく。
出し、

といいつつ。

ルートにあるindex.tsの内容はこちら

import { registerRootComponent } from 'expo';

import App from './App';

// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// It also ensures that whether you load the app in Expo Go or in a native build,
// the environment is set up appropriately
registerRootComponent(App);

ここから/App.tsxを呼んでるみたいだから、ここのパスを変えるだけにうよう。

// registerRootComponentはAppRegistry.registerComponent('main', () => App)を呼び出します。
// また、アプリをExpo Goでロードする場合でも、ネイティブビルドでロードする場合でも、
// 環境が適切に設定されるようにします。

コメントの日本語訳。

registerRootComponentの役割

registerRootComponent(App);

この関数は、React NativeのAppRegistry.registerComponentと同様に動作し、アプリのエントリーポイントとしてAppコンポーネントを登録します。
• 特徴として、アプリをExpo Go(Expoが提供するモバイルアプリ)で実行する場合でも、ネイティブビルド(独自にビルドしたアプリ)で実行する場合でも、適切な環境設定を自動的に行います。これにより、開発者は異なる環境での設定を気にせずにアプリを実行できます。

なぜこのファイルが必要か
React Nativeアプリケーションでは、アプリケーションの開始点として特定のファイル(通常はindex.jsやApp.js)が必要です。このファイルは、アプリのメインコンポーネントを登録し、アプリの実行を開始します。Expoを使用することで、registerRootComponentを利用して、環境に依存しない形でアプリを起動することが可能になります。

もっとわかりやすく

registerRootComponent(App);

• この行で、Appというコンポーネントをアプリの「ルートコンポーネント」として登録します。
• これにより、アプリが起動するときにまずAppが表示されます。
• また、Expoを使ってアプリを実行するときも、ネイティブにビルドしたアプリとして実行するときも、自動的に適切な設定を行ってくれます。

registerRootComponent(App);これを使わずに直接mainでapp.tsx呼び出すとどうなるか

  1. 必要な環境設定が行われない
    registerRootComponent は、アプリがどのように実行されるかに応じて(例えば、Expo Go で実行するのか、ネイティブビルドで実行するのか)適切な設定を自動的に行います。これを使わない場合、これらの設定が行われず、アプリが正しく動作しない可能性があります。
  2. エラーハンドリングが適切に行われない
    registerRootComponent は、アプリがクラッシュしたときにエラーハンドリングを行う仕組みも提供します。これを使わないと、エラーが発生したときに適切に対処できず、デバッグが難しくなることがあります。
  3. ホットリロードやデバッグ機能の制限
    Expo の便利な機能(ホットリロードやデバッグツールなど)が正しく動作しない可能性があります。registerRootComponent を使うことで、これらの機能がスムーズに利用できるようになります。
oo

↑とりあえず、2024/11/18現在では、テンプレートの方を正しいとしていいみたい。

なので、index.tsとpackaga.jsonは立ち上げ後からいじらない。

index.ts
import { registerRootComponent } from 'expo';

import App from './App';

// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// It also ensures that whether you load the app in Expo Go or in a native build,
// the environment is set up appropriately
registerRootComponent(App);
package.json

{
  "name": "react-native-expo-tutorial",
  "version": "1.0.0",
  "main": "index.ts",
  "scripts": {
    "start": "expo start",
    "android": "expo start --android",
    "ios": "expo start --ios",
    "web": "expo start --web",
    "clean": "git clean -xdf .cache .expo android ios node_modules"
  },

その代わり、App.tsxを書き換える。
書き換え前

App.tsx
import { StatusBar } from "expo-status-bar";
import { StyleSheet, Text, View, Button } from "react-native";

export default function App() {
  return (
    <View style={styles.container}>
      <Text>Open up App.tsx to start working on your app!????</Text>
      <Text>Hello?</Text>
      <Button title="Click me" onPress={() => {}} />
      <StatusBar style="auto" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
});

書き換え後

App.tsx
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';

const App = () => {
  return (
    <View style={styles.container}>
      <Text>Open up App.tsx to start working on your app!</Text>
      <StatusBar style="auto" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

export default App;
oo

命名規則について

この教材では、ケバブケース(app-example.tsx)にするぞと言ってるけど、コンポーネントはパスカルケース(AppExample.tsx)で、それ以外のファイルはcamelケースが多いらしいのでそれに従う

oo

srcディレクトリについて

srcを作るのはベストプラクティスならしい。

現状だと、assetsフォルダがsrcと同階層にいるので、srcの中に入れる。
それに合わせてパスを変える。
app.jsonでアセットを参照してるので、それを変える。

:::messages
app.jsonって何?

app.json は、Expo プロジェクトのルートディレクトリに存在する設定ファイルで、アプリケーションのメタデータや設定情報を JSON 形式で記述します。Expo はこのファイルを参照して、アプリのビルドプロセスや実行時の挙動を制御します。

主な役割
1. アプリのメタデータ管理:
• アプリ名、バージョン、アイコン、スプラッシュスクリーンなどの基本情報を定義します。
2. プラットフォーム固有の設定:
• iOS や Android 向けの設定を個別にカスタマイズできます(例: アイコン、スプラッシュスクリーン、パーミッションなど)。
3. ビルドおよびデプロイの設定:
• アプリのビルド設定やデプロイ先の指定など、ビルドプロセスに関連する情報を提供します。
4. 環境変数やプラグインの設定:
• 必要に応じて、環境変数の設定やカスタムプラグインの導入が可能です。

:::

メッセージに収まらないのではみ出す。

app.jsonの基本構造

app.json
{
  "expo": {
    "name": "MyApp", // アプリ名
    "slug": "my-app", // Expo プロジェクトの識別子
    "version": "1.0.0", // アプリのバージョン
    "orientation": "portrait", // 画面の向き
    "icon": "./src/assets/icon.png", // アプリアイコンのパス
    "userInterfaceStyle": "light", // ダークモード対応
    "splash": { // スプラッシュスクリーンの設定
      "image": "./src/assets/splash.png",
      "resizeMode": "contain",
      "backgroundColor": "#ffffff"
    },
    "assetBundlePatterns": [ // バンドルするアセットのパターン
      "**/*"
    ],
    "ios": { // iOS 固有の設定
      "supportsTablet": true,
      "bundleIdentifier": "com.mycompany.myapp",
      "buildNumber": "1.0.0"
    },
    "android": { // Android 固有の設定
      "adaptiveIcon": {
        "foregroundImage": "./src/assets/adaptive-icon.png",
        "backgroundColor": "#ffffff"
      },
      "package": "com.mycompany.myapp",
      "versionCode": 1
    },
    "web": { // Web 固有の設定
      "favicon": "./src/assets/favicon.png"
    }
  }
}

flutterのpubspec.yaml的なやつかな。

• app.json(Expo) は、アプリ全体の設定を一元管理するためのファイルです。Expo専用の設定が含まれており、簡潔にアプリの構成を定義できます。

• Flutterでは、pubspec.yaml が主要な設定ファイル であり、依存関係やアセットの管理を行います。さらに、iOSとAndroidそれぞれに固有の設定ファイル(Info.plist、AndroidManifest.xml)が存在し、プラットフォームごとの詳細な設定を行います。

Flutterの設定は分散型 であり、プラットフォームごとに異なる設定ファイルを使用するため、設定の柔軟性と詳細なカスタマイズが可能です。一方、Expoのapp.jsonは集中型 で、設定が一つのファイルに集約されているため、設定の一貫性が保ちやすいです。

つまり、flutterでInfo.plist、AndroidManifest.xmlこの辺りを編集しないといけなかったものもexpoならapp.jsonで調整可能ならしい。

app.json でできることとできないこと

app.json で可能な設定

•	基本的なアプリ設定: 名前、アイコン、スプラッシュスクリーン、バージョン管理など。
•	プラットフォーム固有の権限設定: iOSやAndroidで必要な権限を設定。
•	アセットの管理: 画像、フォント、その他のリソースの指定。
•	Expoプラグインの設定: 例えば、Push通知やAnalyticsなどの設定。

app.json ではできない設定

•	高度なネイティブカスタマイズ:
•	ネイティブモジュールの追加やカスタマイズ。
•	特定のネイティブAPIへの直接アクセスや設定変更。
•	iOSやAndroidの細かな設定変更:
•	Info.plistやAndroidManifest.xmlでしかできない詳細な設定。
•	カスタムネイティブコードの統合や変更。
oo

コンポーネントファイルの書き方

教材では、

import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';

export default function App() {
  return (
    <View style={styles.container}>
      <Text>Open up App.tsx to start working on your app!</Text>
      <StatusBar style="auto" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

こんな感じに、関数のところにコンポーネント名を書く方法(インラインエクスポート)を採用して書き換えていたが、chatgptとclaudeに聞いた感じ、テンプレートから生成さえれたままの書き方

import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View } from 'react-native';

const App = () => {
  return (
    <View style={styles.container}>
      <Text>Open up App.tsx to start working on your app!</Text>
      <StatusBar style="auto" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

export default App;

こっちの方が推奨されるらしい。(後置エクスポート)

推奨理由(Claude)

可読性が高い
コンポーネントのロジックとエクスポート宣言が分離されており、コードの構造が明確
リファクタリングが容易
コンポーネント名の変更が1箇所で済む
後からnamed exportに変更する場合も修正が簡単
一貫性の維持
1ファイル1コンポーネントの原則に合致
チーム開発での規約として管理しやすい
IDEのサポート
多くのIDEで自動インポート機能が後置エクスポートでより確実に動作

推奨理由(o1-mini)

後置エクスポートは、特に大規模なプロジェクトやチームでの開発において有用です。また、デバッグ時にもコンポーネント名が明確に表示されやすいため、推奨されることが多いです。
• コンポーネント名が明示的に定義されているため、デバッグが容易。
• 複数のコンポーネントやユーティリティ関数を同一ファイルで管理しやすい。
• エディタやIDEでのサポートが向上。

oo

typescriptの設定

tsconfig.json を更新
tsconfig.json をこちらの内容で上書きします。

tsconfig.json
{
  "extends": "expo/tsconfig.base",
  "compilerOptions": {
    "allowJs": true,
    "esModuleInterop": true,
    "jsx": "react-native",
    "lib": ["DOM", "ESNext"],
    "moduleResolution": "node",
    "noEmit": true,
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "target": "ESNext",
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    },
    "strict": true
  },
  "include": [
    "**/*.ts",
    "**/*.tsx",
    "**/*.js",
    "**/*.jsx",
    ".eslintrc.cjs",
    ".prettierrc.mjs",
    "babel.config.cjs",
    "metro.config.js"
  ],
  "exclude": ["node_modules", "jest.config.js"]
}

なんで?

AIに聞いてみたり調べてみたり

結果

  • 教材の設定には、継承元のtsconfig.tsと重複する内容もあるので、ベストではない。
tsconfig.ts
{
  "extends": "expo/tsconfig.base",
  "compilerOptions": {
    "strict": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    },
    "forceConsistentCasingInFileNames": true // ファイル名の大文字と小文字の区別を強制
  }
}

これが今のところのベストプラクティス。

  • includeについて。Typescriptはデフォルトで、プロジェクトルートおよびそのサブディレクトリ内の.ts, .tsx, .d.ts ファイルをコンパイル対象としているので機能的には記載不要。(ただし、明示的にしたい場合などは書いても良い)

• デフォルトでコンパイル対象となるファイル:
• .ts, .tsx, .d.ts ファイル
• プロジェクトルートおよびそのサブディレクトリ内のこれらのファイル
• デフォルトで除外されるファイル・ディレクトリ:
• node_modules/, bower_components/
• *.js, *.jsx, *.json ファイル(設定による)
• 各種設定ファイル(例: babel.config.js)
• カスタマイズが必要な場合:
• include, exclude, files オプションを使用して、コンパイル対象を明示的に指定または除外
• 設定変更後の注意点:
• tsconfig.json の変更後は、開発サーバーやエディタを再起動して設定を反映させる

↑ちなみに、継承元で allowJS:trueになっているので、.js も追加されている。

あと、"forceConsistentCasingInFileNames": trueはファイル名の大文字小文字違いの変な不具合を避けるために設定が必要とした

oo

リントやフォーマッタについて

Next.jsと同じようにESlintとpritteirを使用する

俺はBiomeを使用するぞジョジョー!!!

Biomeを導入する。

https://zenn.dev/akineko/articles/d967d5dcada598

pnpm add -D @biomejs/biome

これでインストール

https://zenn.dev/voluntas/scraps/31de0e6155b43e

未来のためのやつ

  1. プロジェクトルートに、.vscode/extensions.jsonファイルを作り、以下の内容を書いておくと、Gitでcloneした人がBiomeを入れてねってvscodeに言われる
extensions.json
{
  "recommendations": [
    "biomejs.biome"
  ]
}
  1. 同じくルートに.vscode/settings.jsonを作り以下の内容を書く。プロジェクト全体に適用されるVSCodeの設定を管理できる。
settings.json
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "[javascript]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[javascriptreact]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[typescript]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[typescriptreact]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "editor.codeActionsOnSave": {
    "quickfix.biome": true,
    "source.organizeImports.biome": true
  }
}
oo

Biomeのことばっかりやってたらよくないので、一旦設定ファイルは初期設定のままにしておく。
ADHD大発揮していたああアブない

biome.json

{
	"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
	"vcs": {
		"enabled": false,
		"clientKind": "git",
		"useIgnoreFile": false
	},
	"files": {
		"ignoreUnknown": false,
		"ignore": []
	},
	"formatter": {
		"enabled": true,
		"indentStyle": "tab"
	},
	"organizeImports": {
		"enabled": true
	},
	"linter": {
		"enabled": true,
		"rules": {
			"recommended": true
		}
	},
	"javascript": {
		"formatter": {
			"quoteStyle": "double"
		}
	}
}