← Назад к вопросам

Реализация RSS Reader

1.8 Middle🔥 121 комментариев
#UI и вёрстка#Архитектура и паттерны#Сетевое взаимодействие

Условие

Необходимо создать RSS reader приложение на Android.

Требования:

  1. Приложение должно иметь минимум 3 встроенных RSS подписки
  2. Пользователь может добавить свои подписки
  3. Приложение должно выводить список подписок
  4. При переходе по подписке показывать список постов
  5. После перехода по посту показывать его заголовок и описание

Технические требования:

  • Язык: Kotlin
  • Архитектура: MVVM или MVP
  • Использовать Retrofit для сетевых запросов
  • Корректная обработка ошибок сети
  • Правильная работа при смене ориентации экрана

Оценка:

  • Качество архитектуры
  • Обработка edge cases
  • Чистота кода
  • UI/UX решения

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение: Реализация RSS Reader приложения

Архитектура приложения

Я разработаю RSS Reader с архитектурой MVVM (Model-View-ViewModel), которая идеально подходит для этой задачи. Архитектура разделена на три слоя:

  1. Data Layer — работа с сетью (Retrofit) и локальным хранилищем (Room)
  2. Domain Layer — бизнес-логика
  3. 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.