Android Studio开发记账本应用:从零到毕业设计的完整实践指南
2026/9/4 17:29:33 网站建设 项目流程

在实际计算机专业本科毕业设计或大作业中,选择一个功能明确、技术栈主流、工作量适中的项目至关重要。一个基于 Android Studio 开发的记账本应用,就是一个非常经典的选择。它涵盖了移动应用开发的核心流程:UI设计、数据存储、业务逻辑、用户交互,并能自然地融入图表分析等进阶功能,足以体现学生对 Android 开发、数据库操作和软件工程基础的理解。对于初学者而言,从零开始构建一个完整的记账本应用,不仅能巩固 Java/Kotlin 语法,更能深入理解 Activity、Fragment、RecyclerView、SQLite 等核心组件的实际应用。

本文将带你一步步完成一个功能完备的 Android 记账本应用,作为毕业设计或大作业的参考实现。我们将从项目需求分析开始,设计数据库表结构,搭建基础 UI 框架,实现账单的增删改查,并最终集成统计图表功能。整个过程会详细解释关键代码和配置,并指出开发中常见的“坑”及其解决方案。完成后的项目将具备清晰的 MVC 架构,代码规范,便于扩展和答辩展示。

1. 项目需求分析与技术选型

在动手编码之前,明确项目要做什么、用什么技术来做,是确保项目顺利推进的前提。一个记账本的核心功能相对固定,但我们可以为其增加一些亮点以满足毕设的深度要求。

1.1 核心功能需求

一个基础的记账本应用通常需要满足以下功能点,这些也是评审老师关注的重点:

  1. 用户账户管理:注册、登录、登出。这是大多数应用的基础,可以使用本地轻量级存储(如 SharedPreferences)模拟,或集成后端(如 Spring Boot),为简化演示,本文采用本地模拟登录。
  2. 账单记录管理
    • :添加一笔收入或支出,包含类型、金额、日期、备注、分类(如餐饮、交通、购物等)。
    • :删除单条或多条账单记录。
    • :修改已存在的账单记录。
    • :查看所有账单列表,并支持按时间、类型、分类进行筛选。
  3. 数据统计与可视化
    • 以饼图或柱状图展示不同分类的支出/收入占比。
    • 统计指定时间段内的总支出、总收入及结余。
  4. 数据持久化:将账单数据持久化存储到本地数据库,确保应用重启后数据不丢失。

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 组件。

关键配置检查点

  1. SDK 路径:确保 SDK 安装在空间充足的目录。可以在File -> Settings -> Appearance & Behavior -> System Settings -> Android SDK中查看。
  2. JDK 版本:Android Studio 通常自带 JDK,但建议确认使用的是 JDK 8 或 11(LTS 版本)。在File -> Project Structure -> SDK Location中查看。
  3. AVD 创建:通过Tools -> Device Manager创建一个虚拟设备,建议选择Pixel系列,系统镜像选择最新的稳定版 API Level(如 API 33)。

2.2 创建新项目与基础配置

  1. 打开 Android Studio,选择New Project
  2. 模板选择Empty Views Activity(对于更简单的结构,Empty Activity也可)。这个模板会生成一个基础的MainActivity和布局文件。
  3. 配置项目:
    • Name:MyAccountBook
    • Package name:com.example.myaccountbook(可按自己域名规则修改)
    • Save location: 选择一个合适的路径。
    • Language:Java
    • Minimum SDK: 选择API 24: Android 7.0 (Nougat)或更高,以覆盖大多数设备。过低的 API 会限制新特性使用,过高则可能减少应用覆盖率。
  4. 点击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提示,点击同步项目。

