基于Http实现网络操作
Flutter 本身只提供基础 Socket & HTTP 能力;一般用 http
或更强大的 dio
封装库来省去手写报文、序列化、异常处理等繁琐工作。
核心概念
概念 | Flutter 中的体现 | 关键点 |
---|---|---|
HTTP 协议 | GET / POST / PUT / DELETE 方法、Header、Body |
客户端--服务器的"请求/响应"文本协议;HTTPS 则在 TCP 之上加 TLS 加密 |
异步 I/O | Future<T> / async ... await |
所有网络 API 都不会阻塞 UI 线程,结果通过 Future 回调 |
序列化 / 反序列化 | jsonEncode / jsonDecode 、fromJson() |
将 Dart 对象 ↔ JSON 字符串 |
状态管理 | setState 、Provider、Riverpod、BLoC |
网络完成后更新 UI、缓存或全局状态 |
错误处理 | try { await http.get(...) } catch (e) |
捕捉超时、无网络、HTTP 4xx/5xx 等异常 |
连接复用 | HttpClient 默认开启 Keep-Alive & HTTP/2 复用 |
同域名复用 TCP/TLS 连接降低握手成本 |
典型代码骨架(含 GET、POST-Form、POST-JSON)
dart
class ApiService {
static final _client = http.Client();
/* GET */
static Future<String> getText(String url) async {
final res = await _client.get(Uri.parse(url));
_ensureSuccess(res);
return res.body;
}
/* POST 表单 */
static Future<String> postForm(
String url, Map<String, String> data) async {
final res = await _client.post(Uri.parse(url), body: data);
_ensureSuccess(res);
return res.body;
}
/* POST JSON */
static Future<String> postJson(
String url, Map<String, dynamic> json) async {
final res = await _client.post(
Uri.parse(url),
body: jsonEncode(json),
headers: {'content-type': 'application/json'},
);
_ensureSuccess(res);
return res.body;
}
static void _ensureSuccess(http.Response res) {
if (res.statusCode != 200) {
throw HttpException('code=${res.statusCode}');
}
}
static void dispose() => _client.close();
}
常见坑
-
超时与重试
darthttp.get(uri).timeout(const Duration(seconds: 5));
-
线程安全
- 网络回调一定在 UI 线程 (Platform Thread)中执行;重逻辑应放到
compute()
或 Isolate。
- 网络回调一定在 UI 线程 (Platform Thread)中执行;重逻辑应放到
-
大文件下载
- 改用
http.Client.send()
获取StreamedResponse
,边下边写文件。
- 改用
-
SSL 证书
- 测试环境可自定义
badCertificateCallback
,正式环境务必校验或使用合法证书。
- 测试环境可自定义
-
状态码判断
- 仅
200
/204
视为成功,其余统一走错误分支,减少漏网 bug。
- 仅
-
JSON Null Safety
map['key'] as String? ?? ''
避免后端字段缺失导致的类型错误。
替代方案
库 | 亮点 | 适用场景 |
---|---|---|
dio | 拦截器、全局配置、FormData、断点续传 | 中大型项目 |
graphql_flutter | GraphQL 查询/订阅 | 后端 GraphQL |
chopper | 代码生成、类似 Retrofit 注解 | REST + 惯用 Java/Kotlin 风格 |
案例展示
dart
import 'dart:convert'; // JSON 编解码工具
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http; // 轻量级网络请求库
/// 演示 HTTP 网络请求的示例页
class HttpStudy extends StatefulWidget {
const HttpStudy({Key? key}) : super(key: key);
@override
State<HttpStudy> createState() => _HttpStudyState();
}
class _HttpStudyState extends State<HttpStudy> {
/// 用于展示服务器返回的完整原始数据
String resultShow = "";
/// 用于展示解析后的 `msg` 字段
String resultShow2 = "";
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('基于 Http 实现网络操作')),
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_doGetBtn(), // GET 按钮
_doPostBtn(), // POST 表单按钮
_doPostJsonBtn(), // POST JSON 按钮
const SizedBox(height: 12),
Text("返回结果:$resultShow"),
const SizedBox(height: 8),
Text("解析的 msg:$resultShow2"),
],
),
);
}
/* ---------------- 按钮构建 ---------------- */
/// GET 请求按钮
Widget _doGetBtn() =>
ElevatedButton(onPressed: _doGet, child: const Text('发送 GET 请求'));
/// POST 表单请求按钮
Widget _doPostBtn() =>
ElevatedButton(onPressed: _doPost, child: const Text('发送 POST 表单请求'));
/// POST JSON 请求按钮
Widget _doPostJsonBtn() =>
ElevatedButton(onPressed: _doPostJson, child: const Text('发送 POST-JSON'));
/* ---------------- 网络请求 ---------------- */
/// 发送 GET 请求
Future<void> _doGet() async {
final uri = Uri.parse(
'https://api.geekailab.com/uapi/test/test?requestPrams=11');
final response = await http.get(uri); // await 等待异步完成
if (response.statusCode == 200) {
// 请求成功:更新 UI
setState(() => resultShow = response.body);
} else {
// 请求失败:展示错误码与返回体
setState(() => resultShow =
"请求失败:code=${response.statusCode},body=${response.body}");
}
}
/// 发送 POST(x-www-form-urlencoded)请求
Future<void> _doPost() async {
final uri = Uri.parse('https://api.geekailab.com/uapi/test/test');
final params = {"requestPrams": "doPost"}; // 必须是 Map<String,String>
final response = await http.post(uri, body: params);
// 默认 Content-Type: application/x-www-form-urlencoded; charset=utf-8
if (response.statusCode == 200) {
setState(() => resultShow = response.body);
} else {
setState(() => resultShow =
"请求失败:code=${response.statusCode},body=${response.body}");
}
}
/// 发送 POST(application/json)请求
Future<void> _doPostJson() async {
final uri = Uri.parse(
'https://api.geekailab.com/uapi/test/testJson');
final params = {"requestPrams": "doPost"};
final response = await http.post(
uri,
body: jsonEncode(params), // Map → JSON 字符串
headers: {"content-type": "application/json"}, // 想服务端发送json类型的数据
);
if (response.statusCode == 200) {
// 1) 展示原始 JSON
setState(() => resultShow = response.body);
// 2) 解析并显示 msg 字段
final map = jsonDecode(response.body);
setState(() => resultShow2 = map['msg']);
} else {
setState(() => resultShow =
"请求失败:code=${response.statusCode},body=${response.body}");
}
}
}
-
三种请求方式
http.get()
→ 简单 GEThttp.post(uri, body: Map)
→ 表单 POSThttp.post(uri, body: jsonEncode(map), headers: {'content-type': 'application/json'})
→ JSON POST
-
setState
刷新每次网络响应后调用
setState
更新数据字段,自动触发页面重建。 -
错误处理
仅依赖
statusCode == 200
判断成功,生产代码中建议再包一层try / catch
处理超时或解析异常。
JSON解析与Dart Model的使用
一、JSON 数据解析的两种方式
-
转成 Map
- 导入
dart:convert
; - 调用
jsonDecode(jsonString)
将 JSON 字符串解析为Map<String, dynamic>
; - 直接通过
map['key']
访问字段。
- 导入
-
转成 Dart Model 类
- 定义实体类(Model),包含普通构造函数、
.fromJson()
命名构造函数和toJson()
方法; - 先用
jsonDecode
得到 Map,再通过Model.fromJson(map)
构造对象; - 方便后续维护和扩展,调用属性更安全。
- 定义实体类(Model),包含普通构造函数、
二、将 JSON 字符串转成 Map
dart
import 'dart:convert';
void _json2Map() {
var jsonString = '{"code":0,"data":{"code":0,"method":"GET","requestPrams":"11"},"msg":"SUCCESS."}';
Map<String, dynamic> map = jsonDecode(jsonString);
// 取值示例:
var code = map['code'];
var params = map['data']['requestPrams'];
// 更新 UI 或其他逻辑
}
三、将 JSON 字符串转成 Dart Model
小知识:什么是Dart Model类?
实体类可以理解为一种数据的载体,主要是作为数据管理和业务逻辑处理层面上存在的类别。比如:我们将从数据库中或服务端接口中读取到的用户信息转成一个User类来存储;然后在需要的时候获取User类的属性并将其展示在界面上等。
-
定义 Model
dartclass Data { int? code; String? method; String? requestPrams; Data({this.code, this.method, this.requestPrams}); Data.fromJson(Map<String, dynamic> json) : code = json['code'], method = json['method'], requestPrams = json['requestPrams']; Map<String, dynamic> toJson() => { 'code': code, 'method': method, 'requestPrams': requestPrams, }; }
-
解析流程
dartvoid _json2Model() { var jsonString = '{"code":0,"data":{"code":0,"method":"GET","requestPrams":"11"},"msg":"SUCCESS."}'; Map<String, dynamic> map = jsonDecode(jsonString); // 根模型假设为 DataModel,内部包含 Data 类型字段 DataModel model = DataModel.fromJson(map); // 使用示例: var code = model.code; var params = model.data?.requestPrams; }
四、Dart Model 的格式要求
- 字段不可私有:属性名称前不能带下划线;
- 普通构造函数:用于创建空对象或手动赋值;
- 命名构造函数
fromJson
:必须实现,将 Map 转为对象; - 成员方法
toJson
:将对象序列化为Map<String, dynamic>
;
五、常见的 Model 转换方式
-
手动编写
- 适合简单 JSON,灵活可控;
- 示例中展示了完整的
fromJson
/toJson
代码。
-
在线工具生成(推荐)
- 支持简单和复杂 JSON;
- 高效快捷,减少手动出错;
- 生成后可根据需求微调。
完整地演示 Flutter 中 JSON → Map → Model → UI 这一整条数据流转的过程
dart
import 'dart:convert'; // 引入 dart:convert 库,用于 JSON 编解码
import 'package:flutter/material.dart';
import 'package:flutter_net_storage/data_model.dart'; // 引入自定义的 DataModel 类
class JsonParsingPage extends StatefulWidget {
const JsonParsingPage({Key? key}) : super(key: key);
@override
State<JsonParsingPage> createState() => _JsonParsingPageState();
}
class _JsonParsingPageState extends State<JsonParsingPage> {
String resultShow = ''; // 用于在界面上显示解析结果
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('JSON 解析与 Dart Model 的实战应用'),
),
body: Column(
children: [
_json2MapBtn(), // 按钮:将 JSON 转成 Map 并展示
_json2ModelBtn(), // 按钮:将 JSON 转成 Model 并展示
const SizedBox(height: 16),
Text("结果:$resultShow"), // 显示解析后的结果
],
),
);
}
/// 构建 "json 转 Map" 按钮
Widget _json2MapBtn() {
return ElevatedButton(
onPressed: _json2Map,
child: const Text('json 转 Map'),
);
}
/// 构建 "json 转 Model" 按钮
Widget _json2ModelBtn() {
return ElevatedButton(
onPressed: _json2Model,
child: const Text('json 转 Model'),
);
}
/// 将 JSON 字符串解析为 Map,并从中读取字段
void _json2Map() {
// 示例 JSON,包含嵌套对象
const jsonString = '''
{
"code": 0,
"data": {
"code": 0,
"method": "POST",
"jsonParams": { "json": "123" }
},
"msg": "SUCCESS."
}
''';
// 使用 jsonDecode 将 JSON 转为 Map<String, dynamic>
final Map<String, dynamic> map = jsonDecode(jsonString);
// 从 Map 中取出所需字段,并更新 UI
setState(() {
resultShow =
'code:${map['code']}; jsonParams:${map['data']['jsonParams']}';
});
}
/// 将 JSON 字符串解析为 Map 后,再通过 DataModel.fromJson 转为 Dart Model
void _json2Model() {
// 示例 JSON,与 DataModel 定义一致
const jsonString = '''
{
"code": 0,
"data": {
"code": 0,
"method": "GET",
"requestPrams": "11"
},
"msg": "SUCCESS."
}
''';
// 1. 转为 Map
final Map<String, dynamic> map = jsonDecode(jsonString);
// 2. 将 Map 转为 DataModel 对象
final DataModel model = DataModel.fromJson(map);
// 3. 从 Model 中读取字段,并更新 UI
setState(() {
resultShow =
'code: ${model.code}; requestPrams:${model.data?.requestPrams}';
});
}
}
Future与FutureBuilder实战应用
1. Future 基础回顾
场景 | 典型写法 | 说明 |
---|---|---|
延迟计算 | Future.delayed(Duration(seconds: 2), () => 'done') |
用于模拟耗时任务或动画节拍 |
网络/IO | dart\nFuture<String> fetchData() async {\n final res = await http.get(Uri.parse(url));\n return res.body;\n}\n |
async/await 语法最直观 |
链式执行 | fetchA().then((a) => fetchB(a)).then(print) |
适用于无需 UI 刷新的纯业务逻辑 |
并行等待 | await Future.wait([fetchA(), fetchB()]) |
聚合多任务,同时提高效率 |
小技巧:
- 长时间任务可用
timeout()
做保护,避免无尽等待。- 重试逻辑可结合
retry
包或手写循环 +catchError
。
2. FutureBuilder 核心语法
dart
FutureBuilder<T>(
future: someFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text('错误: ${snapshot.error}');
}
return _successWidget(snapshot.data as T);
},
);
T
:期望的数据类型,可为空(如String?
)。snapshot.connectionState
:常用waiting / done
;active
多见于 StreamBuilder。- 重复创建 Future :若 Future 每次 build 都生成,会不断重跑。常见做法是 将 Future 缓存到 State 成员 或使用
FutureBuilder.future ??= ...
。
3. 实战示例
3.1 拉取网络数据并展示列表
dart
class PostList extends StatefulWidget {
const PostList({super.key});
@override
State<PostList> createState() => _PostListState();
}
class _PostListState extends State<PostList> {
late final Future<List<Post>> _postsFuture = _fetchPosts();
Future<List<Post>> _fetchPosts() async {
final res = await http
.get(Uri.parse('https://jsonplaceholder.typicode.com/posts'));
final list = jsonDecode(res.body) as List;
return list.map((e) => Post.fromJson(e)).toList();
}
@override
Widget build(BuildContext context) {
return FutureBuilder<List<Post>>(
future: _postsFuture,
builder: (ctx, snap) {
if (snap.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snap.hasError) {
return Center(child: Text('加载失败: ${snap.error}'));
} else if (snap.hasData) {
final posts = snap.data!;
return ListView.separated(
itemCount: posts.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (_, i) => ListTile(
title: Text(posts[i].title),
subtitle: Text(posts[i].body, maxLines: 2, overflow: TextOverflow.ellipsis),
),
);
}
return const SizedBox(); // 不应到达
},
);
}
}
解释
- 使用
late final
确保 Future 只创建一次。ListView.separated
自带分割线,比在 itemBuilder 里手工插Divider
更简洁。
3.2 结合本地缓存:启动页判断登录
思路:先尝试从
SharedPreferences
读取accessToken
,若无则进入登录页。
dart
class SplashGate extends StatefulWidget {
const SplashGate({super.key});
@override
State<SplashGate> createState() => _SplashGateState();
}
class _SplashGateState extends State<SplashGate> {
late final Future<bool> _loggedIn = _checkToken();
Future<bool> _checkToken() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString('accessToken')?.isNotEmpty ?? false;
}
@override
Widget build(BuildContext context) {
return FutureBuilder<bool>(
future: _loggedIn,
builder: (_, snap) {
if (snap.connectionState == ConnectionState.waiting) {
return const Scaffold(body: Center(child: CircularProgressIndicator()));
}
if (snap.hasError) {
return const Scaffold(body: Center(child: Text('初始化失败')));
}
return snap.data! ? const HomePage() : const LoginPage();
},
);
}
}
3.3 进阶:并行加载多资源 + 缓存
场景:页面需要同时 获取用户资料、Banner 列表和远程配置。可用 Future.wait
将并行任务聚合成一个:
dart
Future<Tuple3<User, List<Banner>, RemoteCfg>> _bootstrap() async {
final results = await Future.wait([
api.fetchUser(),
api.fetchBanners(),
api.fetchRemoteConfig(),
]);
return Tuple3(results[0] as User, results[1] as List<Banner>, results[2] as RemoteCfg);
}
然后 FutureBuilder<Tuple3<...>>
加载即可。为了避免重复请求,可在 initState()
将 _bootstrapFuture
存下来。
4. 常见坑与最佳实践
问题 | 解决方案 |
---|---|
Future 在 build 中被多次执行 | 把 Future 存到 State 的字段或使用 memoizer |
长时间任务阻塞 UI | 用 compute() 或自建 Isolate ,将耗时逻辑移出主线程 |
请求取消 | 使用 CancelableOperation ,或结合 mounted 判断 widget 是否还存在 |
重复构建导致闪烁 | 外层加 AnimatedSwitcher 或 FadeTransition 优化体验 |
5. 何时不用 FutureBuilder?
- 需要持续流式数据 → 用
StreamBuilder
。 - 状态多变且需全局管理 → 用
Provider/Riverpod/Bloc
等状态管理结合FutureProvider
. - 纯业务层 → Future 逻辑可放在 Repository,UI 仅订阅已整理好的状态。
基于shared_preferences的本地存储操作
shared_preferences
插件是实现 轻量级本地存储 的常用方式,适合保存用户设置、登录状态、简单标志位等信息。它本质上是键值对存储,类似于 Android 的 SharedPreferences
和 iOS 的 NSUserDefaults
。
✅ 一、添加依赖
在 pubspec.yaml
文件中添加:
yaml
dependencies:
shared_preferences: ^2.2.2 # 检查 pub.dev 上的最新版本
然后运行:
bash
flutter pub get
✅ 二、基本使用示例
1. 导入包
bash
import 'package:shared_preferences/shared_preferences.dart';
2. 存储数据(写入)
bash
void saveLoginStatus(bool isLoggedIn) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('isLoggedIn', isLoggedIn);
}
支持的数据类型有:
数据类型 | 方法 |
---|---|
bool | setBool(key, val) |
int | setInt(key, val) |
double | setDouble(key, val) |
String | setString(key, val) |
List<String> |
setStringList(key, val) |
3. 读取数据(获取)
bash
Future<bool> getLoginStatus() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool('isLoggedIn') ?? false;
}
?? false
表示如果没有保存过该值,默认返回false
。
4. 删除数据 / 清空数据
bash
// 删除某个键
await prefs.remove('isLoggedIn');
// 清空所有键值对
await prefs.clear();
✅ 三、完整登录状态示例(保存 + 判断)
bash
class LoginService {
static const _key = 'isLoggedIn';
static Future<void> setLoginStatus(bool loggedIn) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_key, loggedIn);
}
static Future<bool> isLoggedIn() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool(_key) ?? false;
}
static Future<void> logout() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_key);
}
}
使用:
bash
final loggedIn = await LoginService.isLoggedIn();
if (loggedIn) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => HomePage()));
} else {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => LoginPage()));
}
✅ 四、注意事项
问题 | 说明 |
---|---|
不能存复杂对象 | 不支持 Map、List<Map> 、自定义类;可考虑使用 jsonEncode + setString() 手动序列化 |
性能注意 | 每次 SharedPreferences.getInstance() 都是异步操作,应缓存实例或在 app 启动时初始化 |
不适合大数据 | 它基于平台原生的小型存储方案,仅适合保存少量数据;较大数据建议用 sqflite 或 hive 等数据库方案 |
✅ 五、组合 FutureBuilder 展示本地数据
bash
FutureBuilder<bool>(
future: LoginService.isLoggedIn(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return CircularProgressIndicator();
}
return snapshot.data! ? HomePage() : LoginPage();
},
);
✅ 六、结论
Flutter 的 shared_preferences
与 Android 原生的 SharedPreferences
在理念上几乎一致:
-
定位:都用于轻量级本地持久化,专门保存"键---值"形式的小数据(布尔值、整型、字符串等)。
-
适用场景:登录状态、用户偏好设置、首次启动标记等,不适合大体量或结构化复杂的数据。
-
平台实现 :在 Android 端,
shared_preferences
内部就是调用原生SharedPreferences
;- iOS/macOS →
NSUserDefaults
- Web →
localStorage
- Windows/Linux → 简易本地文件
- iOS/macOS →
因此,在使用时,我们可以实现:
- 保存用户设置、登录状态、主题偏好等;
- 跨页面共享数据,不需要数据库;
- 配合
FutureBuilder
实现异步加载逻辑。
Flutter的常用调试技巧
