← Frontend / Flutter

10_HTTP网络请求

Flutter HTTP 网络请求和数据处理

Flutter 网络请求,http 包基础用法,dio 生产级封装。


http 包(轻量)

dependencies:
  http: ^1.1.0
import 'dart:convert';
import 'package:http/http.dart' as http;

// GET 请求
Future<void> fetchPost() async {
  final response = await http.get(
    Uri.parse('https://jsonplaceholder.typicode.com/posts/1'),
  );

  if (response.statusCode == 200) {
    final data = jsonDecode(response.body);
    print('标题: ${data['title']}');
  } else {
    print('请求失败: ${response.statusCode}');
  }
}

// GET 带查询参数
Future<void> search(String query) async {
  final uri = Uri.parse('https://api.example.com/search')
      .replace(queryParameters: {'q': query, 'page': '1'});
  final response = await http.get(uri);
  final data = jsonDecode(response.body);
  print(data);
}

// POST 请求
Future<void> createPost() async {
  final response = await http.post(
    Uri.parse('https://jsonplaceholder.typicode.com/posts'),
    headers: {'Content-Type': 'application/json'},
    body: jsonEncode({
      'title': 'Flutter 教程',
      'body': '学习 Flutter 网络请求',
      'userId': 1,
    }),
  );

  if (response.statusCode == 201) {
    print('创建成功: ${jsonDecode(response.body)}');
  }
}

数据模型(Model 类)

// 类似 TypeScript 的 interface + class
class Post {
  final int id;
  final String title;
  final String body;

  Post({required this.id, required this.title, required this.body});

  // 从 JSON 反序列化(类似 TypeScript 的类型断言)
  factory Post.fromJson(Map<String, dynamic> json) => Post(
    id: json['id'],
    title: json['title'],
    body: json['body'],
  );

  // 序列化为 JSON
  Map<String, dynamic> toJson() => {
    'id': id,
    'title': title,
    'body': body,
  };
}

// 使用
final response = await http.get(Uri.parse('...'));
final post = Post.fromJson(jsonDecode(response.body));
final posts = (jsonDecode(response.body) as List)
    .map((e) => Post.fromJson(e))
    .toList();

封装 API Service

class ApiException implements Exception {
  final String message;
  final int? statusCode;
  ApiException(this.message, {this.statusCode});

  @override
  String toString() => 'ApiException: $message (status: $statusCode)';
}

class ApiService {
  static const String _baseUrl = 'https://api.example.com';
  static const Duration _timeout = Duration(seconds: 15);
  static String? _token;

  static void setToken(String token) => _token = token;

  static Map<String, String> get _headers => {
    'Content-Type': 'application/json',
    'Accept': 'application/json',
    if (_token != null) 'Authorization': 'Bearer $_token',
  };

  static Future<dynamic> get(String path, {Map<String, String>? params}) async {
    final uri = Uri.parse('$_baseUrl$path').replace(queryParameters: params);
    try {
      final response = await http.get(uri, headers: _headers).timeout(_timeout);
      return _handle(response);
    } on TimeoutException {
      throw ApiException('请求超时');
    } on http.ClientException catch (e) {
      throw ApiException('网络连接失败: $e');
    }
  }

  static Future<dynamic> post(String path, Map<String, dynamic> body) async {
    try {
      final response = await http.post(
        Uri.parse('$_baseUrl$path'),
        headers: _headers,
        body: jsonEncode(body),
      ).timeout(_timeout);
      return _handle(response);
    } on TimeoutException {
      throw ApiException('请求超时');
    } on http.ClientException catch (e) {
      throw ApiException('网络连接失败: $e');
    }
  }

  static dynamic _handle(http.Response res) {
    final body = res.body.isEmpty ? '{}' : res.body;
    switch (res.statusCode) {
      case >= 200 && < 300: return jsonDecode(body);
      case 401: throw ApiException('未登录或登录过期', statusCode: 401);
      case 403: throw ApiException('没有权限', statusCode: 403);
      case 404: throw ApiException('资源不存在', statusCode: 404);
      case >= 500: throw ApiException('服务器错误', statusCode: res.statusCode);
      default:   throw ApiException('请求失败', statusCode: res.statusCode);
    }
  }
}

