烟草仓储管理系统开发笔记(六)

本文最后更新于:2024年3月18日 晚上

简述

好久没写笔记了,这段时间效率不是很高,中间的五一假期也没有推进进度,所以总体完成的内容不是很多,内容不多,遇到的问题却还不少💢数来这段时间主要完成的内容有:实现RecycleView中Item的添加、移动动画;重新完成了订单管理部分的界面设计;修改了库存管理部分的一些设计;正式引入的数据库,删去假数据;实现了图片的启动时加载;引入viewModel管理数据;完成了订单管理信息和库存信息的联动

RecycleView中的Item动画

首先时RecycleView中的添加删除Item时的动画,因为我每次添加删除数据都是从数据库获取,并且有个撤销操作,需要将数据插回到原来的位置,而这个位置不同的Item是不一样的,因此无法直接使用BRVAH提供的addData方法,此外,即使使用addData方法实现了表面的数据恢复,但由于数据库中使用的是insert方法,因此在数据库中的数据并没有插入到原有位置,而是插入到了最下方,这最终导致的问题是adapter中的数据和数据库中的数据不一致,直观的表现形式就是:“顺序为ABC,删除了B又撤销时,页面显示的为ABC,而实际的数据顺序是ACB,导致点击B Item时,显示的数据是C的。尝试了很多方法都没能实现,最后就采用setList方法重置数据,放弃动画了。

后来在查看新版本的BRV时发现其可以提供我想要的实现形式,于是进行了尝试,但最终没能成功,不过这次尝试也让我又去查看了一遍BRVAH的代码,因为我发现BRV中的这部分内容貌似是直接从BRVAH中继承来的,所以BRVAH中应该也提供了这种实现形式,最终我发现了其中DiffUtil部分的内容(中文版文档还是2.9版本的,其中也没有写相关内容,Wiki中就只有一个DiffUtil的链接,连一个字的介绍都没有==)最终通过该模块实现了我想要的实现形式

1. 新建自己的DiffCallback类

以用户管理为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class UserDiffCallback extends DiffUtil.ItemCallback<User> {  
/**
* Determine if it is the same item * <p>
* 判断是否是同一个item
* * @param oldItem New data
* @param newItem old Data
* @return
*/
@Override
public boolean areItemsTheSame(@NonNull User oldItem, @NonNull User newItem) {
return oldItem.getId().equals(newItem.getId());
}

/**
* When it is the same item, judge whether the content has changed. * <p>
* 当是同一个item时,再判断内容是否发生改变
*
* @param oldItem New data
* @param newItem old Data
* @return
*/
@Override
public boolean areContentsTheSame(@NonNull User oldItem, @NonNull User newItem) {
return oldItem.getUsername().equals(newItem.getUsername())
&& oldItem.getPassword().equals(newItem.getPassword())
&& oldItem.getPermission() == newItem.getPermission();
}
}

该DiffCallback类用于判断数据是否发现了变化,即是否Diff了。

2. 为Adapter设置DiffCallback

为Adapter绑定DiffCallback类,即adapter中的数据根据那个DiffCallback类判断变化,实际代码就一句话

1
2
//设置diffCallback  
adapter.setDiffCallback(new UserDiffCallback());

3. 更新数据

通过新的setDiffNewData方法来设置数据,而不再是setList方法

1
2
3
4
5
6
7
8
9
10
userViewModel.getAllUserLive().observe(getActivity(), new Observer<List<User>>() {  
@Override
public void onChanged(List<User> users) {
if (adapter.getData().size() == 0)
adapter.setNewInstance(users);
//通过setDiffNewData来通知adapter数据发生变化,并保留动画
adapter.setDiffNewData(users);
adapter.setList(users);
}
});

这里的setList方法是我在Adapter进行重写,不再刷新数据,只是将users数组传递到Adapter中而已(这一个漏网之鱼,后面的类都改成setMyList了==)

这样就恢复了添加删除Item时的动画。

订单管理