常见问题 1:Gradle 同步失败,提示 “Connection refused” 或网络错误

  • 现象:同步时卡住或报错。
  • 原因:通常是网络问题或 Gradle 仓库镜像配置问题。
  • 解决
    1. 检查网络连接,尝试切换网络。
    2. 在项目根目录的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' }
    3. 关闭 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") LiveData<List<Bill>> getAllBills(); // 查询指定类型(收入/支出)的账单 @Query("SELECT * FROM bill_table WHERE type = :type ORDER BY date DESC") LiveData<List<Bill>> getBillsByType(String type); // 查询指定时间范围内的账单(示例:查询某月的账单) @Query("SELECT * FROM bill_table WHERE date BETWEEN :start AND :end ORDER BY date DESC") LiveData<List<Bill>> 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") LiveData<List<CategorySum>> getExpenseSummary(Date start, Date end); // 需要一个静态内部类来接收分组查询的结果 static class CategorySum { public String category; public double sumAmount; } }

关键点解释

  • @Dao:标识这是一个 DAO 接口。
  • @Insert,@Update,@Delete:Room 提供的便捷注解,用于简单的增删改。
  • @Query:用于编写自定义 SQL 查询。:type是方法参数的引用。
  • LiveData<List<Bill>>:返回 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 主界面与底部导航

