1. 为什么我选择用对话式编程从零起一个宠物生命周期管理App
宠物生命周期管理这个需求,乍一听好像挺小众,但真正养过宠物的人都知道,从接回家第一天到最终告别,中间涉及的事情琐碎到让人崩溃。疫苗什么时候打、驱虫周期怎么算、体重曲线是否正常、发情期记录、体检报告归档、不同阶段的饮食切换……这些信息如果全靠脑子记或者散落在各种备忘录里,基本等于没有。我身边至少有三位养宠的朋友,在宠物生病需要向医生描述历史情况时,翻手机翻了十几分钟还没找全。
市面上不是没有宠物管理类App,但普遍存在两个问题:一是功能大而全,塞了一堆社交、商城、领养模块,真正核心的健康记录反而做得很浅;二是数据都在云端,想导出或者自己做个分析非常麻烦。所以我就想自己做一个轻量的、以本地数据为主的宠物生命周期管理工具,核心只做一件事——把宠物从到家到离世的每一个关键节点记录清楚,并且能按时间线回溯。
那为什么用Claude Code来做?因为我之前试过纯手写,光是搭项目骨架、配路由、写CRUD就耗掉了大量时间,真正花在业务逻辑上的精力反而很少。对话式编程最大的价值在于,它能把"我想做什么"直接翻译成可运行的代码结构,我只需要在关键决策点上做判断和修正。这篇文章我会把整个从零搭建的过程拆开讲,包括我怎么跟它描述需求、怎么控制项目结构、怎么处理它给出的方案里不合理的地方,以及最终跑起来之后我踩到的那些坑。
适合谁看?如果你有一点编程基础,想用AI辅助的方式快速起一个完整项目,或者你本身就想做一个宠物相关的工具,这篇内容应该能给你省不少时间。完全零基础也能看,但部分代码层面的取舍可能需要你额外补一下基础概念。
2. 项目骨架的搭建:从一句话需求到可运行的目录结构
2.1 我怎么向Claude Code描述这个项目
很多人用对话式编程效果不好,根本原因在于描述太模糊。你如果说"帮我做一个宠物管理App",它给你的东西大概率是一个什么都沾一点但什么都不精的Demo。我的做法是先把需求拆成三层:数据层、交互层、展示层,然后逐层描述。
我实际用的开场描述大概是这样的:
我要做一个宠物生命周期管理App,技术栈用React Native + Expo,数据存储用SQLite本地库。核心实体是宠物(Pet),围绕宠物有几种记录类型:疫苗记录、驱虫记录、体重记录、体检记录、自定义事件。每个记录都有时间戳和备注字段。首页是一个宠物列表,点进去是单个宠物的详情页,详情页按时间线展示所有记录。先帮我把项目初始化和目录结构搭出来。
这段话里其实包含了几个关键决策:技术栈我指定了,存储方案我指定了,核心实体和关系我描述了,页面结构我也给了。Claude Code拿到这些信息之后,给出的目录结构基本就是可用的,不需要大改。
它给我的结构大概是这样的:
pet-lifecycle/ ├── app/ │ ├── (tabs)/ │ │ ├── index.tsx # 宠物列表首页 │ │ └── settings.tsx # 设置页 │ ├── pet/ │ │ └── [id].tsx # 宠物详情页(动态路由) │ └── _layout.tsx # 根布局 ├── db/ │ ├── schema.ts # 数据库表定义 │ ├── migrations.ts # 迁移逻辑 │ └── queries.ts # 封装好的查询函数 ├── components/ │ ├── PetCard.tsx │ ├── TimelineItem.tsx │ └── RecordForm.tsx ├── hooks/ │ └── usePetData.ts ├── utils/ │ └── date.ts └── constants/ └── recordTypes.ts这个结构的好处是职责清晰:db目录只管数据,components只管展示,hooks做数据桥接。我后来加功能的时候,基本只需要在对应目录里加文件,不会出现改一个地方牵连一片的情况。
2.2 数据库表设计里我做的关键调整
Claude Code最初给我的表设计是每种记录类型一张独立的表,比如vaccine_records、deworm_records、weight_records各一张。这个设计在查询单类记录时确实方便,但问题在于详情页需要按时间线混合展示所有记录,如果分表的话,每次都要做多次查询再在应用层合并排序,数据量大了之后性能会明显下降。
我让它改成统一的事件表设计:
CREATE TABLE IF NOT EXISTS pets ( id TEXT PRIMARY KEY, name TEXT NOT NULL, species TEXT NOT NULL, breed TEXT, birth_date TEXT, adopt_date TEXT, avatar_uri TEXT, created_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS events ( id TEXT PRIMARY KEY, pet_id TEXT NOT NULL, event_type TEXT NOT NULL, title TEXT NOT NULL, detail TEXT, event_date TEXT NOT NULL, created_at TEXT DEFAULT (datetime('now')), FOREIGN KEY (pet_id) REFERENCES pets(id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_events_pet_date ON events(pet_id, event_date DESC);这个改动的好处非常直接:时间线查询只需要一条SQL,按event_date倒序取出来就行。event_type字段区分是疫苗还是驱虫还是体重,展示层根据类型渲染不同的图标和颜色。索引加在(pet_id, event_date)上,是因为几乎所有查询都是"某只宠物的记录按时间排序",这个复合索引能完全覆盖。
这里有个细节要注意:SQLite的
datetime('now')返回的是UTC时间,如果你直接展示给用户,会发现时间对不上。我的做法是在应用层统一做时区转换,存储层保持UTC,展示层用本地时区格式化。
2.3 路由方案的选择逻辑
Expo Router是Claude Code默认推荐的方案,我一开始有点犹豫,因为之前用React Navigation比较多。但实际用下来,Expo Router的文件系统路由在这种页面结构不复杂的项目里确实更省心。app/pet/[id].tsx这个文件一建,/pet/123这个路由就自动生效了,不需要额外写导航配置。
不过有一个坑我踩过:动态路由的参数获取方式跟React Navigation不一样。在Expo Router里,你要用useLocalSearchParams而不是route.params。这个细节Claude Code第一次给我的代码里用错了,我跑起来发现id是undefined,排查了一会儿才定位到。后来我养成了一个习惯,凡是它给的涉及路由参数的代码,我都会先确认一下用的是哪套API。
3. 核心功能模块的逐个落地过程
3.1 宠物列表页:从静态渲染到真实数据驱动
列表页看起来简单,但它是整个App的门面,加载速度和空状态处理直接影响第一印象。Claude Code最初给的版本是用FlatList直接渲染一个硬编码的数组,我让它改成从SQLite读取。
这里涉及一个React Native里常见的异步数据加载模式。我的做法是封装一个usePetshook:
import { useState, useCallback } from 'react'; import { useFocusEffect } from 'expo-router'; import { getDb } from '../db/schema'; export function usePets() { const [pets, setPets] = useState([]); const [loading, setLoading] = useState(true); const loadPets = useCallback(async () => { try { const db = await getDb(); const result = await db.getAllAsync( 'SELECT * FROM pets ORDER BY created_at DESC' ); setPets(result); } catch (e) { console.error('加载宠物列表失败', e); } finally { setLoading(false); } }, []); useFocusEffect( useCallback(() => { loadPets(); }, [loadPets]) ); return { pets, loading, refresh: loadPets }; }关键点在于useFocusEffect:每次页面获得焦点时重新加载数据。因为用户可能从详情页返回列表页,如果不在焦点时刷新,新增的宠物或者修改的信息就不会更新。这个模式在React Navigation里也有对应的useFocusEffect,但Expo Router的导入路径不同,需要注意。
空状态的处理我也让Claude Code补上了。当pets为空且loading为false时,展示一个引导用户添加第一只宠物的界面,而不是一片空白。这个细节对新手用户很重要,一片空白会让人不知道下一步该干什么。
3.2 时间线视图:混合记录类型的统一渲染
详情页的时间线是整个App最核心的展示模块。因为所有记录都在events表里,查询很简单:
const events = await db.getAllAsync( `SELECT * FROM events WHERE pet_id = ? ORDER BY event_date DESC`, [petId] );但渲染的时候需要根据event_type做差异化处理。我的做法是建一个配置映射:
const EVENT_CONFIG = { vaccine: { icon: '💉', color: '#4A90D9', label: '疫苗' }, deworm: { icon: '🪱', color: '#7B68EE', label: '驱虫' }, weight: { icon: '⚖️', color: '#50C878', label: '体重' }, checkup: { icon: '🏥', color: '#FF7F50', label: '体检' }, custom: { icon: '📌', color: '#808080', label: '记录' }, };然后TimelineItem组件接收event对象,从配置里取对应的图标和颜色。这样以后要加新的记录类型,只需要在配置里加一项,不用改渲染逻辑。
体重记录有个特殊处理:它需要展示数值和单位,而且最好能看出趋势。我让Claude Code在体重类型的记录上额外渲染一个小的趋势指示,对比上一次体重记录,上升显示绿色向上箭头,下降显示橙色向下箭头。这个功能实现起来不复杂,但体验提升很明显。
时间线排序有个边界情况:同一天有多条记录时,
event_date可能完全相同,排序会不稳定。我的解决方案是在ORDER BY里加一个次级排序字段created_at,保证同一天内的记录按创建顺序排列。
3.3 记录添加表单:动态字段与校验逻辑
添加记录的表单是我花时间最多的地方,因为不同类型的记录需要的字段不一样。疫苗记录需要疫苗名称、接种机构、下次接种时间;体重记录只需要数值;体检记录需要医院名称、体检结论。
Claude Code一开始给的是一个统一的表单,所有字段都展示出来,用户自己填需要的。这个体验很差。我让它改成根据event_type动态渲染字段:
const FIELD_SCHEMAS = { vaccine: [ { key: 'title', label: '疫苗名称', type: 'text', required: true }, { key: 'detail', label: '接种机构', type: 'text' }, { key: 'next_date', label: '下次接种', type: 'date' }, ], weight: [ { key: 'title', label: '体重(kg)', type: 'number', required: true }, ], // ... };表单组件遍历这个schema数组,根据type渲染对应的输入控件。校验逻辑也挂在schema上,required: true的字段为空时阻止提交并提示。
这里有个实操心得:日期选择器在React Native里跨平台表现不一致,iOS的原生日期选择器和Android的差异很大。我最后用的是@react-native-community/datetimepicker,它在两个平台上的表现相对统一,但Android上需要额外处理一下弹出方式。Claude Code第一次给的代码只考虑了iOS,我在Android真机上测试时发现日期选择器直接嵌在页面里而不是弹窗,后来让它改成Android用dialog模式才正常。
3.4 数据导出与备份:被低估的重要功能
这个功能是我在用了两周之后才决定加的。起因是手机存储空间告急,我清理数据时差点把App的数据一起清掉,吓出一身冷汗。宠物生命周期数据是长期积累的,一旦丢失几乎不可能重建,所以导出备份是刚需。
我让Claude Code加了一个导出功能,把所有数据序列化成JSON文件,通过系统分享面板发送出去。核心代码不复杂:
import * as FileSystem from 'expo-file-system'; import * as Sharing from 'expo-sharing'; async function exportData() { const db = await getDb(); const pets = await db.getAllAsync('SELECT * FROM pets'); const events = await db.getAllAsync('SELECT * FROM events'); const backup = { version: 1, exported_at: new Date().toISOString(), pets, events, }; const path = FileSystem.documentDirectory + 'pet_backup.json'; await FileSystem.writeAsStringAsync(path, JSON.stringify(backup)); await Sharing.shareAsync(path); }导入功能稍微麻烦一点,需要处理数据冲突。我的策略是导入时如果宠物ID已存在,就跳过该宠物及其记录,避免重复。这个逻辑不复杂但容易漏掉,Claude Code第一版就没做冲突检测,我测试时导入了两次同一份备份,结果所有记录都翻倍了。
4. 对话式编程中我总结出的协作节奏与避坑经验
4.1 什么时候该让它自由发挥,什么时候必须自己定
用Claude Code做项目的过程中,我逐渐摸清了一个规律:架构决策必须自己定,实现细节可以放手让它做。
什么意思?比如数据库用SQLite还是AsyncStorage、路由用Expo Router还是React Navigation、状态管理用Context还是Zustand,这些决策会影响整个项目的走向,一旦选错后期改起来成本极高。这些必须你自己想清楚,然后明确告诉它。而具体某个组件的样式怎么写、某个查询的SQL怎么拼、某个hook的依赖数组怎么填,这些它做得比我快也比我好,放手让它做就行。
我踩过的一个坑是:早期我让它"帮我设计一下数据模型",它给了一个相当复杂的方案,有单独的品种表、医院表、疫苗类型表,外键关系一大堆。对于我这个单人使用的轻量App来说,这完全是过度设计。后来我全部推倒重来,改成两张核心表,开发效率立刻上来了。
4.2 它给出的代码里最常见的三类问题
用了这段时间,我总结出Claude Code生成的代码里最高频的三类问题,分享出来供你参考:
| 问题类型 | 具体表现 | 我的应对方式 |
|---|---|---|
| API版本过时 | 用了旧版Expo或React Native的API | 跑之前先看官方文档确认当前版本用法 |
| 平台差异忽略 | 只考虑iOS,Android上表现异常 | 每个功能都在两个平台上真机测试 |
| 边界情况遗漏 | 空数据、重复数据、超长文本没处理 | 自己补空状态和异常分支 |
第一类问题最隐蔽,因为代码看起来完全正确,但跑起来就是报错或者行为不对。我的习惯是每次它给出涉及第三方库的代码,我都会去确认一下这个库当前版本的API签名。第二类问题在UI相关的代码里特别常见,尤其是日期选择、键盘避让、状态栏这些和平台强相关的部分。第三类问题需要你自己有意识地去想"如果这里数据是空的会怎样""如果用户快速点了两次提交会怎样"。
4.3 迭代节奏:小步验证比一次性生成更靠谱
我一开始犯的错误是一次性让它生成整个功能模块,比如"帮我实现宠物详情页的所有功能"。结果它给了一大坨代码,跑起来报了三四个错,我逐个排查花了很长时间,而且改了一个地方又引出新的问题。
后来我改成小步走:先让它生成数据查询部分,我跑通确认数据能取到;再让它生成列表渲染,确认能正常展示;最后加交互和样式。每一步都是可验证的,出了问题也容易定位。这个节奏虽然看起来慢,但总体效率反而更高,因为调试成本大幅降低了。
一个具体的操作建议:每次让它生成代码之前,先明确告诉它"只生成XXX部分,不要包含YYY"。比如"只生成从SQLite查询事件列表的函数,不要生成UI组件"。这样它的输出更聚焦,你验证起来也更快。
4.4 真机测试不可省略的几个关键节点
模拟器上跑得再好,真机上还是可能出问题。我总结下来,有几个节点必须上真机测试:
- 数据库操作:模拟器上的SQLite行为和真机有差异,尤其是文件权限和路径。我遇到过一次模拟器正常但真机上数据库文件创建失败的情况,原因是路径用了模拟器特有的目录。
- 文件导出与分享:这个功能在模拟器上基本没法完整测试,必须真机验证分享面板是否正常弹出、文件是否能被其他App接收。
- 日期和时间:模拟器的时区设置可能和真机不同,涉及日期计算的功能一定要在真机上确认。
- 性能表现:当记录数量积累到几百条时,列表滚动的流畅度在模拟器和真机上差异明显。我是在真机上发现时间线滚动卡顿后,才加了
FlatList的windowSize和maxToRenderPerBatch优化。
5. 项目跑通之后,我对这类工具的真实看法
这个宠物生命周期管理App从零到能用,我大概花了三个周末的时间,其中真正写业务逻辑的时间可能只占三分之一,剩下的都花在调试、真机测试和调整交互细节上。Claude Code在这个过程中确实帮我省掉了大量查文档、写样板代码的时间,但它不是一个"你说什么它就做什么"的完美工具,更像是一个反应很快但需要你把关的初级开发者。
我最大的体会是:你越清楚自己要什么,它的价值就越大。如果你自己都没想清楚数据怎么存、页面怎么跳,它给你的东西大概率是一团需要大量返工的代码。但如果你能把需求拆解到"在哪个文件里写一个什么函数,输入是什么,输出是什么"这个粒度,它几乎可以秒级给出可用的实现。
另外一点,不要指望它一次就对。我现在的习惯是把它当成一个可以无限次对话的结对编程伙伴,第一版出来之后我会仔细看,指出具体哪里不对,让它改。通常两到三轮之后,代码质量就能达到可用的水平。这个过程比我自己从头写还是要快不少,尤其是涉及到我不熟悉的库或者API的时候。
后续我打算加的功能还有:多宠物对比视图、体重曲线的图表展示、记录提醒(基于下次接种日期)。这些功能的技术方案我大概有数了,等做出来再另开一篇聊。如果你也在做类似的项目,或者对某个具体模块的实现有疑问,欢迎交流。