在这里插入图片描述

做一个应用,最重要的是先把架构想清楚。今天我们来聊聊如何设计一个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个卡片。crossAxisSpacingmainAxisSpacing都用.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,有轻微的阴影效果。

Containerdecoration用了渐变背景,从深到浅,看起来有层次感。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.shade400Colors.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的响应式状态管理,让数据流更清晰。用ObxGetX组件,代码会更简洁。

主题切换
可以加深色模式,让用户自己选择。Flutter的主题系统很完善,切换主题只需要改一个变量。

国际化
intl包已经引入了,后续可以支持多语言。虽然当前版本只有中文,但架构上已经准备好了。

数据持久化
shared_preferences可以保存用户的使用习惯和偏好设置。比如记住用户最常用的工具,下次打开直接显示。

功能模块化
如果某个功能变复杂了,可以拆成独立的package。这样不同的应用也能复用这些功能。

开发经验总结

做这个项目的过程中,我总结了几点经验:

先简单后复杂
一开始不要想太多,先把基础架构搭起来。等功能多了,自然就知道该怎么优化了。过早优化是万恶之源。

保持一致性
三个页面用了相同的布局和交互模式,这不是偷懒,而是有意为之。一致性让用户学习成本更低。

数据驱动UI
用数组存储功能列表,通过遍历生成UI。这种方式让代码很灵活,添加功能只需要改数据,不需要改逻辑。

适度抽象
不要为了复用而复用。如果抽象会增加理解成本,那就保持简单。代码是给人看的,清晰比简洁更重要。

预留扩展
虽然当前版本功能简单,但架构上要为未来考虑。路由、状态管理、国际化这些接口都预留好了。

小结

一个好的架构不是一开始就设计得很复杂,而是在简单清晰的基础上,为未来的扩展留好接口。

我们的架构做到了:模块清晰、易于扩展、性能良好、代码规范。这些都是通过实践总结出来的经验。

做项目最怕的就是一开始图省事,后面改起来痛苦。花点时间把架构想清楚,后面的开发会顺利很多。

这个架构虽然简单,但该有的都有了。状态管理、屏幕适配、底部导航、页面组织,每个部分都考虑到了。更重要的是,它为未来的扩展留了足够的空间。

记住,好的架构是演进出来的,不是一开始就设计出来的。从简单开始,根据需求逐步完善,这才是正确的做法。


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

Logo

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

更多推荐