Android Studio开发记账本应用:从零到毕业设计的完整实践指南
在实际计算机专业本科毕业设计或大作业中选择一个功能明确、技术栈主流、工作量适中的项目至关重要。一个基于 Android Studio 开发的记账本应用就是一个非常经典的选择。它涵盖了移动应用开发的核心流程UI设计、数据存储、业务逻辑、用户交互并能自然地融入图表分析等进阶功能足以体现学生对 Android 开发、数据库操作和软件工程基础的理解。对于初学者而言从零开始构建一个完整的记账本应用不仅能巩固 Java/Kotlin 语法更能深入理解 Activity、Fragment、RecyclerView、SQLite 等核心组件的实际应用。本文将带你一步步完成一个功能完备的 Android 记账本应用作为毕业设计或大作业的参考实现。我们将从项目需求分析开始设计数据库表结构搭建基础 UI 框架实现账单的增删改查并最终集成统计图表功能。整个过程会详细解释关键代码和配置并指出开发中常见的“坑”及其解决方案。完成后的项目将具备清晰的 MVC 架构代码规范便于扩展和答辩展示。1. 项目需求分析与技术选型在动手编码之前明确项目要做什么、用什么技术来做是确保项目顺利推进的前提。一个记账本的核心功能相对固定但我们可以为其增加一些亮点以满足毕设的深度要求。1.1 核心功能需求一个基础的记账本应用通常需要满足以下功能点这些也是评审老师关注的重点用户账户管理注册、登录、登出。这是大多数应用的基础可以使用本地轻量级存储如 SharedPreferences模拟或集成后端如 Spring Boot为简化演示本文采用本地模拟登录。账单记录管理增添加一笔收入或支出包含类型、金额、日期、备注、分类如餐饮、交通、购物等。删删除单条或多条账单记录。改修改已存在的账单记录。查查看所有账单列表并支持按时间、类型、分类进行筛选。数据统计与可视化以饼图或柱状图展示不同分类的支出/收入占比。统计指定时间段内的总支出、总收入及结余。数据持久化将账单数据持久化存储到本地数据库确保应用重启后数据不丢失。1.2 技术栈选型与理由基于上述需求我们选择以下技术栈它们都是 Android 官方推荐或社区主流方案学习资源丰富开发环境Android Studio版本建议使用稳定的稳定版如 Arctic Fox 或更高。这是 Google 官方的 IDE对 Android 开发支持最完善。开发语言Java。虽然 Kotlin 已是官方首选但考虑到大部分高校课程仍以 Java 为主为保证毕设的普适性本文使用 Java 进行演示。理解原理后迁移到 Kotlin 并不困难。UI 框架标准 Android SDK 组件包括Activity,Fragment,RecyclerView,ViewPager2等。使用ConstraintLayout进行灵活布局。本地数据库SQLite Room Persistence Library。Room 是 Google 官方推荐的 SQLite 对象映射库能极大简化数据库操作代码避免直接编写冗长且易错的 SQL 语句。图表库MPAndroidChart。这是一个功能强大且广泛使用的开源图表库可以轻松绘制饼图、折线图、柱状图等满足数据可视化需求。架构模式采用 Model-View-Controller (MVC) 或更清晰的 Model-View-ViewModel (MVVM) 模式。本文为清晰起见先采用简单的 MVC但会注意分层便于后期重构为 MVVM。依赖管理使用 Gradle 进行项目构建和依赖管理。这个技术栈组合平衡了学习成本、实现难度和项目完整性能够很好地展示一个本科毕设应有的技术深度。2. 环境准备与项目初始化在开始写业务代码前需要确保开发环境就绪并创建好项目的基本骨架。2.1 Android Studio 安装与配置如果你尚未安装 Android Studio可以从其官方网站下载安装包。安装过程中注意勾选 Android SDK 和 Android Virtual Device (AVD) 管理器。安装完成后首次启动会引导你下载必要的 SDK 组件。关键配置检查点SDK 路径确保 SDK 安装在空间充足的目录。可以在File - Settings - Appearance Behavior - System Settings - Android SDK中查看。JDK 版本Android Studio 通常自带 JDK但建议确认使用的是 JDK 8 或 11LTS 版本。在File - Project Structure - SDK Location中查看。AVD 创建通过Tools - Device Manager创建一个虚拟设备建议选择Pixel系列系统镜像选择最新的稳定版 API Level如 API 33。2.2 创建新项目与基础配置打开 Android Studio选择New Project。模板选择Empty Views Activity对于更简单的结构Empty Activity也可。这个模板会生成一个基础的MainActivity和布局文件。配置项目Name:MyAccountBookPackage name:com.example.myaccountbook(可按自己域名规则修改)Save location: 选择一个合适的路径。Language:JavaMinimum SDK: 选择API 24: Android 7.0 (Nougat)或更高以覆盖大多数设备。过低的 API 会限制新特性使用过高则可能减少应用覆盖率。点击Finish等待项目构建完成。2.3 添加项目依赖我们需要在app模块的build.gradle文件中添加 Room 和 MPAndroidChart 的依赖。打开app/build.gradle文件在dependencies块中添加dependencies { implementation androidx.appcompat:appcompat:1.6.1 implementation com.google.android.material:material:1.9.0 implementation androidx.constraintlayout:constraintlayout:2.1.4 testImplementation junit:junit:4.13.2 androidTestImplementation androidx.test.ext:junit:1.1.5 androidTestImplementation androidx.test.espresso:espresso-core:3.5.1 // Room 数据库依赖 def room_version 2.5.2 implementation androidx.room:room-runtime:$room_version annotationProcessor androidx.room:room-compiler:$room_version // MPAndroidChart 图表库依赖 implementation com.github.PhilJay:MPAndroidChart:v3.1.0 }注意依赖版本号可能更新建议查阅官方文档使用最新稳定版。添加后Android Studio 右上角会出现Sync Now提示点击同步项目。常见问题 1Gradle 同步失败提示 “Connection refused” 或网络错误现象同步时卡住或报错。原因通常是网络问题或 Gradle 仓库镜像配置问题。解决检查网络连接尝试切换网络。在项目根目录的build.gradle文件中修改repositories块使用国内镜像源如阿里云。// 在 allprojects 的 repositories 里添加 maven { url https://maven.aliyun.com/repository/public } maven { url https://maven.aliyun.com/repository/google } maven { url https://maven.aliyun.com/repository/gradle-plugin }关闭 Android Studio删除项目根目录下的.gradle文件夹重新打开同步。常见问题 2虚拟机启动失败提示“未启用虚拟化”现象在 Windows 上使用 WSL2 或 AVD 时提示虚拟化未启用。原因计算机 BIOS/UEFI 中的虚拟化技术如 Intel VT-x 或 AMD-V未开启。解决重启计算机进入 BIOS/UEFI 设置开机时按 F2、Del 等键找到Virtualization Technology或类似选项将其设置为Enabled保存退出。3. 数据库设计与 Room 集成数据层是应用的核心。我们将使用 Room 来管理账单数据。3.1 定义实体类 (Entity)实体类对应数据库中的表。首先在java目录下你的包名内新建一个entity包然后创建Bill类。package com.example.myaccountbook.entity; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.PrimaryKey; import java.util.Date; Entity(tableName bill_table) public class Bill { PrimaryKey(autoGenerate true) private int id; ColumnInfo(name type) private String type; // “income” 或 “expense” ColumnInfo(name category) private String category; // 如 “food”, “transport”, “salary” ColumnInfo(name amount) private double amount; ColumnInfo(name date) private Date date; ColumnInfo(name note) private String note; // 构造方法、Getter 和 Setter 省略需自行生成或使用 Lombok若引入 // 建议使用 Android Studio 的 Generate 功能快速生成。 }关键点解释Entity(tableName “bill_table”)声明这是一个 Room 实体并指定表名。PrimaryKey(autoGenerate true)将id设为主键且自增长。ColumnInfo(name “xxx”)指定列名可不写默认使用字段名。字段类型Date类型 Room 可以处理但存储为 LONG 类型。也可以直接使用Long存储时间戳。3.2 创建数据访问对象 (DAO)DAO 是一个接口定义了访问数据库的方法。在dao包下创建BillDao接口。package com.example.myaccountbook.dao; import androidx.lifecycle.LiveData; import androidx.room.Dao; import androidx.room.Delete; import androidx.room.Insert; import androidx.room.Query; import androidx.room.Update; import com.example.myaccountbook.entity.Bill; import java.util.List; Dao public interface BillDao { Insert void insert(Bill bill); Update void update(Bill bill); Delete void delete(Bill bill); Query(DELETE FROM bill_table) void deleteAllBills(); // 查询所有账单按日期降序排列 Query(SELECT * FROM bill_table ORDER BY date DESC) LiveDataListBill getAllBills(); // 查询指定类型收入/支出的账单 Query(SELECT * FROM bill_table WHERE type :type ORDER BY date DESC) LiveDataListBill getBillsByType(String type); // 查询指定时间范围内的账单示例查询某月的账单 Query(SELECT * FROM bill_table WHERE date BETWEEN :start AND :end ORDER BY date DESC) LiveDataListBill getBillsBetween(Date start, Date end); // 统计某时间段内的总收入 Query(SELECT SUM(amount) FROM bill_table WHERE type income AND date BETWEEN :start AND :end) double getTotalIncomeBetween(Date start, Date end); // 统计某时间段内的总支出 Query(SELECT SUM(amount) FROM bill_table WHERE type expense AND date BETWEEN :start AND :end) double getTotalExpenseBetween(Date start, Date end); // 按分类统计支出用于饼图 Query(SELECT category, SUM(amount) as sumAmount FROM bill_table WHERE type expense AND date BETWEEN :start AND :end GROUP BY category) LiveDataListCategorySum getExpenseSummary(Date start, Date end); // 需要一个静态内部类来接收分组查询的结果 static class CategorySum { public String category; public double sumAmount; } }关键点解释Dao标识这是一个 DAO 接口。Insert,Update,DeleteRoom 提供的便捷注解用于简单的增删改。Query用于编写自定义 SQL 查询。:type是方法参数的引用。LiveDataListBill返回 LiveData 对象便于在 UI 层观察数据变化自动更新界面。这是实现响应式 UI 的关键。分组查询getExpenseSummary方法返回一个自定义的CategorySum列表用于后续生成饼图数据。3.3 创建数据库类 (Database)Database 类是数据库的持有者也是访问的入口。在database包下创建AppDatabase类。package com.example.myaccountbook.database; import androidx.room.Database; import androidx.room.Room; import androidx.room.RoomDatabase; import android.content.Context; import com.example.myaccountbook.dao.BillDao; import com.example.myaccountbook.entity.Bill; Database(entities {Bill.class}, version 1, exportSchema false) public abstract class AppDatabase extends RoomDatabase { public abstract BillDao billDao(); private static volatile AppDatabase INSTANCE; public static AppDatabase getDatabase(final Context context) { if (INSTANCE null) { synchronized (AppDatabase.class) { if (INSTANCE null) { INSTANCE Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, account_book_database) .fallbackToDestructiveMigration() // 版本升级时简单处理清空重建 .build(); } } } return INSTANCE; } }关键点解释Database注解声明这是一个 RoomDatabase列出包含的实体类指定版本号。exportSchema false可避免生成架构文件简化项目。abstract方法为每个 DAO 提供一个抽象的 getter 方法。单例模式确保整个应用只有一个数据库实例避免多实例导致的数据不一致或资源浪费。fallbackToDestructiveMigration()数据库版本升级时的迁移策略。这里采用破坏性迁移删除旧表创建新表仅适用于开发阶段。生产环境需要编写详细的Migration类来保留数据。4. UI 设计与核心功能实现数据库层准备好后开始构建用户界面。我们将创建几个主要的 Activity/Fragment。4.1 主界面与底部导航主界面通常包含一个底部导航栏用于切换“首页”、“记账”、“统计”等页面。修改activity_main.xml使用BottomNavigationView和ViewPager2或FragmentContainerView。?xml version1.0 encodingutf-8? androidx.constraintlayout.widget.ConstraintLayout xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:apphttp://schemas.android.com/apk/res-auto android:layout_widthmatch_parent android:layout_heightmatch_parent androidx.fragment.app.FragmentContainerView android:idid/nav_host_fragment android:nameandroidx.navigation.fragment.NavHostFragment android:layout_width0dp android:layout_height0dp app:layout_constraintBottom_toTopOfid/bottom_navigation app:layout_constraintEnd_toEndOfparent app:layout_constraintStart_toStartOfparent app:layout_constraintTop_toTopOfparent app:defaultNavHosttrue app:navGraphnavigation/mobile_navigation / com.google.android.material.bottomnavigation.BottomNavigationView android:idid/bottom_navigation android:layout_width0dp android:layout_heightwrap_content app:layout_constraintBottom_toBottomOfparent app:layout_constraintEnd_toEndOfparent app:layout_constraintStart_toStartOfparent app:menumenu/bottom_nav_menu / /androidx.constraintlayout.widget.ConstraintLayout创建导航图在res/navigation/目录下创建mobile_navigation.xml定义 Fragment 之间的跳转关系。创建底部菜单在res/menu/目录下创建bottom_nav_menu.xml定义导航项。修改MainActivity.java设置底部导航与 NavController 的关联。public class MainActivity extends AppCompatActivity { private ActivityMainBinding binding; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding ActivityMainBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); NavController navController Navigation.findNavController(this, R.id.nav_host_fragment); NavigationUI.setupWithNavController(binding.bottomNavigation, navController); } }4.2 账单列表页面 (HomeFragment)这个页面使用RecyclerView展示所有账单列表。创建列表项布局item_bill.xml设计每条账单的显示样式类型图标、分类、金额、日期、备注。创建BillAdapter适配器继承RecyclerView.Adapter绑定数据到item_bill.xml。创建HomeFragmentpublic class HomeFragment extends Fragment { private FragmentHomeBinding binding; private BillViewModel billViewModel; Override public View onCreateView(NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding FragmentHomeBinding.inflate(inflater, container, false); return binding.getRoot(); } Override public void onViewCreated(NonNull View view, Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); // 初始化 RecyclerView 和 Adapter BillAdapter adapter new BillAdapter(); binding.recyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); binding.recyclerView.setAdapter(adapter); // 获取 ViewModel billViewModel new ViewModelProvider(this).get(BillViewModel.class); // 观察账单列表数据并更新 UI billViewModel.getAllBills().observe(getViewLifecycleOwner(), new ObserverListBill() { Override public void onChanged(ListBill bills) { adapter.submitList(bills); // 可以在这里计算并显示总收入/总支出 double totalIncome 0, totalExpense 0; for (Bill bill : bills) { if (income.equals(bill.getType())) { totalIncome bill.getAmount(); } else { totalExpense bill.getAmount(); } } binding.textViewBalance.setText(String.format(结余: ¥%.2f, totalIncome - totalExpense)); } }); // 设置 FloatingActionButton 点击跳转到添加账单页面 binding.fabAddBill.setOnClickListener(v - { Navigation.findNavController(v).navigate(R.id.action_homeFragment_to_addBillFragment); }); } }关键点这里引入了BillViewModel它是连接 UIFragment和数据Repository/DAO的桥梁遵循 MVVM 模式避免在 Fragment 中直接操作数据库。4.3 添加/编辑账单页面 (AddBillFragment)这个页面是一个表单用于输入账单信息。创建布局fragment_add_bill.xml包含RadioGroup收入/支出、Spinner分类、EditText金额、备注、DatePicker等组件。创建AddBillFragmentpublic class AddBillFragment extends Fragment { private FragmentAddBillBinding binding; private BillViewModel billViewModel; private String currentType expense; // 默认支出 private String currentCategory food; // 默认餐饮 private Calendar selectedDate Calendar.getInstance(); Override public View onCreateView(NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding FragmentAddBillBinding.inflate(inflater, container, false); return binding.getRoot(); } Override public void onViewCreated(NonNull View view, Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); billViewModel new ViewModelProvider(requireActivity()).get(BillViewModel.class); // 设置类型选择监听 binding.radioGroupType.setOnCheckedChangeListener((group, checkedId) - { currentType (checkedId R.id.radio_income) ? income : expense; // 可以根据类型动态更新分类 Spinner 的数据源 updateCategorySpinner(); }); // 设置日期选择 binding.buttonPickDate.setOnClickListener(v - showDatePickerDialog()); // 保存按钮 binding.buttonSave.setOnClickListener(v - saveBill()); // 如果是编辑模式从参数中获取 billId 并加载数据 if (getArguments() ! null getArguments().containsKey(bill_id)) { int billId getArguments().getInt(bill_id); billViewModel.getBillById(billId).observe(getViewLifecycleOwner(), bill - { if (bill ! null) { populateForm(bill); } }); } } private void saveBill() { String amountStr binding.editTextAmount.getText().toString().trim(); if (amountStr.isEmpty()) { binding.editTextAmount.setError(金额不能为空); return; } double amount Double.parseDouble(amountStr); String note binding.editTextNote.getText().toString().trim(); Bill bill new Bill(); bill.setType(currentType); bill.setCategory(currentCategory); bill.setAmount(amount); bill.setDate(selectedDate.getTime()); bill.setNote(note); // 判断是新增还是更新 if (getArguments() ! null getArguments().containsKey(bill_id)) { int billId getArguments().getInt(bill_id); bill.setId(billId); billViewModel.update(bill); } else { billViewModel.insert(bill); } // 保存成功后返回上一页 Navigation.findNavController(requireView()).popBackStack(); } private void showDatePickerDialog() { DatePickerDialog dialog new DatePickerDialog(requireContext(), (view, year, month, dayOfMonth) - { selectedDate.set(year, month, dayOfMonth); binding.buttonPickDate.setText(String.format(%d-%02d-%02d, year, month 1, dayOfMonth)); }, selectedDate.get(Calendar.YEAR), selectedDate.get(Calendar.MONTH), selectedDate.get(Calendar.DAY_OF_MONTH)); dialog.show(); } // ... 其他方法如 updateCategorySpinner, populateForm }4.4 数据仓库与 ViewModel为了遵循清晰的架构我们引入 Repository 层和 ViewModel。创建BillRepository负责协调数据源本地数据库、未来可能的网络。public class BillRepository { private BillDao billDao; private LiveDataListBill allBills; public BillRepository(Application application) { AppDatabase db AppDatabase.getDatabase(application); billDao db.billDao(); allBills billDao.getAllBills(); } public LiveDataListBill getAllBills() { return allBills; } public void insert(Bill bill) { AppDatabase.databaseWriteExecutor.execute(() - billDao.insert(bill)); } public void update(Bill bill) { AppDatabase.databaseWriteExecutor.execute(() - billDao.update(bill)); } public void delete(Bill bill) { AppDatabase.databaseWriteExecutor.execute(() - billDao.delete(bill)); } // ... 其他 DAO 方法的封装 }创建BillViewModel为 UI 提供数据并处理与 Repository 的交互。public class BillViewModel extends AndroidViewModel { private BillRepository repository; private LiveDataListBill allBills; public BillViewModel(NonNull Application application) { super(application); repository new BillRepository(application); allBills repository.getAllBills(); } public LiveDataListBill getAllBills() { return allBills; } public void insert(Bill bill) { repository.insert(bill); } public void update(Bill bill) { repository.update(bill); } public void delete(Bill bill) { repository.delete(bill); } // ... 其他方法 }5. 数据统计与图表展示图表功能是毕设的亮点。我们将使用 MPAndroidChart 在统计页面绘制支出分类饼图。5.1 集成 MPAndroidChart 并绘制饼图创建统计页面StatisticsFragment及其布局布局中包含一个PieChart组件。在 Fragment 中初始化图表并设置数据public class StatisticsFragment extends Fragment { private FragmentStatisticsBinding binding; private BillViewModel billViewModel; private PieChart pieChart; Override public View onCreateView(NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding FragmentStatisticsBinding.inflate(inflater, container, false); pieChart binding.pieChart; return binding.getRoot(); } Override public void onViewCreated(NonNull View view, Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); billViewModel new ViewModelProvider(requireActivity()).get(BillViewModel.class); // 设置图表基本属性 pieChart.getDescription().setEnabled(false); // 不显示描述 pieChart.setDrawHoleEnabled(true); // 显示中间的洞 pieChart.setHoleRadius(30f); // 洞的大小 pieChart.setTransparentCircleRadius(35f); pieChart.setEntryLabelColor(Color.BLACK); pieChart.setEntryLabelTextSize(12f); // 观察数据并更新图表 Calendar calendar Calendar.getInstance(); calendar.set(Calendar.DAY_OF_MONTH, 1); // 本月第一天 Date startOfMonth calendar.getTime(); calendar.add(Calendar.MONTH, 1); calendar.add(Calendar.DAY_OF_MONTH, -1); // 本月最后一天 Date endOfMonth calendar.getTime(); billViewModel.getExpenseSummary(startOfMonth, endOfMonth).observe(getViewLifecycleOwner(), categorySums - { if (categorySums ! null !categorySums.isEmpty()) { setPieChartData(categorySums); } else { // 没有数据时显示提示 pieChart.clear(); pieChart.setNoDataText(本月暂无支出数据); } }); } private void setPieChartData(ListBillDao.CategorySum categorySums) { ArrayListPieEntry entries new ArrayList(); for (BillDao.CategorySum cs : categorySums) { entries.add(new PieEntry((float) cs.sumAmount, cs.category)); } PieDataSet dataSet new PieDataSet(entries, 支出分类); dataSet.setColors(ColorTemplate.MATERIAL_COLORS); // 设置颜色 dataSet.setValueTextSize(12f); dataSet.setValueTextColor(Color.BLACK); PieData pieData new PieData(dataSet); pieChart.setData(pieData); pieChart.invalidate(); // 刷新图表 } }5.2 图表常见问题与优化问题图表不显示或显示异常检查确保已正确添加 MPAndroidChart 依赖并同步。检查setData后是否调用了invalidate()或notifyDataSetChanged()。检查传入的数据集entries是否为空或数值异常如 NaN。优化建议添加图例pieChart.getLegend().setEnabled(true);添加动画pieChart.animateY(1000, Easing.EaseInOutQuad);点击交互可以设置pieChart.setOnChartValueSelectedListener来响应点击事件显示具体数值。6. 项目运行、测试与常见问题排查完成核心代码后需要在真机或模拟器上运行测试。6.1 运行与基础功能测试连接 Android 设备或启动 AVD。点击 Android Studio 的Run ‘app’按钮。依次测试以下功能添加账单点击 FAB填写信息保存。返回列表查看是否新增。查看列表列表是否按时间倒序排列金额、分类显示是否正确。编辑账单长按列表项需实现长按菜单或点击进入详情修改后保存检查列表是否更新。删除账单实现列表项的滑动删除或长按删除确认数据从列表和数据库中移除。统计图表切换到统计页面确保在有支出数据的情况下能正确显示饼图。6.2 常见问题排查清单在开发过程中你可能会遇到以下典型问题问题现象可能原因检查与解决步骤应用安装失败设备已有同名应用签名冲突最低 SDK 版本高于设备系统。1. 卸载设备上的旧版本。 2. 检查build.gradle中的applicationId和minSdkVersion。应用启动后立即崩溃主 Activity 未在AndroidManifest.xml中注册数据库初始化失败依赖冲突。1. 查看Logcat中的红色错误日志AndroidRuntime。 2. 检查AndroidManifest.xml。 3. 检查 Room Database 的实例化代码是否在主线程调用Room 默认不允许主线程操作需使用allowMainThreadQueries或异步任务仅用于测试。列表数据不显示RecyclerView未设置LayoutManagerAdapter未正确绑定数据LiveData观察者未触发。1. 确认setLayoutManager和setAdapter已调用。 2. 在Adapter的onBindViewHolder中打日志看数据是否传入。 3. 检查ViewModel中的数据源是否为空。数据库操作无效数据库操作在 UI 线程执行导致崩溃或被忽略insert/update后未通知LiveData更新。1. 确保数据库操作在子线程如Executor中执行。 2. 确认DAO方法返回void或CompletableLiveData查询会自动更新 UI。图表显示空白PieChart数据为空未调用invalidate()布局中PieChart宽高为 0。1. 检查setPieChartData方法中entries是否为空。 2. 确保调用了pieChart.invalidate()。 3. 检查布局文件确认PieChart的layout_width和layout_height不是wrap_content且父容器有足够空间。编辑账单时数据未回显AddBillFragment未接收到billId参数ViewModel中未实现getBillById方法。1. 检查从列表跳转到编辑页面的Bundle参数传递。 2. 在BillDao和BillViewModel中实现getBillById方法。6.3 针对毕设的扩展与优化建议一个基础的记账本完成后可以考虑以下方向进行深化以提升毕设质量用户系统集成真正的后端如 Spring Boot MySQL实现网络注册登录使用Retrofit进行网络请求SharedPreferences或DataStore存储 Token。数据备份与恢复实现将本地数据库导出为文件如 JSON 或 CSV并支持从文件导入恢复。涉及文件读写权限。预算功能为每个分类设置月度预算在首页或统计页展示预算使用进度。更丰富的图表使用 MPAndroidChart 添加月度支出趋势折线图、收入支出对比柱状图等。数据筛选与搜索在首页增加筛选栏支持按年、月、日、分类多维度筛选账单。应用主题与国际化支持深色/浅色模式切换支持多语言中英文。单元测试为BillDao、BillRepository或BillViewModel编写单元测试展示软件工程能力。性能优化对于大量账单实现列表分页加载Paging库对图表数据进行聚合计算时避免在主线程进行。开发过程中务必养成良好的代码习惯添加有意义的注释、对资源 ID 和字符串使用资源文件引用、处理可能的异常如网络超时、数据解析失败、关注应用的生命周期管理防止内存泄漏。这些细节同样是答辩时的加分项。