之前的订单管理的设计思路是可以展开的条目,即列表中显示的为简单的主要信息,点击条目时下方展开一个新的视图,显示详细的信息,那会儿我花了好长的时间来实现,虽然最终成了,但终究还是有点小问题,而且虽然硬盘损坏,这部分内容也都没了。于是我又想着能不能有更好的呈现方式,后来看到淘宝的购物车界面,发现了里面的一个个购物记录卡片中也显示了很多的信息,也符合”订单“这种设定,于是就想着能不能做个类似淘宝购物车的卡片用于呈现数据。

主界面

卡片中去掉了各种提示标签,像“订单号:”,“用户名:”这种,显得臃肿,改为直接显示数据,通过颜色的明暗来分主次。其中的状态框,根据订单的当前状态不同,有不同的显示效果,更加直观地展示订单的进度。采购订单分为四个状态:申请中、已拒绝、运输中、已完成。销售订单分为五个状态:申请中、待发货、已拒绝、运输中、已完成。订单管理界面的整体框架跟其他页面一致,只是RecycleView中的Item样式不同而已。

添加对话框

添加订单的对话框中就是输入一些基本信息

其中经办人信息默认锁定为当前用户,不允许更改,采购日期设定为当天,用户选择购买的原料名称、原料型号、供货商以及数量和价格,其余信息不再此时添加。其中的名称和型号的下拉框采用了二级联动,即当用户选择完原料名称后,根据名称查询数据库得到该名称下的型号列表,将型号列表填充到型号下拉框中,这里也遇到了一些的问题

在点击确定后便向数据库中添加一条采购订单记录,订单状态为”申请中“,等待管理员审核。

修改对话框

在修改对话框中,订单信息显示得更加完整,在此处用户可以修改上述的基本信息,但入库日期和管理员批注无法修改,入库日期等待库存管理中入库时更新,管理员批注只有管理员可以操作。此外,在Adapter中根据订单状态进行了判断,如果订单状态是申请中和已拒绝,则ok按钮显示的文本是”重新提交“,实现的功能是将订单状态修改为申请中,重新进行申请,如果订单状态是运输中和已完成,则ok按钮显示的文本是”确定“,此时用户不能修改订单内容。

库存管理

库存管理部分相对于之前所完成的内容没有太多的改动,主要就是工具栏的改动。
发现了一个设计的致命漏洞💣例如产品,只提供了确认订单的功能,也就是产品只有出而没有入,哪怕只是增加库存都不行😑,同理,原料只有入而没有出,库存只会越积越多。就非常地不合理。所以将确认订单按钮改成了入库和出库。

产品部分

在产品部分,点击入库时只需要选择产品,并填写数量和存放区域即可,点击确认后库存数量便增加对应的量。而出库时,则是选择“待发货”状态的订单,读取订单信息并显示在对话框中,设定出库时间,当点击确定时,从库存数量中减去订单所需的产品数,在运输中数量中加上订单的产品数,同时将销售订单界面中对应的订单项的状态改为“运输中”(即联动部分内容)

原料部分

原料部分则和产品部分相反,原料部分的出库部分比较简单,就是要出库的原料以及出库数量即可,点击确认后库存数量便减去对应的量。入库部分则对应订单管理,选择“运输中”状态的订单,读取订单信息并显示在对话框中,设定入库时间和存放区域,点击确定时,便从运输中数量中减去对应的原料数,并在库存数量中加上对应的量,同时将采购订单中对应的订单项状态修改为“已完成”

Room、viewModel

Room数据表设计大体和第一部分类似,添加了几个字段,例如产品的图片地址等,这里不写了。主要还是整个Room数据的流程。

1. 创建Entity、Dao、Database

这些部分内容都在第一部分完成了。

2. 创建ViewModel类

