Flutter for OpenHarmony 宠物社区应用实战开发

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

作者:maaath

一、引言

在移动应用开发领域,跨平台技术一直是开发者关注的焦点。Flutter 作为 Google 推出的跨平台 UI 框架,凭借其高性能和一致性体验的特点,被广泛应用于 iOS、Android 等平台。随着 OpenHarmony 生态的蓬勃发展,Flutter for OpenHarmony(简称 Flutter Ohos)应运而生,为开发者提供了在鸿蒙设备上运行 Flutter 应用的能力。本文将通过一个完整的宠物社区应用实例,详细讲解如何使用 Flutter for OpenHarmony 开发跨平台应用,涵盖网络请求、瀑布流布局、下拉刷新、底部选项卡等核心功能的实现。

Flutter for OpenHarmony 的出现,使得开发者能够用一套代码同时覆盖 iOS、Android 和 OpenHarmony 三大平台,极大地提升了开发效率。本文以宠物社区应用为载体,深入剖析 Flutter 跨平台开发的核心技术点,帮助读者快速掌握 Flutter for OpenHarmony 的开发技能。

二、项目概述

2.1 项目背景

宠物社区应用是一款面向宠物爱好者的社交类应用,用户可以在平台上分享自家宠物的日常照片、参与宠物问答讨论、浏览萌宠内容等。该应用需要具备良好的用户体验,支持图片瀑布流展示、流畅的下拉刷新和上拉加载功能,以及直观的底部导航交互。

2.2 功能特性

本项目实现了以下核心功能:

  1. 发现页 - 瀑布流布局展示宠物帖子,支持图片预览
  2. 萌宠页 - 按分类展示萌宠图片集,支持筛选
  3. 问答页 - 宠物相关问答社区,支持问题浏览和回答
  4. 我的页 - 用户个人信息展示和功能入口

2.3 技术架构

项目采用 Flutter 声明式 UI 开发范式,使用 Provider 进行状态管理,通过自定义组件实现瀑布流布局。以下是项目的核心目录结构:

lib/
├── main.dart                 # 应用入口
├── common/
│   └── pet_constants.dart   # 常量配置(颜色、字体、间距)
├── model/
│   └── pet_model.dart       # 数据模型定义
├── network/
│   └── pet_service.dart     # 网络服务层
└── pages/
    ├── pet_community_page.dart      # 主页面
    └── pet_image_preview_page.dart  # 图片预览页

三、核心功能实现

3.1 数据模型设计

良好的数据模型是应用架构的基石。本项目定义了宠物模型、帖子模型、问答模型等多个数据结构,所有模型均使用 Dart 类实现,确保类型安全。

// 帖子数据模型
class PostModel {
  String id = '';
  String type = 'image';
  String title = '';
  String content = '';
  List<String> images = [];
  String authorName = '';
  String authorAvatar = '';
  int likeCount = 0;
  int commentCount = 0;
  bool isLiked = false;
  bool isCollected = false;
  int createTime = 0;
  List<String> tags = [];
  String location = '';

  PostModel({
    this.id = '',
    this.type = 'image',
    this.title = '',
    this.content = '',
    this.images = const [],
    this.authorName = '',
    this.authorAvatar = '',
    this.likeCount = 0,
    this.commentCount = 0,
    this.isLiked = false,
    this.isCollected = false,
    this.createTime = 0,
    this.tags = const [],
    this.location = '',
  });
}

数据模型的设计遵循以下原则:所有字段具有默认值,便于构造和调试;使用 const 修饰空集合,提高内存效率;字段命名采用小驼峰法,符合 Dart 编码规范。

3.2 瀑布流布局实现

瀑布流是图片类应用常见的布局方式,其特点是每列宽度固定,高度根据图片实际比例自动计算,使页面呈现参差有致的视觉效果。本项目采用自定义组件方式实现瀑布流,通过计算每列的累计高度,将新项目放置在最短列的下方。

class WaterfallContainer extends StatefulWidget {
  final List<WaterfallItemData> items;
  final Function(WaterfallItemData) onLikeClick;
  final Function(WaterfallItemData) onItemClick;

  const WaterfallContainer({
    Key? key,
    required this.items,
    required this.onLikeClick,
    required this.onItemClick,
  }) : super(key: key);

  
  State<WaterfallContainer> createState() => _WaterfallContainerState();
}

class _WaterfallContainerState extends State<WaterfallContainer> {
  List<double> _columnHeights = [0, 0];