在组件中使用(加载/错误状态)

class PostsPage extends StatefulWidget {
  const PostsPage({super.key});

  @override
  State<PostsPage> createState() => _PostsPageState();
}

class _PostsPageState extends State<PostsPage> {
  List<Post> _posts = [];
  bool _loading = false;
  String? _error;

  @override
  void initState() {
    super.initState();
    _load();
  }

  Future<void> _load() async {
    setState(() { _loading = true; _error = null; });
    try {
      final data = await ApiService.get('/posts') as List;
      if (mounted) {
        setState(() {
          _posts = data.map((e) => Post.fromJson(e)).toList();
          _loading = false;
        });
      }
    } on ApiException catch (e) {
      if (mounted) setState(() { _error = e.message; _loading = false; });
    }
  }

  @override
  Widget build(BuildContext context) {
    if (_loading) return const Center(child: CircularProgressIndicator());
    if (_error != null) return Center(
      child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
        Text(_error!, style: const TextStyle(color: Colors.red)),
        const SizedBox(height: 16),
        ElevatedButton(onPressed: _load, child: const Text('重试')),
      ]),
    );

    return RefreshIndicator(
      onRefresh: _load,
      child: ListView.builder(
        itemCount: _posts.length,
        itemBuilder: (ctx, i) => ListTile(
          title: Text(_posts[i].title),
          subtitle: Text(_posts[i].body, maxLines: 2, overflow: TextOverflow.ellipsis),
        ),
      ),
    );
  }
}

dio 包(生产环境推荐)

dio 对比 http 包就像 axios 对比原生 fetch,功能更完整:

dependencies:
  dio: ^5.4.0
import 'package:dio/dio.dart';

class DioClient {
  static final Dio _dio = Dio(BaseOptions(
    baseUrl: 'https://api.example.com',
    connectTimeout: const Duration(seconds: 10),
    receiveTimeout: const Duration(seconds: 15),
    headers: {'Accept': 'application/json'},
  ));

  // 添加拦截器(类似 axios.interceptors)
  static void init({String? token}) {
    _dio.interceptors.add(
      InterceptorsWrapper(
        onRequest: (options, handler) {
          if (token != null) {
            options.headers['Authorization'] = 'Bearer $token';
          }
          handler.next(options);
        },
        onResponse: (response, handler) {
          handler.next(response);
        },
        onError: (DioException e, handler) {
          if (e.response?.statusCode == 401) {
            // Token 过期,跳转登录页
          }
          handler.next(e);
        },
      ),
    );

    // 日志拦截器(开发时开启)
    _dio.interceptors.add(LogInterceptor(responseBody: true));
  }

  static Future<T> get<T>(String path, {Map<String, dynamic>? params}) async {
    final response = await _dio.get<T>(path, queryParameters: params);
    return response.data!;
  }

  static Future<T> post<T>(String path, dynamic data) async {
    final response = await _dio.post<T>(path, data: data);
    return response.data!;
  }

  // 取消请求(类似 axios CancelToken)
  static CancelToken newCancelToken() => CancelToken();

  // 文件上传
  static Future<void> uploadFile(String path, String filePath) async {
    final formData = FormData.fromMap({
      'file': await MultipartFile.fromFile(filePath),
    });
    await _dio.post(path, data: formData);
  }
}

FutureBuilder(异步 UI 构建)

Flutter 提供 FutureBuilder 用于在 Widget 中处理异步数据,类似 Vue 的 <Suspense>:

FutureBuilder<List<Post>>(
  future: ApiService.get('/posts').then((data) =>
    (data as List).map((e) => Post.fromJson(e)).toList()),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const Center(child: CircularProgressIndicator());
    }
    if (snapshot.hasError) {
      return Center(child: Text('错误: ${snapshot.error}'));
    }
    final posts = snapshot.data!;
    return ListView.builder(
      itemCount: posts.length,
      itemBuilder: (ctx, i) => ListTile(title: Text(posts[i].title)),
    );
  },
)

注意: FutureBuilder 适合一次性请求。如需刷新,推荐用 StatefulWidget + initState 的方式手动控制,更灵活。