Top View


Author Daiki Urata

【Prebuildなし】Expo(React Native)のAmplify Gen2 Quickstartをやってみる

2024/08/02

はじめに

Amplify Gen2では簡単にバックエンド環境が構築できて便利です。 普段私が開発で使用しているReact Nativeもサポートされていますので、公式ドキュメントのQuickstartをやってみました。

ドキュメントではPrebuildをしてネイティブコードを吐き出す必要があると書かれていますが、それでは運用が辛くなってしまうので、Development Buildでできないかも今回検証してみました。

Expoプロジェクト作成

まずはExpoプロジェクトの作成です。

ここはドキュメントと同じコマンドを実行していくだけで良いです。

npm run start でアプリが起動できればOKです。

npx create-expo-app expo-amplify-gen2-app -t expo-template-blank-typescript
cd expo-amplify-gen2-app
npm run start

Note

Quickstartではネイティブライブラリを呼ぶ関係で npx expo prebuild を実行してネイティブコードをios/androidディレクトリに生成する必要があると書かれていますが、今回は後述するDevelopment Buildでのデバッグ手法を採用するため、Prebuildコマンドは実行不要です。

Amplifyバックエンド作成

アプリ側ができたら次はバックエンド環境を構築します。

ここもドキュメント通りで問題ないです。

npm create amplify@latest

Sandbox環境をデプロイします。

npx ampx sandbox

認証機能追加

開発環境が整ったら認証機能の実装に入ります。

必要なパッケージのインストール

認証機能に必要なAmplifyのUIライブラリなどインストールします。

npm add \
  @aws-amplify/ui-react-native \
  @aws-amplify/react-native \
  aws-amplify \
  @react-native-community/netinfo \
  @react-native-async-storage/async-storage \
  react-native-safe-area-context \
  react-native-get-random-values \
  react-native-url-polyfill

Development Buildアプリ作成

一通り依存関係をインストールしたところで、Quickstartの手順にはないDevelopment Buildアプリを作成します。

今回のように本来Prebuildしてネイティブコードを生成してデバッグする必要があるものでも、Development Buildアプリを一度作成しておくことでExpo Goのような開発体験のまま開発を進めることができます。

さらにPrebuildコマンドで吐き出されるios/androidディレクトリを持たず、ネイティブコードの管理が不要になる利点もあります。

作成方法は私が前に書いた以下記事の手順で進めていただければOKです。

コードを編集

ビルドの実行待ちの間にコードを編集しておきましょう。 基本的にQuickstartのものと同じコードですが、スタイルを少し調整しています。

App.tsx:

import React from "react";
import { Button, View, StyleSheet, SafeAreaView } from "react-native";

import { Amplify } from "aws-amplify";
import { Authenticator, useAuthenticator } from "@aws-amplify/ui-react-native";

import outputs from "./amplify_outputs.json";

Amplify.configure(outputs);

const SignOutButton = () => {
  const { signOut } = useAuthenticator();

  return (
    <View style={styles.signOutButton}>
      <Button title="Sign Out" onPress={signOut} />
    </View>
  );
};

const App = () => {
  return (
    <Authenticator.Provider>
      <Authenticator>
        <SafeAreaView>
          <SignOutButton />
        </SafeAreaView>
      </Authenticator>
    </Authenticator.Provider>
  );
};

const styles = StyleSheet.create({
  signOutButton: {
    height: "100%",
    justifyContent: "center",
    alignItems: "center",
  },
});

export default App;

Development Buildができたら端末へインストールして、npm run start を実行、QRコードを読み取って動かしてみましょう。

無事にサインアップできて、サインインできればOKです。

Amplify login page in Expo app

Todoリスト作成

最後はTodoリストの作成です。

こちらも特に変わったことはせず、スタイル調整だけしてほぼQuickstartそのままのコードで動きました。

apmlify/data/resource.ts:

import { type ClientSchema, a, defineData } from "@aws-amplify/backend";

const schema = a.schema({
  Todo: a
    .model({
      content: a.string(),
      isDone: a.boolean(),
    })
    .authorization((allow) => [allow.owner()]),
});

export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: "userPool",
  },
});

src/TodoList.tsx:

