专注小程序 / APP 开发,用代码改变生活

获取方案
全国分站 主站首页 北京分站 石家庄分站 唐山分站 保定分站 廊坊分站 沧州分站 郑州分站

从零写一个记账App:我的踩坑教程

APP开发 2026-10-21 0 阅读 0 点赞 原创

去年底我想做个记账 App,需求很简单:记一笔、看列表、月底统计。本来以为两三天能搞定,结果断断续续折腾了两周。这篇 app开发教程 不打算从“Hello World”讲起,而是把我真正踩过的坑摊开说,适合已经能跑通基础项目、准备做第一个完整小产品的人。

一、先定技术栈,别在选型上内耗

我最后选了 Flutter,原因很实际:一台电脑同时出 Android 和 iOS,UI 写起来快。如果你只做安卓,Kotlin + Compose 也完全够用;只做 iOS 就 SwiftUI。选型阶段我犯的错是看了太多对比文章,其实对个人项目来说,能让你今晚就动手的那个框架,就是最好的框架。

依赖我只加了三个:sqflite(本地数据库)、provider(状态管理)、intl(日期格式化)。不要一上来就上 Riverpod、Bloc、依赖注入全家桶,小项目会被架构压死。

二、数据模型:先想清楚“一笔账”是什么

我第一版模型是这样的:

class Bill {
  final int? id;
  final double amount;
  final String category;
  final DateTime createdAt;
  final String note;
}

跑了两天发现两个问题:一是金额用 double 做加减会出现 0.1 + 0.2 = 0.30000000000000004,统计页数字很难看;二是没有“收入/支出”区分,只能靠金额正负判断,读代码时特别绕。

改版后金额统一存“分”的整数,类型单独加一个枚举:

enum BillType { income, expense }

class Bill {
  final int? id;
  final int amountInCents;
  final BillType type;
  final String category;
  final DateTime createdAt;
  final String note;
}

展示时再除以 100。这个改动很小,但后面所有统计逻辑都清爽了。教训是:涉及钱的字段,永远别用浮点数存。

三、本地存储:建表与迁移

用 sqflite 建表时,我把版本号写死成 1,结果后来加字段时忘了写 onUpgrade,老用户升级后直接崩溃。正确的做法是从第一天就把迁移留好口子:

Future<Database> _initDb() async {
  final path = join(await getDatabasesPath(), 'bills.db');
  return openDatabase(
    path,
    version: 2,
    onCreate: (db, v) async {
      await db.execute('''
        CREATE TABLE bills(
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          amount_cents INTEGER NOT NULL,
          type INTEGER NOT NULL,
          category TEXT NOT NULL,
          created_at INTEGER NOT NULL,
          note TEXT
        )
      ''');
    },
    onUpgrade: (db, oldV, newV) async {
      if (oldV < 2) {
        await db.execute('ALTER TABLE bills ADD COLUMN note TEXT');
      }
    },
  );
}

另外,日期我存的是毫秒时间戳(INTEGER),不是字符串。字符串排序在跨时区时会出幺蛾子,时间戳省心得多。

四、状态管理:别让 UI 直接碰数据库

我最初的写法是在页面里直接 await db.insert(...),然后 setState 重新查一遍。功能能跑,但列表页、统计页、首页各自查一次库,逻辑重复得厉害。

后来抽了一个 BillStore,所有读写都走它,页面只监听变化:

class BillStore extends ChangeNotifier {
  List<Bill> _bills = [];
  List<Bill> get bills => List.unmodifiable(_bills);

  Future<void> load() async {
    _bills = await BillDao.instance.queryAll();
    notifyListeners();
  }

  Future<void> add(Bill bill) async {
    await BillDao.instance.insert(bill);
    await load();
  }

  int get monthExpense {
    final now = DateTime.now();
    return _bills
        .where((b) => b.type == BillType.expense &&
            b.createdAt.year == now.year &&
            b.createdAt.month == now.month)
        .fold(0, (sum, b) => sum + b.amountInCents);
  }
}

统计逻辑放在 store 里用 fold 算,而不是写 SQL 聚合,是因为数据量小(个人记账撑死几千条),内存计算更灵活,改需求也快。等真到几万条再考虑优化,现在别过度设计。

五、几个容易被忽略的细节

  • 输入校验:金额输入框要限制只能输数字和小数点,且最多两位小数。我一开始没做,用户能输入 1.2.3,解析直接抛异常。
  • 空状态:列表为空时给个引导文案和“记一笔”按钮,比一片白屏体验好太多。
  • 删除确认:左滑删除一定要弹确认框,我测试时误删过好几次数据,用户更会。
  • 时区:统计“本月”时用本地时间,别用 UTC,否则月初月末会错一天。

六、打包发布前检查清单

第一次打 release 包,我遇到图标没换、应用名还是 flutter_app、签名用的 debug key 这几个低级问题。发布前过一遍:应用名和图标、版本号、签名配置、隐私政策页面(应用商店基本都要)、以及真机上的深色模式表现。

做小产品的核心不是技术多新,而是把一条完整链路走通:数据存得住、界面不崩、能装到别人手机上。走通一次,后面再做第二个就快了。

这个记账 App 现在我自己每天在用,代码量不到两千行。如果你也在做第一个 app,建议把范围砍到最小,先让它跑起来,再谈优化。这篇 app开发教程 里的坑,希望你能少踩几个。

找app制作公司前,先搞懂这五件事从想法到上架:我的独立App制作全流程

评论 (0)

还没有评论,快来抢沙发吧~

好想法,值得被认真交付

从小程序、APP 到全栈网站,一站式把想法变成可落地的产品

微信咨询

微信扫码,直接沟通需求