首页是用户打开应用后看到的第一个界面,需要在有限的空间内展示最重要的信息。对于理财应用来说,用户最关心的是:我有多少钱、这个月花了多少、最近的收支记录。这篇文章详细介绍首页的设计思路和实现细节。
请添加图片描述

首页布局规划

首页采用卡片式布局,从上到下依次是:

  1. 资产总览卡片 - 展示总资产、净资产、本月结余
  2. 月份选择器 - 切换查看不同月份的数据
  3. 月度收支统计 - 本月收入和支出的汇总
  4. 快捷功能入口 - 账户、分类、报表、目标的快速入口
  5. 最近交易记录 - 最新的几笔收支记录

这种布局信息密度适中,用户一眼就能看到关键数据,同时也能快速进入其他功能。卡片式设计让每个信息区块有清晰的边界,用户阅读时不会感到混乱。

页面基础结构

先看 HomePage 的整体框架,包括导入依赖和颜色常量定义:

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import '../../core/services/category_service.dart';
import '../../data/models/transaction_model.dart';
import '../../routes/app_pages.dart';
import 'home_controller.dart';

const _primaryColor = Color(0xFF2E7D32);
const _incomeColor = Color(0xFF4CAF50);
const _expenseColor = Color(0xFFE53935);
const _textSecondary = Color(0xFF757575);

导入部分包含了 Flutter 核心库、屏幕适配库、GetX 状态管理、日期格式化库,以及项目内部的服务和模型。这些依赖各司其职,flutter_screenutil 负责屏幕适配,intl 负责日期格式化,GetX 负责状态管理和路由导航。

颜色常量定义在文件顶部,_incomeColor 用绿色表示收入,_expenseColor 用红色表示支出,这是财务软件的通用配色方案。用户看到绿色就知道是收入,看到红色就知道是支出,不需要额外的学习成本。_textSecondary 用于次要文字,灰色调不会抢主要内容的风头。

页面类定义和 build 方法的整体结构:

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

  
  Widget build(BuildContext context) {
    final controller = Get.put(HomeController());
    return Scaffold(
      appBar: AppBar(
        title: const Text('个人理财管家'),
        actions: [
          IconButton(
            icon: const Icon(Icons.search), 
            onPressed: () => Get.toNamed(Routes.search)
          ),
          IconButton(
            icon: const Icon(Icons.calendar_month), 
            onPressed: () => Get.toNamed(Routes.calendar)
          ),
        ],
      ),

HomePage 继承自 StatelessWidget,因为所有状态都由 HomeController 管理,页面本身不需要持有状态。Get.put 在 build 方法中调用,GetX 会自动处理重复注册的情况,如果 Controller 已存在就返回现有实例。

AppBar 右侧放了两个快捷入口:搜索和日历。搜索图标让用户能快速查找历史记录,日历图标提供按日期浏览的视图。这两个功能使用频率较高,放在首页顶部可以减少用户的操作路径,提升使用效率。actions 数组可以放多个按钮,Flutter 会自动处理它们的排列。

页面主体使用 RefreshIndicator 实现下拉刷新:

      body: RefreshIndicator(
        onRefresh: () async => controller.refresh(),
        child: SingleChildScrollView(
          physics: const AlwaysScrollableScrollPhysics(),
          padding: EdgeInsets.all(16.w),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              _buildOverviewCard(controller),
              SizedBox(height: 16.h),
              _buildMonthSelector(controller),
              SizedBox(height: 16.h),
              _buildMonthlyStats(controller),
              SizedBox(height: 16.h),
              _buildQuickActions(),
              SizedBox(height: 16.h),
              _buildRecentTransactions(controller),
            ],
          ),
        ),
      ),
    );
  }

RefreshIndicator 实现下拉刷新功能,这是移动端应用的标准交互模式。用户下拉页面时会触发 onRefresh 回调,调用 controller.refresh() 重新加载数据。SingleChildScrollView 让整个页面可以滚动,AlwaysScrollableScrollPhysics 确保即使内容不足一屏也能触发下拉刷新。

padding 使用 16.w 作为统一的边距,.w 是 flutter_screenutil 的扩展方法,会根据屏幕宽度自动缩放。Column 的 children 按顺序排列各个组件,SizedBox 用于组件之间的间距,16.h 是一个舒适的间距值,既不会太紧凑也不会太松散。

资产总览卡片

