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


项目效果

本文实现的是一个基于 Flutter for OpenHarmony 的灵感便签瀑布流应用。项目中使用 Flutter 第三方库 flutter_staggered_grid_view 实现 Masonry 瀑布流布局,让不同高度的便签卡片以错落有致的方式展示。

最终运行效果如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

页面主要包含以下内容:

  • 顶部标题栏;
  • 灵感便签统计卡片;
  • 分类筛选按钮;
  • Masonry 瀑布流便签墙;
  • 当前选中便签详情;
  • 第三方库使用说明;
  • 页面整体采用 Flutter Material 风格布局。

本文重点是演示如何在 Flutter for OpenHarmony 项目中使用 Flutter 第三方库 flutter_staggered_grid_view。项目代码写在 lib/main.dart 中,依赖配置写在 pubspec.yaml 中,符合 Flutter for OpenHarmony 第三方库实践方向。


前言

在移动应用开发中,瀑布流布局是一种很常见的内容展示方式。它适合展示高度不固定的内容,例如图片墙、笔记卡片、灵感记录、商品推荐、文章摘要、作品集等。

普通网格布局通常要求每个卡片高度一致,页面看起来比较整齐,但也容易显得单调。如果每条内容的长度不同,普通网格可能会出现大量空白区域。

瀑布流布局可以根据内容高度自动排列卡片,让页面空间利用率更高,也更适合展示碎片化内容。

如果自己手动实现瀑布流,需要计算每一列高度、决定卡片插入位置、处理滚动性能和布局刷新。为了排几个卡片写一套布局算法,多少像为了摆桌椅先发明木工史,技术上能行,精神上没必要。

因此本文选择使用 Flutter 第三方库 flutter_staggered_grid_view 来实现瀑布流效果。本项目以“灵感便签瀑布流应用”为例,使用 MasonryGridView.count() 构建错落排列的便签墙。


一、项目目标

本次实践主要实现以下目标:

  • 创建 Flutter for OpenHarmony 项目;
  • pubspec.yaml 中添加第三方库 flutter_staggered_grid_view
  • 使用 flutter pub get 获取依赖;
  • lib/main.dart 中引入第三方库;
  • 使用 MasonryGridView.count() 构建瀑布流布局;
  • 使用不同高度的便签卡片展示灵感内容;
  • 实现便签分类筛选;
  • 实现点击便签查看详情;
  • 使用 Flutter Material 组件构建完整页面;
  • 将应用运行到 OpenHarmony 设备或模拟器中。

二、技术栈

类型 内容
开发方向 Flutter for OpenHarmony
开发语言 Dart
UI 框架 Flutter
第三方库 flutter_staggered_grid_view
功能场景 瀑布流布局 / 灵感便签 / 内容卡片
核心组件 MasonryGridView
项目入口 lib/main.dart
依赖配置 pubspec.yaml
运行平台 OpenHarmony 设备或模拟器

三、为什么选择 flutter_staggered_grid_view

在实际开发中,瀑布流布局可以用于很多场景,例如:

  • 图片展示墙;
  • 灵感便签;
  • 商品卡片;
  • 文章摘要;
  • 课程资源;
  • 美食推荐;
  • 旅行攻略;
  • 作品集展示;
  • 校园活动墙。

如果使用 Flutter 原生 GridView,每个格子的高度通常比较固定,不适合展示不同长度的文本内容。

flutter_staggered_grid_view 提供了多种网格布局方式,其中 MasonryGridView 可以根据每个卡片的实际高度进行排列,非常适合实现瀑布流页面。

在本项目中,flutter_staggered_grid_view 主要完成以下工作:

  • 构建双列瀑布流布局;
  • 根据卡片高度自动排列;
  • 展示不同长度的便签内容;
  • 减少普通网格中的空白区域;
  • 提升页面内容展示效果。

四、创建 Flutter for OpenHarmony 项目

在已经配置好 Flutter for OpenHarmony 开发环境的前提下,可以创建一个 Flutter 项目。

示例项目名称:

flutter create note_wall_demo

进入项目目录:

cd note_wall_demo

项目创建完成后,主要关注两个文件:

note_wall_demo
 ├── pubspec.yaml
 └── lib
     └── main.dart

其中:

文件 作用
pubspec.yaml 配置 Flutter 项目依赖
lib/main.dart 编写 Flutter 页面和业务逻辑