  void _calculatePositions() {
    const columnCount = 2;
    const itemSpacing = 8.0;
    const contentPadding = 12.0;
    final columnWidth = (360 - contentPadding * 2 - itemSpacing) / columnCount;

    _columnHeights = List.filled(columnCount, 0.0);

    for (var i = 0; i < widget.items.length; i++) {
      final item = widget.items[i];
      final shortestColumn = _getShortestColumn();
      final x = contentPadding + shortestColumn * (columnWidth + itemSpacing);
      final y = _columnHeights[shortestColumn];

      item.x = x;
      item.y = y;
      item.width = columnWidth;

      _columnHeights[shortestColumn] += item.height + itemSpacing;
    }
  }

  int _getShortestColumn() {
    int shortestIndex = 0;
    double minHeight = _columnHeights[0];
    for (var i = 1; i < _columnHeights.length; i++) {
      if (_columnHeights[i] < minHeight) {
        minHeight = _columnHeights[i];
        shortestIndex = i;
      }
    }
    return shortestIndex;
  }

  
  void didUpdateWidget(WaterfallContainer oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.items != widget.items) {
      _calculatePositions();
    }
  }

  
  void initState() {
    super.initState();
    _calculatePositions();
  }

  
  Widget build(BuildContext context) {
    return Stack(
      children: widget.items.map((item) {
        return Positioned(
          left: item.x,
          top: item.y,
          child: _buildCard(item),
        );
      }).toList(),
    );
  }

  Widget _buildCard(WaterfallItemData item) {
    return SizedBox(
      width: item.width,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 图片区域
          Stack(
            alignment: Alignment.bottomRight,
            children: [
              GestureDetector(
                onTap: () => widget.onItemClick(item),
                child: ClipRRect(
                  borderRadius: BorderRadius.circular(12),
                  child: Image.network(
                    item.post.images.isNotEmpty ? item.post.images[0] : '',
                    width: item.width,
                    height: item.height,
                    fit: BoxFit.cover,
                  ),
                ),
              ),
              // 多图标识
              if (item.post.images.length > 1)
                Container(
                  padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
                  margin: const EdgeInsets.all(6),
                  decoration: BoxDecoration(
                    color: Colors.black54,
                    borderRadius: BorderRadius.circular(10),
                  ),
                  child: Text(
                    '${item.post.images.length}',
                    style: const TextStyle(color: Colors.white, fontSize: 10),
                  ),
                ),
            ],
          ),
          // 卡片信息
          Padding(
            padding: const EdgeInsets.only(top: 8),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  item.post.title,
                  style: const TextStyle(fontSize: 13, color: Color(0xFF333333)),
                  maxLines: 2,
                  overflow: TextOverflow.ellipsis,
                ),
                const SizedBox(height: 6),
                Row(
                  children: [
                    CircleAvatar(
                      radius: 9,
                      backgroundImage: NetworkImage(item.post.authorAvatar),
                    ),
                    const SizedBox(width: 4),
                    Expanded(
                      child: Text(
                        item.post.authorName,
                        style: const TextStyle(fontSize: 11, color: Color(0xFF999999)),
                        maxLines: 1,
                        overflow: TextOverflow.ellipsis,
                      ),
                    ),
                    GestureDetector(
                      onTap: () => widget.onLikeClick(item),
                      child: Row(
                        children: [
                          Text(
                            item.isLiked ? '❤️' : '🤍',
                            style: const TextStyle(fontSize: 14),
                          ),
                          const SizedBox(width: 4),
                          Text(
                            item.likeCount.toString(),
                            style: const TextStyle(fontSize: 11, color: Color(0xFF999999)),
                          ),
                        ],
                      ),
                    ),
                  ],
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

瀑布流实现的核心逻辑在于 _calculatePositions 方法。该方法首先计算列宽,然后遍历所有数据项,通过 _getShortestColumn 找到当前高度最小的列,将新项目放置在该列下方,同时更新该列的累计高度。这种贪心算法确保了项目均匀分布,且页面高度最小化。

3.3 底部选项卡导航

底部导航是移动应用最常见的导航模式之一。本项目使用自定义组件实现底部选项卡,支持图标和文字的组合展示,并提供点击动画反馈。

class TabBarWidget extends StatelessWidget {
  final int currentIndex;
  final Function(int) onTabChanged;

  const TabBarWidget({
    Key? key,
    required this.currentIndex,
    required this.onTabChanged,
  }) : super(key: key);

  static const List<String> _tabs = ['发现', '萌宠', '问答', '我的'];
  static const List<String> _icons = ['🐾', '🐱', '❓', '👤'];

  
  Widget build(BuildContext context) {
    return Container(
      height: 56,
      decoration: BoxDecoration(
        color: Colors.white,
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.08),
            blurRadius: 20,
            offset: const Offset(0, -5),
          ),
        ],
      ),
      child: Row(
        children: List.generate(_tabs.length, (index) {
          final isSelected = currentIndex == index;
          return Expanded(
            child: GestureDetector(
              onTap: () => onTabChanged(index),
              behavior: HitTestBehavior.opaque,
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text(
                    _icons[index],
                    style: const TextStyle(fontSize: 24),
                  ),
                  const SizedBox(height: 2),
                  Text(
                    _tabs[index],
                    style: TextStyle(
                      fontSize: 11,
                      color: isSelected ? const Color(0xFFFF8A65) : const Color(0xFF999999),
                    ),
                  ),
                ],
              ),
            ),
          );
        }),
      ),
    );
  }
}

