Expoでプッシュ通知・ディープリンク・AsyncStorageを実装した話

エンジニアの記録
個人開発・実装メモ

個人アプリ「メモリーズ」を開発する中で、プッシュ通知・ディープリンク・AsyncStorageの3つを実装しました。

どれも「なんとなく知ってるけど実装したことない」という機能でしたが、実際に動かしてみると思ったより単純でした。実コード付きでまとめます。

対象読者

React Native(Expo)でプッシュ通知・ディープリンク・AsyncStorageを実装しようとしているエンジニアに向けて書きます。

PROFILE 20代ITエンジニア|個人アプリ「メモリーズ」開発中|Raw Ambition 運営
キラ
キラ
3つとも「難しそう」と思ってたけど、Expoのライブラリを使えば思ったより簡単だった。実装してみて初めてわかることが多い。
Contents
  1. AsyncStorage:アプリのデータをローカルに保存する
  2. プッシュ通知:処理完了を知らせる
  3. ディープリンク:外部からアプリの特定画面を開く
  4. 3つを組み合わせた実際の使い方
  5. まとめ

① AsyncStorage:アプリのデータをローカルに保存する

AsyncStorageはReact Nativeでローカルにデータを保存するためのライブラリです。アプリを閉じてもデータが残るので、バックエンドなしで簡単なデータ永続化ができます。

インストール

npx expo install @react-native-async-storage/async-storage

基本的な使い方

import AsyncStorage from '@react-native-async-storage/async-storage';

// データを保存
const saveRecord = async (record) => {
  try {
    // 既存のレコードを取得
    const existing = await AsyncStorage.getItem('records');
    const records = existing ? JSON.parse(existing) : [];

    // 新しいレコードを追加
    records.push(record);

    // 保存
    await AsyncStorage.setItem('records', JSON.stringify(records));
    console.log('保存完了');
  } catch (error) {
    console.error('保存エラー:', error);
  }
};

// データを取得
const getRecords = async () => {
  try {
    const data = await AsyncStorage.getItem('records');
    return data ? JSON.parse(data) : [];
  } catch (error) {
    console.error('取得エラー:', error);
    return [];
  }
};

? POINT

AsyncStorageはキーと値のペアで保存します。値は文字列のみなので、オブジェクトや配列はJSON.stringifyで文字列に変換してから保存し、取得時にJSON.parseで戻します。

⚠️ AsyncStorageの制限

AsyncStorageは暗号化されていないローカルストレージです。パスワードや機密情報は保存しないこと。大量のデータ保存にも向いていません。個人の設定や簡単な記録データには最適です。
スカイ
スカイ
バックエンドなしでデータを保存できるから、個人開発のMVP段階では最高の選択肢。後からバックエンドに移行もできる。

② プッシュ通知:処理完了を知らせる

「メモリーズ」ではGPS情報を使った店舗検索が完了したときにプッシュ通知を出しています。処理に時間がかかる場合、ユーザーをアプリの前で待たせずに完了を通知できます。

インストール

npx expo install expo-notifications

実装

import * as Notifications from 'expo-notifications';

// 通知ハンドラの設定(App.tsxの最上部に書く)
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowBanner: true,   // バナー表示
    shouldShowList: true,     // 通知センターに表示
    shouldPlaySound: true,    // サウンド再生
    shouldSetBadge: false,    // バッジは使わない
  }),
});

// 通知の許可を取得
const requestPermission = async () => {
  const { status } = await Notifications.requestPermissionsAsync();
  return status === 'granted';
};

// 通知を送る
const sendNotification = async (title, body) => {
  await Notifications.scheduleNotificationAsync({
    content: {
      title,
      body,
    },
    trigger: null, // null = 即時送信
  });
};

実際の使用例

// レコード保存完了時に通知
const saveAndNotify = async (record) => {
  await saveRecord(record);  // AsyncStorageに保存

  await sendNotification(
    '保存完了!',
    `${record.placeName} を記録しました`
  );
};

⚠️ 注意点

