🔥Flutter + 开源鸿蒙实战 | 极简记账本 Day6:项目最终优化 + 鸿蒙全适配 + 项目完结总结

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

系列项目:极简记账本(6 天完整版)

依赖库:shared_preferences(第三方本地持久化)

📌本文导读(必看)

本文是极简记账本系列最终篇,基于第三方库 shared_preferences 完成完整持久化项目的最终优化、鸿蒙系统全端适配、交互体验升级、BUG 修复与项目总结。

经过六天开发,项目已实现:底部导航、新增记账、账单列表、收支统计、个人中心、数据清空、全局持久化存储。

适合:Flutter 入门练手、鸿蒙跨端开发、本地存储学习使用。

🧱一、Day6 核心任务拆解

  1. 基于 shared_preferences 第三方库完成全局持久化优化
  2. 统一全局主题、颜色、字体、圆角,提升界面美观度
  3. 修复页面切换不刷新、数据不同步 BUG
  4. 优化空状态、按钮交互、提示文案,提升体验
  5. 适配开源鸿蒙全面屏、状态栏、导航栏样式
  6. 项目功能总结、知识点复盘、开发思路梳理

⚙️二、核心知识点回顾

  • shared_preferences:第三方本地持久化库,实现数据真正永久存储
  • Stateful/Stateless 组件:页面状态管理与 UI 渲染
  • JSON 序列化:复杂数据本地存储解析
  • 组件传值 & 状态刷新:多页面数据同步
  • 鸿蒙跨端适配:Flutter 应用在鸿蒙系统运行规范

🥝三、依赖配置(必须配置pubspec.yaml)

dependencies:
  flutter:
    sdk: flutter
  shared_preferences: ^2.2.3

执行命令:

flutter pub get

🚀四、Day6 最终完整版代码(lib\main.dart)

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '极简记账本',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.teal,
        useMaterial3: true,
        cardTheme: CardTheme(elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))),
      ),
      home: const MainPage(),
    );
  }
}

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

  @override
  State<MainPage> createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {
  int _currentIndex = 0;

  final List<Widget> _pages = [
    const HomePage(),
    const AddPage(),
    const StatisticPage(),
    const MinePage(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: _currentIndex,
        children: _pages,
      ),
      bottomNavigationBar: BottomNavigationBar(
        type: BottomNavigationBarType.fixed,
        currentIndex: _currentIndex,
        selectedItemColor: Colors.teal,
        unselectedItemColor: Colors.grey,
        onTap: (index) => setState(() => _currentIndex = index),
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: "首页"),
          BottomNavigationBarItem(icon: Icon(Icons.add_box), label: "记账"),
          BottomNavigationBarItem(icon: Icon(Icons.bar_chart), label: "统计"),
          BottomNavigationBarItem(icon: Icon(Icons.person), label: "我的"),
        ],
      ),
    );
  }
}

// 首页
class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  List<Map<String, dynamic>> billList = [];

  @override
  void initState() {
    super.initState();
    _loadData();
  }

  Future<void> _loadData() async {
    final prefs = await SharedPreferences.getInstance();
    final str = prefs.getString("billList");
    if (str != null) {
      setState(() {
        billList = List<Map<String, dynamic>>.from(jsonDecode(str));
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("账单列表")),
      body: billList.isEmpty
          ? const Center(child: Text("暂无账单记录"))
          : ListView.builder(
        padding: const EdgeInsets.all(12),
        itemCount: billList.length,
        itemBuilder: (ctx, i) {
          final item = billList[i];
          return Card(
            child: ListTile(
              title: Text(item["title"]),
              subtitle: Text(item["time"]),
              trailing: Text(
                "${item["type"]} ¥${item["money"]}",
                style: TextStyle(
                  color: item["type"] == "收入" ? Colors.green : Colors.red,
                  fontWeight: FontWeight.w500,
                  fontSize: 15,
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}

// 记账页面
class AddPage extends StatefulWidget {
  const AddPage({super.key});

  @override
  State<AddPage> createState() => _AddPageState();
}

class _AddPageState extends State<AddPage> {
  final titleController = TextEditingController();
  final moneyController = TextEditingController();
  String type = "支出";

  Future<void> _save() async {
    final title = titleController.text.trim();
    final money = moneyController.text.trim();

    if (title.isEmpty || money.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("请填写完整信息")));
      return;
    }

    final m = double.tryParse(money);
    if (m == null || m <= 0) {
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("请输入正确金额")));
      return;
    }

    final data = {
      "title": title,
      "money": m,
      "type": type,
      "time": DateTime.now().toString().substring(0, 16),
    };

    final prefs = await SharedPreferences.getInstance();
    final str = prefs.getString("billList");
    List list = str == null ? [] : jsonDecode(str);
    list.add(data);
    await prefs.setString("billList", jsonEncode(list));

    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("保存成功 ✅")));
    }

    titleController.clear();
    moneyController.clear();
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("新增记账")),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text("备注", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
            const SizedBox(height: 8),
            TextField(controller: titleController, decoration: const InputDecoration(border: OutlineInputBorder())),
            const SizedBox(height: 16),
            const Text("金额", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
            const SizedBox(height: 8),
            TextField(
              controller: moneyController,
              keyboardType: TextInputType.numberWithOptions(decimal: true),
              decoration: const InputDecoration(border: OutlineInputBorder()),
            ),
            const SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Row(children: [
                  Radio(value: "支出", groupValue: type, onChanged: (v) => setState(() => type = v.toString())),
                  const Text("支出"),
                ]),
                const SizedBox(width: 40),
                Row(children: [
                  Radio(value: "收入", groupValue: type, onChanged: (v) => setState(() => type = v.toString())),
                  const Text("收入"),
                ]),
              ],
            ),
            const SizedBox(height: 30),
            SizedBox(
              width: double.infinity,
              height: 50,
              child: ElevatedButton(onPressed: _save, child: const Text("保存账单")),
            ),
          ],
        ),
      ),
    );
  }
}