底部选项卡采用 Row 配合 Expanded 实现均分布局,点击时通过回调通知父组件更新当前索引,触发页面切换。这种实现方式简单直观,且具有良好的性能表现。

3.4 图片预览与轮播

图片预览功能允许用户全屏查看图片列表,支持滑动切换和缩放手势。本项目使用 Flutter 的 PageView 组件实现图片轮播,并添加了底部操作栏展示用户信息和互动按钮。

class ImagePreviewPage extends StatefulWidget {
  final List<String> images;
  final int initialIndex;
  final bool isLiked;
  final int likeCount;

  const ImagePreviewPage({
    Key? key,
    required this.images,
    this.initialIndex = 0,
    this.isLiked = false,
    this.likeCount = 0,
  }) : super(key: key);

  
  State<ImagePreviewPage> createState() => _ImagePreviewPageState();
}

class _ImagePreviewPageState extends State<ImagePreviewPage> {
  late PageController _pageController;
  late int _currentIndex;
  late bool _isLiked;
  late int _likeCount;
  bool _showControls = true;

  
  void initState() {
    super.initState();
    _currentIndex = widget.initialIndex;
    _isLiked = widget.isLiked;
    _likeCount = widget.likeCount;
    _pageController = PageController(initialPage: widget.initialIndex);
  }

  
  void dispose() {
    _pageController.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: Stack(
        children: [
          // 图片轮播
          GestureDetector(
            onTap: () {
              setState(() {
                _showControls = !_showControls;
              });
            },
            child: PageView.builder(
              controller: _pageController,
              itemCount: widget.images.length,
              onPageChanged: (index) {
                setState(() {
                  _currentIndex = index;
                });
              },
              itemBuilder: (context, index) {
                return InteractiveViewer(
                  minScale: 1.0,
                  maxScale: 3.0,
                  child: Center(
                    child: Image.network(
                      widget.images[index],
                      fit: BoxFit.contain,
                    ),
                  ),
                );
              },
            ),
          ),
          // 顶部导航栏
          if (_showControls)
            Positioned(
              top: 0,
              left: 0,
              right: 0,
              child: Container(
                height: 56,
                padding: const EdgeInsets.symmetric(horizontal: 8),
                decoration: BoxDecoration(
                  color: Colors.black.withOpacity(0.6),
                ),
                child: Row(
                  children: [
                    IconButton(
                      icon: const Icon(Icons.arrow_back, color: Colors.white),
                      onPressed: () => Navigator.pop(context),
                    ),
                    const Spacer(),
                    IconButton(
                      icon: const Icon(Icons.share, color: Colors.white),
                      onPressed: () {},
                    ),
                    IconButton(
                      icon: const Icon(Icons.more_vert, color: Colors.white),
                      onPressed: () {},
                    ),
                  ],
                ),
              ),
            ),
          // 页码指示器
          if (_showControls && widget.images.length > 1)
            Positioned(
              left: 0,
              right: 0,
              bottom: 120,
              child: Center(
                child: Container(
                  padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
                  decoration: BoxDecoration(
                    color: Colors.black54,
                    borderRadius: BorderRadius.circular(20),
                  ),
                  child: Text(
                    '${_currentIndex + 1} / ${widget.images.length}',
                    style: const TextStyle(color: Colors.white, fontSize: 14),
                  ),
                ),
              ),
            ),
          // 底部操作栏
          if (_showControls)
            Positioned(
              left: 0,
              right: 0,
              bottom: 0,
              child: Container(
                padding: const EdgeInsets.all(16),
                decoration: BoxDecoration(
                  color: Colors.black.withOpacity(0.6),
                ),
                child: SafeArea(
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceAround,
                    children: [
                      _buildActionButton(
                        _isLiked ? '❤️' : '🤍',
                        _likeCount.toString(),
                        () {
                          setState(() {
                            _isLiked = !_isLiked;
                            _likeCount += _isLiked ? 1 : -1;
                          });
                        },
                      ),
                      _buildActionButton('💬', '128', () {}),
                      _buildActionButton('⭐', '收藏', () {}),
                      _buildActionButton('⬇️', '保存', () {}),
                    ],
                  ),
                ),
              ),
            ),
        ],
      ),
    );
  }

  Widget _buildActionButton(String emoji, String text, VoidCallback onTap) {
    return GestureDetector(
      onTap: onTap,
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Text(emoji, style: const TextStyle(fontSize: 22)),
          const SizedBox(height: 4),
          Text(text, style: const TextStyle(color: Colors.white, fontSize: 13)),
        ],
      ),
    );
  }
}

