CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Flutter 开发入门/课时/网络与本地存储
RawBlame
课时目录12 节

Flutter 开发入门

  1. 01Flutter 环境
  2. 02Dart 语言
  3. 03Widget 体系
  4. 04架构原则
  5. 05状态管理
  6. 06动画
  7. 07网络与本地存储
  8. 08Firebase 集成
  9. 09测试
  10. 10CI/CD 与发布
  11. 11分析与监控
  12. 12综合练习

网络与本地存储

REST、GraphQL 与 SQLite。

30 分钟

网络与本地存储

这一课你将能做到

  1. 用 http 或 dio 发 GET/POST/PUT/DELETE 请求,并处理响应状态码与 JSON
  2. 用 shared_preferences 存简单的键值对(token、开关),用 sqflite 做本地结构化数据库
  3. 用 FutureBuilder / StreamBuilder 优雅地把异步数据绑定到 UI,写完整的 CRUD 本地持久化流程

为什么要学

App 不只是静态 UI——要从服务器拿数据、缓存 token、存用户偏好。不会 IO 操作,Flutter 就是纯玩具。

人话:HTTP 是「打电话要外卖」,本地存储是「记在备忘录上」。两者配合,App 才能在线离线都可用。

先建立图像

UI 层
  ├── FutureBuilder / StreamBuilder  ← 异步数据绑定
  └── onPressed → 调用 Repository

Repository 层
  ├── Remote: dio.get('/api/todos') → JSON → Model
  └── Local: sqflite.query() / shared_preferences.getString()

跟着做

步骤 1:添加依赖

pubspec.yaml:

dependencies:
  http: ^1.2.0
  dio: ^5.4.0
  shared_preferences: ^2.2.0
  sqflite: ^2.3.0
  path_provider: ^2.1.0
  path: ^1.8.3

步骤 2:JSON 序列化(手动 + 生成)

class Todo {
  final int id;
  final String title;
  final bool done;

  Todo({required this.id, required this.title, this.done = false});

  // 手动方式
  factory Todo.fromJson(Map<String, dynamic> json) => Todo(
        id: json['id'] as int,
        title: json['title'] as String,
        done: json['done'] as bool? ?? false,
      );

  Map<String, dynamic> toJson() => {'id': id, 'title': title, 'done': done};
}

生产环境加 json_serializable 自动生成 fromJson/toJson。先理解手动方式再迁移。

步骤 3:用 http 发请求

import 'package:http/http.dart' as http;

Future<List<Todo>> fetchTodos() async {
  final resp = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/todos?_limit=5'));
  if (resp.statusCode == 200) {
    final list = jsonDecode(resp.body) as List;
    return list.map((e) => Todo.fromJson(e as Map<String, dynamic>)).toList();
  }
  throw Exception('请求失败: ${resp.statusCode}');
}

步骤 4:用 dio(更强大)

import 'package:dio/dio.dart';

class ApiClient {
  late final Dio _dio;

  ApiClient() {
    _dio = Dio(BaseOptions(
      baseUrl: 'https://jsonplaceholder.typicode.com',
      connectTimeout: const Duration(seconds: 10),
      receiveTimeout: const Duration(seconds: 10),
    ));

    _dio.interceptors.add(InterceptorsWrapper(
      onRequest: (opts, handler) {
        opts.headers['Authorization'] = 'Bearer $token';
        handler.next(opts);
      },
      onError: (err, handler) {
        if (err.response?.statusCode == 401) {
          // 统一跳登录
        }
        handler.next(err);
      },
    ));
  }

  Future<List<Todo>> getTodos() async {
    final resp = await _dio.get('/todos', queryParameters: {'_limit': 5});
    return (resp.data as List).map((e) => Todo.fromJson(e)).toList();
  }
}

dio 优势:拦截器、全局 baseUrl、自动 JSON、FormData 上传、下载进度、并发取消。

步骤 5:FutureBuilder 绑定 UI