资产总览是首页最重要的信息区域,用主题色背景突出显示:

Widget _buildOverviewCard(HomeController controller) {
  return Card(
    color: _primaryColor,
    child: Padding(
      padding: EdgeInsets.all(20.w),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('总资产', 
            style: TextStyle(color: Colors.white70, fontSize: 14.sp)),
          SizedBox(height: 8.h),
          Obx(() => Text(
            '${controller.currency}${controller.totalAssets.value.toStringAsFixed(2)}',
            style: TextStyle(
              color: Colors.white, 
              fontSize: 28.sp, 
              fontWeight: FontWeight.bold
            )
          )),

Card 组件的 color 属性设为主题色,和白色文字形成强对比,视觉上非常突出。这种设计让用户一眼就能看到最重要的数字。Padding 设为 20.w,比普通卡片稍大一些,让内容有更多呼吸空间。

"总资产"标签用 white70 半透明白色,不会太抢眼但又清晰可见。总资产金额用 28.sp 的大字号显示,这是页面上最大的文字,强调其重要性。toStringAsFixed(2) 保留两位小数,金额显示更专业。Obx 包裹 Text 组件,当 totalAssets 变化时只会重建这一小块 UI。

资产总览卡片的下半部分显示净资产和本月结余:

          SizedBox(height: 16.h),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              _buildAssetItem('净资产', controller),
              _buildBalanceItem('本月结余', controller),
            ],
          ),
        ],
      ),
    ),
  );
}

Row 组件让两个指标水平排列,MainAxisAlignment.spaceBetween 让它们分布在两端。净资产 = 总资产 - 负债,对于有信用卡的用户来说这个数字更有参考价值,因为信用卡欠款会影响实际可用资金。本月结余 = 本月收入 - 本月支出,反映当月的财务状况,让用户知道这个月是存钱了还是超支了。

这种布局在视觉上形成了一个倒三角结构:顶部是最重要的总资产,下方是两个次要指标。用户的视线自然从上往下移动,信息层次非常清晰。

净资产指标的实现:

Widget _buildAssetItem(String label, HomeController controller) {
  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Text(label, 
        style: TextStyle(color: Colors.white70, fontSize: 12.sp)),
      SizedBox(height: 4.h),
      Obx(() => Text(
        '${controller.currency}${controller.netWorth.value.toStringAsFixed(2)}',
        style: TextStyle(
          color: Colors.white, 
          fontSize: 16.sp, 
          fontWeight: FontWeight.w500
        )
      )),
    ],
  );
}

_buildAssetItem 方法构建单个指标项,采用上下结构:标签在上,数值在下。标签用 12.sp 的小字号和半透明白色,数值用 16.sp 的中等字号和纯白色。这种大小和颜色的对比让数值更突出,标签起到说明作用但不会喧宾夺主。

Column 的 crossAxisAlignment 设为 start,让内容左对齐。fontWeight 设为 w500 是中等粗细,比普通文字稍粗但不像 bold 那么重,在视觉上形成适度的强调。每个数据项都用 Obx 包裹,这样当数据变化时只会重建这一小块,而不是整个卡片。

本月结余指标的实现与净资产类似:

Widget _buildBalanceItem(String label, HomeController controller) {
  return Column(
    crossAxisAlignment: CrossAxisAlignment.end,
    children: [
      Text(label, 
        style: TextStyle(color: Colors.white70, fontSize: 12.sp)),
      SizedBox(height: 4.h),
      Obx(() => Text(
        '${controller.currency}${controller.monthlyBalance.toStringAsFixed(2)}',
        style: TextStyle(
          color: Colors.white, 
          fontSize: 16.sp, 
          fontWeight: FontWeight.w500
        )
      )),
    ],
  );
}

_buildBalanceItem 和 _buildAssetItem 的主要区别是 crossAxisAlignment 设为 end,让内容右对齐。这样两个指标一左一右,形成对称的布局。右对齐的设计也符合阅读习惯,因为本月结余放在右边,用户的视线从左到右扫过时,最后停留在这个数字上。

monthlyBalance 是一个计算属性,等于 monthlyIncome - monthlyExpense。这个值可能是正数也可能是负数,正数表示本月有结余,负数表示本月超支。在实际应用中,可以根据正负值显示不同的颜色来提醒用户。

月份选择器

用户可能想查看历史月份的数据,月份选择器提供这个能力:

Widget _buildMonthSelector(HomeController controller) {
  return Obx(() => Row(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      IconButton(
        icon: const Icon(Icons.chevron_left), 
        onPressed: controller.previousMonth
      ),
      Text(
        DateFormat('yyyy年MM月').format(controller.selectedMonth.value),
        style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600)
      ),
      IconButton(
        icon: const Icon(Icons.chevron_right), 
        onPressed: controller.nextMonth
      ),
    ],
  ));
}

月份选择器采用经典的左右箭头加中间文字的布局,这种设计用户非常熟悉,几乎不需要学习就能使用。左箭头切换到上个月,右箭头切换到下个月,中间显示当前选中的年月。

DateFormat 来自 intl 包,‘yyyy年MM月’ 格式化为 “2024年01月” 这样的中文格式。Row 的 mainAxisAlignment 设为 center 让整个选择器居中显示。整个组件用 Obx 包裹,当 selectedMonth 变化时会自动更新显示。

这个组件没有用 Card 包裹,视觉上更轻量,不会抢资产卡片的风头。IconButton 自带点击反馈效果,用户点击时会有水波纹动画。fontWeight 设为 w600 让日期文字稍微粗一些,更容易阅读。

月度收支统计

收入和支出用两个并排的卡片展示,让用户一目了然:

Widget _buildMonthlyStats(HomeController controller) {
  return Row(
    children: [
      Expanded(child: _buildStatCard(
        '收入', 
        controller.monthlyIncome, 
        _incomeColor, 
        controller.currency
      )),
      SizedBox(width: 12.w),
      Expanded(child: _buildStatCard(
        '支出', 
        controller.monthlyExpense, 
        _expenseColor, 
        controller.currency
      )),
    ],
  );
}

Row 组件让两个卡片水平排列,Expanded 让它们平分可用宽度。中间用 SizedBox 留出 12.w 的间距,这个间距比组件之间的 16.h 稍小一些,让两个卡片看起来更像是一组相关的信息。

收入卡片传入绿色 _incomeColor,支出卡片传入红色 _expenseColor,颜色的区分让用户不需要看文字就能分辨。这种设计遵循了"颜色即信息"的原则,绿色代表正向的收入,红色代表需要关注的支出。currency 参数传入货币符号,支持多币种显示。

单个统计卡片的实现:

Widget _buildStatCard(String label, RxDouble value, Color color, String currency) {
  return Card(
    child: Padding(
      padding: EdgeInsets.all(16.w),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(children: [
            Container(
              width: 8.w, 
              height: 8.w, 
              decoration: BoxDecoration(
                color: color, 
                shape: BoxShape.circle
              )
            ),
            SizedBox(width: 8.w),
            Text(label, 
              style: TextStyle(color: _textSecondary, fontSize: 14.sp)),
          ]),

每个卡片左上角有一个小圆点,颜色和金额颜色一致,形成视觉关联。Container 设置 width 和 height 相等,配合 BoxShape.circle 形成正圆形。这个小圆点虽然只有 8.w 大小,但起到了重要的视觉引导作用。

标签文字用灰色 _textSecondary,字号 14.sp 比金额小一些。Row 组件让圆点和标签水平排列,SizedBox 在它们之间留出 8.w 的间距。这种布局让标签行看起来整洁有序,圆点作为视觉锚点帮助用户快速定位。

金额显示部分:

          SizedBox(height: 8.h),
          Obx(() => Text(
            '$currency${value.value.toStringAsFixed(2)}',
            style: TextStyle(
              fontSize: 20.sp, 
              fontWeight: FontWeight.bold, 
              color: color
            )
          )),
        ],
      ),
    ),
  );
}

金额用 20.sp 的大字号和粗体显示,颜色和顶部的小圆点一致。这种设计让收入和支出在视觉上有明确的区分,用户扫一眼就能知道哪个是收入哪个是支出。toStringAsFixed(2) 保留两位小数,让金额显示更规范。

Obx 包裹金额 Text,当 value 变化时只重建这个文字组件。RxDouble 是 GetX 的响应式双精度浮点数类型,value.value 获取实际的 double 值。这种响应式设计让数据更新时 UI 能自动同步,不需要手动调用 setState。

快捷功能入口

把常用功能的入口放在首页,减少用户的操作路径:

Widget _buildQuickActions() {
  return Card(
    child: Padding(
      padding: EdgeInsets.all(16.w),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('快捷功能', 
            style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600)),
          SizedBox(height: 16.h),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: [
              _buildQuickAction(Icons.account_balance, '账户', 
                () => Get.toNamed(Routes.accountList)),
              _buildQuickAction(Icons.category, '分类', 
                () => Get.toNamed(Routes.categoryList)),
              _buildQuickAction(Icons.assessment, '报表', 
                () => Get.toNamed(Routes.monthlyReport)),
              _buildQuickAction(Icons.flag, '目标', 
                () => Get.toNamed(Routes.goals)),
            ],
          ),
        ],
      ),
    ),
  );
}

快捷功能区域用 Card 包裹,和其他内容保持一致的视觉风格。标题"快捷功能"用 16.sp 字号和 w600 粗细,和其他卡片的标题样式统一。Row 的 mainAxisAlignment 设为 spaceAround,让四个入口均匀分布。

四个入口分别是:账户管理、分类管理、月度报表、理财目标。这些功能的选择是经过考虑的:账户和分类是数据管理的基础,报表是用户查看分析的入口,目标是理财规划的核心。每个入口都通过 Get.toNamed 进行路由导航,使用命名路由让代码更清晰。

单个快捷入口的实现:

Widget _buildQuickAction(IconData icon, String label, VoidCallback onTap) {
  return GestureDetector(
    onTap: onTap,
    child: Column(
      children: [
        Container(
          padding: EdgeInsets.all(12.w),
          decoration: BoxDecoration(
            color: _primaryColor.withOpacity(0.1), 
            borderRadius: BorderRadius.circular(12.r)
          ),
          child: Icon(icon, color: _primaryColor, size: 24.sp),
        ),
        SizedBox(height: 8.h),
        Text(label, style: TextStyle(fontSize: 12.sp)),
      ],
    ),
  );
}

每个快捷入口采用上下结构:图标在上,文字在下。图标放在一个圆角矩形容器中,背景色是主题色的 10% 透明度,既有颜色又不会太抢眼。borderRadius 设为 12.r,.r 是响应式圆角值,会根据屏幕尺寸自动调整。

GestureDetector 处理点击事件,比 InkWell 更轻量,这里不需要水波纹效果所以选择 GestureDetector。VoidCallback 是无参数无返回值的函数类型,用于传递点击回调。图标大小 24.sp 和文字大小 12.sp 形成 2:1 的比例,视觉上比较协调。

最近交易记录

展示最新的几笔交易,让用户快速了解近期收支情况:

Widget _buildRecentTransactions(HomeController controller) {
  final categoryService = Get.find<CategoryService>();
  return Card(
    child: Padding(
      padding: EdgeInsets.all(16.w),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Text('最近记录', 
                style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600)),
              TextButton(
                onPressed: () => Get.toNamed(Routes.transactionList), 
                child: const Text('查看全部')
              ),
            ],
          ),

最近交易记录区域的标题栏包含标题和"查看全部"按钮。Row 的 mainAxisAlignment 设为 spaceBetween 让它们分布在两端。TextButton 是 Material Design 的文字按钮,自带点击效果,适合这种次要操作。

Get.find() 获取分类服务的实例,用于根据 categoryId 查找分类信息。这里没有在 build 方法开头获取,而是在需要的地方获取,是因为这个服务只在这个方法中使用。这种做法让依赖关系更清晰,也方便后续重构。