图片预览页面使用 PageView 实现图片轮播滑动,InteractiveViewer 组件提供双指缩放功能。页面采用 Stack 布局叠加多个元素:背景图片层、顶部导航栏、页码指示器和底部操作栏。通过状态变量 _showControls 控制 UI 元素的显示与隐藏,点击图片区域可切换控制栏的可见性。

3.5 状态管理与数据加载

良好的状态管理是构建复杂应用的关键。本项目在主页面中集中管理所有状态,包括当前 Tab 索引、帖子列表、加载状态等,通过异步方法加载数据并更新 UI。

class PetCommunityPage extends StatefulWidget {
  const PetCommunityPage({Key? key}) : super(key: key);

  
  State<PetCommunityPage> createState() => _PetCommunityPageState();
}

class _PetCommunityPageState extends State<PetCommunityPage> {
  int _currentTabIndex = 0;
  bool _isLoadingMore = false;
  bool _hasMoreData = true;

  // 发现页数据
  List<PostModel> _discoverPosts = [];
  List<WaterfallItemData> _waterfallItems = [];
  int _discoverPage = 1;

  // 萌宠页数据
  List<CutePetCardData> _cutePets = [];
  int _petsPage = 1;
  int _selectedPetType = 0;

  // 问答页数据
  List<QACardData> _qaList = [];
  int _qaPage = 1;
  int _selectedQAType = 0;

  final PetService _petService = PetService();

  
  void initState() {
    super.initState();
    _loadDiscoverData();
  }

  Future<void> _loadDiscoverData() async {
    final posts = await _petService.getDiscoverPosts(_discoverPage);
    setState(() {
      if (_discoverPage == 1) {
        _discoverPosts = posts;
      } else {
        _discoverPosts.addAll(posts);
      }
      _calculateWaterfallItems();
      _hasMoreData = posts.length >= 20;
    });
  }

  void _calculateWaterfallItems() {
    _waterfallItems = [];
    final aspectRatios = [0.75, 1.0, 1.25, 0.8, 1.33, 0.9, 1.2, 1.4];

    for (var i = 0; i < _discoverPosts.length; i++) {
      final post = _discoverPosts[i];
      final ratio = aspectRatios[i % aspectRatios.length];
      const columnWidth = 168.0;
      final itemHeight = columnWidth / ratio;

      _waterfallItems.add(WaterfallItemData(
        post: post,
        height: itemHeight,
        isLiked: post.isLiked,
        likeCount: post.likeCount,
      ));
    }
  }

  void _onLoadMore() {
    if (!_hasMoreData || _isLoadingMore) return;
    setState(() {
      _isLoadingMore = true;
    });

    if (_currentTabIndex == 0) {
      _discoverPage++;
      _loadDiscoverData().then((_) {
        setState(() => _isLoadingMore = false);
      });
    } else if (_currentTabIndex == 1) {
      _petsPage++;
      _petService.getCutePets(_petsPage).then((pets) {
        setState(() {
          _cutePets.addAll(pets.take(10).map((p) => CutePetCardData(
            id: p.id,
            avatar: p.avatarUrl,
            name: p.name,
            breed: p.breed,
            type: p.type,
            likeCount: p.likeCount,
            commentCount: p.commentCount,
            isLiked: p.isLiked,
          )));
          _isLoadingMore = false;
          _hasMoreData = pets.length >= 20;
        });
      });
    } else if (_currentTabIndex == 2) {
      _qaPage++;
      _petService.getQAList(_qaPage).then((questions) {
        setState(() {
          _qaList.addAll(questions.map((q) => QACardData(
            id: q.id,
            title: q.title,
            content: q.content,
            authorName: q.authorName,
            authorAvatar: q.authorAvatar,
            viewCount: q.viewCount,
            answerCount: q.answerCount,
            likeCount: q.likeCount,
            isSolved: q.isSolved,
            tags: q.tags,
            createTime: q.createTime,
          )));
          _isLoadingMore = false;
          _hasMoreData = questions.length >= 20;
        });
      });
    }
  }

