
纯展示型页面的设计
设置页面在整个应用中是一个独特的页面类型------它不需要自己的状态管理,不需要异步加载数据,不需要处理错误状态。它的全部数据来自已有的 Provider 和服务实例,进入页面时所有信息已经可用。
这种"纯消费"模式是良好架构的自然结果------不是每个页面都需要自己的 Provider。当页面只需要展示已有信息时,直接从现有的 Provider 读取是最简化的做法。

dart
class SettingsScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final authProvider = context.watch<AuthProvider>();
final apiClient = context.read<AtomGitApiClient>();
final isLoggedIn = authProvider.isLoggedIn;
return Scaffold(
appBar: AppBar(title: const Text('设置')),
body: ListView(
children: [
_SectionHeader(title: '账户'),
_buildAccountSection(context, isLoggedIn, authProvider),
const Divider(),
_SectionHeader(title: 'API 信息'),
_buildApiInfoSection(apiClient),
const Divider(),
_SectionHeader(title: '关于'),
_buildAboutSection(context),
],
),
);
}
}
页面结构:AppBar + ListView 包含三个分区,每个分区有标题和内容。ListView 而非 Column 的选择是考虑内容可能超出屏幕时的滚动体验。
context.watch vs context.read
两个方法在设置页同时使用,体现了 Provider 消费模式的核心区别:
context.watch<AuthProvider>():建立订阅。AuthProvider 的notifyListeners()会触发 SettingsScreen 重建。登录/登出时 UI 自动更新("已登录"变"未登录",按钮从"退出登录"变"登录")。context.read<AtomGitApiClient>():一次性读取。ApiClient 是服务对象,不变,不需要订阅。
如果对 AuthProvider 也使用 read,用户在设置页点击"退出登录"后 UI 不会更新------虽然 isLoggedIn 已经变为 false,但由于没有订阅,Widget 不会重建,界面上仍然显示"已登录"。
账户区域
账户区域展示登录状态和操作入口:
dart
Widget _buildAccountSection(
BuildContext context,
bool isLoggedIn,
AuthProvider auth,
) {
return ListTile(
leading: Icon(
isLoggedIn ? Icons.check_circle : Icons.account_circle,
color: isLoggedIn ? Colors.green : null,
),
title: Text('登录状态'),
subtitle: Text(isLoggedIn ? '已登录' : '未登录'),
trailing: isLoggedIn
? TextButton(
onPressed: () => _showLogoutDialog(context, auth),
child: const Text('退出登录'),
)
: TextButton(
onPressed: () =>
Navigator.pushNamed(context, '/login'),
child: const Text('登录'),
),
);
}
UI 细节:
- 图标根据状态切换:已登录显示绿色
check_circle,未登录显示默认色account_circle - 副标题直接显示"已登录"/"未登录"文字
trailing位置的按钮也根据状态切换文本和功能
退出确认对话框
退出是破坏性操作。直接退出会清除 Token、触发全应用 UI 刷新。所以退出前展示确认对话框是必要的:
dart
void _showLogoutDialog(BuildContext context, AuthProvider auth) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('退出登录'),
content: const Text('确定要退出登录吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: const Text('取消'),
),
TextButton(
onPressed: () {
auth.logout();
Navigator.pop(ctx);
},
child: const Text('确定'),
),
],
),
);
}
showDialog 返回一个 Future,resolve 值为 Navigator.pop(ctx) 传入的参数。当前实现不关心对话框结果(不 await),因为退出登录后整个应用状态会刷新。
对话框的 context(ctx)是对话框自己的 context,与 SettingsScreen 的 context 不同。Navigator.pop(ctx) 关闭的是对话框,不是设置页面。
退出登录的数据流
退出操作在 AlertDialog 的"确定"按钮中触发:
auth.logout()
→ _accessToken = null(清除内存中的 Token)
→ _isLoggedIn = false(更新登录标志)
→ _apiClient.setAccessToken(null)(清除 API 客户端认证)
→ LocalStorage.instance.delete('access_token')(删除本地持久化文件)
→ notifyListeners()(通知所有监听者)
这个调用链的完整性至关重要。遗漏任何一步都会导致状态不一致:
- 遗漏
_apiClient.setAccessToken(null):UI 显示未登录,但 API 请求仍携带旧 Token(造成混淆) - 遗漏
LocalStorage.delete:下次启动时tryRestoreSession会恢复已失效的 Token - 遗漏
notifyListeners():UI 不更新,用户看到"已登录"但实际已登出
API 信息区域
这个区域为开发者和高级用户提供 API 相关的诊断信息:
dart
Widget _buildApiInfoSection(AtomGitApiClient apiClient) {
return Column(children: [
ListTile(
leading: const Icon(Icons.link),
title: const Text('API 地址'),
subtitle: const Text(ApiConstants.baseUrl),
),
ListTile(
leading: const Icon(Icons.info_outline),
title: const Text('API 版本'),
subtitle: const Text('2023-02-21'),
),
ListTile(
leading: const Icon(Icons.speed),
title: const Text('频率限制'),
subtitle: Text(
'${apiClient.rateLimitRemaining} / '
'${ApiConstants.rateLimitAuthenticated} 次/小时',
),
),
]);
}
频率限制信息的展示是诊断性的。AtomGit API 对认证用户提供每小时 5000 次调用限制。用户可以通过这里看到当前剩余配额,了解是否接近限流。
apiClient.rateLimitRemaining 的值在每次 API 调用后自动更新(从响应 Header x-ratelimit-remaining 读取)。如果这个数字在快速减少,说明应用可能在短时间内发起了大量请求。
关于区域
dart
Widget _buildAboutSection(BuildContext context) {
return Column(children: [
ListTile(
leading: const Icon(Icons.apps),
title: const Text('应用名称'),
subtitle: const Text('AtomGit'),
),
ListTile(
leading: const Icon(Icons.build),
title: const Text('技术栈'),
subtitle: const Text('Flutter + HarmonyOS'),
),
ListTile(
leading: const Icon(Icons.info_outline),
title: const Text('版本'),
subtitle: const Text('1.0.0'),
),
]);
}
关于区域的信息目前是硬编码的。对于生产应用,版本号可以从 pubspec.yaml 或环境变量动态获取。
分区标题组件
dart
class _SectionHeader extends StatelessWidget {
final String title;
const _SectionHeader({required this.title});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 24, 16, 8),
child: Text(
title,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: Theme.of(context).colorScheme.primary,
),
),
);
}
}
一个简单的标题组件,负责统一的样式管理。被多处复用(账户、API 信息、关于),避免在每个区域重复写 Padding + TextStyle。
EdgeInsets.fromLTRB(16, 24, 16, 8) 各方向的设计含义:
- 16px 左:与 ListTile 的标准左边距对齐
- 24px 上:与上一个区域拉开距离,形成视觉分组
- 16px 右:对称对齐
- 8px 下:标题与内容之间的紧凑间距
为什么不需要自己的 Provider
设置页面的特点决定了它不需要独立的状态管理层:
-
数据均来自现有源 。登录状态来自
AuthProvider(全局),API 信息来自AtomGitApiClient实例(全局),版本和名称来自常量。 -
无异步操作需要追踪。设置页没有"加载中"、"加载失败"、"重试"等异步状态。所有信息在进入页面时即可展示。
-
数据变化来自外部 。当用户登录/登出时,
AuthProvider的notifyListeners自动触发设置页重建,不需要设置页自己管理刷新。 -
无分页/增量加载 。设置页的数据量固定,不需要
hasMore、page等分页状态。
这些特点让设置页成为 Provider 架构中的"叶子消费者"------它只消费状态,不生产或管理状态。这种模式的代码量最少,也最容易理解。
退出登录的连锁反应
设置页面只是退出操作的触发器,实际的状态清理和 UI 更新由 AuthProvider.logout() 驱动:
AuthProvider.logout()
→ _accessToken = null
→ notifyListeners()
所有监听了 AuthProvider 的 Widget 重建:
MainShell
└── Tab 状态不变(只是容器)
HomeTab(context.watch<AuthProvider>)
→ isLoggedIn = false
→ 从"我的仓库+热门仓库"切换到"欢迎页+搜索框"
ExploreTab
→ isLoggedIn = false
→ 显示登录引导
NotificationsTab(context.watch<AuthProvider>)
→ isLoggedIn = false
→ 从"占位"切换到"登录引导"
ProfileTab(context.watch<AuthProvider>)
→ isLoggedIn = false
→ didChangeDependencies 检测到登录状态变化
→ dispose UserProvider
→ 切换到"登录引导"
SettingsScreen(context.watch<AuthProvider>)
→ isLoggedIn = false
→ "已登录"变"未登录"
→ 按钮从"退出登录"变"登录"
整个链条是自动的------设置页不需要知道哪些页面需要更新,Provider 的广播机制自动完成状态传播。
扩展:添加设置项
当前设置页面是功能最少的页面,但架构预留了扩展空间。添加新的设置项只需在 ListView 中插入新的 ListTile:
dart
// 添加主题切换(示例)
_SectionHeader(title: '外观'),
ListTile(
leading: const Icon(Icons.palette),
title: const Text('主题'),
subtitle: const Text('跟随系统'),
trailing: const Icon(Icons.chevron_right),
onTap: () {
// 打开主题选择器
},
),
const Divider(),
// 添加缓存管理(示例)
_SectionHeader(title: '存储'),
ListTile(
leading: const Icon(Icons.storage),
title: const Text('清除缓存'),
subtitle: Text('当前缓存: $_cacheSize'),
onTap: () => _clearCache(),
),
如果设置项数据需要持久化(如主题偏好),可以借助 LocalStorage:
dart
// 读取偏好
final theme = await LocalStorage.instance.read<String>('pref_theme');
// 保存偏好
await LocalStorage.instance.write('pref_theme', 'dark');
设置页在导航体系中的位置
设置页面是全屏路由(/settings),覆盖底部 Tab 栏。这与大多数应用的设计一致------设置是独立页面,不是 Tab 的一部分。用户在设置中完成操作后通过返回按钮(或 AppBar 的 back 箭头)回到之前的页面。