编程闭关教程 · 系列 08–20 · 编制日期 2026-10-14

13 Flutter 专题:两周出一个调通工作台 API 的跨端 APP

Flutter 是"项目驱动"的卫星语言:你接到 APP 项目、且 11 工作台 的 API 已经能调通,才启动本模块。目标不是把你训练成 Flutter 专家,而是让有多年编程经验的你,用两周时间把同一个工作台后端接出一个 Windows 桌面 + Android 双端 APP——登录拿 JWT、带 token 增删改查任务列表。
本模块承诺从装好 SDK 到跑通"登录 → 任务列表 → 增删改查",两周(每日 4h 时间盒)完成;验收 APP 同时在 Windows 桌面窗口和 Android 模拟器/真机上登录同一个 localhost:8080 工作台。没 APP 项目时,本模块不要抢主线时间,直接顺延。
3.47.6
Flutter stable(2026-10-02)
3.13.5
随包 Dart,不单独装
2 平台
Windows 桌面 + Android
1 个
验收 APP(登录 + 任务 CRUD)
2 周
每日 4h 时间盒
前置硬条件:① 11 工作台已在本机跑通(curl http://localhost:8080/healthz 返回 ok);② 你已有多年 C/C++/Java/PHP/Vue 经验,零 Flutter/Dart 经验。两个条件不齐,本模块暂时封存。
08 总纲 09 环境·版本 10 Go 内核 11 工作台 12 Vue3 13 Flutter 14 Python 15 C/C++ 16 MySQL 17 PostgreSQL 18 Redis 19 SQLite3 20 部署
本页目录
  1. 01 · 启动边界:什么时候才打开本模块
  2. 02 · Windows 环境:Flutter SDK + doctor 全绿
  3. 03 · Dart 速览(对照 C/Java/PHP/JS)
  4. 04 · Flutter 核心:Widget 树与常用组件
  5. 05 · 状态管理选型与 Riverpod 完整示例
  6. 06 · HTTP 对接工作台:dio + JWT + CRUD
  7. 07 · 打包:Windows exe 与 Android APK
  8. 08 · 常见报错表(10 条)
  9. 09 · 验收清单
  10. 10 · 版本与来源表
01

启动边界:什么时候才打开本模块

按 08 总纲路径 C:Flutter 是卫星,P1 按需。本模块不是主线课程,不要在没有 APP 项目时提前学它——卫星语言最容易"学完就忘",只有项目逼着你用,两周才能出活。

启动本模块的两个硬条件

① 你手上确实有一个 APP 项目(要给工作台做手机/桌面壳,或客户要求跨端 APP);② 11 工作台 API 已在本机跑通:后端在 localhost:8080,POST /api/v1/auth/login 能返回 token,GET /api/v1/tasks 带 Bearer 能返回列表。两个条件缺一,本模块封存。

什么时候不要启动

主线还在阶段一/二(Go 内核、工作台工程化、四数据库)时,不要提前学 Flutter。它会抢走你每天 4h 的主线时间,且学完没有项目可接就会全部遗忘。正确做法:把本模块加入 backlog,APP 项目来了再按本页从零照做。
本模块的学习法:不读官方长文档,只做三件事——§2 把环境搭绿 → §3/§4 用 1 天扫完语法与 Widget 概念(你有编程经验,不是零基础)→ §5/§6 直接照抄完整代码改成接自己工作台的 APP。理论只到"看得懂自己写出来的代码"为止。
02

Windows 环境:Flutter SDK + doctor 全绿

安装细节(下载链接、PATH 配置截图)属于 09 环境模块;这里只给"为跑通工作台 APP 所必需的最小步骤"。版本锁死 stable 3.47.6(随包 Dart 3.13.5),不选 master/beta。

2.1 装 Flutter SDK(stable 3.47.6)与国内镜像

  1. 从官方归档页取 stable 3.47.6 的 zip,解压到不含空格、不含中文的路径,例如 D:\dev\flutter(不要放 C:\Program Files,权限会坑你)。完整归档清单见 https://docs.flutter.dev/install/archive。
  2. 把 D:\dev\flutter\bin 加入用户 PATH(系统属性 → 环境变量 → 用户变量 Path → 新建)。
  3. 设两个国内镜像环境变量(用户变量,否则 pub 极慢):
# PowerShell(用户变量,永久生效;重开终端后生效)
[Environment]::SetEnvironmentVariable("FLUTTER_STORAGE_BASE_URL","https://storage.flutter-io.cn","User")
[Environment]::SetEnvironmentVariable("PUB_HOSTED_URL","https://pub.flutter-io.cn","User")
# 重开 PowerShell 后验证
echo $env:FLUTTER_STORAGE_BASE_URL
flutter --version   # 应输出 Flutter 3.47.6 / Dart 3.13.5

2.2 flutter doctor 全绿的最小路径

打开新的 PowerShell,运行 flutter doctor -v。按下面三块逐个修:

doctor 检查项要做什么不想做 Android 时
Flutter(自身)装完 SDK + 镜像后应自动绿;红了就 flutter doctor 按提示修。必须绿。
Windows 桌面开发需要 Visual Studio 2022,安装时勾选"使用 C++ 的桌面开发"工作负载(含 MSVC v143、C++ CMake tools、Windows 10/11 SDK)。装完重开终端再 doctor。这是本模块第一个平台,必须绿。
Android 工具链装 Android Studio → Settings → SDK Tools 勾"Android SDK Command-line Tools (latest)"→ 运行 flutter doctor --android-licenses 一路按 y。暂时不做 Android?跳过本项即可:APP 先跑在 Windows 桌面上接工作台,doctor 里这一项是 [!] 不影响 flutter run -d windows。等要打 APK 再回来补。
Chrome / VS CodeChrome 可选(本模块不做 Web);VS Code 装"Dart-Code"扩展即可,非必须。忽略。

目标状态:至少 [✓] Flutter 与 [✓] Windows 桌面开发 两项绿;Android 项可黄。跑 flutter doctor 看到这两项绿就进 §3。

2.3 VS2022 C++ 工作负载为什么必须装

Flutter 的 Windows 桌面端不是"套个 WebView",而是用 C++ 写的桌面壳(win32 runner),构建时需要 MSVC 编译器和 CMake。缺了它,flutter build windows 会在链接阶段报错(见 §8 报错表第 6 条)。装 VS2022 Community 版即可,免费;只勾"使用 C++ 的桌面开发"一个工作负载,别全装(省磁盘)。

03

Dart 速览(对照 C/C++/Java/PHP/JS)

你不是零基础。这一节不教"什么是变量",只列 Dart 和你已知语言的差异点,半小时扫完,够读懂后面的示例代码。

3.1 类型与变量

概念Dart 写法对照
内置类型int double String bool同 Java/JS;注意 Dart 的数字分 int 和 double,不像 PHP 全是弱转换。
推断变量var x = 3;(x 被锁定为 int,不能再赋字符串)同 TypeScript 的 var;比 JS 严格。
不可变绑定final name = 'abc';(运行期确定一次)
const PI = 3.14;(编译期常量)
对照 Java 的 final / Go 的 const。能用 final 就用 final。
动态类型dynamic v = anything;等于 TS 的 any;不要滥用,空安全会被它绕开。
字符串'$name' 、 '${obj.field}'JS 模板字符串;Dart 单双引号等价。

3.2 函数与类

// 箭头函数:单行函数体,对照 JS/Go 的闭包
int add(int a, int b) => a + b;

// 命名参数(花括号):对照 Python 关键字参数;required 必传
String greet({required String name, String title = ''}) => '$title $name';

// 类:默认无 setter/setter 魔法,就是普通字段
class Task {
  final int id;
  String title;
  bool done;
  Task({required this.id, required this.title, this.done = false});
}

// mixin:Dart 的代码复用手段(对照 Java interface default / Go 无)
mixin Loggable {
  void log(String msg) => print('[LOG] $msg');
}
class Api with Loggable {
  void call() => log('called');   // 直接拿到 log 方法
}

3.3 异步:Future / async / await(重点,HTTP 全靠它)

Dart 单线程 + 事件循环,异步模型和 JS 几乎一模一样:Future<T> 就是 JS 的 Promise<T>,async/await 语义完全相同。和 Go 的 goroutine 不是一回事——Dart 没有抢占式线程,靠 await 挂起点让事件循环跑别的任务。

// 对照 JS:const r = await fetch(url).then(r => r.json())
Future<List<Task>> fetchTasks() async {
  final resp = await http.get(Uri.parse('/api/v1/tasks'));
  if (resp.statusCode == 200) {
    return parseTasks(resp.body);   // 同步函数直接返回,包在 Future 里
  }
  throw Exception('HTTP ${resp.statusCode}');
}

// 调用处照样 await;错误用 try/catch(对照 Java/JS)
try {
  final tasks = await fetchTasks();
} catch (e) {
  print('失败: $e');
}

3.4 空安全(你最容易踩的新东西)

写法含义对照
String s;非空,必须有值,否则编译报错Java 没有这层;这是 Dart 2.12+ 的强制空安全
String? s;可空,可能是 null;用它之前要判空对照 Kotlin 的 String? / TS 的 string | null
s!"我确定它不是 null",强解包;真的是 null 会运行时炸对照 Kotlin 的 !!
s?.lengths 为 null 则整个表达式为 null,否则取 length对照 JS/TS 的可选链 ?.
late String s;"我保证稍后会给它赋值",先用着,编译期不报错State 对象里常用(initState 里才初始化)
s ?? 'default'null 则取默认值对照 JS 的 ??(注意不是 ||)

3.5 集合

// List / Map / Set,都是字面量,对照 JS 数组与对象
final List<String> names = ['a', 'b'];
final Map<String, dynamic> json = {'code': 0, 'data': []};

// 展开与 if-in-list:Flutter UI 里天天用
final items = [
  'header',
  if (showExtra) 'extra',      // 集合 if
  ...names,                     // 展开
];
names.map((n) => n.toUpperCase()).toList();   // 链式,同 JS

扫完这节就够了:Dart 对你而言 = "带空安全的 TypeScript,跑在 Flutter 上"。剩下的语法细节在写 §5/§6 代码时遇到再查,不要现在去读 Dart 官方语言之旅。

04

Flutter 核心:Widget 树与常用组件

Flutter 的世界观只有一句话:一切皆 Widget,UI 是 Widget 树,build 方法就是"用当前状态算出这一帧长什么样"。没有 XML 模板、没有虚拟 DOM diff 之外的魔法——你在 build 里 new 一堆 Widget,框架负责高效地画到屏幕上。

4.1 Stateless vs Stateful:什么时候用哪个

类型特征什么时候用
StatelessWidget一旦 build 完就不变;输入变了由父级重建它。纯展示:一个按钮、一行文字、一张卡片。默认先用它。
StatefulWidget有一个配套的 State 对象,setState() 后框架重新调用 build。有"会变的东西":输入框内容、计数器、加载中/成功/失败切换。
// 最小 StatefulWidget:计数器(你将来在 §5 用 Riverpod 替代 setState)
class Counter extends StatefulWidget {
  @override
  State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
  int _n = 0;
  @override
  Widget build(BuildContext context) {
    return Column(children: [
      Text('点击了 $_n 次'),
      ElevatedButton(
        onPressed: () => setState(() => _n++),   // setState 触发重建
        child: const Text('+1'),
      ),
    ]);
  }
}

4.2 布局:四个核心组件先记死

组件干什么易错点
Row子元素水平排列(flex-direction: row)。横向超宽会黄黑条纹报错;用 Expanded 包子元素让它自适应压缩。
Column子元素垂直排列(flex-direction: column)。放进 ListView 时不要设无限高;Column 要配 MainAxisAlignment 控制对齐。
Stack子元素层叠(对照 CSS absolute 定位容器)。里面定位用 Positioned(left:.. ,bottom:..)。
ListView可滚动长列表,懒加载。不要把 Column 直接塞进无限高滚动区;列表就用 ListView.builder。

4.3 常用组件(本模块够用即可)

// 文字 / 按钮 / 输入框 / 卡片:一次认全
Text('你好', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),

ElevatedButton(onPressed: () {}, child: Text('登录')),
TextButton(onPressed: () {}, child: Text('取消')),

TextField(
  decoration: InputDecoration(labelText: '用户名', hintText: '请输入'),
  onChanged: (v) => username = v,
),

Card(
  elevation: 2,
  child: ListTile(
    title: Text('任务标题'),
    subtitle: Text('备注'),
    trailing: Checkbox(value: false, onChanged: (v) {}),
  ),
),

// 列表的标准写法:ListView.builder(懒加载,长列表专用)
ListView.builder(
  itemCount: tasks.length,
  itemBuilder: (ctx, i) => Card(child: ListTile(title: Text(tasks[i].title))),
)

4.4 路由:Navigator 与 go_router 的选型

Navigator.push(默认,够用)

命令式跳页:Navigator.push(context, MaterialPageRoute(builder: (_) => TaskPage()));。登录页 ↔ 任务页这种两三个页面的 APP,用它就够了,不引第三方包。

go_router 18.x(路由变多再上)

声明式路由,URL 式路径,适合要做 Web 深链、底部导航、需要"刷新页面停在当前页"的正式 APP。本模块规模(登录 + 一个列表页)不引入;将来页面超过 5 个再换 go_router 18.0.2。

4.5 主题:MaterialApp 一处管全局

MaterialApp(
  title: '工作台 APP',
  theme: ThemeData(
    colorSchemeSeed: const Color(0xFF0e7a6b),  // 主色,一套色板自动派生
    useMaterial3: true,
  ),
  home: const LoginPage(),   // 初始页;登录成功后换成 TaskPage
)

核心理解:Flutter 没有"找控件 id 改文字"这套(对照 Android View / jQuery)。你只写"在状态 S 下应该长什么样",状态变了就重新调 build。这和 Vue3 的响应式思路一致,你迁移成本很低。

05

状态管理选型与 Riverpod 完整示例

2026 年现状一句话:Provider 是入门最小包,Riverpod 是作者重写的现代化版本(编译安全、不依赖 BuildContext),Bloc 是事件驱动的重武器。本模块 APP 规模小,但要接 dio 异步流,直接上 Riverpod——一次写对,不用将来从 Provider 迁移。

5.1 选型规则(照此二选一)

方案版本(2026-10 核验)什么时候选它本模块结论
Provider6.1.5+1状态极少、团队只想要最小概念、已熟悉 InheritedWidget。概念最少,但要拿 BuildContext 找 Provider,异步样板偏多。
Riverpodflutter_riverpod 3.3.1要类型安全、要优雅接异步 Future、不想到处传 context。本模块推荐:登录态 + 任务列表这种异步场景正好用它。
Bloc bloc 系列大型团队、严格事件-状态流、需要完整可测试的状态机。样板代码重,本模块规模不上。

5.2 第一步:pubspec.yaml 锁依赖

# flutter create workbench_app 之后,编辑 pubspec.yaml 的 dependencies:
dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^3.3.1      # 状态管理(2026-10 核验 latest 3.3.1)
  dio: ^5.11.1                  # HTTP 客户端(2026-10 核验 latest 5.11.1)

# 然后运行(会生成 pubspec.lock = 锁文件,唯一事实来源)
#   flutter pub get

版本纪律:上面写的是"兼容区间"(^),实际装进项目的版本由 pubspec.lock 锁定。换机器/换同事,flutter pub get 会按 lock 还原,不会飘。AI 若建议你升到 3.x 更高 minor,按 08 §7 规则 5 默认拒绝。

5.3 第二步:从计数器到任务列表的完整最小实现

下面 3 个文件拼起来就是一个"全局计数器 + 任务列表"的 Riverpod 最小骨架(HTTP 接工作台在 §6 补全)。

文件 1:lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'tasks_page.dart';

void main() => runApp(const ProviderScope(child: MyApp()));

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '工作台 APP',
      theme: ThemeData(colorSchemeSeed: const Color(0xFF0e7a6b), useMaterial3: true),
      home: const TasksPage(),
    );
  }
}

