Flutter 鸿蒙开发入门:基于三方库实现新闻列表App(实操教程)

一、前言

Flutter 跨端开发的核心优势的是“一次开发,多端部署”,而鸿蒙作为新兴的分布式操作系统,其与 Flutter 的结合能极大提升开发效率。不同于上一篇的待办事项案例,本次将以 「新闻列表App」 为载体,选用更贴合资讯类应用的三方库,讲解 Flutter 在鸿蒙平台的实践细节——包括网络请求、列表渲染、图片加载、下拉刷新等核心功能,帮助新手快速掌握 Flutter 鸿蒙开发的不同应用场景,避开常见适配坑。

本教程适合具备基础 Flutter 语法知识、刚接触鸿蒙开发的开发者,全程手把手操作,所有代码均附带详细注释,可直接复制运行。

二、环境准备(精简优化,突出鸿蒙适配重点)

2.1 核心环境要求

与上一篇基础环境一致,但重点强调鸿蒙适配的关键版本,避免版本不兼容问题:

  • Flutter:3.13+(推荐3.16,鸿蒙适配更稳定)

  • OpenHarmony SDK:4.0+(API Version 10,支持更多Flutter特性)

  • DevEco Studio:4.1+(优化了Flutter项目调试体验)

  • 鸿蒙设备/模拟器:API Version 10 及以上(确保三方库正常运行)

2.2 关键环境配置(补充上一篇未提及的细节)

  1. Flutter 鸿蒙支持开启(同前一篇,补充验证命令)
    \# 开启鸿蒙平台支持 flutter config \-\-enable\-harmonyos \# 验证开启成功(输出中包含 harmonyos) flutter config \-\-list

  2. DevEco Studio 配置补充:
    进入 DevEco Studio → Settings → Appearance & Behavior → System Settings → HarmonyOS SDK,勾选“HarmonyOS SDK 4.0”和“Flutter HarmonyOS Adapter”,点击下载安装,完成后重启IDE。

  3. 设备调试配置:
    鸿蒙设备开启开发者模式后,除了USB调试,还需开启“允许调试应用”权限(设置 → 系统和更新 → 开发者选项 → 允许调试应用),否则Flutter应用无法正常安装。

三、项目创建与鸿蒙平台初始化

3.1 创建Flutter鸿蒙项目(简化命令,补充包名规范)

# 创建项目,包名遵循鸿蒙规范(com.公司名.项目名)
flutter create --org com.example.news news_harmony
cd news_harmony

3.2 鸿蒙平台适配配置(差异化配置)

  1. 修改 pubspec.yaml,添加鸿蒙平台依赖和权限声明:
    `name: news_harmony
    description: Flutter + 鸿蒙 新闻列表App
    version: 1.0.0+1

environment:
sdk: '>=3.0.0 <4.0.0'

dependencies:
flutter:
sdk: flutter
# 后续集成三方库
cupertino_icons: ^1.0.6 # 苹果风格图标(适配鸿蒙UI)

# 新增:鸿蒙平台配置
harmonyos:
# 应用图标配置(后续可替换为自定义图标)
icon:
foreground: assets/icons/app_icon.png
# 应用名称
label: 鸿蒙新闻`

  1. 生成鸿蒙平台目录并同步配置:
    \# 生成鸿蒙平台相关目录(entry、ohosTest等) flutter create \. \-\-platforms harmonyos \# 同步pub依赖到鸿蒙项目 flutter pub get

四、三方库选型与集成(全新选型,贴合新闻场景)

本次选用4个高频三方库,覆盖新闻App核心需求,均已适配鸿蒙平台,避免适配踩坑:

  • dio:网络请求库,用于获取新闻数据(替代上一篇的数据库,聚焦网络场景)

  • cached_network_image:图片缓存加载库,优化新闻封面加载体验

  • pull_to_refresh:下拉刷新、上拉加载库,适配新闻列表交互

  • flutter_screenutil:屏幕适配库,保证不同鸿蒙设备UI一致性

4.1 集成三方库

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.6
  dio: ^5.4.0 # 网络请求
  cached_network_image: ^3.3.0 # 图片缓存
  pull_to_refresh: ^2.0.0 # 下拉刷新
  flutter_screenutil: ^5.9.0 # 屏幕适配

执行安装命令,确保所有依赖下载完成:

flutter pub get

五、核心代码实现(新闻列表完整逻辑,含差异化细节)

5.1 基础配置(屏幕适配、网络请求工具类)

5.1.1 屏幕适配初始化(main.dart 入口配置)

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'pages/news_list_page.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    // 初始化屏幕适配,设计稿按375x667(手机通用尺寸)
    return ScreenUtilInit(
      designSize: const Size(375, 667),
      minTextAdapt: true,
      splitScreenMode: true,
      builder: (context, child) {
        return MaterialApp(
          title: '鸿蒙新闻',
          // 适配鸿蒙系统字体
          theme: ThemeData(
            fontFamily: 'HarmonyOS Sans',
            primarySwatch: Colors.blue,
          ),
          home: const NewsListPage(),
          debugShowCheckedModeBanner: false, // 隐藏调试横幅
        );
      },
    );
  }
}