  void _toggleLike(WaterfallItemData item) {
    setState(() {
      item.isLiked = !item.isLiked;
      item.likeCount += item.isLiked ? 1 : -1;
      item.post.isLiked = item.isLiked;
      item.post.likeCount = item.likeCount;
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFFFFF8F5),
      body: SafeArea(
        child: Column(
          children: [
            Expanded(
              child: IndexedStack(
                index: _currentTabIndex,
                children: [
                  _buildDiscoverPage(),
                  _buildCutePetsPage(),
                  _buildQAPage(),
                  _buildMyPage(),
                ],
              ),
            ),
            TabBarWidget(
              currentIndex: _currentTabIndex,
              onTabChanged: (index) {
                setState(() {
                  _currentTabIndex = index;
                });
              },
            ),
          ],
        ),
      ),
    );
  }

状态管理采用 StatefulWidget 模式,所有状态变量通过 setState 方法更新,确保 UI 能够及时响应数据变化。数据加载采用分页模式,_loadMore 方法通过判断当前 Tab 索引加载相应页面的数据,避免一次性加载过多数据导致内存占用过高和界面卡顿。

四、项目运行截图

4.1 萌宠分类浏览

在萌宠页面,用户可以通过顶部标签筛选不同类型的宠物,如狗狗、猫咪、兔子等。
在这里插入图片描述

4.2 问答社区

问答页面展示用户提出的宠物相关问题,包括问题标题、描述和标签,点击可查看详情和回答。
在这里插入图片描述

4.3 个人中心

我的页面展示用户头像、昵称、宠物数量、帖子数量、获赞数和粉丝数,下方提供功能入口菜单。

在这里插入图片描述

五、技术总结

5.1 Flutter for OpenHarmony 适配要点

在使用 Flutter 开发 OpenHarmony 应用时,需要注意以下几点:

1. 网络权限配置

module.json5 中配置网络请求权限:

"requestPermissions": [
  {"name": "ohos.permission.INTERNET"},
  {"name": "ohos.permission.GET_NETWORK_INFO"}
]

2. 页面路由配置

main_pages.json 中注册页面路由:

{
  "src": [
    "pages/pet_community_page",
    "pages/pet_image_preview_page"
  ]
}

3. 入口 Ability 配置

EntryAbility 中设置启动页面:

onWindowStageCreate(windowStage) {
  windowStage.loadContent('pages/pet_community_page', (err, data) {
    if (err != null) {
      print('Failed to load: ${err.message}');
      return;
    }
    print('Content loaded successfully');
  });
}

5.2 性能优化建议

  1. 图片加载优化 - 使用 cached_network_image 包缓存图片,减少重复下载
  2. 列表渲染优化 - 对于长列表,使用 ListView.builder 实现按需加载
  3. 状态更新优化 - 避免不必要的 setState 调用,使用 const 构造不可变组件
  4. 内存管理 - 及时释放资源,如 PageControllerdispose 方法调用

5.3 代码托管

本项目已托管至 AtomGit 平台,仓库地址为:

https://atomgit.com/maaath/pet-community-app

开发者可通过以下命令克隆项目:

git clone https://atomgit.com/maaath/pet-community-app.git

六、结语

本文通过一个完整的宠物社区应用实例,详细讲解了 Flutter for OpenHarmony 跨平台开发的核心技术。从数据模型设计到 UI 组件实现,从状态管理到页面导航,全面展示了 Flutter 开发鸿蒙应用的最佳实践。

Flutter for OpenHarmony 为开发者打开了新的可能性,通过一套代码同时覆盖多个平台,大大提升了开发效率。随着 OpenHarmony 生态的不断完善,Flutter 开发者将有更广阔的发挥空间。希望本文能够为广大 Flutter 开发者提供有价值的参考,帮助大家快速上手 Flutter for OpenHarmony 开发。

如有问题或建议,欢迎在社区讨论交流!

感谢各位阅读!


Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