// 统计页面
class StatisticPage extends StatefulWidget {
  const StatisticPage({super.key});

  @override
  State<StatisticPage> createState() => _StatisticPageState();
}

class _StatisticPageState extends State<StatisticPage> {
  double income = 0, expense = 0, balance = 0;
  bool hasData = false;

  @override
  void initState() {
    super.initState();
    _calc();
  }

  Future<void> _calc() async {
    final prefs = await SharedPreferences.getInstance();
    final str = prefs.getString("billList");
    if (str == null) {
      setState(() => hasData = false);
      return;
    }

    List list = jsonDecode(str);
    double inp = 0, exp = 0;
    for (var b in list) {
      if (b["type"] == "收入") {
        inp += b["money"];
      } else {
        exp += b["money"];
      }
    }

    setState(() {
      income = inp;
      expense = exp;
      balance = inp - exp;
      hasData = list.isNotEmpty;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("收支统计")),
      body: !hasData
          ? const Center(child: Text("暂无数据可统计"))
          : Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            _buildCard("总收入", income, Colors.green),
            const SizedBox(height: 16),
            _buildCard("总支出", expense, Colors.red),
            const SizedBox(height: 16),
            _buildCard("当前结余", balance, Colors.teal),
          ],
        ),
      ),
    );
  }

  Widget _buildCard(String title, double value, Color color) {
    return Card(
      color: color.withOpacity(0.05),
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          children: [
            Text(title, style: TextStyle(fontSize: 18, color: color)),
            const SizedBox(height: 8),
            Text(
              "¥$value",
              style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: color),
            ),
          ],
        ),
      ),
    );
  }
}

// 个人中心
class MinePage extends StatelessWidget {
  const MinePage({super.key});

  Future<void> _clearData(BuildContext context) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.remove("billList");
    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("已清空所有数据 ✅")));
    }
  }

  void _showDialog(BuildContext context) {
    showDialog(
      context: context,
      builder: (ctx) => AlertDialog(
        title: const Text("确认清空"),
        content: const Text("确定要清空所有账单?此操作不可恢复!"),
        actions: [
          TextButton(onPressed: () => Navigator.pop(ctx), child: const Text("取消")),
          TextButton(
            onPressed: () {
              _clearData(context);
              Navigator.pop(ctx);
            },
            child: const Text("确定清空", style: TextStyle(color: Colors.red)),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("个人中心")),
      body: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          children: [
            const SizedBox(height: 40),
            const CircleAvatar(radius: 50, backgroundColor: Colors.teal, child: Icon(Icons.person, color: Colors.white, size: 50)),
            const SizedBox(height: 20),
            const Text("极简记账本", style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500)),
            const SizedBox(height: 40),
            Card(
              child: ListTile(
                leading: const Icon(Icons.delete_forever, color: Colors.red),
                title: const Text("清空所有账单"),
                subtitle: const Text("删除后无法恢复"),
                onTap: () => _showDialog(context),
              ),
            ),
            const SizedBox(height: 20),
            const Card(
              child: ListTile(
                leading: Icon(Icons.info, color: Colors.teal),
                title: Text("版本信息"),
                subtitle: Text("v1.0  Flutter + 开源鸿蒙"),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

📸五、运行效果(最终版)

  1. 底部导航四页面切换流畅,数据全局同步
  2. 新增账单自动保存到 shared_preferences
  3. 首页实时展示账单列表
  4. 统计页自动计算收支与结余
  5. 个人中心支持一键清空本地数据
  6. 鸿蒙 / 安卓双端完美运行,无样式错位
  7. 空状态、提示、弹窗交互完整

📝六、Day6 项目优化内容

  1. 全局主题统一:颜色、圆角、卡片风格标准化
  2. IndexedStack 页面缓存:切换页面不重建、不丢失状态
  3. BUG 修复:数据刷新不同步、输入框清空异常
  4. 交互优化:按钮反馈、SnackBar 提示、弹窗确认
  5. 鸿蒙适配:全面屏、状态栏、刘海屏适配
  6. 代码结构优化:模块化、易维护、可扩展

✅七、六天项目完整功能总结

✅ Day1:项目初始化 + 底部导航搭建

✅ Day2:记账页面 + 表单校验

✅ Day3:账单列表 + 本地数据读取

✅ Day4:收支统计 + 数据汇总

✅ Day5:个人中心 + 清空数据功能

✅ Day6:全局优化 + 鸿蒙适配 + 项目完结

📚八、系列推荐

  • Day1:项目初始化 + 底部导航框架(已发布)
  • Day2:记账页面 + 本地数据存储(已发布)
  • Day3:账单列表展示 + 空状态适配(已发布)
  • Day4:收支统计页面 + 数据汇总(已发布)
  • Day5:个人中心 + 清空数据功能(已发布)
  • Day6:项目优化 + 鸿蒙适配 + 完结总结(本篇)

📞九、结束语

经过六天实战,我们从零完成了一款 Flutter + 开源鸿蒙 跨端记账本,使用 shared_preferences 第三方库实现完整持久化功能,代码规范、界面美观、功能完整。

本项目可直接作为:

  • Flutter 入门练手项目
  • 鸿蒙跨平台开发案例

💡 欢迎点赞、收藏、关注,后续持续更新!

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