文件 2:lib/tasks_provider.dart(状态定义)

import 'package:flutter_riverpod/flutter_riverpod.dart';

// ① 一个最简单的状态:计数器(用 StateNotifierProvider,对照 Vue ref)
final counterProvider = StateProvider<int>((ref) => 0);

// ② 任务列表状态:状态类 + Notifier( Riverpod 3.x 推荐写法)
class Task {
  final int id;
  final String title;
  bool done;
  Task({required this.id, required this.title, this.done = false});
}

class TasksNotifier extends Notifier<List<Task>> {
  @override
  List<Task> build() => [];   // 初始状态

  void add(String title) {
    state = [...state, Task(id: state.length + 1, title: title)];
  }

  void toggle(int id) {
    state = [
      for (final t in state)
        if (t.id == id) Task(id: t.id, title: t.title, done: !t.done) else t,
    ];
  }

  void remove(int id) {
    state = state.where((t) => t.id != id).toList();
  }
}

文件 3:lib/tasks_page.dart(UI 消费状态)

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'tasks_provider.dart';

class TasksPage extends ConsumerWidget {
  const TasksPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final tasks = ref.watch(tasksProvider);   // 监听,状态变就重建
    final counter = ref.watch(counterProvider);

    return Scaffold(
      appBar: AppBar(title: Text('任务列表(已 $counter 次交互)')),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          ref.read(tasksProvider.notifier).add('新任务');
          ref.read(counterProvider.notifier).state++;
        },
        child: const Icon(Icons.add),
      ),
      body: ListView.builder(
        itemCount: tasks.length,
        itemBuilder: (ctx, i) {
          final t = tasks[i];
          return Card(
            child: ListTile(
              title: Text(t.title,
                  style: TextStyle(
                      decoration: t.done ? TextDecoration.lineThrough : null)),
              leading: Checkbox(
                value: t.done,
                onChanged: (_) => ref.read(tasksProvider.notifier).toggle(t.id),
              ),
              trailing: IconButton(
                icon: const Icon(Icons.delete, color: Colors.red),
                onPressed: () => ref.read(tasksProvider.notifier).remove(t.id),
              ),
            ),
          );
        },
      ),
    );
  }
}

