Сеть и базы данных: Retrofit и Room

Retrofit превращает REST API в Kotlin-интерфейс с suspend-функциями, а Room даёт типобезопасный доступ к локальной SQLite-базе — обе библиотеки изначально дружат с корутинами.
Суть: типичное приложение берёт данные из сети через Retrofit и кэширует их локально в Room, а корутины делают оба источника асинхронными и безопасными для интерфейса.

Почти любое полезное приложение работает с данными: загружает их с сервера и хранит локально. Для сети стандарт — Retrofit. Вы описываете API интерфейсом, помечая методы HTTP-аннотациями, а Retrofit генерирует реализацию. Методы делают suspend, чтобы вызывать их из корутин.

import retrofit2.http.GET

data class Post(val id: Int, val title: String)

interface ApiService {
    @GET("posts")
    suspend fun getPosts(): List<Post>   // suspend: работает в корутине
}

Локальное хранение в Room

Room — это надстройка над SQLite. Вы описываете таблицу через @Entity, операции — через @Dao, а саму базу — через @Database. Запросы на чтение могут возвращать Flow, который автоматически шлёт новые данные при изменении таблицы — UI обновляется сам.

import androidx.room.*
import kotlinx.coroutines.flow.Flow

@Entity
data class PostEntity(
    @PrimaryKey val id: Int,
    val title: String
)

@Dao
interface PostDao {
    @Query("SELECT * FROM PostEntity")
    fun observeAll(): Flow<List<PostEntity>>   // реактивный поток

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun upsertAll(posts: List<PostEntity>)
}

Репозиторий связывает источники

Хорошая практика — спрятать оба источника за репозиторием. ViewModel обращается к репозиторию и не знает, откуда пришли данные — из сети или из кэша. Это и есть слой данных в архитектуре приложения.

class PostRepository(
    private val api: ApiService,
    private val dao: PostDao
) {
    val posts: Flow<List<PostEntity>> = dao.observeAll()

    suspend fun refresh() {
        val fresh = api.getPosts()                       // из сети
        dao.upsertAll(fresh.map { PostEntity(it.id, it.title) }) // в кэш
    }
}

Как работает под капотом

Retrofit использует динамический прокси: по аннотациям интерфейса он строит и выполняет HTTP-запрос, а тело ответа разбирает конвертером (обычно JSON через kotlinx.serialization или Moshi). Suspend-метод выполняется на IO-пуле и не блокирует главный поток. Room генерирует код доступа на этапе компиляции, проверяя SQL-запросы заранее, — ошибка в запросе всплывёт при сборке, а не в рантайме. Flow из Room наблюдает за таблицей: любое изменение данных порождает новую эмиссию, и подписанный UI обновляется автоматически.

  Слой данных (offline-first)

  Сеть (Retrofit) --refresh()--> Room (кэш)
                                    |
                                    | Flow (наблюдение)
                                    v
                                ViewModel --> UI

  UI читает из кэша; сеть лишь обновляет кэш

Частые ошибки

Вызывать сеть на главном потоке. Даже с Retrofit нельзя забывать про suspend и корутины; синхронный вызов повесит UI.

Обращаться к API напрямую из composable. Сеть и база — дело слоя данных и ViewModel, а не UI; иначе теряется управление жизненным циклом.

Игнорировать ошибки сети. Запрос может упасть; оборачивайте в обработку и показывайте состояние ошибки (вспомните sealed UiState).

Best practices

  • Прячьте сеть и базу за репозиторием; ViewModel работает с репозиторием.
  • Делайте сетевые и DAO-методы suspend, а наблюдение — через Flow.
  • Стройте offline-first: UI читает из кэша, сеть обновляет кэш.
  • Обрабатывайте ошибки сети и отражайте их в состоянии экрана.

Логику «обновить из сети, читать из кэша» удобно смоделировать на Python. Запустите врезку.

# Аналог репозитория: сеть обновляет кэш, UI читает кэш
cache = []

def fetch_from_network():
    # имитация ответа сервера
    return [{'id': 1, 'title': 'Первый'}, {'id': 2, 'title': 'Второй'}]

def refresh():
    global cache
    cache = fetch_from_network()   # как Retrofit -> Room

def observe():
    return [p['title'] for p in cache]   # как Flow из Room

print('кэш до:', observe())
refresh()
print('кэш после refresh:', observe())

Попробуй сам ▶ — добавьте обработку «сеть недоступна» (вернуть старый кэш). Это и есть преимущество offline-first: интерфейс продолжает показывать данные из кэша даже без сети.

Итог: Retrofit загружает данные из сети, Room хранит их локально, а репозиторий связывает источники в единый слой данных. Корутины и Flow делают всё это асинхронным и реактивным. Остался последний кирпич — переходы между экранами.

Проверьте себя
1. Что генерирует Retrofit на основе аннотированного интерфейса?
AБазу данных SQLite
BРеализацию интерфейса, которая строит и выполняет HTTP-запросы по аннотациям
CПользовательский интерфейс
DФайл манифеста
2. Какое преимущество даёт возврат Flow из DAO в Room?
AFlow ускоряет SQL-запросы
BFlow автоматически эмитит новые данные при изменении таблицы, и подписанный UI обновляется сам
CFlow шифрует данные
DFlow работает только на главном потоке