如何用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启动时检查调度状态,确保不会遗漏推送。
具体实现步骤
- 通知触发后自动续期:当通知被接收或用户点击时,立即生成第二天的随机时间并调度新通知。
- App启动初始化调度:每次打开App,检查是否有已调度的通知,没有则生成当天的随机时间通知。
- 动态获取推送内容:调度前调用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
相关产品推荐
相关产品推荐