跑一下:flutter run -d windows,点 FAB 加任务、勾完成、点删除。这就是"计数器 → 任务列表"的完整闭环——§6 把 Notifier 里的本地数组换成真正的工作台 HTTP 请求即可。

06

HTTP 对接工作台:dio + JWT + CRUD

这是本模块的真正验收点。严格按 workbench-contract 的端点写:不新增任何端点,请求/响应结构与 11 工作台后端完全一致。

6.1 为什么选 dio 而不是 http

包特点结论
httpDart 官方基础包,API 极简,但拦截器/超时/统一错误处理要自己包一层。单脚本够用;APP 要带 token、要处理 401,不够省事。
dio 5.11.1拦截器、超时、FormData、统一异常 DioException 开箱即用;国内 Flutter 生态事实标准。本模块选 dio:JWT 注入与 401 处理用拦截器一次写好。

6.2 端点对齐表(只准用这些)

方法路径请求体说明
POST/api/v1/auth/login{username, password}返回 {token, user}(包在统一信封里)
GET/api/v1/tasks无任务列表,需 Bearer
POST/api/v1/tasks{title, note, done}新建任务
PUT/api/v1/tasks/:id同上更新(含标记完成)
DELETE/api/v1/tasks/:id无删除

统一响应信封:{"code":0,"data":{...},"msg":""},错误时 code 非 0。所有受保护请求带 Authorization: Bearer <jwt>。

