第 8 章 网络请求与 JSON 解析(完整可运行示例)
这一章用 dio 请求公开测试接口 jsonplaceholder(无需注册、无需 key),完整实现「加载中 → 列表 → 下拉刷新 → 失败重试」。
创建步骤
flutter create http_demo && cd http_demo;flutter pub add dio;- 替换
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}'),
);
},
),
);
},
),
);
}
}
关键点
- 请求写在
initState,不写在build——这是新手最常见的性能坑。 FutureBuilder必须处理waiting/hasError/ 有数据三种情况。- JSON 里数字可能是
int也可能是double,金额类字段用(json['amount'] as num).toDouble()更稳。 RefreshIndicator里要setState换新 Future 并await,否则转圈立即消失。- Android 需要网络权限(模板 debug 版默认有);release 包记得在
AndroidManifest.xml加INTERNET。 - 真要上生产的接口层,用第 15 章项目里的写法:Dio 单例 + 拦截器统一加 token + 统一异常映射。
动态效果 · 加载中 → 列表渲染
delectus aut autem
quis ut nam facilis
fugiat veniam minus