ReactNativeでのアプリ開発、ExpoSDKがいい感じになってきたようなので試してみた：その４

[前回](https://qiita.com/sakai-y/items/947a558f5185ff3478c1)Expo Modulesの素振りをしたので、アプリに組み込んでいきます。

https://docs.expo.dev/modules/use-standalone-expo-module-in-your-project/

上記を参照すると、推奨方法として、前回参照したページのガイドが記載されていますが、あれだとモジュールが主でアプリが従のような形だったのでナゾ・・・。
で、別の方法として、モノレポにするか、npmへ公開する、とあります。
想定している使い方としては、製品としてのアプリ開発でプロプライエタリなライブラリを導入するケースなので、npmへ公開はナシです。

組み込み必要なライブラリが複数になることも考えて、モノレポにしようと思います。

## モノレポ化する
[こちら](https://docs.expo.dev/guides/monorepos/)を参考に[初回](https://qiita.com/sakai-y/items/6f1243f3bcbb435186b9)に作ったアプリをモノレポにします。

### 構造改革
まずは`apps`と`packages`ディレクトリを作成し、今あるアプリのコードを`apps/labapp`へ移動します。
（`.git`と`.gitignore`と`yarn.lock`はルートに残します）
```shell
ExpoLab % mkdir -p apps/labapp
ExpoLab % mkdir packages
ExpoLab % rm -rf node_modules/
ExpoLab % mv App.tsx assets babel.config.js *.json apps/labapp
```

移動した`package.json`の`name`を変更しておきます。

```json:apps/labapp/package.json
  "name": "labapp"
```

次にルートに新しく`package.json`を作成します。

```json:package.json
{
  "name": "expolab",
  "version": "1.0.0",
  "workspaces": ["apps/*", "packages/*"],
  "private": true
}
```

この状態で`yarn`を実行し、依存をインストールします。
ここまでを一旦コミットしておきましょう。

### モノレポ下でのExpoLabアプリ実行
次にモノレポ環境に合わせてカスタム`metro.config`を作成します。
`apps/expolab`へ移動して、まずは存在しない`metro.config.js`を生成します。

```shell
labapp % yarn expo customize metro.config.js
```

生成されたファイルの中身はこうなっていました。

```js:metro.config.js
// Learn more https://docs.expo.io/guides/customizing-metro
const { getDefaultConfig } = require('expo/metro-config');

module.exports = getDefaultConfig(__dirname);
```

サンプル通り`nohoist`なしのモノレポなので、依存パッケージはworkspaceルートの`node_modules`に入ります。Metro Bundlerがこれを参照するよう設定を変更します。

```js:metro.config.js
// Learn more https://docs.expo.io/guides/customizing-metro
const { getDefaultConfig } = require('expo/metro-config');
const path = require('path');

// Find the project and workspace directories
const projectRoot = __dirname;
// This can be replaced with `find-yarn-workspace-root`
const workspaceRoot = path.resolve(projectRoot, '../..');

const config = getDefaultConfig(projectRoot);

// 1. Watch all files within the monorepo
config.watchFolders = [workspaceRoot];
// 2. Let Metro know where to resolve packages and in what order
config.resolver.nodeModulesPaths = [
  path.resolve(projectRoot, 'node_modules'),
  path.resolve(workspaceRoot, 'node_modules'),
];
// 3. Force Metro to resolve (sub)dependencies only from the `nodeModulesPaths`
config.resolver.disableHierarchicalLookup = true;

module.exports = config;
```

次にエントリーポイントを変更します。
Managedなアプリではデフォルトで`index.js`は存在せず、`package.json`の`main`に`"node_modules/expo/AppEntry.js"`と設定されています。Expoが汎用エントリーポイントを用意してくれてるんですね。
このままだとモノレポでは動作しないようなので、`index.js`を作成します。

```js:index.js
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`の`main`も`"index.js"`に変更します。

これでアプリは準備できたので、確認してみます。

```shell
yarn start
```
`a`でAndroidエミュレータ、`i`でiOSシミュレータが起動するので、アプリを確認できれば完了です。

### モジュールをモノレポへ
前回作った`my-module`をモノレポへ移動します。
`.git`と`node_modules`は削除しておきます。

```shell
my-module % rm -rf .git node_modules
my-module % cd ..
expo % mv my-module ExpoLab/packages
```

`my-module/yarn.lock`の中身を全てコピーして、`ExpoLab/yarn.lock`の末尾に追加します。
`yarn`実行して依存インストールが済んだら、`my-module/yarn.lock`は削除しておきます。

`my-module/example`も不要なので削除してOKです。

### アプリからモノレポ内のネイティブモジュールを利用する
`labapp/package.json`に`my-module`を追加します。

```json:labapp/package.json
  "dependencies": {
    "my-module": "*",
    "expo": "~47.0.12",
```

追加したら、`yarn`しておきます。
次に、`labapp/App.tsx`に`my-module`を利用するコードを追加します。

```tsx:labapp/App.tsx
import * as MyModule from 'my-module'; // <-- 追加

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

では、これで動かしてみましょう。

```shell
labapp % yarn start
 :
› Press ? │ show all commands

Logs for your project will appear below. Press Ctrl+C to exit.
```

iOSシミュレータを起動してみます。

```shell
 ERROR  Error: Cannot find native module 'MyModule'
 ERROR  Invariant Violation: Failed to call into JavaScript module method AppRegistry.runApplication(). Module has not been registered as callable. Registered callable JavaScript modules (n = 11): Systrace, JSTimers, HeapCapture, SamplingProfiler, RCTLog, RCTDeviceEventEmitter, RCTNativeAppEventEmitter, GlobalPerformanceLogger, JSDevSupportModule, HMRClient, RCTEventEmitter.
        A frequent cause of the error is that the application entry file path is incorrect. This can also happen when the JS bundle is corrupt or there is an early initialization error when loading React Native.
```

「MyModuleというネイティブモジュールがない」と怒られました。
これは、デフォルトの実行環境であるExpo Go（のネイティブ部分）に、`MyModule`が存在しないため発生しているエラーです。
ならば`MyModule`を含んだExpo Goがあれば解決・・・？

それがDevelopment Buildです。

## Development Buildを試す
### Expoアカウント作成
Development Build作成のために[EAS](https://docs.expo.dev/eas/)を利用します。
ので、Expoアカウントが必要となりますので、ない場合は作ります。

https://expo.dev/

＃ GitHubアカウント連携とかできて欲しい・・・

### EASのセットアップ
`eas-cli`をプロジェクトにインストールします。
ガイドではグローバルへのインストールが推奨されていますが、複数プロジェクトを扱う場合にバージョンによる挙動の問題に遭遇したくないのでローカルへインストールします。

```shell
labapp % yarn add -D eas-cli
```

インストールできたらログインし、

```shell
labapp % yarn eas login
```

EAS Buildのセットアップを行います。

```shell
labapp % yarn eas build:configure
```

対象プラットフォーム、EASプロジェクトを作成するかどうか、を聞かれますが、すべてデフォルトでOKです。

### expo-dev-clientを設定
`expo-dev-client`をインストールします。

```shell
labapp % yarn expo install expo-dev-client
```

`App.tsx`に以下を追加します。
```tsx:App.tsx
import 'expo-dev-client';
```

### Development Build作成
Androidからいきます。

```shell
labapp %  yarn eas build -e development -p android
```

アプリケーションIDを聞かれますがデフォルトでもOKです。

しばらく待つと・・・ビルドが失敗しました。

```shell
✖ Build failed

🤖 Android build failed:
Gradle build failed with unknown error. See logs for the "Run gradlew" phase for more information.
```

上記では分かりにくいですが、`"Run gradlew"`の部分はリンクになっていて、開くとEASのビルドログの該当箇所が確認できます。

どうやら、Kotlinのバージョンミスマッチのようです。

そういえば、最初にcreate-expo-appした際に、あえて１つ前のSDKにしてたんでした。
ネイティブモジュール側と合わない状態になっているようです。
これが原因ですね。

では、予告通り（？）、SDKバージョンを上げましょう。

こちらを参考に、アップグレードします。

https://blog.expo.dev/expo-sdk-48-ccb8302e231

SDK48を入れて・・・

```shell
labapp % yarn add expo@^48.0.0
```

SDK48に合わせた更新をします。

```shell
labapp % yarn expo install --fix
```

・・・これだけです。
簡単すぎますね。

:::note info
素のReactNativeでバージョンアップする場合、こちらのページで利用中のバージョンと更新先のバージョンを選択して表示される差分を取り込みます。

https://react-native-community.github.io/upgrade-helper/

手動です。辛いです。
利用してるライブラリが動かなくなることもあります。
（これはexpoでも利用するライブラリによっては発生するでしょうけど）
動作確認含めて、結構な時間が掛かります。
:::

バージョンアップできたところで、EASビルドをリトライします。

・・・今度は無事にビルドが完了し、QRコードが表示されます。

```shell
🤖 Open this link on your Android devices (or scan the QR code) to install the app:
https://expo.dev/accounts/sakai-y/projects/ExpoLab/builds/aff9798f-823b-4ba5-a7f5-295acfxxxxxx

? Install and run the Android build on an emulator? › (Y/n)
```

最後に「エミュレータで実行する？」って聞かれているので実行します。
EASからビルドしたアプリがダウンロードされ、どのエミュレータを使うのか聞いてきます。
至れり尽くせりですね。

インストールが完了し、アプリが起動すると`npx expo start --dev-client`しろと表示されています。
`package.json`の`start`コマンドを書き換えましょう。

```json:labapp/package.json
  "scripts": {
    "start": "expo start --dev-client",
```

`expo start`します。

```shell
labapp %  yarn start
```

`a`キーでエミュレータで実行中アプリにロードされます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/243061/19e7633c-87c4-fc4d-e69e-c49a30f4c844.png" width="50%">

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/243061/833b09b5-c0be-fd44-7d9b-2a761ef422ae.png" width="50%">

無事にlabappからMyModuleを利用したコードが実行できています。

この状態でアプリのコード（ReactNative部分）を変更すると即座に反映されます。
Expo Goの時と変わらない開発体験で、かつ、カスタムネイティブコードを含んだアプリの開発ができるということです。
なんと素晴らしい。

### iOSシミュレータで実行する

iOSシミュレータでも実行してみます。
まずは`eas.json`にシミュレータ用のビルド設定を追加します。

```json:labapp/eas.json
  "build": {
    "development-simulator": {
      "developmentClient": true,
      "distribution": "internal",
      "ios": {
        "resourceClass": "m-medium",
        "simulator": true
      }
    },
```

EASビルドします。

```shell
labapp % yarn eas build -e development-simulator -p ios
```

しばらく待つと完了し、Androidの時と同じようにシミュレータ起動するか聞いてきます。

```shell
✔ Build finished

🍎 iOS app:
https://expo.dev/artifacts/eas/6v2Spif98ubTZLxxxxxxxx.tar.gz

? Install and run the iOS build on a simulator? › (Y/n)
```

シミュレータにインストール完了して起動が確認できたら、先ほどと同様に`yarn start`して`i`キーでシミュレータへロードします。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/243061/198de498-8661-2d2b-3bd8-28c7892c0a5e.png" width="50%">

## まとめ
ちょっと長くなりましたが、Managed workflowのアプリでカスタムネイティブコードを利用するために、モノレポ構成にした上で、Expo Modulesで作ったネイティブモジュールを組み込むところまでできました。

次は、プロプライエタリなライブラリをExpo Moduleとして作成して、アプリから利用してみたいと思います。

つづく