shouldShowAlertは古いAPIで非推奨になっています。新しいAPIではshouldShowBannershouldShowListを使います。古い記事を参考にすると型エラーになることがあるので注意。
キラ
キラ
プッシュ通知はExpo Goでは本番トークンが取れない。EAS Buildで実機確認するまで「本当に動くか」わからないのが難点。

③ ディープリンク:外部からアプリの特定画面を開く

ディープリンクはmemory://cameraのようなURLスキームで、外部からアプリの特定画面を直接開く仕組みです。共有機能やショートカットの基盤になります。

app.jsonの設定

// app.json
{
  "expo": {
    "scheme": "memory",  // URLスキームを定義
    "android": {
      "intentFilters": [
        {
          "action": "android.intent.action.VIEW",
          "data": [{ "scheme": "memory" }],
          "category": [
            "android.intent.category.DEFAULT",
            "android.intent.category.BROWSABLE"
          ]
        }
      ]
    }
  }
}

App.tsxでのリンク処理

import { Linking } from 'react-native';
import { useEffect } from 'react';
import { useNavigation } from '@react-navigation/native';

const App = () => {
  const navigation = useNavigation();

  useEffect(() => {
    // アプリ起動時のディープリンクを処理
    const handleInitialURL = async () => {
      const url = await Linking.getInitialURL();
      if (url) handleDeepLink(url);
    };

    // アプリ起動中のディープリンクを処理
    const subscription = Linking.addEventListener('url', ({ url }) => {
      handleDeepLink(url);
    });

    handleInitialURL();
    return () => subscription.remove();
  }, []);

  const handleDeepLink = (url) => {
    // memory://camera → カメラ画面を開く
    if (url === 'memory://camera') {
      navigation.navigate('Camera');
    }
  };
};

? POINT

ディープリンクには2つのケースがあります。「アプリが閉じた状態で開かれた場合」(getInitialURL)と「アプリが起動中に開かれた場合」(addEventListener)。両方に対応しないと、状況によって動かないことがあります。

3つを組み合わせた実際の使い方

「メモリーズ」での実際の使い方はこういう流れです:

// 実際のフロー
// 1. ユーザーが写真を撮る(ディープリンクでカメラ画面を開くことも可能)
// 2. GPS + Places APIで店舗を自動特定
// 3. AsyncStorageにレコードを保存
// 4. プッシュ通知で「保存完了」を通知

const handlePhotoSave = async (photo) => {
  // GPS座標を取得
  const location = photo.location;

  // Places APIで店舗を検索
  const place = await searchNearbyPlaces(
    location.latitude,
    location.longitude
  );

  // レコードを作成
  const record = {
    id: Date.now().toString(),
    placeName: place?.name ?? '不明な場所',
    category: place?.types[0] ?? 'other',
    photoUri: photo.uri,
    savedAt: new Date().toISOString(),
  };

  // AsyncStorageに保存
  await saveRecord(record);

  // プッシュ通知を送る
  await sendNotification(
    '記録完了!',
    `${record.placeName} を保存しました`
  );
};
スカイ
スカイ
3つを組み合わせると「撮る→自動保存→通知」という体験が作れるんだね。各機能は単純でも組み合わせると強力。

? この記事のまとめ
  • AsyncStorage:JSONに変換してキーと値で保存。バックエンドなしのMVPに最適
  • プッシュ通知:shouldShowBannerとshouldShowListを使う(shouldShowAlertは非推奨)
  • ディープリンク:getInitialURLとaddEventListenerの両方に対応する必要がある
  • 3つを組み合わせることで「自動保存→完了通知→外部からの起動」が実現できる

Expoのライブラリはドキュメントが充実していて実装しやすいですが、古いAPIが非推奨になっていることがあるので、常に公式ドキュメントの最新版を参照することをおすすめします。

Recommended
ブログを始めるならConoHa WINGが最速
個人開発の記録をブログで発信して収益化。月1,000円以下で始められます。
▶ ConoHa WINGを見てみる
※ アフィリエイトリンクです
Next
次回:「モバイルエンジニアが実際に使っているツール・ライブラリ【全部公開】」
Raw Ambition 始動から3ヶ月目

コメント

タイトルとURLをコピーしました