Реалізація системи ежедневних стриків у мобільному додатку
Стрики — одна з найбільш потужних механік retention у мобільних додатках. Втратити серію з 30 днів болісно. Саме це утримує користувачів навіть тоді, коли пряма цінність продукту знизилася. Але реалізація повна неоднозначних багів, які ломають все.
Головна технічна складність — час та часові пояси
Питання, яке обов'язково потрібно вирішити на початку: за яким часом рахувати «день»? Варіанти:
Локальний час користувача. Стрик не ломається, якщо користувач у Токіо, а сервер у UTC. Вимагає зберігання timezone користувача та пересчета «сьогодні» при кожній перевірці. При зміні часового пояса (перельт) — потенційні артефакти.
UTC-полінич. Простіше технічно, але несправедливо для користувачів у UTC-5 — їх «вчора» закінчується о 7 ранку UTC, а не о полінічі за їх часом.
Скользяче вікно 24 годин. Не «сьогодні», а «протягом останніх 24 годин від останньої дії». Найлояльніший підхід, але ломає інтуїцію «ежедневного» стрика.
На практиці більшість успішних додатків (Duolingo, Headspace) використовує локальний час з зберіганням user_timezone. При першому запуску визначаємо TimeZone.current та зберігаємо на сервер.
Модель даних
user_streak:
user_id UUID
current_streak INT
longest_streak INT
last_activity DATE -- зберігати DATE, не TIMESTAMP
updated_at TIMESTAMP
last_activity — дата в часовому поясі користувача, не UTC timestamp. Це ключове. При перевірці стрика:
today = current_date_in_user_timezone(user.timezone)
days_since = today - last_activity
if days_since == 0: стрик активен, ничего не делаем (уже отмечен сегодня)
if days_since == 1: стрик продовжується, current_streak += 1
if days_since > 1: стрик сломан, current_streak = 1
Атомарне оновлення через SQL з RETURNING — захист від конкурентних запитів.
Freeze та відновлення стрика
Втрата стрика — болісна подія. Деякі додатки дають «заморозки» (streak freeze): користувач може пропустити день без втрати серії. Це підвищує retention при пропущених днях.
streak_freeze — окремий ресурс, який користувач отримує як нагороду або купує. При сломаному стрику перевіряємо: чи є активна заморозка на вчорашній день. Якщо так — не ломаємо стрик, списуємо заморозку.
Відновлення стрика (платна фіча деяких додатків) — технічно простіше, етично спірніше. Якщо реалізуємо: streak_restore_purchase, зберігаємо новий last_activity = yesterday, current_streak = pre_break_value.
Сповіщення
Reminder перед полуноччю (наприклад, о 21:00 за локальним часом) — «Ви ще не виконали завдання сьогодні, стрик X днів під загрозою». Ефективність цих сповіщень висока, але потрібна персоналізація часу: користувач, який завжди активен о 8 ранку, не повинен отримувати reminder о 21:00.
На iOS: UNUserNotificationCenter з UNCalendarNotificationTrigger. Час розраховуємо в часовому поясі користувача. При оновленні активності — якщо користувач вже виконав завдання сьогодні, скасовуємо сьогоднішній reminder.
Візуалізація
Flame icon з числом — стандарт. На Flutter: AnimatedFlipCounter для плавного збільшення лічильника. Тижнева сітка днів (як у GitHub contribution graph) — показує історію останніх 7/30 днів. Це потужно: пусті клітинки візуально «зовуть» їх заповнити.
Milestone-сповіщення
7 днів, 30 днів, 100 днів — спеціальні подій з анімацією. Інтегруємо з системою досягнень: milestone стрика = автоматично розблокована досягнення.
Орієнтири за термінами
Базова система зі стриком, сповіщеннями та milestone — 1–2 дні (клієнт) + 2–3 дні (бекенд). З заморозками, відновленням, персоналізованими reminders та інтеграцією з досягненнями — 1–2 тижні. Вартість розраховується індивідуально.