6.3 完整代码:lib/api.dart(dio 单例 + 拦截器 + 401 处理)

import 'package:dio/dio.dart';

// ★ 本机地址:Windows 桌面跑用 localhost;Android 模拟器跑必须用 10.0.2.2
// (Android 模拟器里 localhost 指它自己,不是你的电脑)
const String baseUrl = String.fromEnvironment(
  'BASE_URL',
  defaultValue: 'http://localhost:8080',
);

class Api {
  Api._() {
    dio = Dio(BaseOptions(
      baseUrl: baseUrl,
      connectTimeout: const Duration(seconds: 5),
      receiveTimeout: const Duration(seconds: 5),
    ));
    // 拦截器:每个请求自动塞 token;401 自动清 token 并跳登录
    dio.interceptors.add(InterceptorsWrapper(
      onRequest: (opt, handler) {
        if (_token != null) {
          opt.headers['Authorization'] = 'Bearer $_token';
        }
        handler.next(opt);
      },
      onError: (e, handler) {
        if (e.response?.statusCode == 401) {
          _token = null;
          // 这里发一个全局事件/改状态,让 UI 切回登录页(见 6.5)
        }
        handler.next(e);
      },
    ));
  }

  static final Api instance = Api._();
  late final Dio dio;
  String? _token;