交易列表的构建逻辑:

          Obx(() {
            final transactions = controller.recentTransactions;
            if (transactions.isEmpty) {
              return Padding(
                padding: EdgeInsets.symmetric(vertical: 32.h),
                child: Center(child: Text(
                  '暂无记录', 
                  style: TextStyle(color: _textSecondary, fontSize: 14.sp)
                )),
              );
            }
            return ListView.separated(
              shrinkWrap: true,
              physics: const NeverScrollableScrollPhysics(),
              itemCount: transactions.length > 5 ? 5 : transactions.length,
              separatorBuilder: (_, __) => const Divider(),
              itemBuilder: (_, index) {
                final t = transactions[index];
                final category = categoryService.getCategoryById(t.categoryId);

Obx 包裹整个列表区域,当 recentTransactions 变化时自动更新。空状态处理很重要,当没有记录时显示"暂无记录"的提示,比空白页面更友好。Padding 的 vertical 设为 32.h,让空状态提示有足够的视觉空间。

ListView.separated 比普通 ListView 多了分隔线功能,separatorBuilder 返回 Divider 组件作为分隔线。shrinkWrap: true 让 ListView 根据内容自适应高度,NeverScrollableScrollPhysics 禁用列表自身的滚动,由外层的 SingleChildScrollView 统一处理滚动。

列表最多显示 5 条记录,避免首页过长。itemCount 使用三元表达式判断,如果记录超过 5 条就只显示 5 条。categoryService.getCategoryById 根据交易的 categoryId 查找对应的分类信息,用于显示分类图标和名称。

单条交易记录的展示:

                return ListTile(
                  contentPadding: EdgeInsets.zero,
                  leading: CircleAvatar(
                    backgroundColor: (category?.color ?? Colors.grey).withOpacity(0.2),
                    child: Icon(
                      category?.icon ?? Icons.help, 
                      color: category?.color ?? Colors.grey, 
                      size: 20.sp
                    ),
                  ),
                  title: Text(category?.name ?? '未知分类'),
                  subtitle: Text(DateFormat('MM-dd HH:mm').format(t.date)),
                  trailing: Text(
                    '${t.type == TransactionType.income ? '+' : '-'}${controller.currency}${t.amount.toStringAsFixed(2)}',
                    style: TextStyle(
                      color: t.type == TransactionType.income 
                        ? _incomeColor 
                        : _expenseColor,
                      fontWeight: FontWeight.w600, 
                      fontSize: 14.sp,
                    ),
                  ),

ListTile 是 Material Design 的列表项组件,自带 leading、title、subtitle、trailing 四个位置。contentPadding 设为 zero 去掉默认内边距,因为外层 Card 已经有 padding 了。CircleAvatar 显示分类图标,背景色是分类颜色的 20% 透明度。

category?.color 使用空安全操作符,如果 category 为 null 就使用 Colors.grey 作为默认值。subtitle 显示交易时间,格式化为 “01-15 14:30” 这样的简短格式。trailing 显示金额,收入前面加 + 号,支出前面加 - 号,颜色也根据类型变化。

点击交易记录跳转到详情页:

                  onTap: () => Get.toNamed(Routes.transactionDetail, arguments: t),
                );
              },
            );
          }),
        ],
      ),
    ),
  );
}

onTap 回调使用 Get.toNamed 进行路由导航,arguments 参数传递交易对象 t。详情页可以通过 Get.arguments 获取这个对象,显示完整的交易信息并提供编辑功能。

这种参数传递方式比通过 URL 参数更灵活,可以传递任意类型的对象。但要注意,如果用户直接通过 URL 访问详情页(比如深度链接),arguments 会是 null,需要做好空值处理。

HomeController 实现

Controller 负责数据的加载和状态管理,是 MVVM 架构中的 ViewModel 层:

import 'package:get/get.dart';
import '../../core/services/transaction_service.dart';
import '../../core/services/account_service.dart';
import '../../core/services/storage_service.dart';
import '../../data/models/transaction_model.dart';

class HomeController extends GetxController {
  final _transactionService = Get.find<TransactionService>();
  final _accountService = Get.find<AccountService>();
  final _storage = Get.find<StorageService>();

  final selectedMonth = DateTime.now().obs;
  final totalAssets = 0.0.obs;
  final netWorth = 0.0.obs;
  final monthlyIncome = 0.0.obs;
  final monthlyExpense = 0.0.obs;
  final recentTransactions = <TransactionModel>[].obs;

HomeController 继承自 GetxController,这是 GetX 提供的控制器基类。通过 Get.find 获取三个服务的实例:TransactionService 管理交易数据,AccountService 管理账户数据,StorageService 管理存储和设置。

状态变量都用 .obs 转为响应式类型:selectedMonth 是当前选中的月份,totalAssets 是总资产,netWorth 是净资产,monthlyIncome 和 monthlyExpense 是月度收支,recentTransactions 是最近的交易记录列表。这些响应式变量在值变化时会自动通知 UI 更新。

计算属性和生命周期方法:

  String get currency => _storage.currency;
  double get monthlyBalance => monthlyIncome.value - monthlyExpense.value;

  
  void onInit() {
    super.onInit();
    _loadData();
    ever(selectedMonth, (_) => _loadMonthlyData());
  }

currency 是一个 getter,直接从 StorageService 获取货币符号。monthlyBalance 是计算属性,返回月度结余。使用 getter 而不是响应式变量,是因为这个值可以从其他值计算得出,不需要单独存储。

onInit 是 GetX 的生命周期方法,在 Controller 创建后立即调用。这里调用 _loadData 加载初始数据,然后用 ever 监听 selectedMonth 的变化。ever 是 GetX 提供的监听器,当 selectedMonth 变化时会自动调用 _loadMonthlyData 重新加载月度数据。

数据加载方法:

  void _loadData() {
    totalAssets.value = _accountService.totalAssets;
    netWorth.value = _accountService.netWorth;
    _loadMonthlyData();
    _loadRecentTransactions();
  }

  void _loadMonthlyData() {
    final start = DateTime(
      selectedMonth.value.year, 
      selectedMonth.value.month, 
      1
    );
    final end = DateTime(
      selectedMonth.value.year, 
      selectedMonth.value.month + 1, 
      0
    );
    monthlyIncome.value = _transactionService.getTotalIncome(
      start: start, 
      end: end
    );
    monthlyExpense.value = _transactionService.getTotalExpense(
      start: start, 
      end: end
    );
  }

_loadData 方法加载所有数据,包括总资产、净资产、月度数据和最近交易。totalAssets 和 netWorth 从 AccountService 获取,这两个值是所有账户余额的汇总。

_loadMonthlyData 计算选中月份的第一天和最后一天,然后调用 TransactionService 获取该时间范围内的收入和支出总额。DateTime 的构造函数很灵活,month + 1 然后 day 设为 0 就能得到当月最后一天,这是一个常用的日期计算技巧。

加载最近交易记录:

  void _loadRecentTransactions() {
    final list = _transactionService.allTransactions.toList();
    list.sort((a, b) => b.date.compareTo(a.date));
    recentTransactions.value = list.take(10).toList();
  }

  void previousMonth() {
    selectedMonth.value = DateTime(
      selectedMonth.value.year, 
      selectedMonth.value.month - 1
    );
  }

  void nextMonth() {
    selectedMonth.value = DateTime(
      selectedMonth.value.year, 
      selectedMonth.value.month + 1
    );
  }

  void refresh() {
    _loadData();
  }
}

_loadRecentTransactions 获取所有交易记录,按日期倒序排列,取前 10 条。toList() 创建副本再排序,避免修改原始数据。take(10) 取前 10 条,再 toList() 转为 List 赋值给响应式变量。

previousMonth 和 nextMonth 方法修改 selectedMonth,触发 ever 监听器重新加载数据。DateTime 构造函数会自动处理月份溢出,比如 month 设为 0 会变成上一年的 12 月,设为 13 会变成下一年的 1 月。

refresh 方法供下拉刷新调用,重新加载所有数据。这种设计让数据刷新逻辑集中在 Controller 中,View 层只需要调用这个方法,不需要关心具体的刷新逻辑。

性能优化考虑

首页是高频访问的页面,性能优化很重要。几个关键点:

  1. 用 const 构造函数创建不变的 Widget,减少重建开销
  2. Obx 包裹最小范围的 Widget,避免不必要的重建
  3. ListView 用 shrinkWrap 而不是固定高度,适应不同数据量
  4. 图片和图标用合适的尺寸,不要加载过大的资源

如果数据量很大,可以考虑分页加载最近交易记录,或者用 ListView.builder 的懒加载特性。但对于个人理财应用,数据量通常不会太大,这些优化可以后续按需添加。

const 的使用是 Flutter 性能优化的基础。const Widget 在编译时就确定了,运行时不需要重新创建。在 build 方法中,尽可能给 Widget 加上 const 修饰符,可以显著减少内存分配和垃圾回收的压力。

小结

首页的设计遵循了几个原则:

  • 信息层次分明,最重要的数据最突出
  • 操作路径短,常用功能一步可达
  • 视觉风格统一,颜色有明确的含义
  • 响应式更新,数据变化自动反映到 UI

资产总览卡片用主题色背景突出显示,让用户一眼就能看到最重要的数字。月度收支用绿色和红色区分,符合用户的认知习惯。快捷功能入口减少了操作路径,最近交易记录让用户快速了解近期收支。

下一篇会介绍添加交易记录页面的实现,包括金额输入、分类选择、账户选择等交互细节。


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

Logo

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

更多推荐