五、添加 flutter_staggered_grid_view 第三方库

打开项目根目录下的 pubspec.yaml 文件,在 dependencies 中添加 flutter_staggered_grid_view

示例配置如下:

dependencies:
  flutter:
    sdk: flutter

  flutter_staggered_grid_view: ^0.7.0

完整结构大致如下:

name: note_wall_demo
description: A Flutter for OpenHarmony staggered grid demo.
publish_to: 'none'

version: 1.0.0+1

environment:
  sdk: '>=3.4.0 <4.0.0'

dependencies:
  flutter:
    sdk: flutter

  flutter_staggered_grid_view: ^0.7.0

dev_dependencies:
  flutter_test:
    sdk: flutter

flutter:
  uses-material-design: true

添加完成后,在终端执行:

flutter pub get

执行成功后,就可以在 Dart 代码中使用 flutter_staggered_grid_view 了。


六、项目结构

本项目主要修改 lib/main.dart 文件:

lib
 └── main.dart

本项目不需要编写 OpenHarmony 原生 ArkTS 页面,也不需要修改 Index.ets

因为这是 Flutter for OpenHarmony 项目,页面主体应该是 Flutter 代码。审核重点会看:

  • 是否使用 pubspec.yaml 添加 Flutter 第三方库;
  • 是否在 Dart 文件中 import package
  • 是否在 lib/main.dart 中实际调用第三方库;
  • 是否属于 Flutter for OpenHarmony 项目。

看到 pubspec.yamllib/main.dartimport 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';,这才是正确方向。别再把 ArkTS 塞进来,题目要吃饭,你递过去一块砖,当然不通过。


七、核心实现思路

本项目的核心流程如下:

  1. pubspec.yaml 中添加 flutter_staggered_grid_view
  2. main.dart 中引入第三方库;
  3. 定义便签数据模型;
  4. 准备不同分类、不同长度的便签内容;
  5. 使用分类按钮筛选便签;
  6. 使用 MasonryGridView.count() 构建瀑布流;
  7. 使用 crossAxisCount 设置双列布局;
  8. 使用 mainAxisSpacingcrossAxisSpacing 设置卡片间距;
  9. 点击便签后展示当前便签详情。

第三方库引入代码如下:

import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';

瀑布流核心代码如下:

MasonryGridView.count(
  crossAxisCount: 2,
  mainAxisSpacing: 12,
  crossAxisSpacing: 12,
  itemCount: notes.length,
  itemBuilder: (context, index) {
    return _buildNoteCard(notes[index]);
  },
)

这段代码是本文的重点,说明项目确实使用了 Flutter 第三方库实现瀑布流布局。


八、main.dart 完整代码

打开文件:

lib/main.dart

将其中内容替换为下面代码:

import 'package:flutter/material.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';

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

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Note Wall Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.deepOrange,
          brightness: Brightness.light,
        ),
        useMaterial3: true,
      ),
      home: const NoteWallHomePage(),
    );
  }
}

class NoteCard {
  const NoteCard({
    required this.title,
    required this.content,
    required this.category,
    required this.icon,
    required this.color,
  });

  final String title;
  final String content;
  final String category;
  final IconData icon;
  final Color color;
}

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

  
  State<NoteWallHomePage> createState() => _NoteWallHomePageState();
}

class _NoteWallHomePageState extends State<NoteWallHomePage> {
  final List<String> _categories = const [
    '全部',
    '学习',
    '生活',
    '项目',
    '灵感',
  ];