  // ① 登录拿 JWT
  Future<void> login(String username, String password) async {
    final resp = await dio.post('/api/v1/auth/login', data: {
      'username': username,
      'password': password,
    });
    final body = resp.data as Map;
    if (body['code'] != 0) throw Exception(body['msg']);
    _token = body['data']['token'] as String;
  }

  // ② 任务模型(对齐工作台契约:id/title/note/done/created_at/updated_at)
  Task taskFromJson(Map<dynamic, dynamic> j) => Task(
        id: (j['id'] as num).toInt(),
        title: j['title'] as String,
        note: (j['note'] ?? '') as String,
        done: (j['done'] ?? false) as bool,
      );

  // ③ 列表
  Future<List<Task>> listTasks() async {
    final resp = await dio.get('/api/v1/tasks');
    final body = resp.data as Map;
    if (body['code'] != 0) throw Exception(body['msg']);
    return (body['data'] as List).map((e) => taskFromJson(e)).toList();
  }

  // ④ 新建
  Future<void> createTask(String title, String note) async {
    final resp = await dio.post('/api/v1/tasks',
        data: {'title': title, 'note': note, 'done': false});
    if ((resp.data as Map)['code'] != 0) throw Exception(resp.data['msg']);
  }

  // ⑤ 更新(标记完成同理,传完整 done)
  Future<void> updateTask(Task t) async {
    await dio.put('/api/v1/tasks/${t.id}',
        data: {'title': t.title, 'note': t.note, 'done': t.done});
  }

