You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用expo-notifications实现每日随机时间的推送通知?

实现类似BeReal的每日随机时间推送通知(Expo Notifications)

我正在用expo-notifications库开发功能,想要实现像BeReal那样每日随机时间推送通知,每次推送展示不同内容(比如从API获取名言)。目前已经写了自定义Hook测试,但没法实现自动每日随机调度——现在只能手动点击按钮触发,尝试过一次性调度多个通知,但需要用户打开App续期,卡在这里了。


现有自定义Hook代码

import { View, Text, Platform } from "react-native";
import React, { useEffect, useState, useRef } from "react";
import * as Notifications from "expo-notifications";
import * as Device from "expo-device";

Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: true,
    shouldSetBadge: false,
  }),
});

export default function useNotifications() {
  const [expoPushToken, setExpoPushToken] = useState("");
  const [givenNotificationPerm, setGivenNotificationPerm] = useState(false);
  const [notification, setNotification] = useState(false);
  const notificationListener = useRef();
  const responseListener = useRef();

  useEffect(() => {
    givenPermissions();
    getScheduled();

    Notifications.cancelAllScheduledNotificationsAsync();

    notificationListener.current =
      Notifications.addNotificationReceivedListener((notification) => {
        setNotification(notification);
        console.log("--- notification received ---");
        console.log(notification);
        console.log("------");
      });

    responseListener.current =
      Notifications.addNotificationResponseReceivedListener((response) => {
        console.log("--- notification tapped ---");
        const screen = response?.notification?.request?.content?.data?.screen;
        const show_time =
          response?.notification?.request?.content?.data?.show_time;
        console.log("this showed: " + show_time);
        console.log("------");
      });

    return () => {
      Notifications.removeNotificationSubscription(
        notificationListener.current
      );
      Notifications.removeNotificationSubscription(responseListener.current);
    };
  }, []);

  async function permissions() {
    registerForPushNotificationsAsync().then((token) =>
      setExpoPushToken(token)
    );
  }

  async function getScheduled() {
    console.log("getting scheduled notification");

    const scheduled = await Notifications.getAllScheduledNotificationsAsync();
    console.log("scheduled:");
    console.log(scheduled);
  }

  function getRandomIntInclusive(min, max) {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min + 1) + min);
  }

  async function schedulePushNotification() {
    console.log("requested notification");
    const randomSeconds = getRandomIntInclusive(61, 75);

    const months = [
      "January",
      "February",
      "March",
      "April",
      "May",
      "June",
      "July",
    ];
    const random = Math.floor(Math.random() * months.length);

    Notifications.scheduleNotificationAsync({
      content: {
        title: `${months[random]} is random`,
        data: { screen: "screenName", show_time: randomSeconds },
      },
      trigger: {
        seconds: randomSeconds,
        repeats: true,
      },
    });
    await Notifications.cancelAllScheduledNotificationsAsync();
    getScheduled();
  }

  async function registerForPushNotificationsAsync() {
    let token;

    if (Platform.OS === "android") {
      await Notifications.setNotificationChannelAsync("default", {
        name: "default",
        importance: Notifications.AndroidImportance.MAX,
        vibrationPattern: [0, 250, 250, 250],
        lightColor: "#FF231F7C",
      });
    }

    if (Device.isDevice) {
      const { status: existingStatus } =
        await Notifications.getPermissionsAsync();
      let finalStatus = existingStatus;
      if (existingStatus !== "granted") {
        const { status } = await Notifications.requestPermissionsAsync();
        finalStatus = status;
      }
      if (finalStatus !== "granted") {
        alert("Failed to get push token for push notification!");
        return;
      }
      token = (await Notifications.getExpoPushTokenAsync()).data;
      console.log(token);
    } else {
      alert("Must use physical device for Push Notifications");
    }

    return token;
  }

  async function givenPermissions() {
    if (Device.isDevice) {
      const { status: existingStatus } =
        await Notifications.getPermissionsAsync();
      setGivenNotificationPerm(existingStatus === "granted");
    } else {
      alert("Must use physical device for Push Notifications");
    }
  }

  return {
    expoPushToken,
    notification,
    permissions,
    schedulePushNotification,
    givenNotificationPerm,
  };
}

App.js代码

export default function App() {
  const { permissions, schedulePushNotification } = useNotifications();

  return(
    <>
    <Button title="Enable notifications - onboarding screen only button" onPress={permissions} />
    <Button title="Schedule notification" onPress={schedulePushNotification} />
    </>
  )
}

