AccountBook 是我自己开发的第一款App,为了将学习的Android基础知识运用于实践中,以达到进一步熟悉并掌握Android开发的目的。这篇笔记记录了该App的开发过程与具体功能是实现的。
项目地址:https://github.com/WayneSun729/AccountBook
能够实现资产管理,账单管理
由于如果使用导入布局来达到布局复用的目的,则其逻辑需要一次次编写。故选择自定义控件,新建BottomOptions类继承自LinearLayout,在构造器中加载布局,获得布局中的控件,并为之绑定点击监听器。
启动活动的标题栏设为可折叠标题栏,并将系统状态栏设置为标题栏颜色。
新建Application,设置一个context变量用于存放context,并修改AndroidManifest中的Applicationname为新建的Application。
public class MyApplication extends Application {
@SuppressWarnings("StaticFieldLeak")
public static Context context;
@Override
public void onCreate() {
super.onCreate();
context = getApplicationContext();
}
}
设置BaseActivity,用于维护一个存放Activity的List,其他Activity均继承此类。
public class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityCollector.addActivity(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
ActivityCollector.removeActivity(this);
}
}
设置ActivityCollector,提供维护List的方法,以及用于一键退出程序的finishAll方法。
public class ActivityCollector {
private static List<Activity> activities = new ArrayList<>();
public static void addActivity(Activity activity){
activities.add(activity);
}
public static void removeActivity(Activity activity){
activities.remove(activity);
}
public static void finishAll(){
for (Activity activity : activities){
if (!activity.isFinishing()){
activity.finish();
}
}
activities.clear();
}
}
DrawerLayout存放两个控件,主界面和菜单界面。菜单界面由NavigationView实现。
资产的展示使用RecyclerView
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp"
android:text="资产账户"
android:textSize="15sp"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/assetsRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent">
androidx.recyclerview.widget.RecyclerView>
这里子项布局外部使用了卡片式布局展示效果
参见博客:沉浸式状态栏
存放与界面相关的数据。需要添加依赖:
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
通常需要为每一个Activity和Fragment都创建一个相应的ViewModel
创建代码如下:
public class AccountViewModel extends ViewModel {
public List<Account> accountList = new ArrayList<>();
public AccountViewModel(){
super();
initAccount();
}
//这里暂时使用静态添加数据的方法测试,具体添加数据的方法还未开发
private void initAccount() {
accountList.add(new Account(MainViewModel.assetList.get(0).getName(), 20.8, "零食", "面包","2020/08/03",R.drawable.cater_icon));
accountList.add(new Account(MainViewModel.assetList.get(1).getName(), 50, "通讯", "话费","2020/08/02",R.drawable.communication_icon));
accountList.add(new Account(MainViewModel.assetList.get(2).getName(), 198, "会员","","2020/08/01",R.drawable.shopping_icon));
}
}
之后如何在Activity中调用ViewModelProvider获取VM并使用即可。
accountViewModel = ViewModelProviders.of(this).get(AccountViewModel.class);
Room会根据我们在项目中声明的注解来动态生成代码,而启用编译时注解功能,Kotlin需要应用插件kapt,Java需要使用annotationProcessor。
这里以Java为例,需要添加依赖:
参考链接
后,还要添加
implementation “android.arch.persistence.room:runtime:1.1.1”
annotationProcessor “android.arch.persistence.room:compiler:1.1.1”
分别封装实体类,数据操作方法,数据库,编写较为简单不再举例。
使用时注意,Room默认是不允许在主线程中进行数据库操作的,所以来开子线程进行操作。
可在构建Database实例的时候加入一个allowMainThreadQueries()方法来允许在主线程中进行数据库操作(只建议在测试环境下使用)
AccountDao accountDao = AppDatabase.getDatabase(this).getAccountDao();
Account newAccount = new Account("支付宝", 83, "餐饮","巴奴","2020/08/01",R.drawable.cater_icon);
new Thread(()->{
accountDao.deleteAllAccounts();
newAccount.setId(accountDao.insertAccount(newAccount));
for (Account account : accountDao.loadAllAccounts()){
Log.d("AccountActivity",account.toString());
}
}).start();
今天主要完成了将账单页面迁移出来,将数据放入ViewModel中,以及使用Room进行数据持久化的部分工作。
接下来还需要完成编写添加资产和添加账单的Activity,以及相关逻辑,大致想法是在确定新增的按键上绑定事件,进行实例化实体,并使用Room插入至数据库中。
昨天建立了Room框架,今天主要就是将界面的数据操作从固定的代码转而使用Room,这里用到了LiveData更新RecyclerView中的数据
主要思想:在主线程为按键绑定事件,开启子线程完成业务逻辑。
以Account为例,Asset与此相同,不再赘述。
修改ViewModel中存放数据的List< Account>为MutableLiveData>,并新增 List< Account> innerList 字段,同时新增两个方法(新增与查询),在方法中完成相关操作。
这里我定义了两个线程类,一个用来新增数据,一个用来查询数据。
如:
public class AddAccountThread extends DataThread {
//为了代码复用,在DataThread里提供了accountDao和assetDao
private Account newAccount;
public AddAccountThread(Account newAccount){
this.newAccount = newAccount;
}
@Override
public void run() {
newAccount.setId(accountDao.insertAccount(newAccount));
AccountViewModel.innerList.add(newAccount);
AccountViewModel.accountList.postValue(AccountViewModel.innerList);
}
}
public class AccountViewModel extends ViewModel {
public static MutableLiveData<List<Account>> accountList = new MutableLiveData<>();
public static List<Account> innerList = new ArrayList<>();
public AccountViewModel() {
super();
queryAccount();
}
public void addAccount(Account newAccount) {
Thread addAccount = new AddAccountThread(newAccount);
addAccount.start();
}
public void queryAccount() {
Thread queryAccount = new QueryAccountThread();
queryAccount.start();
}
}
同时,修改Adapter和AccountBook,在AccountBook中完成绑定操作。
//注册观察者
AccountViewModel.accountList.observe(this, accountList -> {
accountAdapter.setAccountList(accountList);
//数据改变刷新视图
accountAdapter.notifyDataSetChanged();
});
同时,还有保证Activity加载时先加载数据,以保证后续操作正常,避免空引用。因此将下面的代码放入onCreate方法中的较早执行处。
//加载数据
accountViewModel = ViewModelProviders.of(this).get(AccountViewModel.class);
accountViewModel.queryAccount();
使用方法:
建立方法如下:
MutableLiveData<T> T = new MutableLiveData<>();
MutableLiveData<List<Account>> accountList = new MutableLiveData<>();
注册观察者代码如下:
//注册观察者
//一个LiveData对象.observe(LifecycleOwner, obsever接口)
AccountViewModel.accountList.observe(this, accountList -> {
accountAdapter.setAccountList(accountList);
//数据改变刷新视图
accountAdapter.notifyDataSetChanged();
});
今天还对数据库进行了升级,这里直接使用fallbackToDestructiveMigration()对原有数据库清空来进行Room的数据库升级。
即:在修改Database版本(只能为整数)后,在创建数据库实例是添加该方法。
instance = Room.databaseBuilder(MyApplication.context, AppDatabase.class, "app_database")
.fallbackToDestructiveMigration()
.build();
今天主要完成了在Activity中对数据库进行操作,同时使用LiveData+RecyclerView即时更新数据。
同时,使用了SQLiteStudio进行数据库可视化管理。
为昨天的目标 (接下来还需要完成编写添加资产和添加账单的Activity,以及相关逻辑,大致想法是在确定新增的按键上绑定事件,进行实例化实体,并使用Room插入至数据库中)提供了接口,铺平了道路。
明天进行两个Activity的编写,完成插入数据的功能。
参考博客:
Jetpack学习笔记(三):RecyclerView配合LiveData动态显示数据库内容
可视化查看安卓中sqlite数据(超详细)
今天主要搭建了添加账单的Activity的前端显示
其中,展示图标还是用到了RecyclerView。
今天涉及到一个新知识点,在新增账单的时候我们要选择资产或账目类别,那么如何RecyclerView设置选中效果?
可参考:ListView或者RecyclerView选中某一项效果
最终效果如图:
今天完成了新增数据至数据库,并在新增账单时同步修改资产剩余金额的功能。
在菜单栏中的保存按钮中回调函数
这里附上新增资产Activity中save的回调函数。
//从用户输入中获取数据
String money = add_money.getText().toString();
String username = add_username.getText().toString();
if (money.equals("")){
Toast.makeText(this, "您还没有填写资产金额",Toast.LENGTH_SHORT).show();
}
else if (AddAssetViewModel.getSelectedIndex()==-1){
Toast.makeText(this, "您还没有选择资产类型",Toast.LENGTH_SHORT).show();
}
else{
//得到被选中的图标
Icon icon = assetIconAdapter.getIcons().get(AddAssetViewModel.getSelectedIndex());
String name = icon.getName();
int imageId = icon.getImageId();
Asset newAsset = new Asset(name, username, Double.parseDouble(money), imageId);
Thread addAssetThread = new AddAssetThread(newAsset);
try {
addAssetThread.start();
//等待子线程运行完成后弹出Toast提醒用户
addAssetThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
Toast.makeText(this, "保存成功",Toast.LENGTH_SHORT).show();
Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
}
新增账单的回调函数与此类似,只不过新增账单后要先查询资产金额,再修改。这里由于MainViewModel中的assetList是 LiveData,并且已经注册了观察者,之后调整页面显示的过程就不用再操心了。
设计账单属性时,为了方便统计月账单金额,设置了日期这一属性。
这里在新增账单时,如果不输入日期默认添加当前日期,获得当前日期的方法如下:java如何获取当前日期和时间
也可以自行输入日期。
对输入的日期格式做出判断,过滤掉非法格式。
修复了账单页面底部控件遮挡数据的bug。
原因是只使用了NestedScrollView,没有把父布局设置为底部控件的Above。
在MainViewModel中新增了一个数据totalMoney用于计算总资产。
并在assetList的观察函数中加入修改页面totalMoney的语句。保证页面TotalMoney随时为最新数据。
在验证totalMoney的实时性时发现bug——记账时可能会将某资产扣为负值。
TableVIewModel里放了一个Map一个List,Map用于根据日期存放开销,List用于存放日期。
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);
}
}
刚开始每天都要掌握很多新知识,随着开发进度的推进,对项目所需知识越来越熟悉,到最后基本不用再求助于网络即可自己开发功能。感觉到了自己能力的提升。
同时,该项目仍有些许bug和待优化的部分,之后有时间我还会继续完善这个我第一次独立开发的APP。
在创建当前界面时修改控件图片,字体颜色
使用DatePicker,让用户选择日期。
DatePicker使用方法可参考:Android 开发笔记___DatePicker__日期选择器
同时发现获得的月份不知为何从少一,因此这里我人为将月份加一了。
不可编辑状态:
editText.setFocusable(false);
editText.setFocusableInTouchMode(false);
可编辑状态:
editText.setFocusableInTouchMode(true);
editText.setFocusable(true);
editText.requestFocus();
发现这里的0是用于在统计总支出的页面按日期计算数据,在这里稍作调整,改为使用split函数分解日期,获得年月日。
每次创建添加账单界面时都刷新一次资产图标列表
由此想到,不必每次都刷新,只需借由LiveData即可完成对所需数据的刷新。
即,在观察者函数中添加刷新方法即可。
MainViewModel.assetList.observe(this, assetList -> {
assetAdapter.setAssetList(assetList);
//数据改变刷新视图
assetAdapter.notifyDataSetChanged();
totalMoney.setText(getTotalMoney());
//刷新添加资产界面的资产列表
AddAccountVIewModel.initAssetIcon();
});