5.1.2 网络请求工具类(http_util.dart)

import 'package:dio/dio.dart';

// 网络请求工具类,封装GET请求(新闻列表无需POST)
class HttpUtil {
  // 单例模式
  static final HttpUtil instance = HttpUtil._private();
  HttpUtil._private();

  // 初始化Dio实例
  final Dio _dio = Dio(
    BaseOptions(
      baseUrl: 'https://api.apiopen.top', // 免费新闻接口(公开可使用)
      connectTimeout: Duration(seconds: 5), // 连接超时
      receiveTimeout: Duration(seconds: 3), // 接收超时
    ),
  );

  // 获取新闻列表数据(参数:页码、每页数量)
  Future&lt;Map<String, dynamic>> getNewsList({int page = 1, int size = 10}) async {
    try {
      Response response = await _dio.get(
        '/getWangYiNews', // 网易新闻接口(免费公开)
        queryParameters: {'page': page, 'size': size},
      );
      // 返回接口数据(接口格式:{code:200, message:"success", result:[]})
      return response.data;
    } catch (e) {
      // 异常处理,返回错误信息
      return {'code': -1, 'message': '网络请求失败:$e'};
    }
  }
}

5.2 新闻模型类(news_model.dart)

// 新闻模型类,用于解析接口返回的数据
class NewsModel {
  final String title; // 新闻标题
  final String source; // 新闻来源(如网易新闻)
  final String time; // 发布时间
  final String image; // 新闻封面图片地址
  final String content; // 新闻简介

  // 构造方法
  NewsModel({
    required this.title,
    required this.source,
    required this.time,
    required this.image,
    required this.content,
  });

  // 从接口返回的Map数据,转换为NewsModel对象
  factory NewsModel.fromMap(Map<String, dynamic> map) {
    return NewsModel(
      title: map['title'] ?? '无标题',
      source: map['source'] ?? '未知来源',
      time: map['time'] ?? '',
      image: map['image'] ?? 'https://via.placeholder.com/100', // 占位图
      content: map['content'] ?? '暂无内容',
    );
  }
}

5.3 新闻列表页面(核心页面,news_list_page.dart)

import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../utils/http_util.dart';
import '../models/news_model.dart';

class NewsListPage extends StatefulWidget {
  const NewsListPage({super.key});

  
  State<NewsListPage> createState() => _NewsListPageState();
}

class _NewsListPageState extends State<NewsListPage> {
  List<NewsModel> _newsList = []; // 新闻列表数据
  int _currentPage = 1; // 当前页码
  final int _pageSize = 10; // 每页数量
  final RefreshController _refreshController = RefreshController(initialRefresh: true); // 刷新控制器

  
  void dispose() {
    // 页面销毁时释放资源
    _refreshController.dispose();
    super.dispose();
  }

  // 加载新闻数据(下拉刷新、上拉加载共用)
  Future<void> _loadNewsData({bool isRefresh = true}) async {
    // 下拉刷新时,重置页码为1
    if (isRefresh) {
      _currentPage = 1;
    }

    // 调用网络请求工具类,获取新闻数据
    Map<String, dynamic> response = await HttpUtil.instance.getNewsList(
      page: _currentPage,
      size: _pageSize,
    );

    if (response['code'] == 200) {
      // 接口请求成功,解析数据
      List<dynamic> dataList = response['result'] ?? [];
      List<NewsModel> newNewsList = dataList.map((e) => NewsModel.fromMap(e)).toList();

      setState(() {
        if (isRefresh) {
          // 下拉刷新:替换原有数据
          _newsList = newNewsList;
        } else {
          // 上拉加载:追加数据
          _newsList.addAll(newNewsList);
        }
      });

      // 刷新/加载状态更新
      if (isRefresh) {
        _refreshController.refreshCompleted();
      } else {
        // 判断是否有更多数据(如果返回数据少于每页数量,说明没有更多)
        if (newNewsList.length < _pageSize) {
          _refreshController.loadNoData();
        } else {
          _refreshController.loadComplete();
          _currentPage++; // 页码加1,用于下一次加载
        }
      }
    } else {
      // 接口请求失败,提示错误
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(response['message'] ?? '加载失败')),
      );
      // 刷新/加载状态更新
      if (isRefresh) {
        _refreshController.refreshFailed();
      } else {
        _refreshController.loadFailed();
      }
    }
  }

  // 新闻列表项组件
  Widget _newsItem(NewsModel news) {
    return Container(
      padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 10.h),
      decoration: const BoxDecoration(
        border: Border(bottom: BorderSide(color: Colors.grey[200], width: 1)),
      ),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 新闻封面图片(缓存加载)
          CachedNetworkImage(
            imageUrl: news.image,
            width: 100.w,
            height: 80.h,
            fit: BoxFit.cover,
            // 加载中占位图
            placeholder: (context, url) => Container(color: Colors.grey[200]),
            // 加载失败占位图
            errorWidget: (context, url, error) => const Icon(Icons.error),
          ),
          SizedBox(width: 12.w),
          // 新闻标题、来源、时间
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  news.title,
                  maxLines: 2, // 最多显示2行
                  overflow: TextOverflow.ellipsis, // 超出部分省略
                  style: TextStyle(
                    fontSize: 15.sp,
                    fontWeight: FontWeight.w500,
                  ),
                ),
                SizedBox(height: 8.h),
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    Text(
                      news.source,
                      style: TextStyle(fontSize: 12.sp, color: Colors.grey[600]),
                    ),
                    Text(
                      news.time.split(' ')[0], // 只显示日期,去掉时间
                      style: TextStyle(fontSize: 12.sp, color: Colors.grey[600]),
                    ),
                  ],
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(
          '热点新闻',
          style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold),
        ),
        centerTitle: true,
      ),
      // 下拉刷新、上拉加载列表
      body: SmartRefresher(
        controller: _refreshController,
        enablePullDown: true, // 允许下拉刷新
        enablePullUp: true, // 允许上拉加载
        onRefresh: () => _loadNewsData(isRefresh: true), // 下拉刷新回调
        onLoading: () => _loadNewsData(isRefresh: false), // 上拉加载回调
        // 列表内容
        child: ListView.builder(
          itemCount: _newsList.length,
          itemBuilder: (context, index) {
            return _newsItem(_newsList[index]);
          },
        ),
      ),
    );
  }
}