  // ⑥ 删除
  Future<void> deleteTask(int id) async {
    await dio.delete('/api/v1/tasks/$id');
  }
}

// 与 §5 同一份 Task 模型(实际项目里放 models/task.dart,这里合并展示)
class Task {
  final int id;
  String title;
  String note;
  bool done;
  Task({required this.id, required this.title, this.note = '', this.done = false});
}

6.4 把 §5 的 Notifier 换成"真接后端"

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'api.dart';

// 异步任务列表:用 FutureProvider 直接包 dio 调用
final tasksProvider = FutureProvider<List<Task>>((ref) async {
  return Api.instance.listTasks();   // GET /api/v1/tasks
});

// UI 里消费 AsyncValue:loading / error / data 三态一个都不漏
// ref.watch(tasksProvider).when(
//   loading: () => const CircularProgressIndicator(),
//   error: (e, _) => Text('加载失败: $e'),
//   data: (list) => ListView(...),
// );

6.5 登录页与 401 切回登录

6.6 运行时怎么连到本机工作台

# Windows 桌面直接跑(默认 BASE_URL=http://localhost:8080,无需改动)
flutter run -d windows

# Android 模拟器跑:必须用 10.0.2.2 代替 localhost
flutter run -d emulator-5554 --dart-define=BASE_URL=http://10.0.2.2:8080

对照 12 Vue3:你在 Vue3 里用 axios + Pinia 写过的那套(拦截器塞 token、401 跳登录、统一信封解包),这里 dio + Riverpod 是一一对应的,概念平移即可,没有新范式。

07

打包:Windows exe 与 Android APK

开发用 flutter run 热重载;交付物用下面两条命令。iOS 一句话:需要 Mac + Xcode,本模块不展开。

7.1 Windows 桌面:出 exe

