Розробка мобільного додатка для новостного агрегатора
Новостний агрегатор — це не просто лента з RSS. Це персоналізований потік з десятків джерел, з offline-читанням, миттєвим пошуком та 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 на iOS. Offset-based (page=2&per_page=20) ломиться при вставці нових статей в початок ленти — користувач видит дублі. Cursor-based (after_id=article_12345) цього позбавлена.
Offline-читання
Offline працює через два механізми:
- Автоматичний кеш ленти в Room/Core Data (останні N статей).
- Ручне збереження — користувач явно додає статю в «Читати пізніше».
Для повнофункціонального offline-читання потрібно зберігати не тільки метадані, але й HTML-контент статті. Це або зберігання в БД (blob), або файлова система. HTML парситься та відображається через WKWebView (iOS) або WebView з вимкненою мережею (Android).
// iOS — збереження контенту для offline
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 новостного агрегатора (лента, категорії, offline, пошук, push про breaking news) — від 6 до 10 тижнів залежно від кількості платформ та складності бекенда.