解决方案

核心思路

Expo本地通知的重复触发器是固定间隔/时间,无法直接生成每日随机时间。要实现需求,需要每次通知触发后自动调度下一天的随机时间通知,同时在App启动时检查调度状态,确保不会遗漏推送。

具体实现步骤

  1. 通知触发后自动续期:当通知被接收或用户点击时,立即生成第二天的随机时间并调度新通知。
  2. App启动初始化调度:每次打开App,检查是否有已调度的通知,没有则生成当天的随机时间通知。
  3. 动态获取推送内容:调度前调用API获取名言,替换静态内容。

代码修改

1. 更新通知监听与续期逻辑

在自定义Hook中添加调度下一次通知的函数,并修改监听逻辑:

// 新增:生成第二天的随机时间并调度通知
async function scheduleNextDayRandomNotification() {
  // 设定推送时间范围(比如9:00-21:00)
  const randomHour = getRandomIntInclusive(9, 21);
  const randomMinute = getRandomIntInclusive(0, 59);
  
  // 创建明天的目标时间
  const tomorrow = new Date();
  tomorrow.setDate(tomorrow.getDate() + 1);
  tomorrow.setHours(randomHour, randomMinute, 0, 0);

  // 调用API获取名言
  const quote = await fetchRandomQuote();

  // 调度新通知
  await Notifications.scheduleNotificationAsync({
    content: {
      title: "今日打卡提醒",
      body: quote,
      data: { screen: "screenName" },
    },
    trigger: {
      date: tomorrow,
    },
  });
}

// 新增:模拟API获取名言(替换为你的实际API)
async function fetchRandomQuote() {
  try {
    const response = await fetch("https://api.quotable.io/random");
    const data = await response.json();
    return data.content;
  } catch (err) {
    return "今日宜记录生活";
  }
}

// 修改通知接收监听,添加续期逻辑
notificationListener.current = Notifications.addNotificationReceivedListener(async (notification) => {
  setNotification(notification);
  console.log("--- notification received ---");
  await scheduleNextDayRandomNotification();
  console.log("------");
});

// 修改通知点击监听,添加续期逻辑(兼容iOS后台限制)
responseListener.current = Notifications.addNotificationResponseReceivedListener(async (response) => {
  console.log("--- notification tapped ---");
  const screen = response?.notification?.request?.content?.data?.screen;
  await scheduleNextDayRandomNotification();
  console.log("------");
});

2. App启动时初始化调度

在useEffect中添加初始化逻辑,确保App启动时必有一个待调度的通知:

useEffect(() => {
  givenPermissions();
  // 初始化每日通知调度
  initDailyNotification();

  // ... 原有监听逻辑

  return () => {
    // ... 原有清理逻辑
  };
}, []);

// 新增:初始化通知调度
async function initDailyNotification() {
  const scheduled = await Notifications.getAllScheduledNotificationsAsync();
  if (scheduled.length === 0) {
    const now = new Date();
    let targetTime = new Date();
    // 生成当天9:00-21:00的随机时间
    const randomHour = getRandomIntInclusive(9, 21);
    const randomMinute = getRandomIntInclusive(0, 59);
    targetTime.setHours(randomHour, randomMinute, 0, 0);

    // 如果目标时间已过,调度明天的
    if (targetTime < now) {
      targetTime.setDate(targetTime.getDate() + 1);
    }

    const quote = await fetchRandomQuote();
    await Notifications.scheduleNotificationAsync({
      content: {
        title: "今日打卡提醒",
        body: quote,
        data: { screen: "screenName" },
      },
      trigger: {
        date: targetTime,
      },
    });
  }
}

3. 修复原调度函数的错误

原schedulePushNotification中存在先调度再取消所有通知的逻辑,会导致刚创建的通知被删除,需要移除await Notifications.cancelAllScheduledNotificationsAsync();这一行。

额外注意事项

  • iOS后台限制:iOS上通知触发后后台执行时间有限,建议同时在用户点击通知时续期,确保可靠性。
  • API容错处理:添加默认内容,避免因API请求失败导致通知无内容。
  • 权限校验:所有调度操作前需确认已获取通知权限,避免调度失败。

内容的提问来源于stack exchange,提问作者Jack

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 03:16:14