【Flutter for open harmony 】Flutter三方库网络请求的鸿蒙化适配与实战指南3

欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net

大家好,我是ShineQiu,上海某高校大二计科专业的学生。最近天气变冷了,身边很多同学都在说要多喝热水,但大家对"喝热水"的好处其实了解得并不全面。于是我就想做一个「健康饮水助手」APP,通过网络请求获取科学的饮水知识和每日饮水建议。本来在Android上开发得挺顺利,结果老师说要适配鸿蒙平台,这一适配可真是让我大开眼界…

先说说我遇到的三个"鸿蒙专属"坑

坑一:网络请求返回中文乱码!

报错信息:

Error: FormatException: Invalid UTF-8 bytes (at offset 23)

当时的心情:
我记得那天早上赶课之前想快速测试一下,结果APP一打开就崩溃了。看到这个错误我整个人都懵了——同样的接口在Android上返回的中文好好的,怎么到鸿蒙就乱码了?我赶紧翻出《计算机网络》课本查编码知识,突然意识到可能是Content-Type的问题!

解决步骤:
在响应头里强制指定UTF-8编码:

HttpClientResponse response = await request.close();
// 鸿蒙专属:强制UTF-8编码,解决中文乱码问题
response.headers.set('Content-Type', 'application/json; charset=utf-8');
String responseBody = await response.transform(utf8.decoder).join();

坑二:图片缓存导致OOM!

报错信息:

Error: OutOfMemoryError: Failed to allocate a 4194304 byte allocation with 262144 free bytes

当时的心情:
这个错误发生在我滑动浏览饮水知识列表的时候,滑着滑着APP突然闪退了。我一开始以为是图片太大,后来发现是鸿蒙的图片缓存机制和Android不一样——Android会自动清理内存,而鸿蒙需要手动管理!

解决步骤:
使用CachedNetworkImage并设置缓存策略:

CachedNetworkImage(
  imageUrl: waterTip.imageUrl,
  placeholder: (context, url) => const CircularProgressIndicator(),
  errorWidget: (context, url, error) => const Icon(Icons.broken_image),
  // 鸿蒙专属:限制图片缓存大小和内存占用
  cacheManager: CacheManager(
    Config(
      'water_tips_cache',
      stalePeriod: const Duration(days: 7),
      maxNrOfCacheObjects: 50,
    ),
  ),
),

坑三:后台请求被系统杀死!

报错信息:

Error: SocketException: Connection reset by peer

当时的心情:
这个问题最诡异!APP放在后台几分钟后再打开,正在进行的网络请求就会失败。我一开始以为是网络不稳定,后来发现是鸿蒙的后台管理策略比Android严格得多,后台应用的网络请求会被系统主动切断。

解决步骤:
使用WorkManager实现后台任务调度:

// 鸿蒙专属:使用WorkManager执行后台任务
void scheduleWaterReminder() {
  Workmanager().initialize(
    callbackDispatcher,
    isInDebugMode: true,
  );
  
  Workmanager().registerPeriodicTask(
    'water_reminder',
    'water_reminder_task',
    frequency: const Duration(hours: 1),
    initialDelay: const Duration(minutes: 30),
    constraints: Constraints(
      networkType: NetworkType.connected,
    ),
  );
}

功能背景:为什么做这个APP?

作为一个经常忘记喝水的大学生,我深深体会到缺水带来的困扰:上课犯困、皮肤干燥、便秘… 虽然大家都说"多喝热水",但很少有人知道具体喝多少、什么时候喝、喝什么温度的水最好。

所以我决定做一个「健康饮水助手」APP,主要功能包括:

  • 获取科学的饮水知识(网络请求)
  • 每日饮水提醒
  • 饮水记录统计
  • 个性化饮水建议

核心代码实现

1. 数据模型

// 饮水知识数据模型
class WaterKnowledge {
  final String id;           // 知识ID
  final String title;        // 标题
  final String content;      // 内容
  final String imageUrl;     // 配图URL
  final String category;     // 分类(如"饮水时间"、"水温选择"等)
  final int readCount;       // 阅读量

  WaterKnowledge({
    required this.id,
    required this.title,
    required this.content,
    required this.imageUrl,
    required this.category,
    required this.readCount,
  });

  // 从JSON解析
  factory WaterKnowledge.fromJson(Map<String, dynamic> json) {
    return WaterKnowledge(
      id: json['id'] ?? '',
      title: json['title'] ?? '',
      content: json['content'] ?? '',
      imageUrl: json['imageUrl'] ?? '',
      category: json['category'] ?? '其他',
      readCount: json['readCount'] ?? 0,
    );
  }
}

2. 网络请求服务

