Урок 7 из 8 · 25 мин · обновлено
Данные: запросы к серверу и хранение на телефоне
Как Android-приложение получает данные с сервера через Retrofit и корутины, зачем нужно разрешение INTERNET, как сохранить настройки в DataStore и данные в базу Room.
Откуда приложение берёт данные
Список товаров в магазине, курсы валют, история заказов хранятся на сервере. Приложение получает их через API — набор адресов, по которым сервер отдаёт и принимает данные. Обычно в формате JSON:
[
{ "id": 1, "name": "Молоко", "count": 2 },
{ "id": 2, "name": "Хлеб", "count": 1 }
]
Чтобы приложение вообще могло выйти в интернет, в AndroidManifest.xml добавляют разрешение:
<uses-permission android:name="android.permission.INTERNET" />
Retrofit: запросы к серверу
Самая популярная библиотека для запросов — Retrofit. Запросы к серверу описывают интерфейсом, а код запросов библиотека создаёт сама:
@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 через несколько секунд предложит закрыть приложение. Поэтому запросы запускают в корутинах — лёгких задачах, которые умеют ждать, не занимая поток:
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 описывают классом, а запросы — интерфейсом:
@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.
Решение
@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, а если добавить четвёртое состояние, компилятор напомнит обработать и его.
Мини-тест: проверьте себя
Закрепите урок: несколько вопросов, разбор ответов — в конце.
-
Приложение не может загрузить данные: в логах ошибка доступа к сети. Что проверить первым?
Без разрешения INTERNET в манифесте Android не даст приложению выйти в сеть.
-
Зачем сетевые запросы делают в корутинах?
Ответ сервера может идти секунды. Корутина ждёт его, не блокируя главный поток, поэтому экран продолжает отвечать на нажатия.
-
Где лучше хранить на телефоне выбранную тему оформления (светлая или тёмная)?
DataStore создан для небольших настроек вида «ключ — значение». Room нужен для структурированных данных: списков, таблиц, связей.
-
Пропал интернет, а приложение при открытии списка закрылось с ошибкой. Как правильно?
Сеть на телефоне пропадает постоянно: в метро, в лифте. Хорошее приложение к этому готово.