好好学习,天天向上

本文已收录至我的Github仓库DayDayUP:github.com/RobodLee/DayDayUP,欢迎Star,更多文章请前往:目录导航

Java项目实战:账本APP服务端开发

前言

我平时喜欢用账本记一记我的日常消费然后做个总结,一直用本子记不是很方便,从网上下载的APP又太臃肿,很多功能都用不到,就想着能不能自己开发一个APP。刚好最近学完了SpringBoot,我之前也学习过Android,就花了一段时间开发完了。本来以为没什么难度,但还是遇到了一些比较棘手的问题,所以就打算写篇文章总结一下。先给大家看一下我的界面:

ps:一篇文章不可能写的面面俱到,我只写一下大体的思路以及我所遇到的问题,详细的代码我放在了GitHub上,有兴趣的小伙伴请自行阅读。
账本APP源码地址:https://github.com/RobodLee/AccountBookAPP


前期准备

既然是做账本,那么肯定免不了要用到数据库,我用的Android自带的SQLite数据库,为了简化数据库的操作,我使用了LitePal框架;还有一点,我不想在我的代码里写一大堆findViewById,这样会显得代码非常的乱,所以我还使用了xUtils框架去简化我的代码;数据有了,自然要存储到服务器中,免不了要用到HTTP,这里我选择的是大名鼎鼎的OkHttp框架;既然要传输数据,那么我选择了json格式去传输数据,解析json我使用了阿里的FastJson框架,因为我觉得FastJson用起来还是比较顺手的;为了省去一堆的getter和setter方法,我还使用了非常好用的Lombok插件。


一、添加以及修改记录的功能实现

最先实现的功能应该是如何添加记录了,然后再去考虑怎么将数据展示出来。我在这个Activity里面集成了添加以及修改数据的功能,我用了一个boolean值“toModify”去判断当前操作功能,是添加数据的话点击保存就会添加记录,否则就会修改数据,首先肯定要有个Record类,每个字段的作用都在注释里了。

我一开始用的不是Date而是LocalDateTime,然后我从数据库根据时间查询数据的时候一直查询不到数据,本来我以为是查询部分写的不对,结果到处数据库一看,date根本就没有值。我又以为是我在添加数据的时候写的有问题,然后想了一下,其它字段都没问题,怎么唯独date有问题,然后我换成了Date就可以正常保存了。这里要提一点,就是SQLite的主键必须是int型,我本来id是Stirng型的,值就是uuid字符串,结果不行,我就改成了int,然后再加一个String类型的uuid字段去做数据的唯一标识符。

public class Record extends LitePalSupport implements Serializable {private int id;             //主键private String category;    //分类的名称private String content;     //备注private double money;       //金额,大于等于0代表收入,用绿色表示;小于0代表支出,用红色表示,等于0用灰色表示//状态,0:已同步到服务器,1:未同步到服务器,// 2:之前同步到服务器现在本地删除,同步时让服务器删除这条记录,3.之前同步到服务器现在在本地修改,同步时让服务器修改这条记录private int status;private Date date;          //记录的日期private String dateString;  //日期的字符串2020-04-01private String uuid;        //每条记录的唯一标识符}

我先来分析一下我的界面构成:

首先界面上有一些分类的图标,有支出和收入两页,我是用ViewPager去实现两页的切换,展示分类图标我使用了RecyclerView,下面有一个选择时间的控件,用的是DatePickerDialog,其它的就是一些TextView,Button什么的。

再来介绍一下功能:点击分类图标的时候,下面对应的就会出现分类的名称,然后输入金额(正数),点击右上角的保存按钮时就会去判断当前分类是收入还是支出,如果是支出的话就将金额*(-1),这样就变成了负数,保存成功就会finish掉当前活动回到主页面,失败的话会弹出一个Toast。