所有对页面数据的访问全都放在ViewModel类中,在Fragment中调用ViewModel中的方法来对数据进行操作。在ViewModel中存储一个对应的Dao对象,通过调用dao中的方法来对数据库进行操作,同时在ViewModel中实现部分多线程,因为对数据库的操作不能在主线程中执行。
以ProductViewModel为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
public class ProductViewModel extends AndroidViewModel {  
private ProductDao dao;
private LiveData<List<Product>> listLive;

public ProductViewModel(@NonNull Application application) {
super(application);
dao = TestDatabase.Companion.getINSTANCE(application).productDao();
listLive = dao.getAllProduct();
}

public void insertProduct(Product... products) {
new ProductViewModel.InsertAsyncTask(dao).execute(products);
}

public void updateProduct(Product... products) {
new ProductViewModel.UpdateAsyncTask(dao).execute(products);
}

public void deleteProduct(Product... products) {
new ProductViewModel.DeleteAsyncTask(dao).execute(products);
}

public List<String> getProductNameList(){
return dao.getProductNameList();
}

public List<String> getProductModelListByName(String name){
return dao.getProductModelListByName(name);
}

public Double getPriceByModel(String model){
return dao.getProductByModel(model).getPrice();
}

public Product getProductByModel(String model){return dao.getProductByModel(model);}

public LiveData<List<Product>> getAllProductLive() {
return listLive;
}

public List<Product> getAllProductNoLive(){
return dao.getAllProductNoLive();
}

static class InsertAsyncTask extends AsyncTask<Product, Void, Void> {
private ProductDao dao;

public InsertAsyncTask(ProductDao dao) {
this.dao = dao;
}

@Override
protected Void doInBackground(Product... products) {
try {
dao.insertProduct(products);
PopTip.show("添加成功");
} catch (Exception exception) {
exception.printStackTrace();
PopTip.show("添加出错");
}
return null;
}
}

static class UpdateAsyncTask extends AsyncTask<Product, Void, Void> {
private ProductDao dao;

public UpdateAsyncTask(ProductDao dao) {
this.dao = dao;
}

@Override
protected Void doInBackground(Product... products) {
try {
dao.updateProduct(products);
PopTip.show("修改成功");
} catch (Exception exception) {
exception.printStackTrace();
PopTip.show("修改出错");
}
return null;
}
}

static class DeleteAsyncTask extends AsyncTask<Product, Void, Void> {
private ProductDao dao;

public DeleteAsyncTask(ProductDao dao) {
this.dao = dao;
}

@Override
protected Void doInBackground(Product... products) {
dao.deleteProduct(products);
return null;
}
}
}

因为getAllProductLive方法返回的LiveData类型的数据,所以系统在底层已经实现了子线程的操作,本身就是线程安全的,所以不需要再自己实现,而Insert等方法就需要自己实现,这里使用AsyncTask(虽然已经被弃用了),自定义对应的操作类,继承AsyncTask,在构造方法中传入dao对象,在doInBackground方法中执行对应的操作。然后ViewModel中提供一个公共方法供外部调用,在方法中新建操作类并执行其操作。
错误处理
在Dao的Insert等方法中添加注解@Throws(Exception::class),通过注解来抛出错误,在doInBackground中捕获错误并做出对应的操作。

3. 在Fragment中使用基础增删改查方法

在Fragment中首先声明viewModel对象,然后在onViewCreated方法中,通过ViewModelProvider对viewModel进行初始化。

1
2
3
4
ProductViewModel viewModel;
....
//在onViewCreated方法中初始化
viewModel = new ViewModelProvider(this).get(ProductViewModel.class);

接下来当需要对数据进行修改时调用viewModel提供的公共方法即可。

1
2
Product product = new Product(name,model,image,price,usedMaterial);  
viewModel.insertProduct(product);

4. 在Fragment中使用非基础增删改查方法

根据需求不同,我们往往需要查询不同的数据,而不是只有返回全部数据的方法。例如在上述的“Spinner二级联动”中,就需要先查询所有的产品名称,再根据名称查询型号列表。所以我们需要在Dao中添加对应的方法

1
2
3
4
5
6
7
8
@Query("SELECT name FROM product GROUP BY name")  
fun getProductNameList():List<String>

@Query("SELECT model FROM product WHERE name=:name")
fun getProductModelListByName(name:String):List<String>

@Query("SELECT * FROM product WHERE model=:model")
fun getProductByModel(model:String):Product

此处返回的数据不再是LiveData,就需要自己实现子线程查询,而这个通过viewModel中的AsyncTask又似乎不能实现,我还不知道怎么能通过viewModel中的AsyncTask向Fragment传递数据,所以最后还是在Fragment中采用New Thread的形式来执行这些方法,并赋值给Fragment中对应的变量。

