YAOTU INSIGHTS

Flutter集成Dio开发鸿蒙美食列表应用实战

Flutter集成Dio开发鸿蒙美食列表应用实战
1. 项目概述今天要分享的是开源鸿蒙跨平台开发训练营第三天的实战内容——如何在Flutter中集成Dio网络请求库并开发一个本地美食数据清单列表功能。这个项目看似简单但涉及了现代移动开发的几个核心环节网络请求处理、数据解析、UI渲染和状态管理。作为一名经历过多个跨平台项目的开发者我发现很多新手在集成网络请求时容易陷入几个典型误区要么过度封装导致灵活性丧失要么裸写请求缺乏统一管理。而Dio恰好提供了一个平衡点——功能强大又不失简洁。在鸿蒙生态中使用Flutter开发时这种轻量级方案尤为合适。2. 环境准备与项目初始化2.1 Flutter环境确认首先确保你的开发环境已经正确配置。在终端运行flutter doctor这个命令会检查所有依赖项的状态。特别要注意的是对于鸿蒙平台开发需要额外确认OHOS工具链是否正常。常见问题包括Android Studio的鸿蒙插件未安装OHOS SDK路径未正确配置设备调试权限未开启2.2 创建Flutter项目使用以下命令创建新项目flutter create foodie_list --platformsandroid,ios,harmony关键参数说明--platforms指定目标平台添加harmony表示同时支持鸿蒙项目结构中的lib/harmony_main.dart是鸿蒙专属入口文件提示如果创建时没有harmony选项需要先安装Flutter的鸿蒙支持插件flutter pub global activate flutter_harmony3. Dio网络库集成3.1 添加依赖在pubspec.yaml中添加dependencies: dio: ^5.0.0 logger: ^1.1.0 # 用于请求日志执行flutter pub get安装依赖。这里选择Dio 5.x版本是因为完全支持空安全拦截器机制更完善对鸿蒙平台的兼容性更好3.2 基础网络服务封装创建lib/services/network_service.dartimport package:dio/dio.dart; import package:logger/logger.dart; class NetworkService { final Dio _dio Dio(); final Logger _logger Logger(); NetworkService() { _dio.options.baseUrl https://your-api-endpoint.com; _dio.options.connectTimeout Duration(seconds: 5); _dio.options.receiveTimeout Duration(seconds: 10); // 添加日志拦截器 _dio.interceptors.add(InterceptorsWrapper( onRequest: (options, handler) { _logger.d(Request: ${options.method} ${options.path}); return handler.next(options); }, onResponse: (response, handler) { _logger.i(Response: ${response.statusCode}); return handler.next(response); }, onError: (DioException e, handler) { _logger.e(Error: ${e.message}); return handler.next(e); }, )); } FutureResponse get(String path, {MapString, dynamic? params}) async { try { return await _dio.get(path, queryParameters: params); } on DioException catch (e) { _logger.e(GET请求失败: ${e.message}); rethrow; } } }关键点解析超时设置移动端网络环境复杂合理的超时设置能避免UI卡死日志拦截器开发阶段必备的调试工具异常处理统一捕获Dio异常便于上层处理4. 美食数据模型设计4.1 数据结构分析假设API返回的JSON格式如下{ data: [ { id: 1, name: 红烧肉, price: 38.0, rating: 4.5, image: https://example.com/food1.jpg, tags: [家常菜, 下饭菜] } ] }4.2 模型类实现创建lib/models/food_item.dartclass FoodItem { final int id; final String name; final double price; final double rating; final String image; final ListString tags; FoodItem({ required this.id, required this.name, required this.price, required this.rating, required this.image, required this.tags, }); factory FoodItem.fromJson(MapString, dynamic json) { return FoodItem( id: json[id], name: json[name], price: json[price].toDouble(), rating: json[rating].toDouble(), image: json[image], tags: ListString.from(json[tags]), ); } MapString, dynamic toJson() { id: id, name: name, price: price, rating: rating, image: image, tags: tags, }; }注意实际开发中建议使用json_serializable或freezed来自动生成序列化代码减少手写错误。5. 数据获取与状态管理5.1 数据仓库实现创建lib/repositories/food_repository.dartimport ../models/food_item.dart; import ../services/network_service.dart; class FoodRepository { final NetworkService _network; FoodRepository(this._network); FutureListFoodItem fetchFoodList() async { final response await _network.get(/foods); final Listdynamic data response.data[data]; return data.map((json) FoodItem.fromJson(json)).toList(); } }5.2 状态管理方案选择对于简单的列表场景使用Provider足够轻量import package:flutter/material.dart; import ../repositories/food_repository.dart; class FoodListProvider extends ChangeNotifier { final FoodRepository _repository; ListFoodItem _foods []; bool _isLoading false; String? _error; FoodListProvider(this._repository); ListFoodItem get foods _foods; bool get isLoading _isLoading; String? get error _error; Futurevoid loadFoods() async { _isLoading true; notifyListeners(); try { _foods await _repository.fetchFoodList(); _error null; } catch (e) { _error e.toString(); } finally { _isLoading false; notifyListeners(); } } }6. UI界面实现6.1 列表项组件创建lib/widgets/food_card.dartimport package:flutter/material.dart; import ../models/food_item.dart; class FoodCard extends StatelessWidget { final FoodItem food; const FoodCard({super.key, required this.food}); override Widget build(BuildContext context) { return Card( elevation: 2, child: Padding( padding: const EdgeInsets.all(12), child: Row( children: [ ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.network( food.image, width: 80, height: 80, fit: BoxFit.cover, ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( food.name, style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 4), Text( ¥${food.price.toStringAsFixed(1)}, style: TextStyle( color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 4), Wrap( spacing: 4, children: food.tags .map((tag) Chip( label: Text(tag), visualDensity: VisualDensity.compact, )) .toList(), ), ], ), ), Column( children: [ const Icon(Icons.star, color: Colors.amber), Text(food.rating.toStringAsFixed(1)), ], ), ], ), ), ); } }6.2 列表页面集成修改lib/main.dartimport package:flutter/material.dart; import package:provider/provider.dart; import models/food_item.dart; import repositories/food_repository.dart; import services/network_service.dart; import widgets/food_card.dart; void main() { runApp( MultiProvider( providers: [ Provider(create: (_) NetworkService()), Provider(create: (context) FoodRepository(context.readNetworkService())), ChangeNotifierProxyProviderFoodRepository, FoodListProvider( create: (_) FoodListProvider(null!), update: (_, repo, __) FoodListProvider(repo), ), ], child: const MyApp(), ), ); } class MyApp extends StatelessWidget { const MyApp({super.key}); override Widget build(BuildContext context) { return MaterialApp( title: 美食清单, theme: ThemeData( primarySwatch: Colors.blue, useMaterial3: true, ), home: const FoodListScreen(), ); } } class FoodListScreen extends StatefulWidget { const FoodListScreen({super.key}); override StateFoodListScreen createState() _FoodListScreenState(); } class _FoodListScreenState extends StateFoodListScreen { override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { context.readFoodListProvider().loadFoods(); }); } override Widget build(BuildContext context) { final provider context.watchFoodListProvider(); return Scaffold( appBar: AppBar( title: const Text(本地美食), actions: [ IconButton( icon: const Icon(Icons.refresh), onPressed: provider.isLoading ? null : () provider.loadFoods(), ), ], ), body: Builder( builder: (context) { if (provider.isLoading provider.foods.isEmpty) { return const Center(child: CircularProgressIndicator()); } if (provider.error ! null) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text(provider.error!), const SizedBox(height: 16), ElevatedButton( onPressed: () provider.loadFoods(), child: const Text(重试), ), ], ), ); } return ListView.builder( padding: const EdgeInsets.all(8), itemCount: provider.foods.length, itemBuilder: (_, index) FoodCard(food: provider.foods[index]), ); }, ), ); } }7. 鸿蒙平台适配要点7.1 网络权限配置在resources/config.json中添加{ module: { reqPermissions: [ { name: ohos.permission.INTERNET } ] } }7.2 鸿蒙平台差异处理修改网络服务以处理鸿蒙特有行为// 在NetworkService构造函数中添加 if (Platform.isHarmony) { _dio.options.headers[User-Agent] HarmonyOS; // 鸿蒙平台可能需要特殊证书处理 _dio.httpClientAdapter _getHarmonyAdapter(); } HttpClientAdapter _getHarmonyAdapter() { // 实际项目中需要根据鸿蒙版本配置适配器 return DefaultHttpClientAdapter(); }8. 性能优化与调试技巧8.1 图片加载优化使用cached_network_image替代原生Image.networkdependencies: cached_network_image: ^3.2.0修改FoodCard中的图片加载CachedNetworkImage( imageUrl: food.image, width: 80, height: 80, fit: BoxFit.cover, placeholder: (_, __) Container(color: Colors.grey[200]), errorWidget: (_, __, ___) const Icon(Icons.fastfood), )8.2 请求缓存策略添加Dio缓存拦截器import package:dio_http_cache/dio_http_cache.dart; // 在NetworkService中 _dio.interceptors.add(DioCacheManager( CacheConfig(baseUrl: https://your-api-endpoint.com) ).interceptor);使用时指定缓存策略return await _dio.get( path, queryParameters: params, options: buildCacheOptions( Duration(days: 1), // 缓存1天 forceRefresh: false, ), );9. 常见问题排查9.1 鸿蒙平台网络请求失败可能原因未声明网络权限证书校验失败用户代理被服务器拦截解决方案检查config.json权限配置添加证书白名单(_dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate (client) { client.badCertificateCallback (cert, host, port) true; return client; };9.2 列表滚动卡顿优化方案使用ListView.builder的itemExtentListView.builder( itemExtent: 120, // 固定高度提升性能 // ... )对复杂卡片使用RepaintBoundaryRepaintBoundary( child: FoodCard(food: provider.foods[index]), )9.3 数据加载状态管理推荐使用AsyncValue简化状态处理class FoodListProvider extends ChangeNotifier { AsyncValueListFoodItem _foods const AsyncValue.loading(); Futurevoid loadFoods() async { _foods const AsyncValue.loading(); notifyListeners(); _foods await AsyncValue.guard(() _repository.fetchFoodList()); notifyListeners(); } }10. 项目扩展方向10.1 添加搜索过滤扩展FoodRepositoryFutureListFoodItem searchFoods(String keyword) async { final response await _network.get(/foods/search, params: {q: keyword}); // ...解析逻辑 }10.2 实现收藏功能使用shared_preferences持久化收藏状态import package:shared_preferences/shared_preferences.dart; class FavoriteService { static const _key favorites; Futurevoid addFavorite(int foodId) async { final prefs await SharedPreferences.getInstance(); final favorites prefs.getStringList(_key) ?? []; favorites.add(foodId.toString()); await prefs.setStringList(_key, favorites); } Futurebool isFavorite(int foodId) async { final prefs await SharedPreferences.getInstance(); final favorites prefs.getStringList(_key) ?? []; return favorites.contains(foodId.toString()); } }10.3 多平台样式适配使用Platform.isX判断平台Card( elevation: Platform.isIOS ? 0.5 : 2.0, shape: Platform.isAndroid ? RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)) : null, // ... )这个项目虽然基础但涵盖了现代移动开发的完整链路。在实际产品中还需要考虑更多细节请求重试机制分页加载离线缓存策略AB测试支持性能监控埋点FlutterDio的组合在鸿蒙平台表现良好经过适当适配后可以实现真正的跨平台代码复用。对于更复杂的场景可以考虑引入Riverpod或Bloc来管理状态使用Retrofit简化网络层但这些都属于进阶优化范畴了。