  final List<NoteCard> _notes = const [
    NoteCard(
      title: '英语练习',
      content: '每天至少读一段英文材料,重点练习发音和语调。不要只背单词,要多开口。',
      category: '学习',
      icon: Icons.language,
      color: Colors.blue,
    ),
    NoteCard(
      title: 'Flutter 页面想法',
      content: '可以做一个任务卡片页面,用不同颜色区分优先级,再加一个简单的完成统计。',
      category: '项目',
      icon: Icons.phone_iphone,
      color: Colors.deepPurple,
    ),
    NoteCard(
      title: '周末安排',
      content: '整理桌面,清理下载文件夹,顺便把没用的截图删掉。不然电脑迟早变成电子垃圾堆。',
      category: '生活',
      icon: Icons.weekend,
      color: Colors.teal,
    ),
    NoteCard(
      title: '音乐灵感',
      content: '副歌可以用更明亮的和弦走向,鼓点不要太满,留一点空间给人声。',
      category: '灵感',
      icon: Icons.music_note,
      color: Colors.pink,
    ),
    NoteCard(
      title: '论文阅读',
      content: '先看摘要和图表,再看方法部分。不要一上来硬啃公式,那是给自己制造精神灾难。',
      category: '学习',
      icon: Icons.article,
      color: Colors.indigo,
    ),
    NoteCard(
      title: '项目汇报',
      content: '汇报时先讲问题背景,再讲方法,最后讲结果。不要直接堆技术名词,听众不是编译器。',
      category: '项目',
      icon: Icons.slideshow,
      color: Colors.orange,
    ),
    NoteCard(
      title: '宿舍整理',
      content: '把充电线、耳机、U盘和开发板配件分开放。小零件混在一起找起来会让人怀疑人生。',
      category: '生活',
      icon: Icons.home,
      color: Colors.green,
    ),
    NoteCard(
      title: '界面设计',
      content: '卡片之间要留白,按钮不要挤在一起,颜色最多三四种。页面不是调色盘,别把用户眼睛当实验品。',
      category: '灵感',
      icon: Icons.palette,
      color: Colors.redAccent,
    ),
    NoteCard(
      title: '代码检查',
      content: '提交前检查 import、依赖、文件路径和运行截图。文章能不能过审,有时候就差这几个无聊但致命的细节。',
      category: '项目',
      icon: Icons.code,
      color: Colors.brown,
    ),
    NoteCard(
      title: '复习提醒',
      content: '先做题,再回头补概念。只看教程不动手,很容易获得一种虚假的“我好像会了”。',
      category: '学习',
      icon: Icons.school,
      color: Colors.cyan,
    ),
  ];

  String _selectedCategory = '全部';
  NoteCard? _selectedNote;

  List<NoteCard> get _filteredNotes {
    if (_selectedCategory == '全部') {
      return _notes;
    }

    return _notes.where((note) => note.category == _selectedCategory).toList();
  }

  int get _projectCount {
    return _notes.where((note) => note.category == '项目').length;
  }

  int get _studyCount {
    return _notes.where((note) => note.category == '学习').length;
  }

  void _selectCategory(String category) {
    setState(() {
      _selectedCategory = category;
      _selectedNote = null;
    });
  }

