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

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 进行状态管理,通过自定义组件实现瀑布流布局。以下是项目的核心目录结构:

dart 复制代码
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 类实现,确保类型安全。

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 瀑布流布局实现

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

dart 复制代码
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);

  @override
  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;
  }

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

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

  @override
  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 底部选项卡导航

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

dart 复制代码
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 = ['🐾', '🐱', '❓', '👤'];

  @override
  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 组件实现图片轮播,并添加了底部操作栏展示用户信息和互动按钮。

dart 复制代码
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);

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

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

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

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

  @override
  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。

dart 复制代码
class PetCommunityPage extends StatefulWidget {
  const PetCommunityPage({Key? key}) : super(key: key);

  @override
  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();

  @override
  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;
    });
  }

  @override
  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 中配置网络请求权限:

json 复制代码
"requestPermissions": [
  {"name": "ohos.permission.INTERNET"},
  {"name": "ohos.permission.GET_NETWORK_INFO"}
]

2. 页面路由配置

main_pages.json 中注册页面路由:

json 复制代码
{
  "src": [
    "pages/pet_community_page",
    "pages/pet_image_preview_page"
  ]
}

3. 入口 Ability 配置

EntryAbility 中设置启动页面:

dart 复制代码
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

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

bash 复制代码
git clone https://atomgit.com/maaath/pet-community-app.git

六、结语

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

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

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


相关推荐
maaath7 小时前
【maaath】Flutter for OpenHarmony 实战:健身运动应用的跨平台开发指南
flutter·华为·harmonyos
Swift社区7 小时前
传统游戏引擎 vs 鸿蒙 System 架构
架构·游戏引擎·harmonyos
maaath7 小时前
【maaath】 Flutter for OpenHarmony 新闻资讯应用实战开发
flutter·华为·harmonyos
maaath7 小时前
【maaath】Flutter for OpenHarmony 跨平台图书阅读应用开发实践
flutter·华为·harmonyos
xmdy58667 小时前
Flutter+开源鸿蒙实战|智联邻里Day2 首页UI开发+全局组件封装+鸿蒙多端适配
flutter·开源·harmonyos
特立独行的猫a7 小时前
移植 vcpkg 到鸿蒙 PC:vcpkg-tool 交叉编译与实践手记
华为·harmonyos·vcpkg·鸿蒙pc·vcpkg-tool
911hzh8 小时前
Flutter 音视频通话集成实战:WebSocket 做信令,WebRTC 传音视频,附详细事件时序图
websocket·flutter·音视频
万添裁8 小时前
huawei 机考
算法·华为·深度优先
里欧跑得慢16 小时前
15. Web可访问性最佳实践:让每个用户都能平等访问
前端·css·flutter·web