import { useState, useEffect } from "react";
import { View, Button, Text, StyleSheet, FlatList } from "react-native";

import { generateClient } from "aws-amplify/data";
import type { Schema } from "../amplify/data/resource";
import { GraphQLError } from "graphql";
const client = generateClient<Schema>();

const TodoList = () => {
  const dateTimeNow = new Date();
  const [todos, setTodos] = useState<Schema["Todo"]["type"][]>([]);
  const [errors, setErrors] = useState<GraphQLError>();

  useEffect(() => {
    const sub = client.models.Todo.observeQuery().subscribe({
      next: ({ items }) => {
        setTodos([...items]);
      },
    });

    return () => sub.unsubscribe();
  }, []);

  const createTodo = async () => {
    try {
      await client.models.Todo.create({
        content: `${dateTimeNow.getUTCMilliseconds()}`,
      });
    } catch (error: unknown) {
      if (error instanceof GraphQLError) {
        setErrors(error);
      } else {
        throw error;
      }
    }
  };

  if (errors) {
    return <Text>{errors.message}</Text>;
  }

  const renderItem = ({ item }: { item: Schema["Todo"]["type"] }) => (
    <TodoItem {...item} />
  );
  return (
    <View style={{ flex: 1 }}>
      <FlatList
        data={todos}
        renderItem={renderItem}
        keyExtractor={(item) => item.id}
        ItemSeparatorComponent={() => <View style={styles.listItemSeparator} />}
        ListEmptyComponent={() => <Text>The todo list is empty.</Text>}
        style={styles.listContainer}
      ></FlatList>
      <Button onPress={createTodo} title="Create Todo" />
    </View>
  );
};

const TodoItem = (todo: Schema["Todo"]["type"]) => (
  <View style={styles.todoItemContainer} key={todo.id}>
    <Text
      style={{
        ...styles.todoItemText,
        textDecorationLine: todo.isDone ? "line-through" : "none",
        textDecorationColor: todo.isDone ? "red" : "black",
      }}
    >
      {todo.content}
    </Text>
    <Button
      onPress={async () => {
        await client.models.Todo.delete(todo);
      }}
      title="Delete"
    />
    <Button
      onPress={() => {
        client.models.Todo.update({
          id: todo.id,
          isDone: !todo.isDone,
        });
      }}
      title={todo.isDone ? "Undo" : "Done"}
    />
  </View>
);

const styles = StyleSheet.create({
  todoItemContainer: {
    flexDirection: "row",
    alignItems: "center",
    padding: 8,
    gap: 12,
  },
  todoItemText: { flex: 1, textAlign: "center" },
  listContainer: { flex: 1, alignSelf: "stretch", padding: 8 },
  listItemSeparator: { backgroundColor: "lightgrey", height: 2 },
});

export default TodoList;

App.tsx:

import React from "react";
import { Button, View, StyleSheet, SafeAreaView } from "react-native";

import { Amplify } from "aws-amplify";
import { Authenticator, useAuthenticator } from "@aws-amplify/ui-react-native";

import outputs from "./amplify_outputs.json";
import TodoList from "./src/TodoList";

Amplify.configure(outputs);

const SignOutButton = () => {
  const { signOut } = useAuthenticator();

  return (
    <View style={styles.signOutButton}>
      <Button title="Sign Out" onPress={signOut} />
    </View>
  );
};

const App = () => {
  return (
    <Authenticator.Provider>
      <Authenticator>
        <SafeAreaView style={styles.container}>
          <SignOutButton />
          <TodoList />
        </SafeAreaView>
      </Authenticator>
    </Authenticator.Provider>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 8,
    paddingVertical: 60,
  },
  signOutButton: {
    alignSelf: "flex-end",
  },
});

export default App;

Amplify todo list in Expo app

おわりに

今回はQuickstartそのまま実施しただけで、あまり内容はありませんでしたが公式ドキュメントがPrebuildのみの手順だけ紹介していて、Development Buildでも良いのでは?と疑問に思っていたのがきっかけで検証メモのような内容でした。

結果としてDevelopment Buildでも開発は進めていけそうなことがわかったので、実務でも採用できそうです!

Daiki Urata

Daiki Urata

Twitter X

フロントエンド/モバイルアプリなどを主に開発しています。