  void _selectNote(NoteCard note) {
    setState(() {
      _selectedNote = note;
    });
  }

  
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);
    final List<NoteCard> notes = _filteredNotes;

    return Scaffold(
      appBar: AppBar(
        title: const Text('灵感便签瀑布流'),
        centerTitle: true,
      ),
      body: SafeArea(
        child: ListView(
          padding: const EdgeInsets.all(16),
          children: [
            _buildOverviewCard(theme),
            const SizedBox(height: 16),
            _buildCategoryFilter(theme),
            const SizedBox(height: 16),
            _buildMasonryNoteWall(theme, notes),
            const SizedBox(height: 16),
            _buildSelectedNoteCard(theme),
            const SizedBox(height: 16),
            _buildLibraryCard(theme),
          ],
        ),
      ),
    );
  }

  Widget _buildOverviewCard(ThemeData theme) {
    return Card(
      elevation: 3,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(22),
      ),
      child: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          children: [
            Container(
              width: 76,
              height: 76,
              decoration: BoxDecoration(
                color: theme.colorScheme.primaryContainer,
                borderRadius: BorderRadius.circular(24),
              ),
              child: Icon(
                Icons.dashboard_customize,
                size: 42,
                color: theme.colorScheme.onPrimaryContainer,
              ),
            ),
            const SizedBox(height: 18),
            Text(
              'Flutter for OpenHarmony',
              style: theme.textTheme.headlineSmall?.copyWith(
                fontWeight: FontWeight.bold,
              ),
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 8),
            Text(
              '使用 flutter_staggered_grid_view 构建错落排列的灵感便签墙',
              style: theme.textTheme.bodyMedium?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
                height: 1.5,
              ),
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 20),
            Row(
              children: [
                _buildStatItem(
                  theme,
                  title: '便签总数',
                  value: '${_notes.length}',
                  icon: Icons.sticky_note_2,
                ),
                _buildStatItem(
                  theme,
                  title: '学习便签',
                  value: '$_studyCount',
                  icon: Icons.school,
                ),
                _buildStatItem(
                  theme,
                  title: '项目便签',
                  value: '$_projectCount',
                  icon: Icons.code,
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildStatItem(
    ThemeData theme, {
    required String title,
    required String value,
    required IconData icon,
  }) {
    return Expanded(
      child: Column(
        children: [
          Icon(
            icon,
            color: theme.colorScheme.primary,
          ),
          const SizedBox(height: 6),
          Text(
            value,
            style: theme.textTheme.titleLarge?.copyWith(
              fontWeight: FontWeight.bold,
              color: theme.colorScheme.primary,
            ),
          ),
          const SizedBox(height: 2),
          Text(
            title,
            style: theme.textTheme.bodySmall?.copyWith(
              color: theme.colorScheme.onSurfaceVariant,
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildCategoryFilter(ThemeData theme) {
    return Card(
      elevation: 2,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(18),
      ),
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Wrap(
          spacing: 10,
          runSpacing: 10,
          children: _categories.map((category) {
            final bool selected = category == _selectedCategory;

            return ChoiceChip(
              label: Text(category),
              selected: selected,
              onSelected: (_) {
                _selectCategory(category);
              },
              selectedColor: theme.colorScheme.primaryContainer,
              labelStyle: TextStyle(
                color: selected
                    ? theme.colorScheme.onPrimaryContainer
                    : theme.colorScheme.onSurface,
                fontWeight: selected ? FontWeight.bold : FontWeight.normal,
              ),
            );
          }).toList(),
        ),
      ),
    );
  }

  Widget _buildMasonryNoteWall(ThemeData theme, List<NoteCard> notes) {
    if (notes.isEmpty) {
      return Card(
        elevation: 2,
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(18),
        ),
        child: Padding(
          padding: const EdgeInsets.all(24),
          child: Center(
            child: Text(
              '当前分类暂无便签',
              style: theme.textTheme.bodyMedium?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
              ),
            ),
          ),
        ),
      );
    }

    return Card(
      elevation: 2,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(18),
      ),
      child: Padding(
        padding: const EdgeInsets.all(14),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Padding(
              padding: const EdgeInsets.fromLTRB(6, 6, 6, 14),
              child: Row(
                children: [
                  Expanded(
                    child: Text(
                      '便签瀑布流',
                      style: theme.textTheme.titleLarge?.copyWith(
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  ),
                  Text(
                    '${notes.length} 条',
                    style: theme.textTheme.bodyMedium?.copyWith(
                      color: theme.colorScheme.onSurfaceVariant,
                    ),
                  ),
                ],
              ),
            ),
            MasonryGridView.count(
              crossAxisCount: 2,
              mainAxisSpacing: 12,
              crossAxisSpacing: 12,
              shrinkWrap: true,
              physics: const NeverScrollableScrollPhysics(),
              itemCount: notes.length,
              itemBuilder: (context, index) {
                return _buildNoteCard(theme, notes[index]);
              },
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildNoteCard(ThemeData theme, NoteCard note) {
    final bool selected = _selectedNote?.title == note.title;

    return InkWell(
      borderRadius: BorderRadius.circular(18),
      onTap: () {
        _selectNote(note);
      },
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 250),
        padding: const EdgeInsets.all(16),
        decoration: BoxDecoration(
          color: note.color.withOpacity(selected ? 0.20 : 0.12),
          borderRadius: BorderRadius.circular(18),
          border: Border.all(
            color: selected ? note.color : Colors.transparent,
            width: 1.4,
          ),
        ),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Icon(
              note.icon,
              color: note.color,
              size: 28,
            ),
            const SizedBox(height: 12),
            Text(
              note.title,
              style: theme.textTheme.titleMedium?.copyWith(
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 8),
            Text(
              note.content,
              style: theme.textTheme.bodySmall?.copyWith(
                height: 1.5,
                color: theme.colorScheme.onSurfaceVariant,
              ),
            ),
            const SizedBox(height: 12),
            Container(
              padding: const EdgeInsets.symmetric(
                horizontal: 10,
                vertical: 5,
              ),
              decoration: BoxDecoration(
                color: note.color.withOpacity(0.16),
                borderRadius: BorderRadius.circular(20),
              ),
              child: Text(
                note.category,
                style: theme.textTheme.labelSmall?.copyWith(
                  color: note.color,
                  fontWeight: FontWeight.bold,
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildSelectedNoteCard(ThemeData theme) {
    final NoteCard? note = _selectedNote;

    return Card(
      elevation: 2,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(18),
      ),
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: note == null
            ? Row(
                children: [
                  Icon(
                    Icons.touch_app,
                    color: theme.colorScheme.primary,
                  ),
                  const SizedBox(width: 12),
                  Expanded(
                    child: Text(
                      '点击任意便签,可以在这里查看当前选中的便签详情。',
                      style: theme.textTheme.bodyMedium?.copyWith(
                        color: theme.colorScheme.onSurfaceVariant,
                        height: 1.5,
                      ),
                    ),
                  ),
                ],
              )
            : Row(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Container(
                    width: 58,
                    height: 58,
                    decoration: BoxDecoration(
                      color: note.color.withOpacity(0.16),
                      borderRadius: BorderRadius.circular(18),
                    ),
                    child: Icon(
                      note.icon,
                      color: note.color,
                      size: 30,
                    ),
                  ),
                  const SizedBox(width: 14),
                  Expanded(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          note.title,
                          style: theme.textTheme.titleLarge?.copyWith(
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                        const SizedBox(height: 6),
                        Text(
                          note.category,
                          style: theme.textTheme.bodyMedium?.copyWith(
                            color: note.color,
                            fontWeight: FontWeight.w600,
                          ),
                        ),
                        const SizedBox(height: 10),
                        Text(
                          note.content,
                          style: theme.textTheme.bodyMedium?.copyWith(
                            color: theme.colorScheme.onSurfaceVariant,
                            height: 1.5,
                          ),
                        ),
                      ],
                    ),
                  ),
                ],
              ),
      ),
    );
  }

  Widget _buildLibraryCard(ThemeData theme) {
    return Card(
      elevation: 2,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(18),
      ),
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              '第三方库说明',
              style: theme.textTheme.titleLarge?.copyWith(
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 12),
            _buildInfoRow(
              theme,
              title: '库名称',
              value: 'flutter_staggered_grid_view',
            ),
            _buildInfoRow(
              theme,
              title: '配置文件',
              value: 'pubspec.yaml',
            ),
            _buildInfoRow(
              theme,
              title: '导入方式',
              value:
                  "import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';",
            ),
            _buildInfoRow(
              theme,
              title: '核心组件',
              value: 'MasonryGridView.count',
            ),
            _buildInfoRow(
              theme,
              title: '应用场景',
              value: '瀑布流布局、图片墙、便签墙、商品卡片、文章摘要',
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildInfoRow(
    ThemeData theme, {
    required String title,
    required String value,
  }) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 10),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          SizedBox(
            width: 82,
            child: Text(
              title,
              style: theme.textTheme.bodyMedium?.copyWith(
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
          Expanded(
            child: Text(
              value,
              style: theme.textTheme.bodyMedium?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

九、代码实现说明

1. 引入 flutter_staggered_grid_view 第三方库

代码开头引入第三方库:

import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';

这说明项目确实使用了 Flutter 第三方库,而不是 OpenHarmony 原生库。

本项目中主要使用:

MasonryGridView.count

它可以构建类似图片墙、便签墙的瀑布流布局。


2. 定义便签数据模型

项目中定义了便签模型:

class NoteCard {
  const NoteCard({
    required this.title,
    required this.content,
    required this.category,
    required this.icon,
    required this.color,
  });

  final String title;
  final String content;
  final String category;
  final IconData icon;
  final Color color;
}

字段说明如下:

字段 作用
title 便签标题
content 便签内容
category 便签分类
icon 便签图标
color 便签主题色

通过这个模型,可以统一管理每一张便签卡片的数据。


3. 使用 MasonryGridView.count 构建瀑布流

瀑布流布局的核心代码如下:

MasonryGridView.count(
  crossAxisCount: 2,
  mainAxisSpacing: 12,
  crossAxisSpacing: 12,
  shrinkWrap: true,
  physics: const NeverScrollableScrollPhysics(),
  itemCount: notes.length,
  itemBuilder: (context, index) {
    return _buildNoteCard(theme, notes[index]);
  },
)

其中:

参数 作用
crossAxisCount 设置横向列数
mainAxisSpacing 设置主轴方向间距
crossAxisSpacing 设置横轴方向间距
shrinkWrap 根据内容自动计算高度
physics 控制滚动行为
itemCount 便签数量
itemBuilder 构建每一张便签卡片

本项目中设置:

crossAxisCount: 2

表示页面使用双列瀑布流布局。


4. 为什么要设置 shrinkWrap 和 physics

因为页面外层已经使用了 ListView,如果内部的 MasonryGridView 也自己滚动,可能会产生嵌套滚动问题。

所以本项目中设置:

shrinkWrap: true

让瀑布流根据内容自动撑开高度。

同时设置:

physics: const NeverScrollableScrollPhysics()

禁止内部瀑布流单独滚动,让外层 ListView 统一负责滚动。

这一步很重要。否则页面可能出现滚动冲突,用户滑半天不知道自己滑的是哪一层。软件开发最无聊的痛苦之一,恭喜又见到了。


5. 实现分类筛选

页面上方使用 ChoiceChip 实现分类筛选:

ChoiceChip(
  label: Text(category),
  selected: selected,
  onSelected: (_) {
    _selectCategory(category);
  },
)

点击分类后调用:

void _selectCategory(String category) {
  setState(() {
    _selectedCategory = category;
    _selectedNote = null;
  });
}

这样可以切换当前分类,并刷新瀑布流中的便签内容。


6. 根据分类过滤便签

筛选逻辑如下:

List<NoteCard> get _filteredNotes {
  if (_selectedCategory == '全部') {
    return _notes;
  }

  return _notes.where((note) => note.category == _selectedCategory).toList();
}

如果选择“全部”,显示所有便签。

如果选择“学习”“生活”“项目”或“灵感”,只显示对应分类的便签。


7. 构建便签卡片

每张便签卡片由 _buildNoteCard() 构建:

Widget _buildNoteCard(ThemeData theme, NoteCard note) {
  return InkWell(
    onTap: () {
      _selectNote(note);
    },
    child: AnimatedContainer(...),
  );
}

其中使用了:

AnimatedContainer

让便签选中状态变化时有简单的动画效果。

便签内容包括:

  • 图标;
  • 标题;
  • 正文;
  • 分类标签;
  • 主题色边框。

不同便签的文字长度不同,因此卡片高度也不同,这正好适合瀑布流布局展示。


8. 点击便签查看详情

点击便签后执行:

void _selectNote(NoteCard note) {
  setState(() {
    _selectedNote = note;
  });
}

页面下方会显示当前选中的便签详情。

如果还没有选中便签,则显示提示文字:

点击任意便签,可以在这里查看当前选中的便签详情。

9. 使用 setState 更新页面

分类切换和便签点击都使用了 setState()

例如:

setState(() {
  _selectedCategory = category;
  _selectedNote = null;
});

Flutter 页面状态发生变化后,需要通过 setState() 通知页面重新构建。

不调用 setState(),数据变了页面也不会刷新。Flutter 不会读心,它只是框架,不是占卜摊。


十、运行项目

完成代码后,在终端执行:

flutter pub get

然后连接 OpenHarmony 设备或启动 OpenHarmony 模拟器。

查看设备:

flutter devices

运行项目:

flutter run

如果环境配置正确,应用会运行到 OpenHarmony 设备或模拟器中。

运行成功后,页面会显示“灵感便签瀑布流”。用户可以点击不同分类查看对应便签,也可以点击某一张便签查看详情。


十一、开发中遇到的问题

1. flutter_staggered_grid_view 依赖没有生效

如果代码中出现找不到 flutter_staggered_grid_view 的问题,可以检查 pubspec.yaml 中是否添加了:

flutter_staggered_grid_view: ^0.7.0

然后重新执行:

flutter pub get

如果还是不行,可以重启编辑器。


2. import 导入报错

如果下面代码报错:

import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';

通常有几种原因:

  • pubspec.yaml 中没有添加依赖;
  • 没有执行 flutter pub get
  • YAML 缩进错误;
  • 包名写错;
  • 编辑器没有刷新依赖。

其中 YAML 缩进最容易出问题。依赖必须写在 dependencies 下面,并且缩进要正确。一个空格能毁掉一天,编程世界真体贴。


3. MasonryGridView 没有显示

如果瀑布流没有显示,可以检查:

  • itemCount 是否大于 0;
  • itemBuilder 是否返回有效组件;
  • 是否正确引入第三方库;
  • 外层是否给了足够布局空间;
  • 是否存在滚动嵌套冲突。

本项目中使用:

shrinkWrap: true
physics: const NeverScrollableScrollPhysics()

是为了让它可以安全放在外层 ListView 中。


4. 页面滚动异常

如果页面滚动时卡顿或冲突,可以检查是否出现多个滚动组件嵌套。

本项目结构是:

ListView
 └── MasonryGridView.count

所以内部瀑布流需要关闭自身滚动:

physics: const NeverScrollableScrollPhysics()

让外层 ListView 负责整体滚动。


5. 卡片没有瀑布流效果

如果所有卡片看起来高度接近,瀑布流效果就不明显。

可以让不同便签内容长度不同,例如有的便签一句话,有的便签三四句话。卡片高度有差异后,瀑布流效果会更明显。

瀑布流不是魔法,它需要内容高度不同才能看出错落感。不然所有卡片一样高,它也只能整整齐齐地站军姿。


6. 点击分类后内容没有变化

如果点击分类后内容没有变化,可以检查:

setState(() {
  _selectedCategory = category;
});

同时检查过滤逻辑:

return _notes.where((note) => note.category == _selectedCategory).toList();

分类名称必须完全一致,例如 学习学习 就不是同一个字符串,后面多一个空格也会翻车。字符串比较就是这么冷血。


7. 运行不到 OpenHarmony 设备

如果项目无法运行到 OpenHarmony 设备或模拟器,可以检查:

  • Flutter for OpenHarmony 环境是否配置完成;
  • 设备是否连接成功;
  • flutter devices 是否能识别设备;
  • 是否执行了 flutter pub get
  • 是否选择了正确的运行设备;
  • 项目是否为 Flutter 项目,而不是原生鸿蒙项目。

如果 flutter devices 都识别不到设备,那应该先处理环境问题,而不是盯着瀑布流代码反复怀疑人生。


十二、本文和原生鸿蒙项目的区别

本文是 Flutter for OpenHarmony 第三方库实践,不是 OpenHarmony 原生 ArkTS 项目。

主要区别如下:

对比项 本文写法 原生鸿蒙写法
UI 技术 Flutter ArkUI
主要语言 Dart ArkTS
页面入口 lib/main.dart Index.ets
依赖配置 pubspec.yaml oh-package.json5
依赖安装 flutter pub get ohpm install
第三方库 flutter_staggered_grid_view OpenHarmony 原生库
页面组件 MaterialApp / Scaffold / MasonryGridView @Entry / @Component

因此本文符合 Flutter for OpenHarmony 第三方库实践方向。


十三、总结

本篇完成了一个基于 flutter_staggered_grid_view 的 Flutter for OpenHarmony 灵感便签瀑布流应用。项目通过 Flutter 第三方库实现 Masonry 瀑布流布局,并结合分类筛选和便签详情展示构建了一个完整的内容卡片页面。

通过本次实践,我主要完成了以下内容:

  • 创建 Flutter for OpenHarmony 项目;
  • pubspec.yaml 中添加 flutter_staggered_grid_view 依赖;
  • 使用 flutter pub get 获取第三方库;
  • lib/main.dart 中引入第三方库;
  • 使用 MasonryGridView.count() 构建瀑布流布局;
  • 使用不同高度的卡片展示便签内容;
  • 使用 ChoiceChip 实现分类筛选;
  • 使用 InkWell 实现便签点击交互;
  • 使用 setState() 更新分类和选中便签;
  • 使用 Flutter Material 组件构建完整页面;
  • 将项目运行到 OpenHarmony 设备或模拟器中。

这个项目虽然只是一个基础便签墙应用,但完整展示了 Flutter for OpenHarmony 项目中第三方库的使用流程。

后续可以在这个基础上继续扩展,例如:

  • 添加新建便签功能;
  • 添加删除便签功能;
  • 添加编辑便签功能;
  • 添加本地数据保存;
  • 添加搜索功能;
  • 添加标签管理;
  • 添加图片便签;
  • 添加长按排序;
  • 添加暗色主题;
  • 添加云端同步。

整体来看,flutter_staggered_grid_view 可以帮助 Flutter 开发者快速实现瀑布流布局。通过这个项目,可以理解 Flutter for OpenHarmony 中第三方库依赖配置、Masonry 瀑布流组件使用和页面状态更新之间的基本关系。

Logo

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

更多推荐