← Назад к вопросам
Реализация RSS Reader
1.8 Middle🔥 121 комментариев
#UI и вёрстка#Архитектура и паттерны#Сетевое взаимодействие
Условие
Необходимо создать RSS reader приложение на Android.
Требования:
- Приложение должно иметь минимум 3 встроенных RSS подписки
- Пользователь может добавить свои подписки
- Приложение должно выводить список подписок
- При переходе по подписке показывать список постов
- После перехода по посту показывать его заголовок и описание
Технические требования:
- Язык: Kotlin
- Архитектура: MVVM или MVP
- Использовать Retrofit для сетевых запросов
- Корректная обработка ошибок сети
- Правильная работа при смене ориентации экрана
Оценка:
- Качество архитектуры
- Обработка edge cases
- Чистота кода
- UI/UX решения
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение: Реализация RSS Reader приложения
Архитектура приложения
Я разработаю RSS Reader с архитектурой MVVM (Model-View-ViewModel), которая идеально подходит для этой задачи. Архитектура разделена на три слоя:
- Data Layer — работа с сетью (Retrofit) и локальным хранилищем (Room)
- Domain Layer — бизнес-логика
- UI Layer — Activity, Fragment, ViewModel, Adapter
Структура проекта
com.example.rssreader/
├── data/
│ ├── api/
│ │ ├── RssApi.kt
│ │ └── RssClient.kt
│ ├── db/
│ │ ├── AppDatabase.kt
│ │ ├── SubscriptionDao.kt
│ │ └── FeedItemDao.kt
│ └── repository/
│ └── RssRepository.kt
├── domain/
│ ├── model/
│ │ ├── Subscription.kt
│ │ └── FeedItem.kt
│ └── usecase/
│ ├── GetSubscriptionsUseCase.kt
│ ├── GetFeedItemsUseCase.kt
│ └── AddSubscriptionUseCase.kt
├── ui/
│ ├── subscriptions/
│ │ ├── SubscriptionsFragment.kt
│ │ ├── SubscriptionsViewModel.kt
│ │ └── SubscriptionAdapter.kt
│ ├── feeditems/
│ │ ├── FeedItemsFragment.kt
│ │ ├── FeedItemsViewModel.kt
│ │ └── FeedItemAdapter.kt
│ ├── details/
│ │ ├── DetailsFragment.kt
│ │ └── DetailsViewModel.kt
│ └── MainActivity.kt
Ключевые компоненты
1. Data Models
// domain/model/Subscription.kt
data class Subscription(
val id: String,
val title: String,
val url: String,
val description: String = ""
)
// domain/model/FeedItem.kt
data class FeedItem(
val id: String,
val subscriptionId: String,
val title: String,
val description: String,
val link: String,
val pubDate: String,
val imageUrl: String? = null
)
2. Retrofit API
// data/api/RssApi.kt
import retrofit2.http.GET
import retrofit2.http.Url
interface RssApi {
@GET
suspend fun getFeed(@Url url: String): RssFeed
}
// XML парсинг через Retrofit-конвертер
data class RssFeed(
val channel: Channel
)
data class Channel(
val title: String,
val description: String,
val item: List<RssItem>
)
data class RssItem(
val title: String,
val description: String,
val link: String,
val pubDate: String
)
3. Retrofit Client с обработкой ошибок
// data/api/RssClient.kt
import retrofit2.Retrofit
import retrofit2.converter.simplexml.SimpleXmlConverterFactory
import okhttp3.OkHttpClient
import java.util.concurrent.TimeUnit
object RssClient {
private val okHttpClient = OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.build()
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl("https://example.com/")
.addConverterFactory(SimpleXmlConverterFactory.create())
.client(okHttpClient)
.build()
val api: RssApi = retrofit.create(RssApi::class.java)
}
4. Repository с обработкой ошибок
// data/repository/RssRepository.kt
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class RssRepository(
private val api: RssApi,
private val subscriptionDao: SubscriptionDao,
private val feedItemDao: FeedItemDao
) {
suspend fun getSubscriptions(): Result<List<Subscription>> = withContext(Dispatchers.IO) {
try {
val subscriptions = subscriptionDao.getAllSubscriptions()
Result.success(subscriptions)
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun fetchFeedItems(subscriptionUrl: String, subscriptionId: String): Result<Unit> =
withContext(Dispatchers.IO) {
try {
val feed = api.getFeed(subscriptionUrl)
val items = feed.channel.item.map { rssItem ->
FeedItem(
id = rssItem.link,
subscriptionId = subscriptionId,
title = rssItem.title,
description = rssItem.description,
link = rssItem.link,
pubDate = rssItem.pubDate
)
}
feedItemDao.insertAll(items)
Result.success(Unit)
} catch (e: IOException) {
Result.failure(Exception("Ошибка сети: ${e.message}"))
} catch (e: Exception) {
Result.failure(Exception("Ошибка парсинга: ${e.message}"))
}
}
suspend fun addSubscription(subscription: Subscription): Result<Unit> =
withContext(Dispatchers.IO) {
try {
subscriptionDao.insert(subscription)
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
}
5. ViewModel с сохранением состояния
// ui/subscriptions/SubscriptionsViewModel.kt
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class SubscriptionsViewModel(private val repository: RssRepository) : ViewModel() {
private val _subscriptions = MutableStateFlow<List<Subscription>>(emptyList())
val subscriptions = _subscriptions.asStateFlow()
private val _isLoading = MutableStateFlow(false)
val isLoading = _isLoading.asStateFlow()
private val _error = MutableStateFlow<String?>(null)
val error = _error.asStateFlow()
init {
loadSubscriptions()
}
private fun loadSubscriptions() {
viewModelScope.launch {
_isLoading.value = true
val result = repository.getSubscriptions()
result.onSuccess { subs ->
_subscriptions.value = subs
_error.value = null
}.onFailure { error ->
_error.value = "Ошибка загрузки: ${error.message}"
}
_isLoading.value = false
}
}
fun addSubscription(title: String, url: String) {
viewModelScope.launch {
val subscription = Subscription(
id = UUID.randomUUID().toString(),
title = title,
url = url
)
val result = repository.addSubscription(subscription)
result.onSuccess {
loadSubscriptions()
}.onFailure { error ->
_error.value = "Ошибка добавления: ${error.message}"
}
}
}
}
6. UI Layer (Fragment)
// ui/subscriptions/SubscriptionsFragment.kt
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
class SubscriptionsFragment : Fragment() {
private val viewModel: SubscriptionsViewModel by viewModels {
SubscriptionsViewModelFactory(RssRepository(...))
}
private lateinit var adapter: SubscriptionAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
setupObservers()
}
private fun setupRecyclerView() {
adapter = SubscriptionAdapter { subscription ->
// Навигация на список статей
val action = SubscriptionsFragmentDirections
.actionToFeedItems(subscription.id, subscription.title)
findNavController().navigate(action)
}
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
binding.recyclerView.adapter = adapter
}
private fun setupObservers() {
viewModel.subscriptions
.onEach { subscriptions ->
adapter.submitList(subscriptions)
}
.launchIn(viewLifecycleOwner.lifecycleScope)
viewModel.error
.onEach { error ->
if (error != null) {
showError(error)
}
}
.launchIn(viewLifecycleOwner.lifecycleScope)
}
private fun showError(message: String) {
Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show()
}
}
Обработка ошибок и edge cases
1. Ошибки сети:
- Используется
Result<T>для обработки успехов и ошибок - Timeout установлен на 15 секунд
- Отдельная обработка IOException и парсинг-ошибок
2. Смена ориентации экрана:
- ViewModel сохраняет состояние автоматически
- StateFlow обновляет UI при восстановлении Fragment
- Данные загружаются один раз в init блоке
3. Пустые списки:
- Проверка на пустоту перед отображением
- Сообщение "Нет подписок" для улучшенного UX
4. Дублирование подписок:
- Проверка URL перед добавлением
- Уникальный индекс в БД для URL
Встроенные подписки
val DEFAULT_SUBSCRIPTIONS = listOf(
Subscription("1", "Habr", "https://habr.com/rss/hubs/android/"),
Subscription("2", "Medium Android", "https://medium.com/feed/tag/android"),
Subscription("3", "AndroidDevs.by", "https://androiddevs.by/rss/")
)
Преимущества этого подхода
- Многоуровневая архитектура: разделение ответственности
- Coroutines: асинхронная работа без блокировки UI
- StateFlow: реактивное обновление UI
- Room: локальное кэширование для работы offline
- Правильная обработка жизненного цикла: ViewModel и lifecycleScope
- Масштабируемость: легко добавить новые фичи
Это полнофункциональное решение, которое покрывает все требования задачи и следует best practices разработки на Android.