如何在Flutter Web应用中实现加载指示器并封装通用模块?
通用加载状态管理与API调用封装方案(Flutter Web)
核心方案
通过封装加载状态管理器和通用API调用工具,统一处理加载状态、请求流程与错误捕获,彻底避免在页面、容器、对话框中重复编写加载逻辑。
实现步骤
1. 轻量加载状态管理器
用ValueNotifier实现极简的状态管理,支持全局或局部加载状态控制:
class LoadingManager { final ValueNotifier<bool> _globalLoading = ValueNotifier(false); // 支持局部加载:用键值对管理不同组件的加载状态 final Map<String, ValueNotifier<bool>> _localLoadings = {}; ValueNotifier<bool> get globalLoading => _globalLoading; // 获取局部加载状态(不存在则创建) ValueNotifier<bool> getLocalLoading(String key) { return _localLoadings.putIfAbsent(key, () => ValueNotifier(false)); } void setGlobalLoading(bool value) => _globalLoading.value = value; void setLocalLoading(String key, bool value) => getLocalLoading(key).value = value; }
2. 通用API调用封装
结合加载状态管理,统一处理HTTP请求的加载、响应与错误:
import 'package:http/http.dart' as http; // 或用Dio,这里以原生http为例 class ApiClient { final LoadingManager loadingManager; final String baseUrl; ApiClient(this.loadingManager, this.baseUrl); // 全局加载的API调用 Future<http.Response> callGlobalApi( Future<http.Response> Function() request, ) async { try { loadingManager.setGlobalLoading(true); return await request(); } catch (e) { // 全局错误捕获:比如打印日志、统一弹窗提示 rethrow; // 抛出错误让业务层处理具体逻辑 } finally { loadingManager.setGlobalLoading(false); } } // 局部加载的API调用(指定key) Future<http.Response> callLocalApi( String loadingKey, Future<http.Response> Function() request, ) async { try { loadingManager.setLocalLoading(loadingKey, true); return await request(); } catch (e) { rethrow; } finally { loadingManager.setLocalLoading(loadingKey, false); } } }
3. 全局注入(可选)
用Provider或GetIt实现全局依赖注入,方便所有组件直接使用:
// main.dart初始化 void main() { final loadingManager = LoadingManager(); final apiClient = ApiClient(loadingManager, 'https://your-backend-api.com'); runApp( Provider<ApiClient>.value( value: apiClient, child: Provider<LoadingManager>.value( value: loadingManager, child: const MyApp(), ), ), ); }
4. 组件使用示例
页面/容器中用全局加载
class DataPage extends StatelessWidget { const DataPage({super.key}); @override Widget build(BuildContext context) { final apiClient = Provider.of<ApiClient>(context); final loadingManager = Provider.of<LoadingManager>(context); return Scaffold( body: Stack( children: [ Center( child: ElevatedButton( onPressed: () async { try { final response = await apiClient.callGlobalApi( () => http.get(Uri.parse('${apiClient.baseUrl}/data')), ); // 处理成功响应 } catch (e) { // 处理业务错误 } }, child: const Text('获取数据'), ), ), // 全局加载遮罩 ValueListenableBuilder<bool>( valueListenable: loadingManager.globalLoading, builder: (_, isLoading, __) { return isLoading ? Container( color: Colors.black54, child: const Center(child: CircularProgressIndicator()), ) : const SizedBox.shrink(); }, ), ], ), ); } }
对话框中用局部加载
void showSubmitDialog(BuildContext context) { const loadingKey = 'submit_dialog'; final apiClient = Provider.of<ApiClient>(context, listen: false); final loadingManager = Provider.of<LoadingManager>(context, listen: false); showDialog( context: context, builder: (context) { return AlertDialog( title: const Text('提交数据'), content: ValueListenableBuilder<bool>( valueListenable: loadingManager.getLocalLoading(loadingKey), builder: (_, isLoading, __) { return isLoading ? const Center(child: CircularProgressIndicator()) : const Text('确认提交吗?'); }, ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('取消'), ), TextButton( onPressed: () async { try { await apiClient.callLocalApi( loadingKey, () => http.post( Uri.parse('${apiClient.baseUrl}/submit'), body: {'content': 'test'}, ), ); Navigator.pop(context); // 提示提交成功 } catch (e) { // 提示提交失败 } }, child: const Text('确认'), ), ], ); }, ); }
额外优化建议
- 封装通用
LoadingOverlay组件,直接替换示例中的加载UI代码,进一步减少重复。 - 在
ApiClient中添加响应解析逻辑,比如自动将JSON转为实体类,避免业务层重复解析。 - 局部加载的key可以用组件的
hashCode或唯一标识,避免冲突。
内容的提问来源于stack exchange,提问作者Tony Lam
相关产品推荐
相关产品推荐