class TodoPage extends StatefulWidget {
  const TodoPage({super.key});
  @override
  State<TodoPage> createState() => _TodoPageState();
}

class _TodoPageState extends State<TodoPage> {
  late Future<List<Todo>> _future;

  @override
  void initState() {
    super.initState();
    _future = fetchTodos();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('待办')),
      body: FutureBuilder<List<Todo>>(
        future: _future,
        builder: (ctx, snap) {
          if (snap.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }
          if (snap.hasError) {
            return Center(child: Text('出错了: ${snap.error}'));
          }
          final items = snap.data ?? [];
          return ListView.builder(
            itemCount: items.length,
            itemBuilder: (ctx, i) => ListTile(title: Text(items[i].title)),
          );
        },
      ),
    );
  }
}

经验:用 FutureBuilder 时,future 字段不要每次 build 都重新创建(否则一直 loading),放 initState 或 Riverpod provider 里。

步骤 6:SharedPreferences 存简单值

import 'package:shared_preferences/shared_preferences.dart';

class Prefs {
  static const _keyToken = 'token';
  static Future<String?> getToken() async =>
      (await SharedPreferences.getInstance()).getString(_keyToken);
  static Future<void> setToken(String token) async =>
      (await SharedPreferences.getInstance()).setString(_keyToken, token);
  static Future<void> clearToken() async =>
      (await SharedPreferences.getInstance()).remove(_keyToken);
}

注意:只存 String/bool/int/double/StringList。不要存大对象或列表。JSON 小对象可以 stringify 存,多的用 sqflite。

步骤 7:sqflite 本地数据库

import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';

class LocalDb {
  static Database? _db;

  static Future<Database> get instance async {
    _db ??= await _init();
    return _db!;
  }

  static Future<Database> _init() async {
    final dbPath = join(await getDatabasesPath(), 'app.db');
    return openDatabase(dbPath, version: 1, onCreate: (db, ver) async {
      await db.execute('''
        CREATE TABLE todos (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          title TEXT NOT NULL,
          done INTEGER NOT NULL DEFAULT 0
        )
      ''');
    });
  }

  static Future<int> insertTodo(Map<String, dynamic> row) async {
    final db = await instance;
    return db.insert('todos', row);
  }

  static Future<List<Map<String, dynamic>>> getTodos() async {
    final db = await instance;
    return db.query('todos', orderBy: 'id DESC');
  }

  static Future<int> updateTodo(int id, Map<String, dynamic> row) async {
    final db = await instance;
    return db.update('todos', row, where: 'id = ?', whereArgs: [id]);
  }

  static Future<int> deleteTodo(int id) async {
    final db = await instance;
    return db.delete('todos', where: 'id = ?', whereArgs: [id]);
  }
}

步骤 8:文件读写(缓存图片、保存日志)

import 'package:path_provider/path_provider.dart';
import 'dart:io';

Future<String> localFilePath(String name) async {
  final dir = await getApplicationDocumentsDirectory();
  return '${dir.path}/$name';
}

Future<void> writeLog(String text) async {
  final path = await localFilePath('log.txt');
  await File(path).writeAsString('$text\n', mode: FileMode.append);
}

Future<String> readLog() async {
  final path = await localFilePath('log.txt');
  if (!await File(path).exists()) return '';
  return File(path).readAsString();
}
  • getApplicationDocumentsDirectory():文档目录,备份到 iCloud/Google
  • getTemporaryDirectory():缓存目录,系统会清
  • getApplicationSupportDirectory():应用数据,不备份

讲清楚要点

  1. 所有网络请求都是异步:必须在 initState / 按钮回调 / provider 中发起,build 里不直接 await。
  2. SharedPreferences 是异步单例:SharedPreferences.getInstance() 很快但仍是 async,不要想当然同步用。
  3. sqflite 表结构用 onCreate/onUpgrade:schema 变更要升级 version 并写 migration。
  4. dio 拦截器 = AOP:统一加 token、统一处理 401、统一打日志,避免每个请求写样板。
  5. 文件操作要权限:Android 高版本 AndroidManifest.xml 加 INTERNET 和存储权限声明。