flutter build windows --release
# 产物在:build\windows\x64\release\
# 里面 workbench_app.exe 连同 data\ 目录一起拷贝即可运行(绿色版)

前提:§2.2 的 VS2022"使用 C++ 的桌面开发"工作负载已装。无需签名就能自己用/内网分发;要上微软商店再另做 MSIX。

7.2 Android:出 APK(gradle + 签名简述)

  1. 首次打 release 包前要签名:用 JDK 自带 keytool 生成 keystore(一个命令,密码自己记牢,丢了就永远没法更新这个 APP):
keytool -genkey -v -keystore workbench.jks -keyalg RSA -keysize 2048 -validity 10000 -alias workbench
  1. 在 android/ 下建 key.properties(不要提交进 git),再在 android/app/build.gradle.kts 的 signingConfigs 里读它——这是标准做法,具体 gradle 语法在你第一个真实打包任务时让 AI 按 Flutter 官方"Android 发布"文档生成,本模块不抄长配置。
  2. 构建:
flutter build apk --release
# 产物:build\app\outputs\flutter-apk\app-release.apk,拷到手机安装即可

7.3 iOS(一句话)

iOS 构建必须有一台 Mac(Xcode 是苹果独占),且需要 Apple Developer 账号签名才能装到真机。你当前是 Windows 机器,本模块不做 iOS;将来真要发 App Store 再借/买 Mac,按 Xcode Archive 流程走。

08

常见报错表(10 条,照单抓药)

卡住了先查这张表。报错原文、原因、解决一一对应;还不行就按 08 §6.3 把报错原文喂给 AI。

报错原文/现象原因解决
[!] Windows Desktop developement component not found. Visual Studio Studio is not installed… 缺 VS2022 C++ 工具链。 装 Visual Studio 2022 Community,勾选"使用 C++ 的桌面开发"工作负载(含 MSVC v143 + CMake + Windows SDK),重开终端再 flutter doctor。
[!] Android license status unknown. Run flutter doctor --android-licenses Android SDK 命令行工具装了但没接受协议。 运行 flutter doctor --android-licenses,全部按 y;还不行先在 Android Studio SDK Tools 里勾"Android SDK Command-line Tools (latest)"。
Gradle 下载卡住 / Could not GET … dl.google.com … timeout 国内网络访问 Google Maven 慢。 编辑 android/build.gradle(或 settings.gradle),把 repositories 里的 google()/mavenCentral() 换成阿里云镜像前缀 maven { url 'https://maven.aliyun.com/repository/google' } 等,再 flutter clean 后重跑。
pub get 长时间不动 / TlsException: Handshake error 没配国内 pub 镜像。 设用户环境变量 PUB_HOSTED_URL=https://pub.flutter-io.cn 与 FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn,重开终端。
Error: The non-nullable local/var … must be assigned before it can be used. Dart 空安全:非空变量没初始化就用。 声明处给初值;State 里延迟初始化的字段加 late;可空的类型加 ?。
type 'String' is not a subtype of type 'int' of 'index' 或 …'double' is not a subtype of 'int' JSON 转换想当然:id 后端返回数字直接 as int,或拿字符串当下标。 数字一律先 (j['id'] as num).toInt();字符串/枚举按真实类型解析。参考 §6.3 taskFromJson。
Connection refused / dio DioException [connectionTimeout] 工作台没起;或 Android 上用了 localhost。 先确认 curl http://localhost:8080/healthz 通;Android 模拟器改 --dart-define=BASE_URL=http://10.0.2.2:8080;真机用电脑局域网 IP(如 192.168.x.x)并关防火墙。
Android 上 dio 请求直接失败,日志里 cleartext HTTP traffic not permitted Android 9+ 默认禁止明文 HTTP。 开发期在 android/app/src/main/AndroidManifest.xml 的 application 标签加 android:usesCleartextTraffic="true";上线前必须改 HTTPS。
flutter build windows 报缺 cmake / MSVC / 链接器错误 VS2022 装了但没勾 C++ CMake 工具或 Windows SDK。 Visual Studio Installer → 修改 → "使用 C++ 的桌面开发"→ 右侧勾选"适用于 Windows 的 C++ CMake 工具"与最新 Windows 10/11 SDK → 修复安装。
[!] Android toolchain - Unable to locate Android SDK 装了 Android Studio 但 Flutter 不知道 SDK 在哪。 flutter config --android-sdk "C:\Users\<你>\AppData\Local\Android\Sdk",再 flutter doctor --android-licenses。暂时不做 Android 可忽略此项。
09