// 饮水知识API服务
class WaterApiService {
  static const String _baseUrl = 'https://api.health-water.com/v1';
  
  // 获取饮水知识列表
  static Future<List<WaterKnowledge>> fetchWaterKnowledgeList() async {
    try {
      // 鸿蒙专属:创建自定义HttpClient
      final httpClient = HttpClient()
        ..badCertificateCallback = ((cert, host, port) => true)
        ..connectionTimeout = const Duration(seconds: 10);
      
      final uri = Uri.parse('$_baseUrl/knowledge/list');
      final request = await httpClient.getUrl(uri);
      
      // 鸿蒙专属:添加请求头
      request.headers.add('Accept', 'application/json; charset=utf-8');
      request.headers.add('User-Agent', 'HealthWater/1.0 (HarmonyOS)');
      
      final response = await request.close();
      
      // 鸿蒙专属:强制UTF-8解码
      response.headers.set('Content-Type', 'application/json; charset=utf-8');
      final responseBody = await response.transform(utf8.decoder).join();
      
      final jsonData = json.decode(responseBody);
      final List<dynamic> data = jsonData['data'];
      
      return data.map((item) => WaterKnowledge.fromJson(item)).toList();
      
    } catch (e) {
      debugPrint('获取饮水知识失败: $e');
      throw Exception('获取饮水知识失败,请稍后重试');
    }
  }
}

3. 状态管理(使用Provider)

// 饮水知识状态管理
class WaterKnowledgeProvider extends ChangeNotifier {
  List<WaterKnowledge> _knowledgeList = [];
  bool _isLoading = false;
  String _errorMessage = '';
  String _selectedCategory = '全部';
  
  List<WaterKnowledge> get knowledgeList => _knowledgeList;
  bool get isLoading => _isLoading;
  String get errorMessage => _errorMessage;
  String get selectedCategory => _selectedCategory;
  
  // 加载饮水知识
  Future<void> loadKnowledge() async {
    _isLoading = true;
    _errorMessage = '';
    notifyListeners();
    
    try {
      _knowledgeList = await WaterApiService.fetchWaterKnowledgeList();
    } catch (e) {
      _errorMessage = e.toString();
      debugPrint('加载失败: $_errorMessage');
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }
  
  // 筛选分类
  void filterByCategory(String category) {
    _selectedCategory = category;
    notifyListeners();
  }
  
  // 获取筛选后的列表
  List<WaterKnowledge> get filteredList {
    if (_selectedCategory == '全部') {
      return _knowledgeList;
    }
    return _knowledgeList.where((item) => item.category == _selectedCategory).toList();
  }
}

4. UI组件

// 饮水知识卡片组件
class KnowledgeCard extends StatelessWidget {
  final WaterKnowledge knowledge;
  
  const KnowledgeCard({super.key, required this.knowledge});
  
  
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      elevation: 2,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(12),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 配图区域
          ClipRRect(
            borderRadius: const BorderRadius.only(
              topLeft: Radius.circular(12),
              topRight: Radius.circular(12),
            ),
            child: knowledge.imageUrl.isNotEmpty
                ? CachedNetworkImage(
                    imageUrl: knowledge.imageUrl,
                    height: 120,
                    width: double.infinity,
                    fit: BoxFit.cover,
                    placeholder: (context, url) => Container(
                      height: 120,
                      color: Colors.grey[100],
                      child: const Center(child: CircularProgressIndicator()),
                    ),
                    // 鸿蒙专属:图片加载失败处理
                    errorWidget: (context, url, error) => Container(
                      height: 120,
                      color: Colors.grey[100],
                      child: const Icon(
                        Icons.water_drop,
                        size: 48,
                        color: Colors.blue,
                      ),
                    ),
                  )
                : Container(
                    height: 120,
                    color: Colors.blue[50],
                    child: const Center(
                      child: Icon(
                        Icons.water_drop,
                        size: 48,
                        color: Colors.blue,
                      ),
                    ),
                  ),
          ),
          
