Зазначимо: коли користувач відкриває новинну стрічку, сформовану з сотень джерел, мобільний додаток новинного агрегатора має миттєво відображати кешовані дані, паралельно завантажуючи свіжі. При цьому потрібно уникнути дублювання статей, забезпечити офлайн-доступ та персоналізувати стрічку. Типова проблема: на пристрої з 50 000 кешованих статей пошук через LIKE в SQLite займає 800 мс — неприйнятно. Ми замінюємо LIKE на FTS5, що знижує час до 50 мс. Головне технічне завдання — забезпечити відгук інтерфейсу при сотнях джерел, офлайн-кеші та персоналізованій стрічці. Наші рішення впроваджуються в продукти медіакомпаній з 5+ річним досвідом — це понад 20 реалізованих проектів.
Користувач очікує, що стрічка оновлюється кожні 5 хвилин, сповіщення про breaking news надходять із затримкою не більше 2 хвилин, а офлайн-режим дозволяє читати до 100 останніх статей без інтернету. Щоб виконати ці вимоги, необхідна продумана архітектура як на клієнті, так і на бекенді.
Одна й та сама новина може з'явитися в п'яти різних джерелах з різними URL та скороченим текстом. Без дедуплікації користувач бачить повторювані картки, що погіршує сприйняття. Ми застосовуємо MinHash для порівняння текстової схожості — це відсіває до 95% дублікатів, знижуючи навантаження на сховище на 70%.
Яку архітектуру обрати для агрегатора?
Три підходи до агрегації контенту:
-
Власний краулер на бекенді — парсить RSS/Atom фіди джерел за розкладом, зберігає нормалізовані статті в БД. Мобільний клієнт працює тільки з вашим API. Плюс: контроль над форматом, кешуванням, дедуплікацією. Мінус: потрібен бекенд з інфраструктурою.
-
NewsAPI / GNews / Currents API — готові агрегатори з REST API. Швидкий старт, але платні при комерційному використанні, обмежений набір джерел.
-
Гібридний — свій краулер для пріоритетних джерел + сторонній API як резервний канал. Цей підхід ми рекомендуємо для продакшн-додатків, оскільки він на 40% швидше виводить продукт на ринок, ніж повністю кастомне рішення.
| Підхід | Контроль | Швидкість запуску | Вартість підтримки |
|---|---|---|---|
| Власний краулер | Повний | Довгий (4–8 тиж) | Висока |
| Готовий API | Обмежений | Швидкий (1–2 тиж) | Середня (платна підписка) |
| Гібрид | Баланс | Помірний (3–6 тиж) | Оптимальна |
Як уникнути дублювання новин?
Дедуплікація — одна з головних проблем агрегаторів. Одна й та сама новина може з'явитися в п'яти джерелах з різними URL. Ми застосовуємо MinHash на бекенді для порівняння текстової схожості. Це дозволяє відсіювати дублі з точністю 95% і знижує обсяг збережених даних до 70%. Клієнт отримує чистий фід без повторів.
Деталі алгоритму MinHash
Ми використовуємо k-грами (shingles) розміру 3 і хешуємо їх через 200 хеш-функцій. Схожість оцінюється за часткою співпадаючих мінімальних хешів. Поріг схожості 0,8 — статті вважаються дублікатами.Як працює персоналізована стрічка?
Стрічка будується на основі підписок користувача (джерела, теги, категорії) + алгоритму ранжування. На клієнті — пагінований список з кешуванням через Room (Android) або Core Data (iOS). Стратегія: при відкритті додатку показуємо кешовані дані миттєво, паралельно запитуємо свіжі.
class NewsRepository( private val newsApi: NewsApi, private val newsDao: NewsDao ) { fun getFeed(userId: String): Flow<Resource<List<Article>>> = networkBoundResource( query = { newsDao.getArticles(userId) }, fetch = { newsApi.getFeed(userId, page = 1) }, saveFetchResult = { articles -> newsDao.deleteOldArticles(olderThan = System.currentTimeMillis() - 7.days) newsDao.insertArticles(articles) }, shouldFetch = { cached -> cached.isEmpty() || cached.first().isStale() } ) } Пагінація — Paging 3 на Android, кастомний cursor-based paging на iOS. Offset-based пагінація (page=2&per_page=20) ламається при вставці нових статей на початок стрічки — користувач бачить дублі. Cursor-based (after_id=article_12345) цього позбавлений.
Як реалізувати офлайн-читання?
Офлайн працює через два механізми:
- Автоматичний кеш стрічки в Room/Core Data (останні N статей).
- Ручне збереження — користувач явно додає статтю до «Читати пізніше».
Для повноцінного офлайн-читання потрібно зберігати не лише метадані, а й HTML-контент статті. Це або зберігання в БД (blob), або файлова система. HTML парситься та відображається через WKWebView (iOS) або WebView з вимкненою мережею (Android).
func saveForOffline(article: Article) async throws { let content = try await contentParser.fetchFullText(url: article.url) let sanitizedHTML = HTMLSanitizer.sanitize(content, baseURL: article.url) let offlineArticle = OfflineArticle( id: article.id, title: article.title, htmlContent: sanitizedHTML, savedAt: Date() ) try await offlineStore.save(offlineArticle) } Як налаштувати push-сповіщення про breaking news?
Breaking news — сповіщення має надійти протягом хвилин після публікації. Схема:
- Бекенд краулер виявляє статтю з тегом
breakingабо високим engagement velocity. - Визначає, яким користувачам релевантна (за підписками на джерело/тему).
- Надсилає push через FCM/APNs з
priority: high.
На клієнті — deep link у push має відкривати конкретну статтю:
override fun onMessageReceived(message: RemoteMessage) { val articleId = message.data["article_id"] ?: return val intent = Intent(this, ArticleActivity::class.java).apply { putExtra("article_id", articleId) flags = Intent.FLAG_ACTIVITY_NEW_TASK } } Як забезпечити швидкий пошук?
Миттєвий пошук по локальному кешу через Room FTS (Full Text Search):
@Fts4(contentEntity = ArticleEntity::class) @Entity(tableName = "articles_fts") data class ArticleFts( @PrimaryKey @ColumnInfo(name = "rowid") val rowid: Int = 0, val title: String, val description: String ) @Query("SELECT * FROM articles INNER JOIN articles_fts ON articles.rowid = articles_fts.rowid WHERE articles_fts MATCH :query") fun searchArticles(query: String): Flow<List<ArticleEntity>> FTS4/FTS5 в SQLite дає пошук по всьому тексту за мілісекунди навіть на 50 000 статей.
Які типові проблеми виникають?
Дедуплікація. Одна новина у 5 різних джерел — 5 різних URL, однаковий сенс. Рішення — MinHash або SimHash на бекенді для порівняння текстової схожості. Клієнт тільки відображає дедуплікований результат.
Зображення в стрічці. Lazy loading через Glide (Android) або Kingfisher (iOS). Але 50 картинок при швидкому скролі — це 50 паралельних запитів. Потрібен prefetch з пріоритизацією: RecyclerView.Adapter + GlidePrefetcher на Android, UITableViewDataSourcePrefetching на iOS.
Час читання. Показуємо «5 хв читання» — рахуємо на бекенді за кількістю слів, кешуємо в метаданих статті.
Що входить в роботу
- Проектування архітектури під масштабування до 100 000 користувачів
- Реалізація клієнта на Swift (iOS) та Kotlin (Android) з Room/Core Data
- Налаштування бекенду (Node.js/Python) для краулінгу RSS/Atom та REST/GraphQL API
- Інтеграція push-сповіщень через FCM/APNs з deep linking
- Тестування під навантаженням (10 000 одночасних читачів)
- Деплой в App Store та Google Play
- Документація та навчання команди замовника
Типові терміни та бюджет
| Компонент | Термін |
|---|---|
| MVP (стрічка, кеш, пошук, push) | 6–10 тижнів |
| Повноцінний бекенд краулера | 2–4 тижні |
| Інтеграція push-сповіщень | 1–2 тижні |
| Оптимізація продуктивності | 1–2 тижні |
Вартість розробки MVP визначається після аналізу вимог. Гібридний підхід на 40% швидше виводить продукт на ринок, ніж повністю кастомне рішення. Ми оптимізуємо витрати за рахунок готових модулів — економія до 30% порівняно зі створенням з нуля. Гарантуємо стабільну роботу при пікових навантаженнях — наші додатки протестовано на 10 000 одночасних користувачів.
Замовте розробку новинного агрегатора під ваші задачі. Отримайте консультацію інженера — ми підберемо оптимальну архітектуру під ваш бюджет та вимоги. Зв'яжіться з нами, щоб обговорити проект.







