第 8 章 网络请求与 JSON 解析(完整可运行示例)

这一章用 dio 请求公开测试接口 jsonplaceholder(无需注册、无需 key),完整实现「加载中 → 列表 → 下拉刷新 → 失败重试」。

创建步骤
  1. flutter create http_demo && cd http_demo;
  2. flutter pub add dio;
  3. 替换 lib/main.dart 后运行;想验证错误分支,把 URL 改成 https://jsonplaceholder.typicode.com/todosX。
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';

void main() => runApp(const MaterialApp(home: TodoPage()));

// ---------- ① 数据模型:手写 fromJson,字段类型要对齐接口 ----------
class Todo {
  const Todo({required this.id, required this.title, required this.completed});

  final int id;
  final String title;
  final bool completed;

  factory Todo.fromJson(Map<String, dynamic> json) => Todo(
        id: json['id'] as int,
        title: json['title'] as String,
        completed: json['completed'] as bool,
      );
}

// ---------- ② 页面:StatefulWidget 管理请求生命周期 ----------
class TodoPage extends StatefulWidget {
  const TodoPage({super.key});

  @override
  State<TodoPage> createState() => _TodoPageState();
}

class _TodoPageState extends State<TodoPage> {
  // Dio 只创建一次,放 State 里避免每次 build 都 new 一个
  final Dio _dio = Dio(BaseOptions(
    baseUrl: 'https://jsonplaceholder.typicode.com',
    connectTimeout: const Duration(seconds: 10),
    receiveTimeout: const Duration(seconds: 15),
  ));

  late Future<List<Todo>> _future;

  @override
  void initState() {
    super.initState();
    // 请求放 initState,绝不能放 build(build 会频繁执行,导致请求风暴)
    _future = _fetch();
  }

  Future<List<Todo>> _fetch() async {
    final response = await _dio.get<List<dynamic>>(
      '/todos',
      queryParameters: {'_limit': 20},
    );
    return (response.data ?? [])
        .map((e) => Todo.fromJson(e as Map<String, dynamic>))
        .toList();
  }

  Future<void> _refresh() async {
    final future = _fetch();
    setState(() => _future = future); // 换一个新的 Future,FutureBuilder 会重新等
    await future;                     // RefreshIndicator 需要等它结束
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('网络请求示例')),
      body: FutureBuilder<List<Todo>>(
        future: _future,
        builder: (context, snapshot) {
          // 三种状态:等待 / 出错 / 成功,一个都不能少
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }
          if (snapshot.hasError) {
            return Center(
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  const Icon(Icons.cloud_off, size: 46, color: Colors.grey),
                  const SizedBox(height: 12),
                  Text('加载失败:${snapshot.error}',
                      textAlign: TextAlign.center,
                      style: const TextStyle(fontSize: 13)),
                  const SizedBox(height: 14),
                  OutlinedButton.icon(
                    onPressed: () => setState(() => _future = _fetch()),
                    icon: const Icon(Icons.refresh),
                    label: const Text('重试'),
                  ),
                ],
              ),
            );
          }

          final todos = snapshot.data ?? [];
          return RefreshIndicator(
            onRefresh: _refresh,
            child: ListView.separated(
              physics: const AlwaysScrollableScrollPhysics(),
              itemCount: todos.length,
              separatorBuilder: (_, __) => const Divider(height: 1, indent: 16),
              itemBuilder: (context, index) {
                final todo = todos[index];
                return ListTile(
                  leading: Icon(
                    todo.completed
                        ? Icons.check_circle
                        : Icons.radio_button_unchecked,
                    color: todo.completed ? Colors.green : Colors.grey,
                  ),
                  title: Text(
                    todo.title,
                    maxLines: 2,
                    overflow: TextOverflow.ellipsis,
                  ),
                  subtitle: Text('ID: ${todo.id}'),
                );
              },
            ),
          );
        },
      ),
    );
  }
}
关键点
动态效果 · 加载中 → 列表渲染
delectus aut autem
quis ut nam facilis
fugiat veniam minus
← 上一章第 7.5 章 Riverpod 入门详解(重点)下一章 →第 9 章 数据持久化(两个完整示例)
✉ 联系作者:linghurongjun@sina.com