Flutter网络编程与数据存储技术

基于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 / jsonDecodefromJson() 将 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();
}

常见坑

  1. 超时与重试

    dart 复制代码
    http.get(uri).timeout(const Duration(seconds: 5));
  2. 线程安全

    • 网络回调一定在 UI 线程 (Platform Thread)中执行;重逻辑应放到 compute() 或 Isolate。
  3. 大文件下载

    • 改用 http.Client.send() 获取 StreamedResponse,边下边写文件。
  4. SSL 证书

    • 测试环境可自定义 badCertificateCallback,正式环境务必校验或使用合法证书。
  5. 状态码判断

    • 200/204 视为成功,其余统一走错误分支,减少漏网 bug。
  6. 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}");
    }
  }
}
  1. 三种请求方式

    • http.get() → 简单 GET
    • http.post(uri, body: Map) → 表单 POST
    • http.post(uri, body: jsonEncode(map), headers: {'content-type': 'application/json'}) → JSON POST
  2. setState 刷新

    每次网络响应后调用 setState 更新数据字段,自动触发页面重建。

  3. 错误处理

    仅依赖 statusCode == 200 判断成功,生产代码中建议再包一层 try / catch 处理超时或解析异常。

JSON解析与Dart Model的使用

一、JSON 数据解析的两种方式

  1. 转成 Map

    • 导入 dart:convert
    • 调用 jsonDecode(jsonString) 将 JSON 字符串解析为 Map<String, dynamic>
    • 直接通过 map['key'] 访问字段。
  2. 转成 Dart Model 类

    • 定义实体类(Model),包含普通构造函数、.fromJson() 命名构造函数和 toJson() 方法;
    • 先用 jsonDecode 得到 Map,再通过 Model.fromJson(map) 构造对象;
    • 方便后续维护和扩展,调用属性更安全。

二、将 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类的属性并将其展示在界面上等。

  1. 定义 Model

    dart 复制代码
    class 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,
      };
    }
  2. 解析流程

    dart 复制代码
    void _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 转换方式

  1. 手动编写

    • 适合简单 JSON,灵活可控;
    • 示例中展示了完整的 fromJson/toJson 代码。
  2. 在线工具生成(推荐)

    • 支持简单和复杂 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 / doneactive 多见于 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(); // 不应到达
      },
    );
  }
}

解释

  1. 使用 late final 确保 Future 只创建一次。
  2. 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 是否还存在
重复构建导致闪烁 外层加 AnimatedSwitcherFadeTransition 优化体验

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 启动时初始化
不适合大数据 它基于平台原生的小型存储方案,仅适合保存少量数据;较大数据建议用 sqflitehive 等数据库方案

✅ 五、组合 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 → 简易本地文件

因此,在使用时,我们可以实现:

  • 保存用户设置、登录状态、主题偏好等;
  • 跨页面共享数据,不需要数据库;
  • 配合 FutureBuilder 实现异步加载逻辑。

Flutter的常用调试技巧

使用Android Studio 中的调试程序

相关推荐
xjt_0901几秒前
浅析Web存储系统
前端
foxhuli22938 分钟前
禁止ifrmare标签上的文件,实现自动下载功能,并且隐藏工具栏
前端
青皮桔1 小时前
CSS实现百分比水柱图
前端·css
影子信息1 小时前
vue 前端动态导入文件 import.meta.glob
前端·javascript·vue.js
青阳流月1 小时前
1.vue权衡的艺术
前端·vue.js·开源
样子20181 小时前
Vue3 之dialog弹框简单制作
前端·javascript·vue.js·前端框架·ecmascript
kevin_水滴石穿1 小时前
Vue 中报错 TypeError: crypto$2.getRandomValues is not a function
前端·javascript·vue.js
孤水寒月2 小时前
给自己网站增加一个免费的AI助手,纯HTML
前端·人工智能·html
CoderLiu2 小时前
用这个MCP,只给大模型一个figma链接就能直接导出图片,还能自动压缩上传?
前端·llm·mcp
伍哥的传说2 小时前
鸿蒙系统(HarmonyOS)应用开发之实现电子签名效果
开发语言·前端·华为·harmonyos·鸿蒙·鸿蒙系统