六、鸿蒙平台适配与运行验证(差异化重点)

6.1 鸿蒙权限配置(新增网络权限,新闻场景必需)

新闻App需要网络权限才能获取数据,修改鸿蒙项目的 module.json5 文件:

// 路径:harmonyos/entry/src/main/module.json5
{
  "module": {
    "name": "entry",
    "type": "entry",
    "description": "新闻列表App入口",
    "mainElement": ".EntryAbility",
    "deviceTypes": ["phone", "tablet"],
    // 新增网络权限
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET", // 网络权限(必需)
        "reason": "获取新闻数据需要访问网络",
        "usedScene": {
          "abilities": [".EntryAbility"],
          "when": "inuse"
        }
      }
    ],
    "abilities": [
      {
        "name": ".EntryAbility",
        "srcEntry": "ets/entryability/EntryAbility.ets",
        "description": "应用入口能力",
        "icon": "$media:app_icon",
        "label": "$string:app_name",
        "visible": true,
        "launchType": "standard"
      }
    ]
  }
}

6.2 运行与功能验证

  1. 连接鸿蒙设备或启动模拟器(确保API Version 10+);

  2. 执行运行命令(直接运行,无需手动构建hap包):
    flutter run \-d harmonyos

  3. 功能验证要点(与上一篇差异化):

    • 首次启动:自动下拉刷新,加载第一页新闻数据;

    • 下拉刷新:刷新最新新闻,验证网络请求是否正常;

    • 上拉加载:滑动到底部,加载下一页新闻,验证分页功能;

    • 图片加载:检查新闻封面是否正常显示,验证缓存功能;

    • 屏幕适配:切换不同尺寸的鸿蒙设备/模拟器,检查UI是否正常。

七、常见问题与解决(新增新闻场景专属问题)

  1. **网络请求失败,提示“无网络权限”**解决:检查 module.json5 中是否添加了 ohos.permission.INTERNET 权限,添加后重新运行项目;若仍失败,重启DevEco Studio和设备。

  2. 新闻封面图片加载失败,显示错误图标解决:检查图片地址是否有效(接口返回的图片地址可能失效),可替换为自定义占位图;同时确认设备/模拟器已连接网络。

  3. 下拉刷新/上拉加载无响应解决:检查 RefreshController 是否正确初始化,_loadNewsData 方法中是否调用了 refreshCompleted/loadComplete 等状态更新方法。

  4. UI在不同鸿蒙设备上显示错乱解决:确保所有尺寸相关的数值(width、height、fontSize)都使用 ScreenUtil 适配(如 15.w、18.sp),避免使用固定数值。

八、总结与拓展

本次教程以新闻列表App为案例,覆盖了 Flutter 鸿蒙开发中 网络请求、图片加载、下拉刷新、屏幕适配 等核心场景,选用的三方库均为行业高频且适配鸿蒙的版本,与上一篇的待办事项案例形成互补。

拓展方向(差异化拓展):

  • 集成 `flutter_html` 库,实现新闻详情页的富文本显示;

  • 添加 `shared_preferences` 库,实现新闻阅读记录、收藏功能;

  • 结合鸿蒙原生能力,实现新闻推送、后台刷新功能;

  • 优化UI,添加夜间模式、字体大小调整等功能。

后续学习建议

如果你想继续深入,我可以帮你:

  • 补充新闻详情页的完整实现代码;

  • 讲解鸿蒙原生能力与Flutter的交互方法(如推送、分享);

  • 优化项目性能(如图片缓存优化、网络请求拦截)。

Logo

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

更多推荐