验收清单

本模块只认一个验收:APP 真的登录了你的工作台,并能操作任务列表。复制下面清单逐条打勾。

#验收项通过标准
1工作台后端在本机跑通:curl http://localhost:8080/healthz 返回 ok。启动本模块的前置条件成立
2flutter --version 输出 3.47.6 / Dart 3.13.5;flutter doctor 里 Flutter 与 Windows 桌面两项绿。环境达标
3新建 workbench_app,pubspec 锁定 flutter_riverpod ^3.3.1 + dio ^5.11.1,flutter pub get 通过,pubspec.lock 已生成。依赖冻结
4登录页输入工作台账号密码,调用 POST /api/v1/auth/login 成功,拿到 token 并被拦截器自动塞到后续请求头。JWT 链路通
5任务列表页 GET /api/v1/tasks 渲染出后端真实数据(不是 §5 的假本地数组)。读通
6能新建任务(POST)、勾选完成(PUT)、删除任务(DELETE),操作后列表刷新与工作台 Web 端一致。增删改查通
7人为让 token 失效(或后端重启改 JWT_SECRET),APP 下次请求收到 401,自动清 token 并切回登录页。401 处理通
8同一份代码在 Windows 桌面窗口跑通;若已补 Android 环境,在 Android 模拟器也跑通(用 10.0.2.2)。跨端成立
9flutter build windows --release 出绿色版 exe;(可选)flutter build apk --release 出 APK。打包通

全部打勾 = 本模块结业:你交付了一个真正接工作台的跨端 APP,而不是一个 demo。把这套 dio 拦截器 + Riverpod 任务列表骨架 fork 进你的 assets/scaffolds/(见 08 §5),下一个 APP 项目直接复用——这就是四件套里"架子"的卫星版。

10

版本与来源表

本模块所有版本号与端点均来自 versions.md 与 workbench-contract;Dart 包版本由本模块在 pub.dev 现场核验。

组件锁定版本(2026-10-14 核验)来源
Flutter SDKstable 3.47.6(2026-10-02)versions.md A 节;https://docs.flutter.dev/install/archive
Dart3.13.5(随 Flutter,不单独装)同上
dio5.11.1https://pub.dev/packages/dio
flutter_riverpod3.3.1https://pub.dev/packages/flutter_riverpod
provider(备选)6.1.5+1https://pub.dev/packages/provider
go_router(页面变多再上)18.0.2https://pub.dev/packages/go_router
工作台端点login / tasks CRUD,Bearer JWT,统一信封workbench-contract.md §4(全系列唯一契约)

来源表

Flutter stable 3.47.6 归档与发布说明https://docs.flutter.dev/install/archive · https://docs.flutter.dev/release/release-notes
dio 5.11.1(2026-10 pub.dev latest)https://pub.dev/packages/dio
flutter_riverpod 3.3.1(2026-10 pub.dev latest)https://pub.dev/packages/flutter_riverpod
provider 6.1.5+1(备选方案版本核验)https://pub.dev/packages/provider
go_router 18.0.2(官方维护,feature-complete)https://pub.dev/packages/go_router
Flutter Windows 桌面构建需 VS C++ 工作负载;Android 模拟器访问本机用 10.0.2.2;国内镜像 storage.flutter-io.cn / pub.flutter-io.cnhttps://docs.flutter.dev/platform-integration/windows/building · https://docs.flutter.dev/development/data-and-backend/networking · https://docs.flutter.dev/community/china
工作台 API 契约(端点/信封/JWT/Task 模型)workbench-contract.md(本系列内部规范 v1.0)

(编者整理:本模块的"两周路线、选型规则、报错表、验收清单"为基于 07 报告与 08 总纲路径 C 的执行化设计,无外部独立来源。)