Kodokon kodokon.com

Вызов HTTP API: декодирование и состояния экрана

Выстрой профессиональный сетевой вызов с пакетом http: таймаут, коды статуса, типизированное декодирование и исчерпывающие состояния.

11 мин · 3 вопросов

Открыть этот урок в Kodokon

Пакет http закрывает самое главное: запросы, заголовки, тело. dio добавляет перехватчики, отмену и повторные попытки, ценой более тяжёлой зависимости - хороший выбор, когда приложение перерастает пару-тройку эндпоинтов. Каким бы ни был клиент, структура серьёзного сетевого вызова не меняется: таймаут, проверка кода статуса HTTP, типизированное декодирование и три состояния экрана (загрузка, ошибка, данные), обработанные явно.

BASH
flutter pub add http
DART
import 'dart:convert';

import 'package:http/http.dart' as http;

class Article {
  const Article({required this.title});

  factory Article.fromJson(Map<String, dynamic> json) =>
      Article(title: json['title'] as String);

  final String title;
}

Future<List<Article>> fetchArticles() async {
  final uri = Uri.https('api.example.com', '/articles');
  final response = await http
      .get(uri)
      .timeout(const Duration(seconds: 8));
  if (response.statusCode != 200) {
    throw Exception('HTTP ${response.statusCode}');
  }
  final body = utf8.decode(response.bodyBytes);
  final data = jsonDecode(body) as List<dynamic>;
  return [
    for (final item in data)
      Article.fromJson(item as Map<String, dynamic>),
  ];
}
Таймаут, проверенный статус, декодирование UTF-8, затем типизация.

Два поведения часто удивляют. Первое: http не выбрасывает исключение при статусе 404 или 500 - с точки зрения сети запрос «удался», так что проверять statusCode должен ты. Второе: jsonDecode возвращает dynamic; приводи тип сразу (as List<dynamic>, as Map<String, dynamic>), чтобы вернуть типизацию. Для больших объёмов данных выноси декодирование в изолят через compute, чтобы не подвешивать интерфейс.

Остаётся представить состояние экрана. Булевы флаги вроде isLoading вместе с error != null рано или поздно приводят к несогласованным состояниям (загрузка и ошибка одновременно). Класс sealed делает состояния взаимоисключающими, а switch - исчерпывающим: если завтра ты добавишь состояние, компилятор укажет на каждый экран, который надо обновить.

DART
import 'package:flutter/material.dart';

sealed class ArticlesState {
  const ArticlesState();
}

class ArticlesLoading extends ArticlesState {
  const ArticlesLoading();
}

class ArticlesError extends ArticlesState {
  const ArticlesError(this.message);

  final String message;
}

class ArticlesLoaded extends ArticlesState {
  const ArticlesLoaded(this.titles);

  final List<String> titles;
}

Widget buildArticlesBody(ArticlesState state) {
  return switch (state) {
    ArticlesLoading() => const Center(
        child: CircularProgressIndicator(),
      ),
    ArticlesError(:final message) => Center(
        child: Text(message),
      ),
    ArticlesLoaded(:final titles) => ListView.builder(
        itemCount: titles.length,
        itemBuilder: (context, index) => ListTile(
          title: Text(titles[index]),
        ),
      ),
  };
}
Исчерпывающий switch: ни одно состояние не забыто.

Проверка знаний

Убедись, что запомнил ключевые моменты этого урока.

  1. Твой вызов http.get получает ответ 500. Что делает пакет http?
    • Выбрасывает исключение, которое можно поймать через try/catch.
    • Обычным образом возвращает Response: проверять statusCode должен ты.
    • Автоматически повторяет запрос три раза.
  2. Символы с диакритикой отображаются в полученных данных неправильно. Самая вероятная причина?
    • jsonDecode не поддерживает UTF-8.
    • Flutter не умеет показывать символы с диакритикой без отдельного пакета.
    • Сервер не указывает charset: response.body тогда декодируется как latin-1; используй utf8.decode(response.bodyBytes).
  3. В чём решающее преимущество класса sealed для моделирования состояния экрана?
    • switch становится исчерпывающим: компилятор укажет на любое необработанное состояние.
    • Экран отрисовывается быстрее.
    • Он позволяет менять состояние, не перестраивая виджет.