完成散步功能後,我希望可以在適當的時間提醒使用者出去走走,而不是每次都要自己打開 App。
一開始我想得很簡單,不就是每天晚上七點跳一則通知嗎?但仔細想想,每個人的生活習慣都不一樣。
有人習慣早上散步,有人下班後才出門,也有人只有假日有空。
如果所有人都在同一個時間收到通知,那通知就沒有意義了。
因此今天要來實作提醒功能,不過會分成兩種:

固定時間的實作很單純,只要依照使用者設定排程即可。
今天主要想分享的是第二種,也就是 App 如何從散步紀錄找出真正適合提醒的時間。
App 的通知大致可以分成兩種:
後端
│
▼
FCM / APNs
│
▼
使用者裝置
這種方式適合聊天訊息、好友邀請、活動公告等需要由後端主動通知的情境。但現在需要的只是取得最近的散步紀錄,分析出使用者的散步習慣,再把通知交給 Android 或 iOS 排程即可,不需要每天都由後端在固定時間推播。
整個流程如下:
後端提供散步紀錄
│
▼
App 分析散步習慣
│
▼
決定提醒時間
│
▼
Local Notification
│
▼
Android / iOS 排程
│
▼
時間到自動顯示通知
因此這裡選擇使用 Local Notification。
React Native 本身沒有提供完整的跨平台通知排程功能,因此我使用的是 react-native-notify-kit,它封裝了 Android 與 iOS 的通知 API,App 只需要提供通知內容以及通知時間即可。
首先請求通知權限:
import notifee from 'react-native-notify-kit';
await notifee.requestPermission();
Android 還需要建立 Notification Channel:
import notifee, { AndroidImportance } from 'react-native-notify-kit';
await notifee.createChannel({
id: 'walk-reminder',
name: '散步提醒',
importance: AndroidImportance.HIGH,
});
建立通知時,只需要提供通知內容與觸發時間:
import notifee, {
TriggerType,
TimestampTrigger,
} from 'react-native-notify-kit';
const trigger: TimestampTrigger = {
type: TriggerType.TIMESTAMP,
timestamp: reminderTime.getTime(),
};
await notifee.createTriggerNotification(
{
id: notificationId,
title: '該出去散步囉!',
body: '差不多到平常散步的時間了,出去走走吧!',
android: {
channelId: 'walk-reminder',
},
},
trigger,
);
reminderTime 就是最後決定好的提醒時間。
建立完成後,App 不需要一直在背景等待,Android 或 iOS 會在指定時間自動顯示通知。
真正困難的地方,其實不是通知本身,而是 reminderTime 到底該是多少。
如果提醒採用固定時間,例如每天晚上七點,那當然沒有問題。
但如果希望 App 自己判斷,就必須先回答一個問題「使用者平常到底是幾點散步?」
例如最近十四天的紀錄:
18:10
18:20
18:30
18:25
18:15
人一眼就能看出這個使用者平時都是 18:00 左右散步,但是程式不會知道,因此需要一個 Habit Analyzer,把散步紀錄一步一步轉成 Habit,再決定提醒時間。
生活習慣通常不會完全一樣,例如:
平日
18:20
18:30
18:40
假日
08:00
08:10
07:50
如果全部混在一起分析,可能會得到一個使用者實際上不會散步的時間。
因此會先將資料拆成平日與假日,再分別進行分析。
最後可能得到:
平日 Habit
18:30
假日 Habit
08:00
到了平日就使用平日 Habit,到了星期六、星期日則使用假日 Habit,兩者互不影響。
接著要將時間相近的散步紀錄放進同一個群組。
例如目前將相差不超過 60 分鐘的紀錄視為同一群:
18:10
18:25
18:40
這三筆紀錄都會進入傍晚的時間群組。
但下面這筆與傍晚相差太遠,因此會屬於另一個群組:
08:00
除了普通的時間差,還需要考慮跨過0點的情況:
23:50
00:10
雖然日期不同,但實際只差二十分鐘。
如果直接把分鐘數相減,就會得到接近二十四小時的差距,因此要使用循環時間差來判斷,避免把原本屬於同一個習慣的資料拆開。
只有把時間分群還不夠,還需要確認這個時間群組是否經常出現在不同日期。
例如同一天散步三次:
星期一
18:10
18:20
18:40
如果直接拿來統計,18 點這個群組就會增加三筆資料。
但真正想知道的是「有多少天是在這個時間附近散步」,因此在同一個時間群組內,統計的是不同日期數,而不是散步次數。
上面的例子最後只會算成:
18 點群組
星期一
而不是:
18 點群組
星期一
星期一
星期一
不過,同一天仍然可能出現不同的散步習慣,例如:
星期一
08:00
14:00
20:00
這三筆紀錄相隔很遠,因此會分別進入不同的時間群組:
08 點群組
星期一
14 點群組
星期一
20 點群組
星期一
也就是說,同一個日期可以出現在不同的 Habit 群組中,但在同一個群組內,同一天最多只計算一次。
目前的門檻設定為:
假日門檻較低,是因為最近十四天內,本來就只有四天左右是星期六或星期日。如果假日也要求三到四個不同日期,需要更長的時間才可能形成 Habit。
即使不同日期數量足夠,也不代表使用者真的有固定習慣。
例如:
18:10
18:20
18:30
18:35
這組資料非常集中,可以合理認為使用者通常會在 18:30 左右開始散步。
但下面這組資料就沒有這麼穩定:
17:00
18:00
19:00
雖然都發生在傍晚,但最早與最晚相差兩個小時。
如果仍然直接選擇 18:00 建立提醒,通知出現時,使用者不一定真的有散步的打算。
因此還要計算群組內時間的分散程度。
目前設定最大平均偏差為 35 分鐘。如果群組內的平均時間偏差超過這個範圍,就代表這個 Habit 還不夠穩定。
通過前面的初步條件後,最後再將不同日期數、樣本數以及時間集中程度整合成一個 confidence。
confidence 代表系統對這個 Habit 的信心程度,其計算主要結合了三個維度的權重:
例如:
18:10
18:20
18:25
18:30
分析後可能得到:
平日 Habit
18:30
confidence = 0.91
綜合計算出 0 到 1 之間的分數後,目前只有當 confidence >= 0.65 時才會將這個時間群組視為有效 Habit,並繼續建立提醒。
如果 confidence 沒有達到門檻 (>=0.65),就代表散步時間還不夠穩定,因此不會建立自動提醒,而是等累積更多散步紀錄後再重新分析。
經過前面的分析後,找出的是使用者通常開始散步的時間,而不是通知實際出現的時間。
例如分析結果為:
平日 Habit
18:30
假日 Habit
08:00
需要根據今天是平日還是假日,選擇對應的時間發送通知。
為了讓使用者有時間準備,通知會比 Habit 提前十分鐘。
例如今天是星期三,那就是 18:20 發送通知,而如果是星期六,就 7:50 發送通知。
最終流程為:
最近 14 天散步紀錄
│
▼
分成平日與假日
│
▼
將相近時間分群
│
▼
確認是否形成穩定習慣
│
▼
計算 confidence
│
▼
confidence 是否達標?
│ │
│否 |是
▼ ▼
不建立提醒 產生 Habit
│
▼
提前 10 分鐘
│
▼
建立 Local Notification
提醒時間不是計算一次後就永遠不變。
使用者原本可能習慣六點半散步,之後因為工作時間改變,逐漸變成七點才出門。
因此目前會在兩個時機重新分析:
恢復 Session
│
▼
初始化通知與權限
│
▼
取得最近 14 天散步紀錄
│
▼
重新分析 Habit
│
▼
更新通知排程
這樣即使使用者很久沒有開啟 App,或是在其他裝置完成散步,也能重新同步最新的提醒時間。
完成散步
│
▼
資料同步完成
│
▼
重新取得最近 14 天散步紀錄
│
▼
重新分析 Habit
│
▼
更新提醒
新增一筆散步後,原本不足的樣本可能形成新的 Habit,也可能讓原本六點半散步的 Habit 逐漸變成七點。
目前的作法只會保留平日與週末 confidence 最高的 Habit,因此一天最多只有一次提醒。
在這個前提下,如果使用者今天已經完成散步,就可以取消今天尚未觸發的提醒,再重新排到下一個符合 Habit 的日期,避免剛散步完又收到提醒。
如果未來要改成一天支持多個時段提醒,那就會遇到下面兩個問題:
目前只保留平日與假日中 confidence 最高的 Habit,實作和排程都比較單純,但使用者也可能每天固定散步不只一次。
例如最近的散步紀錄是:
平日
├── Habit #1
│ 08:00
├── Habit #2
│ 14:00
└── Habit #3
20:00
如果三個群組都有足夠的不同日期、時間也足夠集中,而且 confidence 都通過門檻,就可以保留三個 Habit,而不是只留下分數最高的那一個。
此時 Scheduler 管理的就不再是「今天的一個提醒」,而是每個 Habit 各自對應的提醒。
為了避免產生過多通知可以加上限制,例如:
支援多個 Habit 後,就不能再用「今天是否已經散步」決定要不要取消所有提醒。
例如今天有兩個 Habit:
08:00 Habit
18:30 Habit
早上 07:50 發送提醒,使用者在 08:10 開始散步並完成,只代表早上的 Habit 已經完成:
08:00 Habit 已完成
18:30 Habit 尚未完成
因此只能取消或標記 08:00 Habit 對應的提醒,晚上的提醒仍然需要保留。
要依照 Habit 個別管理,還需要判斷每次完成的散步對應哪一個 Habit。
最直接的作法是使用散步的開始時間,尋找當天時間最接近的 Habit,並限制可以接受的時間範圍。
例如:
08:00 Habit
允許配對範圍
07:00~09:00
如果使用者在 08:20 開始散步,就可以視為完成 08:00 Habit。
但如果使用者在 11:00 才開始散步,與 08:00 及 14:00 Habit 都相差太遠,這次就只記錄為一般散步,不會將任何 Habit 標記為完成。
如果同時有多個 Habit 落在允許範圍內,則選擇時間差最小的那一個,避免同一筆散步同時完成多個 Habit。
因此支援多次提醒後,完成散步時不再是取消「今天所有通知」,而是:
完成散步
│
▼
尋找時間最接近的 Habit
│
▼
是否落在允許範圍內?
│ │
│否 │是
▼ ▼
一般散步 標記該 Habit 完成
│
▼
保留其他 Habit 的提醒