项目介绍
项目开发的目的与实现的功能
该项目是我为了锻炼自己的Android开发能力,检验自己自学效果所开发的一个记账APP,主要实现了资产管理和账单管理两个功能。
项目使用到的技术
在该项目中,为了既保证项目结构的清晰,又避免Activity的臃肿,我使用了MVVM架构,将所有Activity的数据都放到了对应的ViewModel中。为了使部分数据具备实时性,还加入了LiveData。数据库借助Room实现。此外,还根据需要自定义了一个底部控件。
项目难点及收获
尽管自学Android基础时写过许多demo,但该项目仍是我真正意义上第一个开发的APP。因此,它对我而言的难点主要就体现在如何去实现自己想要的功能或想要达到的效果,以及遇到问题或Bug时应该如何处理。
所以说,这个项目为我带来的最大收获就是拥有了基本的Android开发经验。
项目中遇到的问题
如何实现新增数据的功能?
如何保证数据的实时性?
将资产数据设置为了LiveData,保证界面显示的是最新数据。
项目展示
涉及到的知识点
MVC、MVP、MVVM
Android MVP和MVC和MVVM模式区别
MVC、MVP 和 MVVM 框架模式的区别?
ViewModel
用于分担Activity工作,存放与界面相关的数据。
手机屏幕旋转时Activity会被重新创建,存放在Activity的数据也会丢失。而ViewModel的生命周期与Activity不同,它可以保证在手机屏幕发生旋转的时候不会被重新创建。只有当Activity退出的时候它才会跟着一起销毁。
生存期长于Activity。
获得ViewModel实例时不能直接创建,而是要通过ViewModelProviders获取,否则每次onCreate执行时就会创建一个新的实例,当手机屏幕旋转的时候就无法保留其中数据了。
Lifecycles
它可以让任何一个类都能轻松感知到Activity的生命周期,同时又不需要在Activity中编写大量的逻辑。
新建一个类,实现LifecycleObserver接口,定义方法,附上注解。在Activity的onCreate中添加监听器。
可通过在类中添加Lifecycle字段,在构造器中传入,即可主动获知当前的生命周期状态。
返回的是一个枚举类型。
Activity生命周期状态与事件的对应关系
LiveData
它是一种响应式编程组件,可以包含任何类型的数据,并在数据变化时通知观察者。
在VM中将设置LiveData字段,
getValue、setValue、postValue方法
任何LiveData对象都可以调用其observe方法来观察数据的变化。
map,将实际包含数据的LiveData和仅用于观察数据的LiveData进行转换。
switchMap,将调用其他方法获得的LiveData对象转换成一个可观察的LiveData对象。
Room
ORM对象关系映射,支持用面向对象的思维和数据库交互。
Room由Entity、Dao、Database这三部分组成。
Entity是实体类,Dao是数据访问对象,Database定义数据库的关键信息。
各种注解,什么SQL用什么注解
自杀式升级,迁移式升级
自定义控件
java代码
public class BottomOptions extends LinearLayout implements View.OnClickListener {
public BottomOptions(Context context, AttributeSet attrs) {
super(context, attrs);
//加载布局
LayoutInflater.from(context).inflate(R.layout.bottom_options, this);
//得到按钮
ImageButton homeBtn = findViewById(R.id.homeBtn);
ImageButton accountBtn = findViewById(R.id.accountBtn);
ImageButton tableBtn = findViewById(R.id.tableBtn);
//绑定事件
homeBtn.setOnClickListener(this);
accountBtn.setOnClickListener(this);
tableBtn.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.homeBtn:
Intent intent1 = new Intent(MyApplication.context, MainActivity.class);
intent1.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
MyApplication.context.startActivity(intent1);
break;
case R.id.accountBtn:
Intent intent2 = new Intent(MyApplication.context, AccountBook.class);
intent2.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
MyApplication.context.startActivity(intent2);
break;
case R.id.tableBtn:
Intent intent3 = new Intent(MyApplication.context, Table.class);
intent3.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
MyApplication.context.startActivity(intent3);
break;
}
}
}
xml代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom">
<LinearLayout
android:orientation="vertical"
android:gravity="center"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="80dp">
<ImageButton
android:id="@+id/homeBtn"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="@drawable/asset_icon"
android:layout_marginBottom="5dp"
android:text="首页" />
<TextView
android:id="@+id/assetText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="资产"
android:textColor="@color/colorIcon"/>
</LinearLayout>
<LinearLayout
android:orientation="vertical"
android:gravity="center"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="80dp">
<ImageButton
android:id="@+id/accountBtn"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="@drawable/account_icon"
android:layout_marginBottom="5dp"
/>
<TextView
android:id="@+id/accountText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="账单"
android:textColor="@color/colorIcon"/>
</LinearLayout>
<LinearLayout
android:orientation="vertical"
android:gravity="center"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="80dp">
<ImageButton
android:id="@+id/tableBtn"
android:background="@drawable/total_icon"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginBottom="5dp"
/>
<TextView
android:id="@+id/tableText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="报表"
android:textColor="@color/colorIcon"/>
</LinearLayout>
</LinearLayout>
具体功能点的实现
选中效果
在ViewModel中存放被选中的position(默认值为-1),并设置访问器修改器方法。
在Activity中,在创建ViewHolder时为每个itemView绑定点击方法——将ViewModel中的selectedIndex设置为该position,并通知刷新。
在onBindViewHolder方法中,判断ViewModel中的selectedIndex是否为当前position,如果是则显示被选中的图标,如果不是则正常显示。
插入数据
获取当前布局中的各个输入,封装成一个目标对象类型,调用插入数据的线程,并将该对象传入子线程中,让子线程执行插入动作。
更新数据与此类似,不再赘述。
未来的优化方向
美化自定义控件(已实现)
对自定义控件做了美化,增加了选中效果(根据当前界面显示不同的颜色)
一键退出程序(已实现)
新增两个类,
ActivityCollector:负责具体管理一个存放Activity的静态字段List,并提供管理该List的两个静态方法add和remove,以及实现一键退出功能的静态finishAll方法。具体实现为遍历List中所有Activity,根据其finish属性判断是否需要对其调用finish方法。最后清空该List。
BaseActivity:继承自所有Activity的父类AppCompatActivity,所有Activity继承自BaseActivity,在BaseActivity的onCreate和onDestroy中加入了将当前Activity添加/删除至ActivityCollector的List的操作。
添加账单时用对用户更加友好的方式记录日期(已实现)
使用DatePickerDialog
首先让该Activity实现DatePickerDialog.OnDateSetListener接口,重写onDateSet方法如下:
@Override
public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
yearEdit.setText(String.valueOf(year));
monthEdit.setText(String.valueOf(monthOfYear+1));
dayEdit.setText(String.valueOf(dayOfMonth));
}
之后设置点击选择日期的按钮后弹出选择日期的对话框。
chooseDate = findViewById(R.id.chooseDate);
chooseDate.setOnClickListener(v -> {
Calendar calendar = Calendar.getInstance();
DatePickerDialog dialog = new DatePickerDialog(this, this,
calendar.get(Calendar.YEAR),
calendar.get(Calendar.MONTH),
calendar.get(Calendar.DAY_OF_MONTH));
dialog.show();
});
让账单统计倒序显示(已实现)
TableActivity中获取到List后,转换为String[],调用Arrays.sort()方法排序,在Adapter中绑定item方法中控制数组次序获得日期。
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
//控制数组次序,使数据倒序显示
String date = dateList[dateList.length-position-1];
double money = map.get(date);
holder.dateTextView.setText(date);
holder.moneyTextView.setText("¥"+money);
try {
int month = Integer.parseInt(date.substring(5));
int imageId = getImageId(month);
holder.icon.setImageResource(imageId);
}catch (Exception e){
//这里为了防止用户错误输入日期格式发生导致无法正常解析月份设置了try catch
int imageId = getImageId(-1);
holder.icon.setImageResource(imageId);
}
}
账单展示页面按日期倒序排列账单
可能的思路:或许可在RecyclerView中嵌套RecyclerView?
完善管理功能,提供更多操作
如更新,删除功能。
账单统计页面采用更为直观的方式展示,如数据可视化
涉及视图绘制等技术。
优化折叠式标题栏折叠上去后的系统状态栏
选择日期时使用户无法选择晚于当下的日期
目前已知的bug
用户初次使用添加资产时,添加后资产不能及时显示在添加账单页面中(已实现)
原因:数据更新不及时
解决方法:在MainViewModel中的LiveData字段assetList的观察者函数中添加更新TableVIewModel的代码。
MainViewModel.assetList.observe(this, assetList -> {
assetAdapter.setAssetList(assetList);
//数据改变刷新视图
assetAdapter.notifyDataSetChanged();
totalMoney.setText(getTotalMoney());
//刷新添加资产界面的资产列表
AddAccountVIewModel.initAssetIcon();
});
当新增账单金额大于资产时,不会阻止用户新增此账单,而会把资产扣为负数值
可能的思路:新增前计算该资产是否有足够资金结算该账单,如果没有则提示用户。