                Date date = new Date(year-1900,month-1,day);boolean saveSuccess;String categoryNameStr = categoryName.getText().toString();String contentStr = contentEdit.getText().toString();if (toModify) {Log.d(TAG, toModifyRecord.toString());toModifyRecord.setCategory(categoryName.getText().toString());toModifyRecord.setContent(contentEdit.getText().toString());double money = Double.parseDouble(moneyEdit.getText().toString());toModifyRecord.setMoney(isIncome?(money):(money*-1));toModifyRecord.setStatus(((toModifyRecord.getStatus()==0)?(3):(1)));toModifyRecord.setDate(date);toModifyRecord.setDateString(dateString);saveSuccess = toModifyRecord.save();Log.d(TAG, toModifyRecord.toString());} else {Record record = new Record();record.setCategory((!TextUtils.isEmpty(categoryNameStr)?(categoryNameStr):("无")));record.setContent((!TextUtils.isEmpty(contentStr)?(contentStr):("无")));double money = Double.parseDouble(moneyEdit.getText().toString());record.setMoney(isIncome?(money):(money*-1));record.setStatus(1);    //1代表未同步到服务器record.setDate(date);record.setUuid(UuidUtil.getUuid());record.setDateString(dateString);saveSuccess = record.save();}if (saveSuccess) {ToastUtil.Pop("保存成功");finish();} else {ToastUtil.Pop("保存失败");}

这个toModifyRecord就是需要修改的数据,是在主页面中点击修改然后传入一个uuid,再根据uuid查出来的:

    @Overrideprotected void onCreate(Bundle savedInstanceState) {············try {toModifyRecord = LitePal.where("uuid = ? ",getIntent().getStringExtra("uuid")).find(Record.class).get(0);} catch (Exception e) {//e.printStackTrace();}toModify = toModifyRecord!=null;············}

看这行代码的朋友们可能有点疑惑,我为什么不直接传个对象过来而是拿着uuid再去数据库中查一遍,这不是有点多此一举吗,在《第一行代码》中提到过这样一段话:

首先,最简单的一种更新方式就是对已存储的对象重新设值,然后重新调用save() 方法即可。那么这里我们就要了解一个概念,什么是已存储的对象?
对于LitePal来说,对象是否已存储就是根据调用model.isSaved() 方法的结果来判断的,返回true 就表示已存储,返回false 就表示未存储。那么接下来的问题就是,什么情况下会返回true ,什么情况下会返回false呢?实际上只有在两种情况下model.isSaved() 方法才会返回true ,一种情况是已经调用过model.save() 方法去添加数据了,此时model 会被认为是已存储的对象。另一种情况是model 对象是通过LitePal提供的查询API查出来的,由于是从数据库中查到的对象,因此也会被认为是已存储的对象。

意思就是我如果直接传一个对象的实例过来,那么这个对象就是数据库中的对象的拷贝,并不是数据库中的对象的引用,我如果用对象的拷贝去执行save()方法是不会对数据库中的数据产生影响,那就不对了,所以我需要拿着uuid再去从数据库中把数据查询出来,再去调用save()方法才有用。如果没查到就说明我没有传uuid过来,那么就是去使用添加数据的功能。

二、数据的展示

ee

说完了数据的保存再来说一下数据的展示,这是整个APP比较核心的功能。标题栏上有一个显示当前月份的TextView,点击的话会出现一个图示的日期选择控件。至于数据的展示呢,我使用了RecyclerView嵌套ListView去实现的,RecyclerView的每一个子项是每一天记录的集合,每一天的数据是用一个ListView去展示的。我在做这个界面的时候遇到了一个问题,就是一直只有一条数据显示,我本来以为是我数据没添加上,结果导出来一看是有数据的。我就一直在调试代码,怎么都没看出问题,然后往上一滑才发现原来是一条数据占满了屏幕。原来是RecyclerView嵌套ListView的时候ListView不知道自己的高度,需要重新测量高度,我在网上找到了解决方案:

    //重新计算ListView的高度public static void setListViewHeightBasedOnChildren(ListView listView) {// 获取ListView对应的AdapterListAdapter listAdapter = listView.getAdapter();if (listAdapter == null) {return;}int totalHeight = 0;for (int i = 0, len = listAdapter.getCount(); i < len; i++) {// listAdapter.getCount()返回数据项的数目View listItem = listAdapter.getView(i, null, listView);// 计算子项View 的宽高listItem.measure(0, 0);// 统计所有子项的总高度totalHeight += listItem.getMeasuredHeight();}// listView.getDividerHeight()获取子项间分隔符占用的高度// params.height最后得到整个ListView完整显示需要的高度ViewGroup.LayoutParams params = listView.getLayoutParams();params.height = totalHeight + (listView.getDividerHeight() * (listAdapter.getCount() - 1));listView.setLayoutParams(params);}

怎么从数据库中查询出数据然后展示出来废了我好一番劲,好在问题完美地解决了。现在就来说一下我的具体实现过程吧,先把核心的代码贴出来:

    /*** 用于更新界面信息*/private void upgradeMainList() {thisMonthAllRecords.clear();thisMonthAllRecords = LitePal.where("dateString like ? and status != ? ", +mYear[0] + "-" +((mMonth[0] < 10) ? ("0" + mMonth[0]) : (mMonth[0])) + "%", "2").find(Record.class);if (thisMonthAllRecords != null) {Map<String, List<Record>> map = new HashMap<>((int) (thisMonthAllRecords.size() / 0.75) + 1);for (Record record : thisMonthAllRecords) {List<Record> staList = map.get(record.getDateString());if (staList == null) {staList = new ArrayList<>();}staList.add(record);Collections.sort(staList, new Comparator<Record>() {@Overridepublic int compare(Record record1, Record record2) {return (record1.getDateString()).compareTo(record2.getDateString());}});map.put(record.getDateString(), staList);}Set<String> set = map.keySet();List<List<Record>> thisMonthRecordsByDay = new ArrayList<>();   //按每一天分开的List<List>集合for (String s : set) {List<Record> list = map.get(s);thisMonthRecordsByDay.add(list);}LinearLayoutManager layoutManager = new LinearLayoutManager(this);recyclerView.setLayoutManager(layoutManager);RecyclerViewAdapter recyclerAdapter = new RecyclerViewAdapter(thisMonthRecordsByDay);recyclerView.setAdapter(recyclerAdapter);}}

首先从数据库中查询出符合条件的数据,封装成一个List集合,然后对List集合进行排序,排完序后把List集合封装成List<List> thisMonthRecordsByDay,每个子项是一天的记录的集合。然后就创建了一个自定义的RecyclerViewAdapter的实例,在RecyclerViewAdapter中绑定数据的时候再去创建ListViewAdapter的实例进行每一天的数据的展示。具体代码我就不贴了,看我的源码就知道了。

现在就是每条记录的点击事件了,我是在RecyclerViewAdapter中的onCreateViewHolder()方法中进行事件绑定的。

            holder.todayRecordList.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {@Overridepublic boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {final Record selectRecord = oneMonthRecordsByDay.get(holder.getAdapterPosition()).get(position);final View popView = LayoutInflater.from(MainActivity.this).inflate(R.layout.pop_dialog_view, null);popView.findViewById(R.id.pop_modification).setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {popDialog.dismiss();Intent intent = new Intent(MainActivity.this, AddAndModifyRecordActivity.class);intent.putExtra("uuid", selectRecord.getUuid());startActivity(intent);}});popView.findViewById(R.id.pop_delete).setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {if (selectRecord.getStatus() == 0) {selectRecord.setStatus(2);selectRecord.save();} else {selectRecord.delete();}popDialog.dismiss();upgradeMainList();}});popDialog = new AlertDialog.Builder(MainActivity.this).setView(popView).create();popDialog.show();return true;}});

长按记录的时候会弹出两个选项,一个是删除,一个是修改,点击修改的话会进入到刚才添加记录的Activity中进行数据的修改;点击删除的话首先会去判断status的值是不是0,0代表该条记录之前已经同步到服务器中,现在改成2,代表下次同步的时候把该条记录在服务器中删除后再在本地删除,如果不是直接删了就好。

三、数据同步功能

现在就来说最后一个功能了。如果数据一直保存在本地的话,那么数据迟早有一天会丢失的,所以应该有个同步功能将数据保存在服务器里,这样数据就不会丢失了。服务器端的开发我会放在下一篇文章里,现在先来看看客户端的具体实现。界面很简单,只有两个按钮,一个是同步到云端,一个是下载到本地,来看一下具体的实现:

1.上传到云端功能:

            final List<Record> toUpgradeRecords = LitePal.where("status > ?", "0").find(Record.class);HttpUtil.uploadRecords(ip+Constant.UPLOAD_RECORDS, phoneNumber, toUpgradeRecords, new Callback() {@Overridepublic void onFailure(Call call, IOException e) {showToast("服务器异常");finish();}@Overridepublic void onResponse(Call call, Response response) throws IOException {String responseBody = response.body().string();ResultInfo resultInfo = JSONObject.parseObject(responseBody, ResultInfo.class);if (resultInfo.isFlag()) {for (Record record : toUpgradeRecords) {if (record.getStatus() != 2) {record.setStatus(0);record.save();} else {record.delete();}}showToast("同步成功");finish();} else {showToast("同步失败");finish();}}});

首先查询出未同步到云端的记录集合,然后调用HttpUtil.uploadRecords()方法去传输数据,来看一下HttpUtil.uploadRecords()方法干了什么吧:

    public static void uploadRecords(String address, String phoneNumber , List<Record> toUpgradeRecords , okhttp3.Callback callback) {OkHttpClient client = new OkHttpClient();RequestBody requestBody = new FormBody.Builder().add("phoneNumber",phoneNumber).add("recordsJson" , JSON.toJSONString(toUpgradeRecords)).build();Request request = new Request.Builder().url(address).post(requestBody).build();client.newCall(request).enqueue(callback);}

就是把List集合用FastJson转换成Json字符串,然后和phoneNumber一起发送给服务器,用的是POST请求,然后就会回到前面的回调当中去处理结果,如果是同步成功的话,就去判断每条记录的status值,该干啥干啥,失败的话就给个提示然后finish掉当前的Activity。

2.下载到本地功能实现:

看完了上传功能再来看一下下载功能:

            String address = ip+Constant.DOWNLOAD_RECORDS + phoneNumber;HttpUtil.sendOkHttpGetRequest(address, new Callback() {@Overridepublic void onFailure(Call call, IOException e) {showToast("服务器异常");finish();}@Overridepublic void onResponse(Call call, Response response) throws IOException {String responseBody = response.body().string();ResultInfo resultInfo = JSONObject.parseObject(responseBody, ResultInfo.class);Log.d(TAG, "onResponse: "+resultInfo.getData().toString());List<Record> recordsDownloadFormServer = JSONArray.parseArray(resultInfo.getData().toString(),Record.class);if (resultInfo.isFlag()) {for (Record record : recordsDownloadFormServer) {try {LitePal.where("uuid = ? ", record.getUuid()).find(Record.class).get(0);} catch (Exception e) {e.printStackTrace();record.setDateString(ConvertUtils.dateToString(record.getDate()));record.save();}}showToast("下载成功");}finish();}});

首先使用HttpUtil.sendOkHttpGetRequest()方法将请求发送到服务器,用的是get请求,服务器会根据传过去的phoneNumber将该用户的所有记录都传回来,传过来的记录集合也是Json,所以先用FastJson解析成List。然后循环遍历List判断该条记录在本地有没有,有的话就跳过,没有的话就保存到SQLite中。

总结

到此为止,整个账本APP的核心功能和一些我所遇到的问题以及解决方法都已经写完了,其它的一些比较简单的功能我就不再赘述。文章写的不是很好,大家如果不满意的话直接关掉就好,或者是在下面留言,谢谢!

码字不易,可以的话,给我来个点赞收藏关注

如果你喜欢我的文章,欢迎关注微信公众号 『 R o b o d 』

本文已收录至我的Github仓库DayDayUP:github.com/RobodLee/DayDayUP,欢迎Star

Android项目实战:账本APP开发相关推荐

  1. Android项目实战(二十二):启动另一个APP or 重启本APP

    Android项目实战(二十二):启动另一个APP or 重启本APP 原文:Android项目实战(二十二):启动另一个APP or 重启本APP 一.启动另一个APP 目前公司项目需求,一个主AP ...

  2. 【Android项目实战 | 从零开始写app(十二)】实现app首页智慧服务热门推荐热门主题、新闻

    说在前面,由于各种adapter,xml布局,bean实体类,Activity,也为了让看懂,代码基本都是"简单粗暴直接不好看",没啥okhttp和util工具类之类的封装,本篇幅 ...

  3. 【Android项目实战 | 从零开始写app (六) 】用TabLayout+ViewPager搭建App 框架主页面底部导航栏

    本篇实现效果: 搭建app框架的方式有很多,本节主要用TabLayout+ViewPager搭建App框架,这种方式简单易实现,在主页中加载Fragment碎片,实现不同功能页面的切换效果图如下: 文 ...

  4. 【Android项目实战 | 从零开始写app(十三)】实现用户中心模块清除token退出登录信息修改等功能

