Flutter for OpenHarmony 第三方库实战:使用 flutter_staggered_grid_view 构建灵感便签瀑布流应用
欢迎加入开源鸿蒙跨平台社区:
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.yaml、lib/main.dart、import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';,这才是正确方向。别再把 ArkTS 塞进来,题目要吃饭,你递过去一块砖,当然不通过。
七、核心实现思路
本项目的核心流程如下:
- 在
pubspec.yaml中添加flutter_staggered_grid_view; - 在
main.dart中引入第三方库; - 定义便签数据模型;
- 准备不同分类、不同长度的便签内容;
- 使用分类按钮筛选便签;
- 使用
MasonryGridView.count()构建瀑布流; - 使用
crossAxisCount设置双列布局; - 使用
mainAxisSpacing和crossAxisSpacing设置卡片间距; - 点击便签后展示当前便签详情。
第三方库引入代码如下:
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 瀑布流组件使用和页面状态更新之间的基本关系。
更多推荐
所有评论(0)