# Flutter 应用怎样管理页面和异步状态

原文: https://yishto.com/learn/application-platforms/flutter-reading-library/

作者: [Yishto](https://yishto.com/about/)

以教学资料库解释Flutter组件身份、页面导航和异步请求顺序，建立本地仓库并处理旧响应、失败和页面销毁。

首次发布: 2026-11-10
内容更新: 2026-11-10

教学资料库有分类列表、搜索结果和正文页。用户输入新关键词时，前一个请求可能还没结束；后发请求先完成，旧结果又晚到，列表就会倒退。本课用这个问题解释 Flutter 的组件状态与异步边界。

资料采用合成 ID，网络层先返回测试数据，最终再换为受控服务。第一版只支持列表、详情、保存本地副本与重试，暂不加入跨设备账户和复杂推荐。

## Widget 描述与资料身份

Widget 是界面的不可变描述。Flutter 的 element 保存树中的身份与关联，render object 参与布局和绘制。重建 Widget 不等于每个像素都重新绘制；是否需要布局或绘制由具体变化决定。不能把重建次数直接当作性能结论。

StatefulWidget 的 State 有自己的生命周期，资料记录不应依赖某次组件实例存活。列表条目以资料 ID 作为逻辑身份；标题和位置都可能变化。需要 key 时选择能表达这个身份的值，避免用随机值让框架无法保留应保留的状态。

界面状态包括加载、空结果、成功和失败。已存在的列表正在刷新时，可以保留旧内容并显示刷新提示，不必清空整页。选中资料由 ID 表示，正文读取失败只影响详情，不让其他已保存资料消失。

## 仓库与异步对象的职责

Dart 的 Future 表达以后完成的一次结果或错误，Stream 表达连续事件。网络请求通常适合 Future，资料库的持续变化可以作为事件流表达。两者都不会自动把 CPU 密集计算并行化；大量解析需要评估 isolate 等机制的使用条件。

仓库提供读取列表、读取正文、保存副本和重试操作。它维护持久数据与请求信息，页面发送意图并展示状态。不要让两个页面分别缓存可修改的同一条资料，否则一边删除后另一边仍可能把它显示为可离线。

取消、错误与完成都要经过同一个状态提交规则。请求失败应保留可识别的错误，不把异常变成空列表；空列表表示查询成功但无结果。页面关闭后不能再随意更新已销毁的 State，既要处理生命周期，也要判断结果是否仍属于当前请求。

## 建立可以运行的 Flutter 工程

安装与目标平台兼容的 Flutter 工具链，在终端先执行 `flutter doctor` 查看缺少的依赖，再用 `flutter create reading_library` 建立工程。记录 SDK 与目标设备信息，运行 `flutter run` 确认模板能启动。这些是供读者执行的工程步骤，本文没有声称已运行该应用。

将代码按资料模型、仓库、列表页和详情页分开。先放两份文本资源，通过工程资源配置加入应用，仓库读取合成目录并返回列表。实现列表选择、详情正文与返回路径，导航只传资料 ID，详情自己根据 ID 读取当前记录。

接着选择目标平台支持的本地持久化方案，记录插件与原生配置。保存副本后写入索引，重启从索引恢复。先验证资源读取与持久化，再接异步搜索。不要把 Web 上的路径或桌面目录规则直接搬到移动端；沙盒、文件选择和权限需要适配器处理。

## 晚到结果怎样被丢弃

每次查询递增请求序号，结果返回时仅允许最新序号提交。关键词变化也可以加入防抖，但防抖只减少请求数量，不解决所有响应乱序。真正的网络取消可以节省资源，仍需要结果校验，因为取消可能发生在响应已产生之后。

下面离线 Python 例子模拟请求 2 已经成为当前请求，请求 1 的结果先被拒绝，请求 2 被接受。它不包含 Dart Future，也没有取消 HTTP 请求或运行 Flutter。

```python
latest_request = 2
library = []
def finish(request_id, items):
    global library
    if request_id != latest_request:
        return False
    library = list(items)
    return True
assert not finish(1, ["old"])
assert finish(2, ["new"])
assert library == ["new"]
print(library)
```

输出 `['new']`。被拒绝的旧响应没有修改 `library`，断言验证最终列表。真实应用还要让请求错误与加载结束遵守相同序号规则，否则旧请求的异常可能把新请求成功后的页面改为失败。

页面销毁后判断是否仍可提交 UI 更新；仓库可以继续完成它拥有的保存任务。`mounted` 检查解决生命周期的一部分，不能替代最新请求判断。页面仍挂载时，同样可能已经切换到另一个关键词。

## 本地状态与平台适配

本地索引至少保存 ID、标题、内容版本与副本位置。只有完整写入并可读取的副本才标为可离线。把目录加载和正文读取分开，让损坏的一篇资料不必阻止整个资料库打开。

网络失败时允许用户打开已保存内容，未保存资料显示离线不可用。下载失败保留已有版本，记录失败原因和重试入口。用户删除本地副本后更新仓库，再让页面派生新的状态，避免只移除文件却保留可离线标签。

桌面、移动和浏览器的窗口尺寸、返回操作、键盘和文件能力不同。共享 Widget 树可以复用布局，但要在目标环境检查长标题、文字缩放、键盘遮挡和返回路径。插件支持某个平台也不代表无需配置系统权限或构建设置。

正文较长时还要检查列表和阅读区域的滚动职责。目录页按可见需求构建条目，详情保持可恢复的阅读位置，不把正文拆成每次刷新都改变身份的随机组件。搜索失败后保留输入关键词，用户可以改词或重试，不能要求重新进入页面。

适配器给资料排序定义稳定规则，同标题资料按ID等确定字段区分。分页响应需要查询身份和页标识，刷新后旧页不应追加进新列表。测试最后一页、空页和重试同一页，重复资料只保留一次；这套分页协议需要服务端与客户端共同定义，不能由Widget自行猜测。

## 练习：验证请求与页面恢复

为搜索仓库加入可控延迟，让旧请求晚于新请求完成，再加入旧请求失败的情况。快速输入不同关键词后，最终只显示当前关键词的结果和错误，加载提示不会被旧事件提前关闭。

保存两份正文，退出后断网重开，应能读到两份。删除其中一份副本，其列表项应保留明确的不可离线状态。退出详情时延迟请求继续返回，应用不出现销毁后更新错误。记录这些实际观察值，再扩展分类和分页；小模型只能验证此次请求顺序契约。

## 资料来源与延伸阅读

- [Flutter architectural overview](https://docs.flutter.dev/resources/architectural-overview)
- [Asynchronous programming in Dart](https://dart.dev/libraries/async/async-await)

资料核验：2026-11-10。教学示例不代表线上实测。

## 相关文章

- [页面、路由和状态应该放在哪里](https://yishto.com/learn/web-interface/state-routing-and-frontend-architecture/)
- [JavaScript 为什么会先显示加载状态](https://yishto.com/learn/web-interface/javascript-runtime-and-async/)