主界面通常包含一个底部导航栏,用于切换“首页”、“记账”、“统计”等页面。

  1. 修改activity_main.xml:使用BottomNavigationViewViewPager2FragmentContainerView
    <?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.fragment.app.FragmentContainerView android:id="@+id/nav_host_fragment" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="0dp" android:layout_height="0dp" app:layout_constraintBottom_toTopOf="@+id/bottom_navigation" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:defaultNavHost="true" app:navGraph="@navigation/mobile_navigation" /> <com.google.android.material.bottomnavigation.BottomNavigationView android:id="@+id/bottom_navigation" android:layout_width="0dp" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:menu="@menu/bottom_nav_menu" /> </androidx.constraintlayout.widget.ConstraintLayout>
  2. 创建导航图:在res/navigation/目录下创建mobile_navigation.xml,定义 Fragment 之间的跳转关系。
  3. 创建底部菜单:在res/menu/目录下创建bottom_nav_menu.xml,定义导航项。
  4. 修改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展示所有账单列表。

  1. 创建列表项布局item_bill.xml:设计每条账单的显示样式(类型图标、分类、金额、日期、备注)。
  2. 创建BillAdapter适配器:继承RecyclerView.Adapter,绑定数据到item_bill.xml
  3. 创建HomeFragment
    public 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 Observer<List<Bill>>() { @Override public void onChanged(List<Bill> 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,它是连接 UI(Fragment)和数据(Repository/DAO)的桥梁,遵循 MVVM 模式,避免在 Fragment 中直接操作数据库。

4.3 添加/编辑账单页面 (AddBillFragment)

这个页面是一个表单,用于输入账单信息。

  1. 创建布局fragment_add_bill.xml:包含RadioGroup(收入/支出)、Spinner(分类)、EditText(金额、备注)、DatePicker等组件。
  2. 创建AddBillFragment
    public 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。

  1. 创建BillRepository:负责协调数据源(本地数据库、未来可能的网络)。
    public class BillRepository { private BillDao billDao; private LiveData<List<Bill>> allBills; public BillRepository(Application application) { AppDatabase db = AppDatabase.getDatabase(application); billDao = db.billDao(); allBills = billDao.getAllBills(); } public LiveData<List<Bill>> 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 方法的封装 }
  2. 创建BillViewModel:为 UI 提供数据,并处理与 Repository 的交互。
    public class BillViewModel extends AndroidViewModel { private BillRepository repository; private LiveData<List<Bill>> allBills; public BillViewModel(@NonNull Application application) { super(application); repository = new BillRepository(application); allBills = repository.getAllBills(); } public LiveData<List<Bill>> 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 并绘制饼图

  1. 创建统计页面StatisticsFragment及其布局,布局中包含一个PieChart组件。
  2. 在 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(List<BillDao.CategorySum> categorySums) { ArrayList<PieEntry> 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 运行与基础功能测试

  1. 连接 Android 设备或启动 AVD。
  2. 点击 Android Studio 的Run ‘app’按钮。
  3. 依次测试以下功能:
    • 添加账单:点击 FAB,填写信息,保存。返回列表查看是否新增。
    • 查看列表:列表是否按时间倒序排列,金额、分类显示是否正确。
    • 编辑账单:长按列表项(需实现长按菜单或点击进入详情),修改后保存,检查列表是否更新。
    • 删除账单:实现列表项的滑动删除或长按删除,确认数据从列表和数据库中移除。
    • 统计图表:切换到统计页面,确保在有支出数据的情况下能正确显示饼图。

6.2 常见问题排查清单

在开发过程中,你可能会遇到以下典型问题:

问题现象可能原因检查与解决步骤
应用安装失败设备已有同名应用;签名冲突;最低 SDK 版本高于设备系统。1. 卸载设备上的旧版本。 2. 检查build.gradle中的applicationIdminSdkVersion
应用启动后立即崩溃主 Activity 未在AndroidManifest.xml中注册;数据库初始化失败;依赖冲突。1. 查看Logcat中的红色错误日志(AndroidRuntime)。 2. 检查AndroidManifest.xml。 3. 检查 Room Database 的实例化代码是否在主线程调用(Room 默认不允许主线程操作,需使用allowMainThreadQueries或异步任务,仅用于测试)。
列表数据不显示RecyclerView未设置LayoutManagerAdapter未正确绑定数据;LiveData观察者未触发。1. 确认setLayoutManagersetAdapter已调用。 2. 在AdapteronBindViewHolder中打日志,看数据是否传入。 3. 检查ViewModel中的数据源是否为空。
数据库操作无效数据库操作在 UI 线程执行导致崩溃或被忽略;insert/update后未通知LiveData更新。1. 确保数据库操作在子线程(如Executor)中执行。 2. 确认DAO方法返回voidCompletableLiveData查询会自动更新 UI。
图表显示空白PieChart数据为空;未调用invalidate();布局中PieChart宽高为 0。1. 检查setPieChartData方法中entries是否为空。 2. 确保调用了pieChart.invalidate()。 3. 检查布局文件,确认PieChartlayout_widthlayout_height不是wrap_content且父容器有足够空间。
编辑账单时数据未回显AddBillFragment未接收到billId参数;ViewModel中未实现getBillById方法。1. 检查从列表跳转到编辑页面的Bundle参数传递。 2. 在BillDaoBillViewModel中实现getBillById方法。

6.3 针对毕设的扩展与优化建议

一个基础的记账本完成后,可以考虑以下方向进行深化,以提升毕设质量:

  1. 用户系统:集成真正的后端(如 Spring Boot + MySQL),实现网络注册登录,使用Retrofit进行网络请求,SharedPreferencesDataStore存储 Token。
  2. 数据备份与恢复:实现将本地数据库导出为文件(如 JSON 或 CSV),并支持从文件导入恢复。涉及文件读写权限。
  3. 预算功能:为每个分类设置月度预算,在首页或统计页展示预算使用进度。
  4. 更丰富的图表:使用 MPAndroidChart 添加月度支出趋势折线图、收入支出对比柱状图等。
  5. 数据筛选与搜索:在首页增加筛选栏,支持按年、月、日、分类多维度筛选账单。
  6. 应用主题与国际化:支持深色/浅色模式切换,支持多语言(中英文)。
  7. 单元测试:为BillDaoBillRepositoryBillViewModel编写单元测试,展示软件工程能力。
  8. 性能优化:对于大量账单,实现列表分页加载(Paging库);对图表数据进行聚合计算时,避免在主线程进行。

开发过程中,务必养成良好的代码习惯:添加有意义的注释、对资源 ID 和字符串使用资源文件引用、处理可能的异常(如网络超时、数据解析失败)、关注应用的生命周期管理(防止内存泄漏)。这些细节同样是答辩时的加分项。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询