Flutter for OpenHarmony Web开发助手App实战:项目架构设计
先简单后复杂一开始不要想太多,先把基础架构搭起来。等功能多了,自然就知道该怎么优化了。过早优化是万恶之源。保持一致性三个页面用了相同的布局和交互模式,这不是偷懒,而是有意为之。一致性让用户学习成本更低。数据驱动UI用数组存储功能列表,通过遍历生成UI。这种方式让代码很灵活,添加功能只需要改数据,不需要改逻辑。适度抽象不要为了复用而复用。如果抽象会增加理解成本,那就保持简单。代码是给人看的,清晰比简

做一个应用,最重要的是先把架构想清楚。今天我们来聊聊如何设计一个Web开发助手应用的整体架构。
项目定位与功能规划
这个应用是为Web开发者准备的工具集合。包含30个实用功能,分成4个大模块:
- 首页工具:HTML预览、CSS生成器、JS格式化等8个常用功能
- 开发工具:单位转换、Base64编码、MD5生成等8个专业工具
- 参考资料:HTML参考、CSS参考、HTTP状态码等8个查询手册
- 个人中心:收藏夹、历史记录、设置等6个管理功能
每个模块都有自己的职责,但又能无缝协作。这种设计让应用既功能丰富,又不会显得杂乱。用户打开应用就能快速找到需要的工具,不用在复杂的菜单里翻来翻去。
技术选型与依赖管理
我们选择Flutter for OpenHarmony作为开发框架。为什么?因为它能让我们用一套代码同时支持OpenHarmony和其他平台。这对于工具类应用来说特别重要,用户可能在不同设备上都需要用到。
依赖包的选择也很关键,每个包都要经过仔细考虑:
dependencies:
flutter:
sdk: flutter
get: ^4.6.5
flutter_screenutil: ^5.9.0
convex_bottom_bar: ^3.0.0
intl: ^0.20.2
crypto: ^3.0.3
get是整个应用的核心,负责状态管理和路由。它的API简洁,性能也不错,特别适合这种中小型应用。
flutter_screenutil解决屏幕适配问题。我们设置设计稿尺寸为375x812,然后所有的尺寸都用.w和.h来标注,这样在不同屏幕上都能保持合适的比例。
convex_bottom_bar提供漂亮的底部导航栏。它的凸起效果比Flutter原生的BottomNavigationBar好看很多,而且动画流畅。
intl处理国际化和日期格式化。虽然当前版本只支持中文,但预留了多语言的可能性。时间戳转换等功能也会用到它。
crypto提供MD5、SHA等加密算法。这些都是纯Dart实现的,不依赖平台特性,兼容性很好。
这些依赖都是经过OpenHarmony适配的版本,稳定性有保证。我们不会随便引入没用过的包,每个包都要确保它真的能解决问题。
目录结构设计
lib/
├── main.dart # 应用入口
├── all_pages.dart # 所有页面实现
└── pages/ # 页面目录(预留扩展)
我们采用扁平化的结构。对于这种工具类应用,不需要太复杂的层级。所有页面都放在all_pages.dart中,方便管理和查找。
有人可能会问,为什么不按模块分文件?因为我们的页面都比较简单,放在一起反而更容易维护。如果某个页面变复杂了,再拆出去也不迟。
pages/目录是预留的,后续如果需要添加复杂的页面,可以单独放在这里。这种设计既保持了当前的简洁,又为未来留了空间。
应用入口实现
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:convex_bottom_bar/convex_bottom_bar.dart';
import 'all_pages.dart';
void main() => runApp(const MyApp());
入口很简洁,就一行代码。runApp是Flutter的标准启动方式,传入根Widget就行。
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return ScreenUtilInit(
designSize: const Size(375, 812),
builder: (context, child) => GetMaterialApp(
title: 'Web开发助手',
theme: ThemeData(
primarySwatch: Colors.blue,
useMaterial3: true,
),
home: const MainApp(),
debugShowCheckedModeBanner: false,
),
);
}
}
ScreenUtilInit包裹整个应用,设置设计稿尺寸为375x812。这是iPhone 13的尺寸,也是目前比较主流的手机屏幕比例。
GetMaterialApp替代了MaterialApp,这样就能使用GetX的所有功能。debugShowCheckedModeBanner设为false,去掉右上角的debug标签,让界面更干净。
useMaterial3开启Material 3设计规范,界面会更现代一些。虽然变化不大,但细节上会更精致。
主框架搭建
class MainApp extends StatefulWidget {
const MainApp({Key? key}) : super(key: key);
State<MainApp> createState() => _MainAppState();
}
主框架用StatefulWidget,因为需要维护当前选中的Tab索引。这是最基础的状态管理,不需要用复杂的方案。
class _MainAppState extends State<MainApp> {
int _selectedIndex = 0;
final List<Widget> _pages = [
const HomePage(),
const ToolsPage(),
const ReferencePage(),
const ProfilePage(),
];
_selectedIndex记录当前选中的Tab,初始值是0,也就是首页。
_pages数组存储4个页面实例。这里有个关键点:我们在初始化时就创建了所有页面,而不是每次切换时重新创建。这样做的好处是页面状态会被保留,用户在某个页面的操作,切换回来后还在。
比如用户在JSON查看器里输入了一段JSON,切换到工具页再切回来,输入的内容还在。这种体验比每次都重新创建页面要好得多。
Widget build(BuildContext context) {
return Scaffold(
body: _pages[_selectedIndex],
bottomNavigationBar: ConvexAppBar(
style: TabStyle.react,
backgroundColor: Colors.blue,
items: const [
TabItem(icon: Icons.home, title: '首页'),
TabItem(icon: Icons.build, title: '工具'),
TabItem(icon: Icons.library_books, title: '参考'),
TabItem(icon: Icons.person, title: '我的'),
],
initialActiveIndex: _selectedIndex,
onTap: (index) => setState(() => _selectedIndex = index),
),
);
}
}
Scaffold是Flutter的基础布局组件,提供了AppBar、Body、BottomNavigationBar等标准结构。
body直接用_pages[_selectedIndex],通过索引来显示对应的页面。这种方式很简单,但很有效。
ConvexAppBar是我们选择的底部导航组件。TabStyle.react会在点击时产生反应式动画,用户体验更好。每个Tab都有图标和标题,图标用Material Icons,不需要额外的资源文件。
onTap回调里用setState更新索引,触发重建,显示新的页面。这是Flutter最基础的状态更新方式,简单可靠。
首页模块设计
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
Widget build(BuildContext context) {
final features = [
{'title': 'HTML预览', 'icon': Icons.code, 'color': Colors.orange, 'route': '/html'},
{'title': 'CSS生成器', 'icon': Icons.palette, 'color': Colors.blue, 'route': '/css'},
{'title': 'JS格式化', 'icon': Icons.format_align_left, 'color': Colors.purple, 'route': '/js'},
{'title': '颜色选择器', 'icon': Icons.color_lens, 'color': Colors.pink, 'route': '/color'},
{'title': '响应式检查', 'icon': Icons.phone_android, 'color': Colors.green, 'route': '/responsive'},
{'title': 'API测试', 'icon': Icons.api, 'color': Colors.red, 'route': '/api'},
{'title': 'JSON查看器', 'icon': Icons.data_object, 'color': Colors.teal, 'route': '/json'},
{'title': '正则表达式', 'icon': Icons.text_fields, 'color': Colors.indigo, 'route': '/regex'},
];
首页用StatelessWidget,因为它不需要维护状态。功能列表用数组存储,这是数据驱动UI的典型做法。
每个功能都是一个Map,包含标题、图标、颜色和路由。这种结构很清晰,要添加新功能只需要在数组里加一项,不需要改动其他代码。
图标的选择很讲究。Icons.code代表HTML,Icons.palette代表CSS,Icons.format_align_left代表格式化,这些图标能让用户一眼就明白功能。
颜色也是精心挑选的。我们用了8种不同的颜色,每个功能都有自己的视觉标识。这样用户看到颜色就能想起对应的功能,形成记忆点。
return Scaffold(
appBar: AppBar(title: const Text('Web开发助手')),
body: GridView.builder(
padding: EdgeInsets.all(16.w),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12.w,
mainAxisSpacing: 12.w,
),
itemCount: features.length,
itemBuilder: (context, index) {
final f = features[index];
return _buildCard(
f['title'] as String,
f['icon'] as IconData,
f['color'] as Color,
f['route'] as String,
);
},
),
);
}
GridView.builder比直接用GridView性能更好,特别是列表项多的时候。虽然我们只有8个项,但养成好习惯很重要。
crossAxisCount设为2,每行显示2个卡片。crossAxisSpacing和mainAxisSpacing都用.w来适配,保证在不同屏幕上间距比例一致。
itemBuilder里从Map中取出数据,传给_buildCard方法。这里需要做类型转换,因为Map的值是dynamic类型。
Widget _buildCard(String title, IconData icon, Color color, String route) {
return GestureDetector(
onTap: () => Get.toNamed(route),
child: Card(
elevation: 4,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.r),
gradient: LinearGradient(
colors: [
color.withOpacity(0.8),
color.withOpacity(0.4),
],
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 40.sp, color: Colors.white),
SizedBox(height: 12.h),
Text(
title,
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
);
}
}
_buildCard是一个可复用的组件方法。虽然现在只在首页用,但保持这种组件化的思维很重要。
GestureDetector处理点击事件,用Get.toNamed跳转到对应的路由。虽然当前版本还没实现路由,但预留了接口,后续扩展很方便。
Card提供基础的卡片样式,elevation设为4,有轻微的阴影效果。
Container的decoration用了渐变背景,从深到浅,看起来有层次感。borderRadius用.r来适配,保证圆角在不同屏幕上都合适。
图标和文字都用白色,在彩色背景上很醒目。图标大小40.sp,文字14.sp,这个比例在各种屏幕上都比较协调。
工具页模块实现
class ToolsPage extends StatelessWidget {
const ToolsPage({Key? key}) : super(key: key);
Widget build(BuildContext context) {
final tools = [
{'title': '单位转换', 'icon': Icons.straighten, 'color': Colors.amber},
{'title': 'Base64编码', 'icon': Icons.vpn_key, 'color': Colors.cyan},
{'title': 'MD5生成', 'icon': Icons.fingerprint, 'color': Colors.lime},
{'title': '二维码生成', 'icon': Icons.qr_code_2, 'color': Colors.deepOrange},
{'title': 'URL编码', 'icon': Icons.link, 'color': Colors.lightBlue},
{'title': '时间戳转换', 'icon': Icons.schedule, 'color': Colors.deepPurple},
{'title': '密码生成器', 'icon': Icons.security, 'color': Colors.red},
{'title': '文本统计', 'icon': Icons.text_fields, 'color': Colors.brown},
];
工具页的结构和首页类似,也是8个功能的网格布局。不同的是这里的工具更偏向于数据处理和转换。
每个工具都选了合适的图标和颜色。Icons.fingerprint代表MD5,Icons.qr_code_2代表二维码,Icons.security代表密码生成器,这些图标都很形象。
颜色的选择也有讲究。我们用了不同的色系,避免视觉疲劳。amber、cyan、lime这些颜色都比较鲜艳,在小卡片上显示效果好。
return Scaffold(
appBar: AppBar(title: const Text('开发工具')),
body: GridView.builder(
padding: EdgeInsets.all(16.w),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12.w,
mainAxisSpacing: 12.w,
),
itemCount: tools.length,
itemBuilder: (context, index) {
final t = tools[index];
return Card(
elevation: 4,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.r),
gradient: LinearGradient(
colors: [
(t['color'] as Color).withOpacity(0.8),
(t['color'] as Color).withOpacity(0.4),
],
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
t['icon'] as IconData,
size: 40.sp,
color: Colors.white,
),
SizedBox(height: 12.h),
Text(
t['title'] as String,
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
fontWeight: FontWeight.bold,
),
),
],
),
),
);
},
),
);
}
}
工具页的实现和首页几乎一样,只是数据不同。这种一致性很重要,让用户在不同模块间切换时不会感到困惑。
我们没有把卡片的构建逻辑提取成公共方法,因为两个页面的代码已经很简单了。过度抽象反而会增加理解成本。
参考页模块构建
class ReferencePage extends StatelessWidget {
const ReferencePage({Key? key}) : super(key: key);
Widget build(BuildContext context) {
final refs = [
{'title': 'HTML参考', 'icon': Icons.language, 'color': Colors.orange},
{'title': 'CSS参考', 'icon': Icons.style, 'color': Colors.blue},
{'title': 'JavaScript参考', 'icon': Icons.code, 'color': Colors.yellow},
{'title': 'HTTP状态码', 'icon': Icons.http, 'color': Colors.green},
{'title': '快捷键', 'icon': Icons.keyboard, 'color': Colors.grey},
{'title': '颜色名称', 'icon': Icons.palette, 'color': Colors.purple},
{'title': 'HTML实体', 'icon': Icons.text_fields, 'color': Colors.red},
{'title': 'CSS单位', 'icon': Icons.straighten, 'color': Colors.teal},
];
参考页收集了Web开发中常用的参考资料。HTML、CSS、JavaScript的参考手册是基础,HTTP状态码和快捷键也很实用。
这个模块的特点是内容相对静态,主要是查询和浏览。所以界面设计上保持简洁,让用户能快速找到需要的信息。
图标的选择尽量贴近内容。Icons.language代表HTML,Icons.style代表CSS,Icons.keyboard代表快捷键,这些都是很直观的对应关系。
return Scaffold(
appBar: AppBar(title: const Text('参考资料')),
body: GridView.builder(
padding: EdgeInsets.all(16.w),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12.w,
mainAxisSpacing: 12.w,
),
itemCount: refs.length,
itemBuilder: (context, index) {
final r = refs[index];
return Card(
elevation: 4,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.r),
gradient: LinearGradient(
colors: [
(r['color'] as Color).withOpacity(0.8),
(r['color'] as Color).withOpacity(0.4),
],
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
r['icon'] as IconData,
size: 40.sp,
color: Colors.white,
),
SizedBox(height: 12.h),
Text(
r['title'] as String,
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
fontWeight: FontWeight.bold,
),
),
],
),
),
);
},
),
);
}
}
参考页的实现延续了前面的风格,保持了整个应用的一致性。用户在使用时会感觉很自然,不需要重新学习交互方式。
三个页面用了相同的布局和交互模式,这是有意为之的。一致性是好的用户体验的基础。
个人中心模块设计
class ProfilePage extends StatelessWidget {
const ProfilePage({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('我的')),
body: ListView(
padding: EdgeInsets.all(16.w),
children: [
Container(
padding: EdgeInsets.all(20.w),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue.shade400, Colors.blue.shade600],
),
borderRadius: BorderRadius.circular(12.r),
),
child: Column(
children: [
CircleAvatar(
radius: 40.r,
backgroundColor: Colors.white,
child: Icon(Icons.person, size: 40.sp, color: Colors.blue),
),
SizedBox(height: 12.h),
Text(
'Web开发助手',
style: TextStyle(
color: Colors.white,
fontSize: 18.sp,
fontWeight: FontWeight.bold,
),
),
],
),
),
个人中心用ListView布局,因为这里的内容是纵向排列的列表。顶部是用户信息卡片,用渐变蓝色背景,看起来比较高级。
CircleAvatar做头像,虽然现在只是一个图标,但后续可以很容易地替换成真实头像。半径40.r,在不同屏幕上都能保持合适的大小。
渐变色用了Colors.blue.shade400到Colors.blue.shade600,这是Material Design的标准色阶。不需要自己调颜色,用系统提供的就很好看。
SizedBox(height: 24.h),
_buildListTile(Icons.favorite, '收藏夹', Colors.red),
_buildListTile(Icons.history, '历史记录', Colors.blue),
_buildListTile(Icons.settings, '设置', Colors.grey),
_buildListTile(Icons.info, '关于', Colors.green),
_buildListTile(Icons.feedback, '反馈', Colors.orange),
_buildListTile(Icons.star, '快速访问', Colors.purple),
],
),
);
}
下面是6个功能入口。收藏夹、历史记录、设置这些是工具类应用的标配。快速访问可以让用户把常用功能固定在这里,提高效率。
每个功能都用不同的颜色,让界面更活泼。这些颜色不是随便选的,都是Material Design的标准色,搭配起来很和谐。
Widget _buildListTile(IconData icon, String title, Color color) {
return Card(
margin: EdgeInsets.only(bottom: 8.h),
child: ListTile(
leading: Icon(icon, color: color),
title: Text(title),
trailing: const Icon(Icons.arrow_forward_ios),
),
);
}
}
ListTile是Flutter里做列表项的标准组件。leading放图标,title放文字,trailing放箭头,这是最常见的布局。
每个ListTile外面包一层Card,加上margin,这样列表项之间有间隔,不会挤在一起。这个小细节让界面看起来更舒服。
箭头用Icons.arrow_forward_ios,这是iOS风格的箭头,比Android的箭头更细腻。虽然我们用的是Material Design,但借鉴iOS的设计元素也没问题。
架构优势分析
这种架构设计有几个明显的好处:
模块清晰
4个Tab对应4个模块,职责分明。首页是常用工具,工具页是专业功能,参考页是查询资料,个人中心是设置管理。用户一看就知道该去哪里找功能。
易于扩展
要添加新功能,只需要在对应模块的数组里加一项。不需要改动其他代码,降低了出错的风险。这种数据驱动的方式让代码很灵活。
状态保持
页面实例存在数组里,切换Tab时状态不会丢失。用户在某个页面的操作,切换回来后还在,体验很好。这是通过简单的设计就能达到的效果。
性能优化
用GridView.builder而不是GridView,只渲染可见的项。虽然我们的列表不长,但养成好习惯很重要。这种细节积累起来,应用的性能就会很好。
代码复用
虽然三个页面的代码看起来很相似,但我们没有过度抽象。因为每个页面的数据不同,强行抽象反而会增加复杂度。保持适度的重复,代码反而更清晰。
性能优化策略
虽然是个小应用,但性能优化的意识要有:
const构造
能用const的地方都用const,比如const HomePage()。这样Flutter会复用实例,减少内存占用。这是最简单但很有效的优化。
builder模式GridView.builder只渲染可见的项,比一次性渲染所有项效率高。虽然我们只有8个项,但这个习惯要保持。
状态管理
用StatefulWidget管理状态,避免不必要的重建。只有_selectedIndex变化时才重建,其他部分不受影响。这是最基础的性能优化。
图标资源
用Material Icons而不是图片,加载快,占用小,还能自动适配主题。这些图标都是矢量的,放大缩小都不会失真。
屏幕适配
用flutter_screenutil统一处理尺寸,不需要在每个地方都计算。.w、.h、.sp、.r这些后缀让代码很清晰,一眼就知道是什么单位。
代码规范与最佳实践
我们遵循几个简单的原则:
命名规范
私有方法用下划线开头,变量名用驼峰命名。这些都是Flutter的约定,遵守规范让代码更易读。
组件化思维_buildCard、_buildListTile这些方法都是可复用的组件。虽然现在只在一个地方用,但保持这种习惯很重要。
数据驱动UI
功能列表用数组存储,通过遍历生成UI。这样添加功能不需要写重复代码,维护起来也方便。
一个文件一个职责main.dart是入口,all_pages.dart是页面集合。职责清晰,不会混乱。如果某个页面变复杂了,再拆出去也不迟。
适度抽象
不要过度设计,保持简单。三个页面的代码虽然相似,但我们没有强行抽象成一个通用组件。因为它们的数据结构不同,抽象反而会增加复杂度。
未来扩展方向
这个架构为未来的扩展留了很多空间:
路由系统
现在的Get.toNamed还没实现,后续可以加上完整的路由配置。GetX的路由很强大,支持参数传递、中间件、过渡动画等。
状态管理
可以引入GetX的响应式状态管理,让数据流更清晰。用Obx和GetX组件,代码会更简洁。
主题切换
可以加深色模式,让用户自己选择。Flutter的主题系统很完善,切换主题只需要改一个变量。
国际化intl包已经引入了,后续可以支持多语言。虽然当前版本只有中文,但架构上已经准备好了。
数据持久化shared_preferences可以保存用户的使用习惯和偏好设置。比如记住用户最常用的工具,下次打开直接显示。
功能模块化
如果某个功能变复杂了,可以拆成独立的package。这样不同的应用也能复用这些功能。
开发经验总结
做这个项目的过程中,我总结了几点经验:
先简单后复杂
一开始不要想太多,先把基础架构搭起来。等功能多了,自然就知道该怎么优化了。过早优化是万恶之源。
保持一致性
三个页面用了相同的布局和交互模式,这不是偷懒,而是有意为之。一致性让用户学习成本更低。
数据驱动UI
用数组存储功能列表,通过遍历生成UI。这种方式让代码很灵活,添加功能只需要改数据,不需要改逻辑。
适度抽象
不要为了复用而复用。如果抽象会增加理解成本,那就保持简单。代码是给人看的,清晰比简洁更重要。
预留扩展
虽然当前版本功能简单,但架构上要为未来考虑。路由、状态管理、国际化这些接口都预留好了。
小结
一个好的架构不是一开始就设计得很复杂,而是在简单清晰的基础上,为未来的扩展留好接口。
我们的架构做到了:模块清晰、易于扩展、性能良好、代码规范。这些都是通过实践总结出来的经验。
做项目最怕的就是一开始图省事,后面改起来痛苦。花点时间把架构想清楚,后面的开发会顺利很多。
这个架构虽然简单,但该有的都有了。状态管理、屏幕适配、底部导航、页面组织,每个部分都考虑到了。更重要的是,它为未来的扩展留了足够的空间。
记住,好的架构是演进出来的,不是一开始就设计出来的。从简单开始,根据需求逐步完善,这才是正确的做法。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)