该项目是我为了锻炼自己的Android开发能力,检验自己自学效果所开发的一个记账APP,主要实现了资产管理和账单管理两个功能。
在该项目中,为了既保证项目结构的清晰,又避免Activity的臃肿,我使用了MVVM架构,将所有Activity的数据都放到了对应的ViewModel中。为了使部分数据具备实时性,还加入了LiveData。数据库借助Room实现。此外,还根据需要自定义了一个底部控件。
尽管自学Android基础时写过许多demo,但该项目仍是我真正意义上第一个开发的APP。因此,它对我而言的难点主要就体现在如何去实现自己想要的功能或想要达到的效果,以及遇到问题或Bug时应该如何处理。
所以说,这个项目为我带来的最大收获就是拥有了基本的Android开发经验。
如何实现新增数据的功能?
如何保证数据的实时性?
将资产数据设置为了LiveData,保证界面显示的是最新数据。
Android MVP和MVC和MVVM模式区别
用于分担Activity工作,存放与界面相关的数据。
手机屏幕旋转时Activity会被重新创建,存放在Activity的数据也会丢失。而ViewModel的生命周期与Activity不同,它可以保证在手机屏幕发生旋转的时候不会被重新创建。只有当Activity退出的时候它才会跟着一起销毁。
生存期长于Activity。
获得ViewModel实例时不能直接创建,而是要通过ViewModelProviders获取,否则每次onCreate执行时就会创建一个新的实例,当手机屏幕旋转的时候就无法保留其中数据了。
它可以让任何一个类都能轻松感知到Activity的生命周期,同时又不需要在Activity中编写大量的逻辑。
新建一个类,实现LifecycleObserver接口,定义方法,附上注解。在Activity的onCreate中添加监听器。
可通过在类中添加Lifecycle字段,在构造器中传入,即可主动获知当前的生命周期状态。
返回的是一个枚举类型。
Activity生命周期状态与事件的对应关系
它是一种响应式编程组件,可以包含任何类型的数据,并在数据变化时通知观察者。
在VM中将设置LiveData字段,
getValue、setValue、postValue方法
任何LiveData对象都可以调用其observe方法来观察数据的变化。
map,将实际包含数据的LiveData和仅用于观察数据的LiveData进行转换。
switchMap,将调用其他方法获得的LiveData对象转换成一个可观察的LiveData对象。
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代码
<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>
对自定义控件做了美化,增加了选中效果(根据当前界面显示不同的颜色)
新增两个类,
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?
原因:数据更新不及时
解决方法:在MainViewModel中的LiveData字段assetList的观察者函数中添加更新TableVIewModel的代码。
MainViewModel.assetList.observe(this, assetList -> {
assetAdapter.setAssetList(assetList);
//数据改变刷新视图
assetAdapter.notifyDataSetChanged();
totalMoney.setText(getTotalMoney());
//刷新添加资产界面的资产列表
AddAccountVIewModel.initAssetIcon();
});
可能的思路:新增前计算该资产是否有足够资金结算该账单,如果没有则提示用户。