Отметим: когда пользователь открывает новостную ленту, сформированную из сотен источников, мобильное приложение новостного агрегатора должно мгновенно отображать кешированные данные, параллельно загружая свежие. При этом нужно избежать дублирования статей, обеспечить офлайн-доступ и персонализировать ленту. Типичная проблема: на устройстве с 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 варьируется от 150 000 до 300 000 рублей в зависимости от сложности. Гибридный подход на 40% быстрее выводит продукт на рынок, чем полностью кастомное решение. Мы оптимизируем затраты за счёт готовых модулей — экономия до 30% по сравнению с созданием с нуля. Гарантируем стабильную работу при пиковых нагрузках — наши приложения протестированы на 10 000 одновременных пользователей.
Закажите разработку новостного агрегатора под ваши задачи. Получите консультацию инженера — мы подберем оптимальную архитектуру под ваш бюджет и требования. Свяжитесь с нами, чтобы обсудить проект.