    五一后,被ji金伤了,哇呜呜,还是得苦逼老老实实打工写代码,看下面吧 本篇实现效果: 实现登录用户名展示到用户中心页面上,并且页面有个人信息,订单列表,修改密码,意见反馈发送到服务端,前面登录后,通过 ...

  5. 开发android项目实战,Android 项目实战:手机安全卫士开发案例解析

    Android 项目实战:手机安全卫士开发案例解析 作 者:王家林,王家俊,王家虎 出版时间:2013 丛编项:移动互联应用开发系列 内容简介 本书通过对一款手机安全卫士开发案例的详细解析,讲解了一个 ...

  6. 【Android项目实战 | 从零开始写app一一智慧服务】完结篇系列导航篇、源代码

    目录 文章介绍 涉及知识 系列汇总 项目源代码 文章介绍 本系列小文是一个简单的Android app项目实战,对于刚入门Android 的初学者来说,基础学完了,但是怎么综合的去写一个小app,可能 ...

  7. Android项目实战(三十二):圆角对话框Dialog

    原文:Android项目实战(三十二):圆角对话框Dialog 前言: 项目中多处用到对话框,用系统对话框太难看,就自己写一个自定义对话框. 对话框包括:1.圆角 2.app图标 , 提示文本,关闭对 ...

  8. 《Android项目实战-博学谷》应用图标欢迎界面

    前言 本项目使用Android Studio 3.0.1作为开发工具,参照传智播客教材<Android项目实战--博学谷> 创建项目 可参照落萚简书文集--Android安全卫士开发笔记, ...

  9. Android项目实战--手机卫士

    Android项目实战--手机卫士--结束 很久都没有来更新博客了,之前一直忙着工作的事,接触到了一些以前从来没有接触过的东西,真的挺有挑战性的,但也有很多的无奈,但也学习到了很多东西,我会慢慢的写到 ...

  10. 关于《基于eclipse的android项目实战—博学谷》的问题,为了这个差点疯了

    前面都是废话,想要干的直接点我你就对了 <基于eclipse的android项目实战-博学谷>这篇文章已经一个星期没有更新了,原因是后面出了些问题,然后我花了整整一个星期才解决. < ...

最新文章

  1. # 学号 2017-2018-20172309 《程序设计与数据结构》第十一周学习总结
  2. 「蚂蚁呀嘿」克星来了!中科院23岁博士生开发「听音识人」,准确率近90%
  3. 雷达装置 (POJ 1328/ codevs 2625)题解
  4. pmcaff2013产品经理时尚文化屌丝style--马克杯投票。
  5. 爬虫系统Lucene分词
  6. [蓝桥杯2017初赛]贪吃蛇长度-模拟(水题)
  7. 《数学的思维方式与创新》课程感悟与总结
  8. torch.rand() 和 torch.randn() 有什么区别?
  9. linux 修改文件夹权限_Linux新手非常实用的20个命令
  10. 水经注地图下载器为什么叫万能下载器
  11. solidworks重建模型好慢_解决SolidWorks拉伸模型提示“重建模型错误”的方法
  12. 增量型编码器与绝对值编码器
  13. 百度之星2017 HDU 6114 Chess 组合数学
  14. 百万人同时在线直播的服务器,QQ游戏百万人同时在线的服务器架构实现
  15. 用寄存器HAL库完成LED流水灯程序以及通过MDK5模拟示波器观察波形
  16. 【论文解读】Mining Dual Emotion for Fake News Detection
  17. 水晶报表 发布 部署
  18. 【Flask】response响应
  19. Silverlight资源概述
  20. 《纸牌屋》——交换才是硬道理?

热门文章

  1. 从用户的角度看 java_[Java教程]开发网站要从用户的角度出发!
  2. 安卓手机真的不行了,搞不定卡顿问题,只能抄袭苹果iOS系统,然而各怀鬼胎的它们终究画虎不成反类犬...
  3. 词嵌入、句向量等方法汇总
  4. 【软件测试】—— 水杯测试用例
  5. 新来的妹纸 rm -rf 把公司整个数据库删没了!!!
  6. 旅游攻略应该怎么做,你做对了吗?
  7. ArcGIS提取栅格数据中的指定部分(可以是矢量数据也可时栅格数据)
  8. 《AngularJS深度剖析与最佳实践》一2.10 承诺
  9. 高中计算机操作题frontpage步骤,计算机一级Frontpage操作试题
  10. 基于NRF24L01的CAN数据透传