Урок 7 из 8 · 25 мин · обновлено

Данные: запросы к серверу и хранение на телефоне

Как Android-приложение получает данные с сервера через Retrofit и корутины, зачем нужно разрешение INTERNET, как сохранить настройки в DataStore и данные в базу Room.

Откуда приложение берёт данные

Список товаров в магазине, курсы валют, история заказов хранятся на сервере. Приложение получает их через API — набор адресов, по которым сервер отдаёт и принимает данные. Обычно в формате JSON:

JSON
[
  { "id": 1, "name": "Молоко", "count": 2 },
  { "id": 2, "name": "Хлеб", "count": 1 }
]

Чтобы приложение вообще могло выйти в интернет, в AndroidManifest.xml добавляют разрешение:

XML
<uses-permission android:name="android.permission.INTERNET" />

Retrofit: запросы к серверу

Самая популярная библиотека для запросов — Retrofit. Запросы к серверу описывают интерфейсом, а код запросов библиотека создаёт сама:

Kotlin
@Serializable
data class Product(val id: Int, val name: String, val count: Int)

interface ShoppingApi {
    @GET("products")
    suspend fun getProducts(): List<Product>
}
  • @GET("products") — запрос по адресу базовый_адрес/products;
  • ответ в JSON библиотека сама превратит в список Product с помощью сериализации Kotlin;
  • suspend — функция работает в корутине: может подождать ответа сервера, не блокируя интерфейс.

Ещё одна популярная библиотека для запросов — Ktor Client от JetBrains. Её часто выбирают в проектах на Kotlin Multiplatform.

Корутины и ViewModel

Ответ сервера может идти секунды. Если ждать его в главном потоке, экран замёрзнет, а Android через несколько секунд предложит закрыть приложение. Поэтому запросы запускают в корутинах — лёгких задачах, которые умеют ждать, не занимая поток:

Kotlin
class ShoppingViewModel(private val api: ShoppingApi) : ViewModel() {

    private val _state = MutableStateFlow<ListState>(ListState.Loading)
    val state: StateFlow<ListState> = _state.asStateFlow()

    fun load() {
        viewModelScope.launch {
            _state.value = ListState.Loading
            _state.value = try {
                ListState.Success(api.getProducts())
            } catch (e: IOException) {
                ListState.Error("Нет соединения. Проверьте интернет")
            }
        }
    }
}

sealed interface ListState {
    object Loading : ListState
    data class Success(val products: List<Product>) : ListState
    data class Error(val message: String) : ListState
}
  • viewModelScope.launch запускает корутину, которая сама отменится, когда пользователь уйдёт с экрана;
  • try / catch перехватывает ошибку сети. Без него приложение упадёт, как только пропадёт интернет;
  • ListState описывает три состояния экрана: загрузка, данные и ошибка. Экран показывает индикатор загрузки, список или сообщение с кнопкой «Повторить», в зависимости от текущего состояния.

Хранение на телефоне

Не всё нужно брать с сервера. Что-то хранят прямо на устройстве:

  • DataStore — небольшие настройки вида «ключ — значение»: тема оформления, выбранный город, показывали ли подсказку;
  • Room — база данных SQLite с удобным доступом из Kotlin: списки, таблицы, всё, что должно работать без интернета.

Таблицу Room описывают классом, а запросы — интерфейсом:

Kotlin
@Entity
data class ProductEntity(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    val name: String,
    val count: Int,
)

@Dao
interface ProductDao {
    @Query("SELECT * FROM ProductEntity")
    fun getAll(): Flow<List<ProductEntity>>

    @Insert
    suspend fun insert(product: ProductEntity)
}

Запрос в @Query написан на SQL — языке запросов к базам данных. Если хотите разобраться в нём, на сайте есть курс «SQL с нуля». Flow автоматически присылает новый список при каждом изменении таблицы, поэтому экран обновляется сам.

Попробуйте сами. Опишите, что должен показывать экран списка в каждом из трёх состояний ListState, и напишите для него when.

Решение
Kotlin
@Composable
fun ShoppingContent(state: ListState, onRetry: () -> Unit) {
    when (state) {
        is ListState.Loading -> CircularProgressIndicator()
        is ListState.Success -> ShoppingList(state.products)
        is ListState.Error -> Column {
            Text(state.message)
            Button(onClick = onRetry) {
                Text("Повторить")
            }
        }
    }
}
  • загрузка — крутящийся индикатор, чтобы пользователь видел, что приложение работает;
  • данные — список товаров;
  • ошибка — понятный текст и кнопка «Повторить», которая снова вызывает load().

Интерфейс sealed означает, что других вариантов нет. Поэтому when обходится без else, а если добавить четвёртое состояние, компилятор напомнит обработать и его.

Мини-тест: проверьте себя

Закрепите урок: несколько вопросов, разбор ответов — в конце.

  1. Приложение не может загрузить данные: в логах ошибка доступа к сети. Что проверить первым?

  2. Зачем сетевые запросы делают в корутинах?

  3. Где лучше хранить на телефоне выбранную тему оформления (светлая или тёмная)?

  4. Пропал интернет, а приложение при открытии списка закрылось с ошибкой. Как правильно?