鸿蒙Flutter http包网络请求踩坑实录:权限、编码与状态
在鸿蒙上使用Flutter开发应用时,网络请求是绕不开的基础能力。相比ArkTS的@ohos.net.http模块(回调风格),Flutter的http包凭借简洁的async/await写法更受开发者青睐。本文以一个完整的Demo为例,记录在鸿蒙Flutter项目中用http包调用REST API的实践过程,重点梳理参数配置、状态管理以及常见踩坑点。dependencies:
http: ^1.2.2
添加依赖后,无需额外配置即可在鸿蒙设备上运行。如需拦截器、请求取消等高级功能,可后续迁移至dio。
GET请求:拉取列表
最基本的操作是发送GET请求并解析JSON。代码如下:
import 'dart:convert';
import 'package:http/http.dart' as http;
Future<void> _fetchPosts() async {
final resp = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/posts?_limit=20'),
);
if (resp.statusCode == 200) {
final list = jsonDecode(resp.body) as List;
final posts = list.map((e) => _Post.fromJson(e)).toList();
}
}
http.get返回Response对象,statusCode为状态码,body为响应体字符串。jsonDecode将JSON字符串转换为Dart的Map或List,再通过fromJson工厂构造函数转为强类型Model。
Model类封装
建议将JSON解析封装为Model类,避免业务代码中散落json['xxx']:
class Post {
final int id;
final String title;
final String body;
Post({required this.id, required this.title, required this.body});
factory Post.fromJson(Map<String, dynamic> json) {
return Post(
id: json['id'] as int,
title: json['title'] as String,
body: json['body'] as String,
);
}
}
字段较多时可使用json_serializable自动生成,但小规模手写更直观。
状态管理:loading / success / error
网络请求至少需要三种状态:加载中、成功、失败。使用枚举管理比布尔值更清晰:
enum _LoadState { idle, loading, success, error }
_LoadState _state = _LoadState.idle;
List<_Post> _posts = [];
String _error = '';
UI根据状态渲染不同组件:
Widget _buildBody() {
switch (_state) {
case _LoadState.loading:
return const Center(child: CircularProgressIndicator());
case _LoadState.error:
return _buildError();
case _LoadState.success:
case _LoadState.idle:
return _posts.isEmpty ? _buildEmpty() : _buildList();
}
}
GET详情与POST请求
点选列表项获取详情:
Future<void> _fetchDetail(int id) async {
final resp = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/posts/$id'),
);
if (resp.statusCode == 200) {
final data = jsonDecode(resp.body);
// 使用 data['title'], data['body']
}
}
POST创建资源需设置Content-Type头部:
Future<void> _createPost() async {
final resp = await http.post(
Uri.parse('https://jsonplaceholder.typicode.com/posts'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'title': '鸿蒙Flutter测试',
'body': '通过http.post创建',
'userId': 1,
}),
);
if (resp.statusCode == 201) {
final data = jsonDecode(resp.body);
// data['id'] 为新ID
}
}
注意:POST成功通常返回201 Created而非200 OK。
DELETE与超时控制
DELETE请求最简:
Future<void> _deletePost(Post post) async {
final resp = await http.delete(
Uri.parse('https://jsonplaceholder.typicode.com/posts/${post.id}'),
);
if (resp.statusCode == 200) {
// 从列表移除
}
}
超时控制使用Dart的timeout扩展方法:
final resp = await http
.get(Uri.parse('https://jsonplaceholder.typicode.com/posts'))
.timeout(const Duration(seconds: 10));
超时会抛出TimeoutException,建议在catch中统一处理。
错误友好化
直接显示系统错误信息体验差,可根据异常字符串匹配友好提示:
String _friendlyError(Object e) {
final msg = e.toString();
if (msg.contains('SocketException') || msg.contains('Failed host')) {
return '网络连接失败,请检查网络';
}
if (msg.contains('TimeoutException')) {
return '请求超时,请稍后重试';
}
if (msg.contains('HandshakeException')) {
return 'SSL握手失败';
}
return msg;
}
下拉刷新与ApiClient封装
使用RefreshIndicator包裹可滚动组件:
RefreshIndicator(
onRefresh: _fetchPosts,
child: _buildList(),
)
注意child必须为ListView等可滚动组件,Column无法触发下拉手势。
项目规模扩大后建议封装ApiClient,统一管理baseUrl、headers和异常:
class ApiClient {
static const _baseUrl = 'https://jsonplaceholder.typicode.com';
static Future<List<Post>> getPosts() async {
final resp = await http.get(Uri.parse('$_baseUrl/posts?_limit=20'));
if (resp.statusCode == 200) {
final list = jsonDecode(resp.body) as List;
return list.map((e) => Post.fromJson(e)).toList();
}
throw ApiException(resp.statusCode, resp.body);
}
}
class ApiException implements Exception {
final int statusCode;
final String body;
ApiException(this.statusCode, this.body);
}
与ArkTS网络请求对比
ArkTS的@ohos.net.http使用Promise链式调用,对比Flutter的async/await风格,后者代码更接近同步写法。但ArkTS原生支持自定义CA证书和配置校验,企业内网场景更方便;Flutter需通过HttpClient手动设置SecurityContext,稍复杂。
鸿蒙专属踩坑集锦
[*]忘记设置Content-Type:POST请求若不设置`'Content-Type': 'application/json'`,服务端返回400 Bad Request。
[*]状态码判断错误:创建资源成功应判断201 Created而非200 OK。
[*]jsonDecode返回类型:返回dynamic,若直接强转Map可能失败(当JSON顶层为数组时)。应先检查类型:
final data = jsonDecode(resp.body);
if (data is List) {
// 处理数组
} else if (data is Map) {
// 处理对象
}
[*]mounted检查遗漏:异步回调中调用setState前需判断 mounted:
if (!mounted) return;
setState(() { ... });
[*]网络权限缺失:鸿蒙应用必须在module.json5中声明ohos.permission.INTERNET权限,否则请求被拦截,报SocketException。配置如下:
{
"name": "ohos.permission.INTERNET",
"reason": "$string:reason_internet",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "always"
}
}
调试模式下可能侥幸通过,但正式包会暴露问题,建议开发初期即配置。
[*]编码问题:http包默认UTF-8解码,若服务端返回GBK编码(较少见)会出现乱码,需手动指定编码。绝大多数API无此问题。
结语
http包的get/post/put/delete四个方法覆盖了90%的网络请求场景,配合状态枚举和友好错误处理即可构建稳定用法。当项目复杂度提升时,可在http基础上封装ApiClient,统一处理baseUrl、token注入和异常转换。更高级的需求(拦截器、文件上传、请求取消)可迁移至dio。最后再次强调:鸿蒙开发中务必先配置INTERNET权限,否则卡在连接错误上浪费大量调试时间。
Re: 鸿蒙Flutter http包网络请求踩坑实录:权限、编码与状态
感谢楼主的详细分享,非常实用的踩坑记录。我最近也在鸿蒙Flutter上做网络请求,GET和POST基本没问题,但碰到DELETE返回200有时body为空,处理时容易忽略。另外,我用的是http包的1.2.2版本,在鸿蒙模拟器上偶尔会遇到SocketException,加了timeout后稳定多了。想问下楼主在权限配置上有没有额外需要设置的?比如网络权限是默认就有还是要在配置里手动声明?Re: 鸿蒙Flutter http包网络请求踩坑实录:权限、编码与状态
感谢分享,很详细的实战总结!有几个地方想请教一下: - 在鸿蒙上使用 `http` 包时,是否需要手动处理网络权限(比如在 `module.json5` 里配置 `ohos.permission.INTERNET`)?帖子里说“无需额外配置即可运行”,但我印象中鸿蒙应用默认可能没有网络权限,是不是要先在配置里加上? - 对于状态枚举里的 `idle` 状态,在UI中直接和 `success` 一起复用 `_buildEmpty()` 或 `_buildList()`,但 `idle` 时通常列表还没请求吧?这里会不会出现 `_posts` 为空时意外显示空视图的情况?还是说初始状态 `idle` 时直接返回一个空容器更合适? - 超时处理那块,如果 `timeout` 抛出异常,建议在 `catch` 中统一处理,但帖子里没给示例代码,能补充一下 `catch` 里区分网络异常和超时异常的写法吗? 另外,个人觉得对于 `Content-Type` 头,有时后端不要求显式设置也能自动识别(比如 `jsonEncode` 默认就是JSON),但写成 `headers` 明确指定更安全,这点赞同。Re: 鸿蒙Flutter http包网络请求踩坑实录:权限、编码与状态
写得非常详细,尤其是状态管理的枚举用法和超时控制的例子,很实用。之前在鸿蒙上做Flutter开发时也踩过jsonDecode的类型转换坑,看到你把Model层单独拆出来挺有启发。另外POST成功返回201这点容易忽略,感谢提醒。
页:
[1]