十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android Studio记账本开发实战:从零构建MVVM架构应用

Android Studio记账本开发实战:从零构建MVVM架构应用 1. 项目背景与核心价值在计算机科学与技术、软件工程等相关专业的毕业设计或期末大作业中开发一个功能完整、贴近实际应用的移动端App是检验学生综合能力的重要方式。其中“记账本”应用因其需求明确、业务闭环、技术栈覆盖全面成为众多同学的首选课题。它不仅能锻炼Android原生开发的核心技能还能深入理解数据库设计、UI/UX交互、数据可视化等关键知识点。然而从零开始构建一个记账本应用新手开发者常会遇到一系列挑战Android Studio环境配置复杂、项目结构不清晰、数据库操作繁琐、界面适配困难以及如何将零散的功能模块整合成一个稳定可用的产品。本文将以一个完整的“Android Studio记账本”项目为例系统性地拆解从环境搭建、需求分析、数据库设计、功能实现到打包上线的全流程。无论你是正在为毕设选题发愁还是希望夯实Android开发基础这篇实战指南都将提供一条清晰的路径和可复用的代码。2. 环境准备与开发工具工欲善其事必先利其器。一个稳定的开发环境是项目成功的第一步。以下是本教程所使用的核心工具及版本建议读者尽量保持一致以避免不必要的兼容性问题。2.1 核心开发环境操作系统: Windows 10/11, macOS Monterey 或更高版本 Ubuntu 20.04 LTS 或更高版本。本文演示以Windows 11为例。Android Studio: 推荐使用Android Studio Flamingo (2022.2.1)或Giraffe (2022.3.1)等较新稳定版本。你可以从Android开发者官网下载。Java开发工具包 (JDK): Android Studio 通常内置了JDK。为确保一致性我们使用JDK 17LTS版本。你可以在Android Studio的File - Project Structure - SDK Location中查看和修改JDK路径。Android SDK: 确保已安装必要的SDK平台和构建工具。本项目主要面向API 33 (Android 13)进行开发并向下兼容至API 26 (Android 8.0)。2.2 项目依赖与配置本项目将使用以下主要依赖库它们能极大提升开发效率Room Persistence Library: Android官方推荐的SQLite对象映射库用于简化数据库操作。ViewModel LiveData: 用于以生命周期感知的方式管理界面相关数据。Material Components for Android: 提供现代化的Material Design UI组件。MPAndroidChart: 一个功能强大的图表库用于实现收支数据的可视化分析。2.3 初始化Android Studio项目打开Android Studio点击New Project。选择模板Empty Views Activity对于新手此模板结构最清晰。点击Next。配置项目信息Name:MyMoneyBook(项目名称)Package name:com.example.mymoneybook(包名可按需修改)Save location: 选择你的项目存放路径Language:Java(本教程使用JavaKotlin同理)Minimum SDK:API 26: Android 8.0 (Oreo)以覆盖大多数设备。点击FinishAndroid Studio会自动构建项目。3. 需求分析与功能模块设计在动手编码前明确的需求和清晰的设计是避免后期返工的关键。一个典型的记账本应用应包含以下核心功能模块3.1 核心功能清单用户认证可选: 登录/注册功能用于隔离不同用户的数据。对于简单的毕设也可以跳过此模块专注于记账核心功能。账目记录CRUD:新增记录: 输入金额、选择类型收入/支出、选择分类餐饮、交通、购物等、添加备注、选择日期。查看记录: 以列表形式展示所有记录支持按日、周、月筛选。编辑记录: 修改已存在的账目信息。删除记录: 单条删除或批量删除。数据统计与分析:概览视图: 显示当前周期的总支出、总收入、结余。分类统计: 以饼图或条形图展示支出/收入的分类占比。趋势分析: 以折线图展示一段时间内的收支趋势。分类管理: 允许用户自定义收入或支出的分类如新增“学习”、“医疗”等分类。数据备份与恢复进阶: 将账本数据导出为文件或从文件导入恢复。3.2 技术架构设计我们将采用Android官方推荐的MVVM (Model-View-ViewModel)架构模式配合Room数据库使代码结构清晰、易于测试和维护。Model层: 实体类(Entity)、数据访问对象(DAO)、数据库(Database)。负责数据的定义和持久化操作。ViewModel层: 继承自AndroidViewModel或ViewModel。负责从Model层获取数据并处理UI相关的业务逻辑为View层提供数据。View层:Activity和Fragment。负责UI的展示和用户交互通过观察ViewModel中的LiveData来更新界面。4. 数据库设计与实现Room数据库是记账本应用的核心。我们使用Room来管理账目记录和分类数据。4.1 定义实体类 (Entity)首先创建两个实体类Record账目记录和Category分类。// 文件路径app/src/main/java/com/example/mymoneybook/entity/Record.java package com.example.mymoneybook.entity; import androidx.room.Entity; import androidx.room.PrimaryKey; import androidx.room.TypeConverters; import java.util.Date; Entity(tableName record_table) public class Record { PrimaryKey(autoGenerate true) private int id; private double amount; // 金额 private int type; // 类型0-支出1-收入 private String category; // 分类名称 private String remark; // 备注 private Date date; // 日期 // Room需要无参构造函数 public Record() {} // 带参构造函数方便创建对象 public Record(double amount, int type, String category, String remark, Date date) { this.amount amount; this.type type; this.category category; this.remark remark; this.date date; } // Getter和Setter方法 (此处省略实际代码中必须完整生成) public int getId() { return id; } public void setId(int id) { this.id id; } public double getAmount() { return amount; } public void setAmount(double amount) { this.amount amount; } // ... 其他getter/setter }// 文件路径app/src/main/java/com/example/mymoneybook/entity/Category.java package com.example.mymoneybook.entity; import androidx.room.Entity; import androidx.room.PrimaryKey; Entity(tableName category_table) public class Category { PrimaryKey(autoGenerate true) private int id; private String name; // 分类名称如“餐饮” private int type; // 所属大类型0-支出1-收入 public Category(String name, int type) { this.name name; this.type type; } // Getter和Setter public int getId() { return id; } public void setId(int id) { this.id id; } public String getName() { return name; } public void setName(String name) { this.name name; } public int getType() { return type; } public void setType(int type) { this.type type; } }注意Room默认不能直接存储Date或复杂对象。我们需要一个TypeConverter来将Date转换为Long时间戳存储。// 文件路径app/src/main/java/com/example/mymoneybook/utils/Converters.java package com.example.mymoneybook.utils; import androidx.room.TypeConverter; import java.util.Date; public class Converters { TypeConverter public static Date fromTimestamp(Long value) { return value null ? null : new Date(value); } TypeConverter public static Long dateToTimestamp(Date date) { return date null ? null : date.getTime(); } }4.2 定义数据访问对象 (DAO)DAO是一个接口Room会在编译时为其生成具体的实现代码。// 文件路径app/src/main/java/com/example/mymoneybook/dao/RecordDao.java package com.example.mymoneybook.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.mymoneybook.entity.Record; import java.util.List; Dao public interface RecordDao { Insert void insert(Record record); Update void update(Record record); Delete void delete(Record record); // 查询所有记录按日期降序排列最新的在前 Query(SELECT * FROM record_table ORDER BY date DESC) LiveDataListRecord getAllRecords(); // 查询某一段时间内的记录用于统计 Query(SELECT * FROM record_table WHERE date BETWEEN :start AND :end ORDER BY date DESC) LiveDataListRecord getRecordsBetween(Date start, Date end); // 根据类型收入/支出查询记录 Query(SELECT * FROM record_table WHERE type :type ORDER BY date DESC) LiveDataListRecord getRecordsByType(int type); }CategoryDao的编写类似包含插入、查询所有分类、根据类型查询分类等方法。4.3 创建数据库类 (Database)这是Room的核心用于连接实体和DAO并定义数据库版本。// 文件路径app/src/main/java/com/example/mymoneybook/database/AppDatabase.java package com.example.mymoneybook.database; import androidx.room.Database; import androidx.room.RoomDatabase; import androidx.room.TypeConverters; import com.example.mymoneybook.dao.RecordDao; import com.example.mymoneybook.dao.CategoryDao; import com.example.mymoneybook.entity.Record; import com.example.mymoneybook.entity.Category; import com.example.mymoneybook.utils.Converters; Database(entities {Record.class, Category.class}, version 1, exportSchema false) TypeConverters({Converters.class}) public abstract class AppDatabase extends RoomDatabase { public abstract RecordDao recordDao(); public abstract CategoryDao categoryDao(); // 单例模式防止重复打开数据库 private static volatile AppDatabase INSTANCE; // ... 获取单例的静态方法通常在Application中初始化 }4.4 在Application中初始化数据库为了全局访问数据库我们在自定义的Application类中初始化它。// 文件路径app/src/main/java/com/example/mymoneybook/MyApplication.java package com.example.mymoneybook; import android.app.Application; import androidx.room.Room; import com.example.mymoneybook.database.AppDatabase; public class MyApplication extends Application { private static AppDatabase database; Override public void onCreate() { super.onCreate(); database Room.databaseBuilder(getApplicationContext(), AppDatabase.class, moneybook_db) .allowMainThreadQueries() // 仅用于演示正式项目应在子线程操作 .build(); // 可以在这里初始化一些默认分类数据 initDefaultCategories(); } public static AppDatabase getDatabase() { return database; } private void initDefaultCategories() { // 在子线程中插入默认支出/收入分类 new Thread(() - { ListCategory defaultCategories Arrays.asList( new Category(餐饮, 0), new Category(交通, 0), new Category(购物, 0), new Category(娱乐, 0), new Category(工资, 1), new Category(兼职, 1) ); database.categoryDao().insertAll(defaultCategories); }).start(); } }别忘了在AndroidManifest.xml的application标签中指定这个Application类android:name.MyApplication。5. 界面实现与业务逻辑 (View ViewModel)接下来我们实现主界面——记录列表页。5.1 创建记录列表的ViewModelRecordViewModel负责获取记录数据并处理删除等操作。// 文件路径app/src/main/java/com/example/mymoneybook/ui/record/RecordViewModel.java package com.example.mymoneybook.ui.record; import android.app.Application; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import com.example.mymoneybook.database.AppDatabase; import com.example.mymoneybook.entity.Record; import java.util.List; public class RecordViewModel extends AndroidViewModel { private final LiveDataListRecord allRecords; private final AppDatabase database; public RecordViewModel(Application application) { super(application); database ((MyApplication) application).getDatabase(); allRecords database.recordDao().getAllRecords(); } public LiveDataListRecord getAllRecords() { return allRecords; } public void insert(Record record) { // 数据库操作必须在非UI线程执行 new Thread(() - database.recordDao().insert(record)).start(); } public void delete(Record record) { new Thread(() - database.recordDao().delete(record)).start(); } }5.2 实现记录列表界面 (Activity RecyclerView)布局文件 (activity_main.xml): 主要包含一个RecyclerView用于展示列表一个FloatingActionButton用于添加新记录。?xml version1.0 encodingutf-8? androidx.coordinatorlayout.widget.CoordinatorLayout 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.recyclerview.widget.RecyclerView android:idid/recyclerView android:layout_widthmatch_parent android:layout_heightmatch_parent app:layoutManagerandroidx.recyclerview.widget.LinearLayoutManager/ com.google.android.material.floatingactionbutton.FloatingActionButton android:idid/fab_add android:layout_widthwrap_content android:layout_heightwrap_content android:layout_gravitybottom|end android:layout_margin16dp android:srcandroid:drawable/ic_input_add app:backgroundTintcolor/purple_500/ /androidx.coordinatorlayout.widget.CoordinatorLayout列表项布局 (item_record.xml): 设计每个记账记录的显示样式。适配器 (RecordAdapter.java): 负责将Record数据绑定到RecyclerView的每一项上并处理点击事件。public class RecordAdapter extends RecyclerView.AdapterRecordAdapter.ViewHolder { private ListRecord mRecordList; private OnItemClickListener mListener; public interface OnItemClickListener { void onItemClick(Record record); void onItemLongClick(Record record); } // ... 构造函数、ViewHolder定义 Override public void onBindViewHolder(NonNull ViewHolder holder, int position) { Record record mRecordList.get(position); holder.tvAmount.setText(String.format(¥%.2f, record.getAmount())); holder.tvCategory.setText(record.getCategory()); holder.tvRemark.setText(record.getRemark()); // 根据收入支出设置不同颜色 int color record.getType() 0 ? Color.RED : Color.GREEN; holder.tvAmount.setTextColor(color); holder.itemView.setOnClickListener(v - { if (mListener ! null) mListener.onItemClick(record); }); holder.itemView.setOnLongClickListener(v - { if (mListener ! null) mListener.onItemLongClick(record); return true; }); } }主Activity逻辑 (MainActivity.java): 集成ViewModel、Adapter并观察数据变化。public class MainActivity extends AppCompatActivity { private RecordViewModel recordViewModel; private RecordAdapter adapter; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); RecyclerView recyclerView findViewById(R.id.recyclerView); adapter new RecordAdapter(); recyclerView.setAdapter(adapter); // 初始化ViewModel recordViewModel new ViewModelProvider(this).get(RecordViewModel.class); // 观察数据变化并更新UI recordViewModel.getAllRecords().observe(this, records - { adapter.setRecordList(records); adapter.notifyDataSetChanged(); }); // 设置列表项点击事件跳转到编辑页面 adapter.setOnItemClickListener(record - { Intent intent new Intent(MainActivity.this, AddEditRecordActivity.class); intent.putExtra(record_id, record.getId()); startActivity(intent); }); // 设置长按事件删除记录 adapter.setOnItemLongClickListener(record - { new AlertDialog.Builder(MainActivity.this) .setTitle(确认删除) .setMessage(确定要删除这条记录吗) .setPositiveButton(确定, (dialog, which) - recordViewModel.delete(record)) .setNegativeButton(取消, null) .show(); return true; }); // 点击FAB跳转到新增记录页面 findViewById(R.id.fab_add).setOnClickListener(v - { startActivity(new Intent(this, AddEditRecordActivity.class)); }); } }6. 数据统计与图表集成数据可视化能让记账本应用的价值倍增。我们使用MPAndroidChart库来实现饼图和柱状图。6.1 添加依赖在app/build.gradle文件的dependencies块中添加implementation com.github.PhilJay:MPAndroidChart:v3.1.06.2 实现统计功能ViewModel创建一个StatisticsViewModel用于处理统计数据的计算。public class StatisticsViewModel extends AndroidViewModel { public StatisticsViewModel(NonNull Application application) { super(application); } // 获取指定日期范围内的支出分类统计数据 public LiveDataMapString, Double getExpenseCategorySummary(Date start, Date end) { MutableLiveDataMapString, Double result new MutableLiveData(); new Thread(() - { ListRecord records getDatabase().recordDao() .getRecordsBetweenSync(start, end); // 需要一个同步查询方法 MapString, Double summary new HashMap(); for (Record r : records) { if (r.getType() 0) { // 支出 summary.put(r.getCategory(), summary.getOrDefault(r.getCategory(), 0.0) r.getAmount()); } } result.postValue(summary); }).start(); return result; } }6.3 创建图表展示界面在统计页面的Fragment或Activity中观察ViewModel的数据并渲染图表。// 在Fragment的onViewCreated或Activity的onCreate中 PieChart pieChart view.findViewById(R.id.pieChart); statisticsViewModel.getExpenseCategorySummary(startDate, endDate).observe(getViewLifecycleOwner(), summaryMap - { ListPieEntry entries new ArrayList(); for (Map.EntryString, Double entry : summaryMap.entrySet()) { entries.add(new PieEntry(entry.getValue().floatValue(), entry.getKey())); } PieDataSet dataSet new PieDataSet(entries, 支出分类); dataSet.setColors(ColorTemplate.MATERIAL_COLORS); // 设置颜色 dataSet.setValueTextSize(12f); PieData pieData new PieData(dataSet); pieChart.setData(pieData); pieChart.getDescription().setEnabled(false); // 隐藏描述 pieChart.setCenterText(支出分布); // 中心文字 pieChart.animateY(1000); // 动画效果 pieChart.invalidate(); // 刷新图表 });7. 常见问题与排查思路 (FAQ)在开发过程中你可能会遇到以下典型问题。这里提供排查思路和解决方案。问题现象可能原因解决思路项目编译失败提示Connection refused或Gradle sync failed1. 网络问题无法下载Gradle或依赖。2. Gradle版本与项目不兼容。3. Android Studio代理设置错误。1. 检查网络尝试切换网络或使用稳定连接。2. 在File - Settings - Build - Gradle中将Gradle user home目录下的缓存文件删除后重新同步。3. 检查File - Settings - Appearance - System Settings - HTTP Proxy确保设置正确或设置为“No proxy”。运行App时崩溃报错Room cannot verify the data integrity数据库实体类 (Entity) 或数据库版本 (Database(version)) 发生了更改但未提供迁移策略。1.开发阶段可以临时在Room.databaseBuilder()后加上.fallbackToDestructiveMigration()但这会清空所有数据2.正式项目必须实现RoomDatabase.Callback或提供Migration对象来处理版本升级。RecyclerView列表不显示数据1. 数据源 (LiveDataList) 为空。2.Adapter未正确设置到RecyclerView。3.LayoutManager未设置。4. 列表项布局 (item_record.xml) 高度为0或不可见。1. 在ViewModel中打印或调试查询结果确认数据库是否有数据。2. 检查onCreate中是否调用了recyclerView.setAdapter(adapter)。3. 检查布局文件或代码中是否为RecyclerView设置了LinearLayoutManager。4. 检查item_record.xml根布局的高度是否设置为wrap_content或固定值。图表 (MPAndroidChart) 不显示或报错1. 依赖未正确添加或同步。2. 传递给图表的数据集 (PieDataSet,LineDataSet) 为空。3. 未调用chart.invalidate()方法刷新视图。1. 执行File - Sync Project with Gradle Files并检查build.gradle依赖是否下载成功。2. 在设置数据前检查entries列表是否为空并打印日志确认。3. 确保在数据设置完成后调用了pieChart.invalidate()或lineChart.invalidate()。新增/编辑记录页面点击保存后数据未更新到列表1.ViewModel的insert或update方法未在后台线程执行。2. 列表页的LiveData观察者未被触发如Activity生命周期问题。3. 数据库操作失败但未捕获异常。1. 确保所有数据库写操作insert,update,delete都在子线程如new Thread()或Executor中执行。2. 确认列表页的Observer是在onCreate或onViewCreated中注册的并且其生命周期所有者 (this或getViewLifecycleOwner()) 正确。3. 在数据库操作处添加try-catch并打印异常日志。8. 项目优化与扩展建议完成基础功能后你可以从以下方向优化和扩展你的记账本应用这能让你的毕设作品更加出彩。8.1 用户体验优化数据空状态提示: 当RecyclerView列表为空时显示一个友好的提示界面如一个插图和“暂无记账记录”的文字而不是空白屏幕。下拉刷新: 集成SwipeRefreshLayout让用户可以手动下拉刷新账单列表。搜索与筛选: 在列表顶部增加搜索框支持按分类、备注关键字搜索。增加按时间范围今日、本月、本年筛选的快捷按钮。数据导出: 实现将账单记录导出为CSV或Excel文件的功能方便用户在电脑上分析。主题切换: 提供浅色/深色主题切换功能遵循Material Design规范。8.2 功能深度扩展预算管理: 允许用户为每月总支出或某个分类如“餐饮”设置预算并在接近或超支时给出提醒。多账户管理: 支持创建多个钱包/账户如现金、支付宝、银行卡记录时可以指定账户并查看各账户余额。周期性账单: 支持设置周期性账单如每月房租、每周会员费到期自动生成记录或提醒。数据云同步进阶: 集成Firebase Firestore或国内BaaS服务实现用户数据的跨设备云同步。注意此功能涉及网络和用户认证复杂度较高。本地化与无障碍: 做好应用的多语言支持中英文和无障碍功能如为按钮添加contentDescription体现工程素养。8.3 代码质量与架构使用Repository模式: 在ViewModel和DAO之间引入Repository层统一管理数据来源本地数据库、网络使架构更清晰。依赖注入: 考虑使用Hilt或Dagger进行依赖注入管理ViewModel、Repository、Database等对象的创建提升代码的可测试性。单元测试与UI测试: 为ViewModel、Repository编写单元测试为关键用户流程如添加记录编写EspressoUI测试。这是高质量毕设的加分项。代码混淆与加固: 在app/build.gradle中启用minifyEnabled和shrinkResources并使用ProGuard或R8规则对发布包进行混淆和优化保护代码。8.4 毕设文档与演示准备需求规格说明书: 清晰描述项目背景、目标用户、功能列表和非功能需求。系统设计文档: 包括架构图MVVM、数据库ER图、主要界面原型图。核心代码说明: 对关键模块如数据库设计、图表集成的代码进行注释和说明。测试报告: 记录功能测试、兼容性测试的结果。演示视频与PPT: 准备一个5-10分钟的演示视频展示应用的主要功能和亮点。答辩PPT应简洁明了突出技术难点和你的解决方案。通过以上步骤你不仅完成了一个功能完备的Android记账本应用更系统地实践了现代Android开发的核心技术栈和工程实践。这个项目可以作为你求职作品集中的一个有力证明展示你解决实际问题的能力。
返回列表