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 解码,然后进行类型化

有两种行为常常让人意外。其一:面对 404 或 500 状态,http 不会抛出任何异常 - 从网络的角度看这次请求"成功了",所以由你来检查 statusCode。其二:jsonDecode 返回的是 dynamic;请立即进行类型转换(as List<dynamic>as Map<String, dynamic>)以恢复类型。对于体积很大的负载,用 compute 把解码交给一个 isolate,这样就不会冻结 UI。

接下来就是表示界面的状态了。像 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 就无法显示带重音符号的字符。
    • 服务器省略了字符集:于是 response.body 会被按 latin-1 解码;请使用 utf8.decode(response.bodyBytes)
  3. 用一个 sealed 类来为界面的状态建模,决定性的优势是什么?
    • switch 变得穷尽:编译器会标出任何未处理的状态。
    • 它让界面渲染得更快。
    • 它让你无需重建 widget 就能修改状态。