Разработка мобильного приложения для новостного агрегатора
Новостной агрегатор — это не просто лента из RSS. Это персонализированный поток из десятков источников, с офлайн-чтением, мгновенным поиском и push-уведомлениями о breaking news. Главная техническая задача — сделать ленту отзывчивой при сотнях источников и тысячах материалов в кеше, не убив при этом батарею.
Архитектура: откуда берутся новости
Три подхода к агрегации контента:
-
Собственный краулер на бэкенде — парсит RSS/Atom фиды источников по расписанию (cron), хранит нормализованные статьи в БД. Мобильный клиент работает только с вашим API. Плюс: контроль над форматом, кешированием, дедупликацией. Минус: нужен бэкенд с инфраструктурой.
-
NewsAPI / GNews / Currents API — готовые агрегаторы с REST API. Быстрый старт, но платные при коммерческом использовании, ограниченный набор источников.
-
Гибридный — свой краулер для приоритетных источников + сторонний API как резервный канал.
Для продакшн-приложения с реальными пользователями — первый или третий вариант.
Персонализированная лента: архитектура на клиенте
Лента строится на основе подписок пользователя (источники, теги, категории) + алгоритма ранжирования.
На клиенте — пагинированный список с кешированием через Room (Android) или Core Data (iOS). Стратегия: при открытии приложения показываем кешированные данные мгновенно, параллельно запрашиваем свежие.
// Android — Repository с NetworkBoundResource паттерном
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).
// iOS — сохранение контента для офлайна
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 должен открывать конкретную статью:
// Android — обработка 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
}
// Показываем уведомление с PendingIntent
}
Поиск
Мгновенный поиск по локальному кешу через 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 мин чтения» — считаем на бэкенде по количеству слов, кешируем в метаданных статьи.
Стек и сроки
| Компонент | iOS | Android |
|---|---|---|
| Список | UICollectionView + DiffableDataSource | RecyclerView + ListAdapter |
| БД | Core Data или SQLite.swift | Room + FTS5 |
| Изображения | Kingfisher | Glide |
| Сеть | URLSession + Combine | Retrofit + Coroutines |
| Push | APNs через OneSignal | FCM через OneSignal |
MVP новостного агрегатора (лента, категории, офлайн, поиск, push о breaking news) — от 6 до 10 недель в зависимости от количества платформ и сложности бэкенда.







