by hzwusibo 20181118
(书架重构基本按照《书架重构设计文档》来实现, 本文主要是对于设计文档的实现方案的一些补充与问题的记录。)
云阅读产品的历史比较长,书架模块没有进行过大的更新,难以扩展与维护,且其他的模块对书架模块的依赖比较严重,耦合较强,影响用户体验,后期的开发成本高。并且阅读项目是 myeclipse时代升级而来,并要之前兼容pad版本,工程结构不是android的标准工程结构。 刚好有机会,我们团队决定对书架进行一次重构,经过团队的讨论决定对项目进行一些初步的模块化。
一、客户端模块分层
将书架模块、正文模块、搜索等不相干业务相互独立,(每个模块只包含与其功能相关的内容 )通过Arouter路由进行通讯。
1、各业务模块之间不允许存在相互依赖关系,它们之间的跳转通讯采用路由框架Router来实现
2、对于业务模块, 单一业务组件只能对应某一项具体的业务,个性化需求对外部提供接口让调用方定制;
(各业务模块间肯定会有公用的部分,公用的部分我们会根据业务相关性下放到业务组件层或者基础组件层。)
1、框架服务层: 包括各种业务无关的基础的组件,如网络、图片、通讯、工具类等。
2、业务模块层:书架、阅读器等。
3、应用层:主界面的框架,组装业务模块。
好处:
1、多团队并行开发测试;
2、模块间解耦、重用;
3、可单独编译打包某一模块,提升开发效率。
在我们实现的过程中,遇到了一些问题,这是目前的初步模块化结构图,最终实现时业务层的搜索没有拆分成module,本来打算管理公共数据模型的module_common_model暂时没有使用,因为目前公用的数据模型还无法完全剥离,暂时还是放在module_base中, 另外模块通信层目前看算是各个业务依赖的中间层,通信用到的数据模型都放在了module_communication中。 后期我们会在不断的重构迭代中视情况进行进一步的拆分。
客户端模块分层后的工程结构:base基础的组件、 book_shelf书架、communication模块之间通讯等。
对业务进行模块化拆分后,为了使各业务模块间解耦,因此各个Module 都是独立的模块,它们之间是没有依赖关系,对业务进行模块化拆分后,为了使各业务模块间解耦,因此各个Module 都是独立的模块,它们之间是没有依赖关系。
模块之间的通讯通过Arouter的服务管理实现, 以书架为例
1、暴露书架对外提供的服务(声明接口,其他组件通过接口来调用书架的服务),接口统一在module_communication模块中声明,例如这里书架暴露了void addShelfBook(String id);接口。
2、在书架模块中实现外提供的服务的接口
3、其他模块调用,如购物车模块调用书架提供的加入服务。
补充: 书架模块对外接口:
二、书架模块内部架构
书架主要分为三层、 BookBean(对应数据库)、ShelfItemData(对应内存)、 ShelfModel(对应界面)
BookBean (和书架数据库一一对应, 用于网络获取、数据库存储)
ShelfItemData 列表为内存中数据(是 DB 读取,进行处理后(分组、分区、排序)的数据 list)。
ShelfModel 只用于显示(从 ShelfItemData 层生成),类似于 MVVM 中的 ViewModel。
ManagerShelf 进行对 ShelfItemData 层数据进行管理操作(置顶、分组、删除等),操作完成后更新 ShelfModel界面层与 DB 。
备注 : BookBean、ShelfItemData)、 ShelfModel中详细字段可见最后附录一
书架模块工程结构:
communication主要是书架对外接口的实现、 db数据库、 ui界面、eventbus模块内部通讯、config配置常量、 manager操作(manager分为对数据的操作、对ui的操作、对网络的操作)、util书架中专用的工具。
书架模块类图:
三、Manager中关健实现
manager的实现是书架中最重要的部分、基本按照《书架重构设计文档》进行实现,本文主要进行补充。
3.1 data包
1、 ShelfManagerUtil 是书架基本操作的专属工具类, protected static 方法(分组函数、 排序、分区、合并置顶和非置顶区数据、新建一个组、多本书加入一个组、加入一个组、移出分组等方法)
2、ShelfManager 实现书架的各种操作,如初始化数据、删除图书、移动书籍位置、置顶、取消置顶、创建组、 加入对应的组、从组中删除、判断书是否在书架上等。
void cancleTopPart(ShelfModel shelfModel){
1、ShelfManagerUtil..addTopPart(shelfItemData);
2、获取重新排列和设置新order的数据
3、更新数据库 (异步)
4、通知 显示刷新(主界面、 组内, 批量删除界面)
5、saveGroupInfo
}
ShelfManager 中流程基本相似, 以取消置顶为例,会先调用 ShelfManagerUtil工具类中的cancelTopPart方法,对内存中的数据进行操作, 内心更新后,会异步更新数据库,并且通知ui刷新与上报后端,位置变化。
3、ShelfManagerUpdate 和ShelfManager类似,主要是一些更新的操作,比如更新书籍阅读进度、更新是否是语音阅读、更新章节本地更新时间等。
4、ShelfManagerBookBean 主要是与后端有交互的业务方法如 getinfo、getdetai、addshelf等, 后端返回的Bookbean,主要流程也是后端回来bookbean, 根据条件更新内存与DB,通知界面刷新(info回来有些不一样, 直接更新DB, 在init重新读取DB到内存中)
3.2 net包
主要包括以下这几种方法:
主要注意
1、 加入书架的回调 addShelfCallBack
使用的是观察着模式,加入书架需要回调的地方添加监听, 如果发生了加入书架事件,所有注册的地方都能收到回调。
2、由于书架在主界面,网络回调回来后需要释放掉,防止内存泄露。
例如删除上报,成功后,需要remove掉网络请求。
3.3 ui包
ManagerBookManage批量操作的一些辅助工具类
ManagerBookOperate 长按书架书菜单的管理类
ShelfModelsUtil ShelfItemData 生成 ShelfModel list
ManagerShelfModel ShelfItemData 生成 ShelfModel 具体过程
四、书架操作数据库
ManagerBookDao 主要对数据库更新进行管理: 主要包括以下方法。
1、 deleteBooks(String userName,ist<BookBean> bookBeans) 删除数据
2、updateBookBean(String userName,BookBean bookbean) 更新数据
3. updateBookBeans(String userName,ist<BookBean> bookBeans) 批量更新
3、addBooks(String userName,List<BookBean> bookBeans) add数据
4、queryBooks(String userName) 查询数据
... ....
五. UI主界面设计
书架界面不是此次重构的核心,所以很多地方沿用老的设计, 对新的结构进行了适配与整理,梳理下来,主要包括以下展示的类。
越红色越重要, BookShelf为书架主要最重要的类,ShelfListAdapter和ShelfGridAdapter为书架的adapter。 BookGroupDialog为组内书的弹出框,BookGroupDialogAdapter为相应的adapter。 以上5个类是书架最核心的类。 BookOperateGroup为分组操作, BookManageActivity为批量操作, BookOperatePopWindow为书籍长按操作弹框 。 绿色的部分为推展功能, ShelfFilterPopWindow为筛选框, BookRecommendPopWindow为书架推荐。
工程结构:
BookShelf为书架主要最重要的类,adapter适配器、 Filter左上角筛选、group是组的弹窗与分组管理、menu是显示弹出的书籍主菜单、Operate书架书籍长按操作、recommend是书架推荐模块、view是书架的一些自定义view。
六. 本地书同步的处理
首先看看同步的关健步骤(同设计文档)
1、更新数据库
2、通知 显示界面重新从数据库读到内存, 再 initBookData (主界面、 组内, 批量删除界面),更新界面
本地书的处理(本期沿用老的策略):
除了本地书, 其他书根据操作时间戳的大小,更是最后的顺序(info接口不修改本地书内容), 如果info接口回来时间戳大于本地时间戳,以info回来接口为准, 更改非本地书的order值, 此时本地书的order值不会变, 排序的时候按照order来进行排序
例如, 原来本地order为 - 5 -4 -3(本地书) -2 -1 , 服务器时间戳更新,order为 -6 -5 -3 -2 , 本地书order没有被修改还是-3, 此时排序为 -6 -5 -3 -3(本地书) -2 或者 -6 -5 -3 (本地书) -3 -2 。
七、其他
7.1 断网重连后,需要同步的数据:
1、书架同步所有书籍状态:
网络状态变化后, 书架接受到广播通知 SHELF_OPERATE_DATA_UPLOAD
书架接收到通知:
a、本地操作上传服务器(savegroup等)
b、对删除操作进行上报
2、书签
3、笔记功能
4、阅读时长
7.2 书架接收到广播:
1、登陆登出
a、reLoadDataWhenSwitchAccount();切换用户数据
b、获取书架弹窗推荐
2、网络状态变为有网的操作
见7.1
3、书架fragment被选中
a、替换tab更新时间
7.3 书架跳转外部:
1、BookShelfFragment onclick
a、搜索
b、阅读历史
c、去书城页
2、ShelfFilterAdapter.OnShelfFilterItemClickListener
a、查看一本包月详情
3、长按一本书
a、详情
b、下载
4、BookManagerActivity
a、去书城
7.4 本地书同步:
Main界面监听上传书成功回调,通过书架提供的对外接口,更新书架表信息(正文与历史记录也需要做相应的更新)
7.5 离线删除上报问题:
在sp中保存未上报成功的删除json string
上报时机:
1、每次启动app
2、监听到有网络变化时候
如果sp不为null, 进行上报
上报成功后设置为null
每次删除,如果上次未上报成功,会先取出上次还未成功的json,把当前的id添加在后面。
7.6 BookState问题::
外界更改书架状态: 界面需要 BookState 字段
1、downloadOneBookAllCatalog(BookState state) 下载不使用Bookstate了 ,默认设置5%
附录一:
BookBean (和书架数据库一一对应, 用于网络获取、数据库存储)
ShelfItemData(数据处理) :
private String mSourceType;// 大的类型,包月,普通书
private int mZoneType;// 置顶或者非置顶
private boolean sGroup;//是否是一个组
private boolean isInGroup;// 是否在一个组内
private List<ShelfItemData> mChildren = new ArrayList<>();// 如果是组,组内书
private int mOrder;// 组的order为children中最小
private boolean isSorted;// 是否排过序
private int mConer;// 角标 0: 没有 1: 限时畅读 2: 节选 3: 包月
private int mBaoyueCount;// 包月的数量
private long mBaoyueExpires;// 包月到期的时间
private String mid;// Id
private String mTitle;// 标题
private String mCover;// 封面
private String mAuthor;// 作者
private String mAnchor;// 播者
private int mPublicType;// 书籍发布类型: -1: 未同步过得本地书 0: 原创书 1 :出饭 2 : 同步过得本地书 21: 音频书
private boolean isLocalBook;// 是否是本地书
private boolean isUploadBook;// 是否是上传书
private boolean isAudioBook;// 是否是音频书
private String mUpdateTitle;// 最新的章节标题
private int mUpdateCount;// 更新的章节总数
private int mTotalCount;// 总章节数
private long mBookUpdateTime;// 书籍更新时间
private long mBookUpdateTimeLocal;// 服务端获取的书籍更新时间
private float mTotalPercent;// 整本书阅读的百分比
private int mReadCount;// 已读章节
private long mProgressTime;// 进度更新时间
private String mGroupId;// groupid
private String mGroupName;// groupName
private long mSubRecordTime;// 什么时候订阅的时间戳,与数据库中record_time一致
private int mBookFreeReadFromNewWelfare;// 1表示来自新手福利,此时不需要显示免费角标
private int mIsRecommendBook;// 书架书籍是否是推荐书籍: 0 不是 1 是
private int isVoiceReading; // 0: 不是语言阅读 1: 是语言阅读
private int isReading;// 客户端字段是否已经阅读 0: 没有阅读过,取服务端时间 1: 已经阅读过,不在取服务端时间
private int integrity;// 是否连载完成 1: 完结 3: 连载
public int updateState;
private int mBookType; // 书籍来源
private int mOpenBookType = -1; // 打开书籍的type 0普通书,1杂志, 2pdf 默认-1
ShelfModel(显示) :
private String mid;
private List<ShelfModel> mChildren = new ArrayList<>();// 如果是个组,组内书
private String mCover;// 封面的url
private String mTitleText;// 标题
private String mListSecondText;// 作者,播者,或者更新章节名,共几本
private String mListThirdText;// 进度,包月剩余时间/已过期
private String mGridSecondText;// 进度,共几本
private String mRemindUpdateCount;// 更新章节
private Drawable mConerRes;// 角标资源,没有为-1
private boolean mChecked;// 是否被选中
private boolean showTopTag;// 是否显示置顶图标
private boolean isAudioBook;// 是否显示音频书图标
private boolean isTtsPlayer;// 是否tts继续播放图标
private String mSourceType;// 大的类型,包月,普通书
private boolean isGroup;// 是否是一个组
private boolean isInGroup;// 是否在一个组内
private boolean isShowDownloadProgress;// 是否显示下载进度
private int mPublicType; // 书籍发布类型 -1 (未同步的本地书) 0 (原创)、 1 (出版) 、 2 (已同步的本地书) 21(音频书)
private long mBookUpdateTimeLocal;// 服务端获取的书籍
private int mOpenBookType = -1; // 打开书籍的type 0普通书,1杂志, 2pdf 默认-1
书架全局变量:BookShelfConfig(SharedPreferences)
private static final String IS_LIST_MODE = "is_list_mode"; // 列表模式 1 还是 grid模式 0
private static final String IS_LOCAL_BOOK_MODE = "is_localbook_mode"; // 本地书模式 1 还是 全部书模式 0
常量:
/**
* 书架显示模式
*/
public static final int TYPE_GRID_MODE = 0; // grid模式
public static final int TYPE_LIST_MODE = 1; // 列表模式
/**
* 书架状态模式
*/
public static final int TYPE_ALL_BOOK_MODE = 0; // 全部书模式
public static final int TYPE_LOCAL_BOOK_MODE = 1; // 本地书模式
/**
* 书架 筛选类型
*/
public static final int TYPE_FILTER_ALL = 0; // 所有书
public static final int TYPE_FILTER_LOCAL = 1; // 本地书
public static final int TYPE_FILTER_BAOYUE = 2;// 包月
public static final int TYPE_FILTER_GROUP = 3; // 组
public static final int TYPE_FILTER_TITLE_BAOYUE = 4;// 包月TITLE
public static final int TYPE_FILTER_TITLE_GROUP = 5; // 组TITLE
/**
* 是否置顶区
*/
public static final int TYPE_COMMON = 0;// 非置顶区
public static final int TYPE_TOP = 1;// 置顶区
/**
* 大的类型,包月,普通书
*/
public static final String BOOK_SOURCE_TYPE_BAOYUE = "monthly"; // 包月
public static final String BOOK_SOURCE_TYPE_BOOK = "book";// 书、组
/**
* 书的类型
*/
public static final int BOOK_SMALL_TYPE_NOT_SYNC_LOCAL = -1; // 自定义的 未同步过的本地书
public static final int BOOK_SMALL_TYPE_ORIGINAL_BOOK = 0;// 原创
public static final int BOOK_SMALL_TYPE_PUBLIC_BOOK = 1;// 出版
public static final int BOOK_SMALL_TYPE_SYNC_LOCAL = 2;// 同步过的本地书
public static final int BOOK_SMALL_TYPE_AUDIO_BOOK = 21;// 音频书
/**
* 书的来源
*/
public static final int BOOK_TYPE_NORMAL = 0; // 普通
public static final int BOOK_TYPE_CHINAMOBILE = 7;// 咪咕
public static final int BOOK_TYPE_AUDIO = 21;// 有声书
/**
* 角标
*/
public static final int BOOK_CORNER_TYPE_NULL = 0;// 没有角标
public static final int BOOK_CORNER_TYPE_CHANGDU = 1;//限时畅读
public static final int BOOK_CORNER_TYPE_JIEXUAN = 2;// 节选
public static final int BOOK_CORNER_TYPE_BAOYUE = 3;// 包月
/**
* 书籍是否完结 isFinish
*/
public static final int BOOK_END = 1; //完结
public static final int BOOK_NOT_END = 3; //连载
/**
* 分组操作
*/
public static final int TYPE_GROUP_REMOVE_GROUP = 0; //表示不分组(全部内容)
public static final int TYPE_GROUP_RECOMMAND_CREATE_NEW_GROUP = 1;//表示一个推荐的组,去新建
public static final int TYPE_GROUP_RECOMMAND_MERGE_GROUP = 2;//表示已存在一个推荐的粗,去合并操作
/**
* 书架item 长按操作
*/
public static final int TYPE_LONG_TOP = 1; //置顶
public static final int TYPE_LONG_GROUP = 2; //分组
public static final int TYPE_LONG_BOOK_DETAIL = 3;//书籍详情
public static final int TYPE_LONG_BOOK_BAOYUE_DETAIL = 4;//包月详情
public static final int TYPE_LONG_DOWNLOAD = 5; //下载
public static final int TYPE_LONG_DELETE = 6; //删除
public static final int TYPE_LONG_EDIT_GROUP_NAME = 7; //编辑组名
public static final int TYPE_LONG_CANCLE_TOP = 8; //取消置顶
public static final int TYPE_LONG_REORDER = 10; //排序
/**
* 书架item 下载状态
*/
public static final int TYPE_ITEM_NEED_DOWNLOAD = 1; //需要下载
public static final int TYPE_ITEM_DOWNLOADING = 2; //正在下载在中
public static final int TYPE_ITEM_STOP_DOWNLOAD = 3;//暂停下载
public static final int TYPE_ITEM_FINISH_DOWNLOAD = 4;//下载完成
/**
* 删除时更新数据库状态
*/
public static final int REAL_DELETE = 2; //确实需要删除数据
public static final int REAL_UPADTE = 1; //只需要更新组状态
/**
* 主编推荐
*/
public static final int TYPE_RECOMMEND_BOOK = 1; //主编推荐
public static final int TYPE_NO_RECOMMEND_BOOK = 0; //正常书