你可能会懵的地方

  • http vs dio 选哪个? 快速原型/小包用 http;生产环境/复杂网络用 dio(拦截器、全局配置、FormData、取消请求)。

  • FutureBuilder 一直 loading? future 字段在 build 里创建,每次 rebuild 都新建一个 Future。放 initState / provider 里。

  • JSON 字段是 int,但服务器偶尔给 null? as int? ?? 0,先转可空再加默认值。建议每个字段都防御性处理。

  • sqflite 要自己做 ORM 吗? 可以用 drift 或 floor,但先理解 sqflite 原生 API,迁移到 ORM 更容易。

  • 文件在 iOS 沙盒哪里? iOS 有严格的沙盒:Documents/、Library/、tmp/,Flutter 的 path_provider 帮你封装了这些路径。

错法对照

错法问题正解
build 方法里直接 await 网络请求编译错:build 不能 async用 FutureBuilder / Riverpod FutureProvider
SharedPreferences 存复杂对象不支持,读取崩溃JSON stringify 后再存,或换 sqflite
忘记处理 401 统一跳转网络挂了用户不知道dio 拦截器 onError 里统一处理
jsonDecode 后不加 as Map<String, dynamic>dynamic 导致后续报错显式转换,或用 json_serializable
sqflite 的 onCreate 写多表重建时只执行一次,改 schema 要改 versionversion++ 并写 onUpgrade
文件路径写死 /data/data/...iOS 不兼容用 path_provider 获取跨平台路径

卡点急救

  1. 网络请求报 Connection refused:检查模拟器 IP(Android 模拟器 10.0.2.2 指向宿主机);真机确保同一局域网。
  2. iOS 报 Insecure HTTP:iOS 9+ 默认禁止 HTTP,Info.plist 里加 NSAppTransportSecurity → NSAllowsArbitraryLoads true(仅开发)。
  3. SharedPreferences 写进去读不出来:Key 大小写不一致;或者是在写完后立刻读但 await 顺序不对。检查 log。
  4. sqflite 报 no such table:onCreate 没执行,删了 App 重装或调 version++。真机调试时用 flutter run --debug 看 adb 日志。
  5. FutureBuilder 刷新不及时:传一个能变的 future 引用,或者改用 Riverpod FutureProvider / AsyncNotifier。

面试常问

  • FutureBuilder 的原理? StatefulWidget + initState 订阅 Future;future 完成后通过 setState 触发重建;builder 里按 snapshot 的 connectionState / hasData / hasError 决定 UI。

  • dio 的拦截器有什么场景? 统一加 token / 签名;统一处理 401/403 跳转登录;统一日志 / 埋点;请求重试 / 节流。

  • 本地存储方案对比? SharedPreferences:简单键值对;sqflite:结构化数据/查询;file:大文件/日志;hive:高性能键值(免 SQL);drift:ORM 层。

  • JSON 反序列化怎么保证安全? 字段级防御:先转可空类型再加默认值;生成代码(json_serializable)处理 null;用 freezed 做不可变数据类。

自测三问

  1. 为什么 FutureBuilder 的 future 字段不能在 build 方法里重新创建?
  2. 用 dio 写一段代码:发 GET 带 query parameter,并在拦截器里统一加 Header。
  3. sqflite 表结构改了怎么办?(onCreate 已经不会再跑)

小结一句话

Flutter 的数据层 = http/dio 请求网络 + json 转换 + FutureBuilder/StreamBuilder 绑定 UI + SharedPreferences/sqflite/文件持久化;分层清晰,App 才能在线离线都稳定。

延伸

  • http 文档:<https://pub.dev/packages/http>
  • dio 文档:<https://pub.dev/packages/dio>
  • path_provider 文档:<https://pub.dev/packages/path_provider>
  • 下一课:Firebase 集成,一键接入认证、数据库和推送。
加载本节练习…
← 上一课动画下一课 →Firebase 集成