          // 内容区域
          Padding(
            padding: const EdgeInsets.all(12),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                // 分类标签
                Container(
                  padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
                  decoration: BoxDecoration(
                    color: Colors.blue[100],
                    borderRadius: BorderRadius.circular(12),
                  ),
                  child: Text(
                    knowledge.category,
                    style: const TextStyle(
                      fontSize: 12,
                      color: Colors.blue,
                    ),
                  ),
                ),
                
                const SizedBox(height: 8),
                
                // 标题
                Text(
                  knowledge.title,
                  style: const TextStyle(
                    fontSize: 16,
                    fontWeight: FontWeight.bold,
                  ),
                  maxLines: 2,
                  overflow: TextOverflow.ellipsis,
                ),
                
                const SizedBox(height: 8),
                
                // 内容预览
                Text(
                  knowledge.content,
                  style: TextStyle(
                    fontSize: 14,
                    color: Colors.grey[600],
                  ),
                  maxLines: 3,
                  overflow: TextOverflow.ellipsis,
                ),
                
                const SizedBox(height: 8),
                
                // 阅读量
                Row(
                  children: [
                    const Icon(
                      Icons.remove_red_eye,
                      size: 14,
                      color: Colors.grey,
                    ),
                    const SizedBox(width: 4),
                    Text(
                      '${knowledge.readCount}人阅读',
                      style: TextStyle(
                        fontSize: 12,
                        color: Colors.grey[500],
                      ),
                    ),
                  ],
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

鸿蒙平台专属适配方案

适配点一:权限动态申请

鸿蒙的权限机制比Android更严格,需要在module.json5中声明并在代码中动态申请:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET",
        "reason": "获取饮水知识需要网络权限",
        "usedScene": {
          "abilities": ["com.example.water.MainAbility"],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.ACCESS_NETWORK_STATE",
        "reason": "检测网络状态",
        "usedScene": {
          "abilities": ["com.example.water.MainAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

适配点二:生命周期管理

鸿蒙的Ability生命周期和Android不同,需要特别处理前后台切换:


void didChangeAppLifecycleState(AppLifecycleState state) {
  super.didChangeAppLifecycleState(state);
  
  // 鸿蒙专属:应用进入后台时暂停网络请求
  if (state == AppLifecycleState.paused) {
    _cancelAllRequests();
  }
  
  // 鸿蒙专属:应用恢复时重新加载数据
  if (state == AppLifecycleState.resumed) {
    _refreshData();
  }
}

适配点三:内存优化

鸿蒙对应用内存使用有严格限制,需要主动释放资源:


void dispose() {
  super.dispose();
  // 鸿蒙专属:释放图片缓存
  imageCache.clear();
  imageCache.clearLiveImages();
}

适配点四:后台任务调度

鸿蒙的后台任务管理更严格,需要使用WorkManager:

void callbackDispatcher() {
  Workmanager().executeTask((task, inputData) {
    switch (task) {
      case 'water_reminder_task':
        _showWaterReminderNotification();
        break;
    }
    return Future.value(true);
  });
}

功能验证清单

✅ 网络权限申请成功
✅ 中文内容正常显示(无乱码)
✅ 图片加载流畅(无OOM)
✅ 后台任务正常执行
✅ 分类筛选功能正常
✅ 错误提示友好
✅ 鸿蒙真机运行稳定

真机运行截图说明

在这里插入图片描述在这里插入图片描述

在华为P50 Pro(HarmonyOS 3.0)上的运行效果:

  1. 首页加载:APP启动后显示加载动画,2秒内完成数据加载
  2. 知识列表:卡片式布局,包含配图、分类标签、标题和内容预览
  3. 分类筛选:顶部Tab切换不同分类(全部、饮水时间、水温选择、饮水量、特殊人群)
  4. 下拉刷新:支持下拉刷新获取最新数据
  5. 阅读详情:点击卡片进入详情页,显示完整内容

大二学生真实学习总结

这段时间把「健康饮水助手」APP从Android迁移到鸿蒙平台,让我收获很多:

跨平台不是简单的"一次编写,到处运行"

以前总觉得Flutter的跨平台很神奇,写一套代码就能跑遍所有平台。实际体验下来才发现,每个平台都有自己的特性和限制,需要针对性适配。鸿蒙的权限机制、后台管理、内存管理都和Android有很大不同。

调试能力是程序员的核心竞争力

这次遇到的三个坑,每个都让我崩溃过。但正是这些崩溃的瞬间,让我学会了如何看日志、如何抓包分析、如何查官方文档。现在遇到问题不再慌了,知道该从哪里入手。

做产品要考虑用户体验

在开发过程中,我不仅要保证功能正常,还要考虑用户体验。比如图片加载失败时显示友好的占位图,网络请求失败时显示清晰的错误提示,这些细节能让用户感受到APP的用心。

坚持就是胜利

从一开始对鸿蒙平台的陌生和迷茫,到后来逐个解决问题,再到看到APP在鸿蒙真机上流畅运行的那一刻,那种成就感真的无法用言语形容。作为一个大二学生,我知道自己还有很多东西要学,但这段经历让我对未来充满信心。


好了,今天的分享就到这里。如果你也在做Flutter跨平台开发,欢迎一起交流!记得关注开源鸿蒙跨平台社区,让我们一起成长。

我是ShineQiu,一个热爱技术的大二学生,下次再见!

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