1
2
3
4
5
6
7
new Thread(new Runnable() {  
@Override
public void run() {
productNameList = productViewModel.getProductNameList();
customerList = customerViewModel.getNameList();
}
}).start();

图片的启动时加载

该部分内容可见Android读取相册图片并显示

订单管理信息和库存信息联动

要实现在订单信息管理界面中修改库存信息,则需要在订单信息管理的Adapter中传入库存信息的ViewModel类,然后在Adapter中调用库存信息的ViewModel中的对应的方法即可。同理在库存信息中修改订单状态也需要在库存信息页面中传入订单的ViewModel,并修改订单的入\出库时间和订单状态,然后调用ViewModel中的更新方法。

1
2
3
4
5
6
7
order[0].setState(SaleOrder.STATE_DELIVERY);  
order[0].setDeliveryDate(deliveryDate);
saleOrderViewModel.updateSaleOrder(order);

inventory[0].setHostCount(inventory[0].getHostCount()-count);
inventory[0].setDeliveryCount(inventory[0].getDeliveryCount()+count);
viewModel.updateInventory(inventory[0]);

遇到的问题

Only the original thread that created a view hierarchy can touch its views.

这里的报错原因是在子线程中直接操作UI,还是Spinner二级联动中出现的问题,因为需要根据第一个Spinner中的名称来查询型号列表并填充到第二个Spinner中,所以我直接开了子线程执行查询和填充操作。

1
2
3
4
5
6
7
8
9
new Thread(new Runnable() {  
@Override
public void run() {
String name = spinnerName.getSelectedItem().toString();
materialModelList = materialViewModel.getMaterialModelListByName(name);
ArrayAdapter<String> modelAdapter = new ArrayAdapter<String>(getContext(), com.lihang.R.layout.support_simple_spinner_dropdown_item,modelList);
spinnerModel.setAdapter(modelAdapter);
}
}).start();

这里我直接通过spinnerModel.setAdapter()对UI进行了操作,所以报了这个错(不知为啥我之前这么用也没报错==)
解决方法
使用Handler来对UI进行操作,在子线程中运行完成时通过Message来传递数据给Handler,Handler接收到信息后更新UI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Handler mHandler = new Handler(Looper.myLooper()){  
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
if(msg.what == SET_SECOND_SPINNER){
List<String> modelList = msg.getData().getStringArrayList("modelList");
ArrayAdapter<String> modelAdapter = new ArrayAdapter<String>(getContext(), com.lihang.R.layout.support_simple_spinner_dropdown_item,modelList);
spinnerModel.setAdapter(modelAdapter);
}
}
};
spinnerName.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
materialModelList.clear();
new Thread(new Runnable() {
@Override
public void run() {
String name = spinnerName.getSelectedItem().toString();
materialModelList = materialViewModel.getMaterialModelListByName(name);
Message message = Message.obtain();
message.what = SET_SECOND_SPINNER;;
Bundle bundle = new Bundle();
bundle.putStringArrayList("modelList", (ArrayList<String>) materialModelList);
message.setData(bundle);
mHandler.sendMessage(message);
}
}).start();
}

@Override
public void onNothingSelected(AdapterView<?> adapterView) { }
});

在使用时发现Hanlder被弃用了==,于是查询有没有什么替代,发现直接弃用了Handler的无参构造函数,只要直接指定Looper对象就行了。

  • 在主线程内运行,指定为主线程的Looper
1
Handler mhandler = new Handler(Looper.getMainLooper());
  • 在当前线程内运行,指定为当前线程的Looper
1
Handler mhandler = new Handler(Looper.myLooper());

Android常见报错之 - Only the original thread that created a view hierarchy can touch its views
Android Handler被弃用,那么以后怎么使用Handler,或者类似的功能

ToDo

  • [ ] 登录注册界面
  • [ ] 报表分析界面
  • [ ] 管理员的权限鉴别
  • [ ] 模糊搜索功能

烟草仓储管理系统开发笔记(六)
http://starnight.top/2022/05/04/烟草仓储管理系统开发笔记(六)/
作者
Cardy Xie
发布于
2022年5月4日
许可协议