version :v1.0 - 2021-6-05
version :v1.1 - 2021-6-27

分离关注点 适应新需求 更高的协作要求

规划

1.维护性

  1. - -输出**有效**文档,技术文档保证代码规范,组织架构清晰
  2. - -注释和统一模板
  3. - -框架稳定性(大厂、优秀、start多的三方框架)
  4. - -封装好基础类,保证灵活性
  5. - -MVVM针对耦合
  6. - -所有三方框架必须封装
  7. - -组件分包(模块独立,入口单一,产品业务独立性强)

2.易用性

  1. - -上手程度
  2. - -代码可读性
  3. - -学习成本
  4. - -技术文档,官方文档

3.扩展性

  1. - -新业务新模块,模块单独运行,每个组件都可以独立打包
  2. - -组件化优势:
  3. - 业务拆分,把人力变动风险降到最低
  4. - 降低决策风险,如果业务变动较大,及时响应
  5. - 大幅度减少编译速度
  6. - -利用设计模式
  7. - -三方开源框架的扩展性选择

4.安全性

  1. - -混淆
  2. - -数据加密存储和传输
  3. - -操作安全性(统一规范避免程序出现一些非崩溃性的异常等)
  4. - -减少依赖,避免交叉

设计

架构示意

  • 组件化

组件化架构.png

  • 该方案是把有独立业务产品形态独立出来,业务可上可下,不影响其他模块,随时在代码上插拔
  • 其二方案是在common中集成所有的独立模块,但是要在初始化上做优先级。
  • App壳工程负责管理各个业务组件和配置一些整体打包的功能。
  • common模块是在开发层面最底层,是支持业务组件的基础,比如提供网络,缓存等。其中Res的作用是分担common模块压力
  • Base层为基类封装和远程库依赖,与业务无关
  • MVVM基本交互

Android - 解放行新版架构 - 图2

MVVM

  1. - Model
  2. - ViewModel 处理数据,处理业务逻辑,一个Activity最好对应一个ViewModel,以便VM及时清除数据
  3. - View 视图层:处理界面交互逻辑,根据业务处理滑动事件,吸顶事件等,一般根据数据逻辑来
  • Repository层 - 为各个ViewModel提供纯净原始数据,可复用。
  • 单一数据源获取方式

1.直接从网络获取数据
2.直接从数据库获取数据
3.网络获取数据并做缓存
网络请求并缓存.png

项目结构(按照功能分包)

DriverAndroid .gradle(忽略) .idea(忽略) build(忽略) app(壳工程) library(公共组件) -base(业务无关) -common(与业务紧密相关) -3lib-gallery(图片相册选择库) -3lib-map(百度地图服务库) -3lib-calendar(日历库) module(业务Module文件夹) -login(登陆组件) -main(负责项目启动组件) -info(资讯组件) -add(加车,实名认证组件) -car(爱车组件) -control(控车组件) -friends(卡友组件) -mine(我的组件) -monitor(实时监控组件) -msg(消息组件) -service(一键服务组件)

build.gradle(项目工程配置) module.build.gradle(各个组件的gradle基类配置) config.gradle(管理依赖库版本和基础配置) code_rules(Android开发规范)


开发实现

抽象接口

基类
-BaseRootActivity (Butterknife初始化,固定屏幕方向,防抖)
-BaseActivity(状态栏,网络情况监听,loading)
initTitle(): Title的操作
initView():view初始化
initData(): 数据初始化
initVM(): viewModel 初始化和监听
showLoadingDialog : Loading展示
dismissLoadingDialog : 取消loading
-BaseRootFragment
-BaseFragment(懒加载)常用于Viewpager2的结合使用
-BaseSHFragment (Show | Hide)常用于Fragment的Show 和 Hide方式

工具类整合

  1. - 已把开发中非常多常用的工具类整合到本地,并且文末有链接,有详细说明。
  2. - 如果需要编写业务相关工具类,需要归类到common下,并写好注释,方便维护和其他模块使用。

网络请求封装

  1. - FAWUrl 包含域名和接口名
  2. - RetrofitClient :网络请求的基本配置(域名,log拦截器,header拦截器,超时时间等)
  3. - HttpClientManager 网络请求封装(动态切换域名)
  4. - NetworkBoundResource 数据获取,存储,状态支持的核心类

网络结果处理

  1. - LiveDataCallAdapter LiveData数据转换适配器
  2. - Resource 资源获取的状态和结果
  3. - Result 网络请求获取的结果基础类

数据存储

  1. - MmkvHelper key-value组件,MMKV封装,加密存储
  2. - FCache 封装业务相关的存储,方便获取和删除某一表
  3. - DatabaseManager :数据库管理(初始化和关闭数据库的方法,访问数据库的方法等)

权限管理

  1. - Normal权限统一common管理
  2. - Dangerour权限在Module中管理

组件化

  1. - 组件间通信
  2. - 清单文件合并
  3. - 统一管理依赖库的版本号
  4. - gradle配置
  5. - 资源名冲突控制:前期resourcePrefix控制
  6. - SDK初始化时机
  7. - 公共组件
  8. - 混淆:固定底层三方库在common统一管理;Module库独有的三方库独立管理。shrinkResources 使用等

事件总线

  1. - FEventBus 封装了 LiveEventBus 和一些常用方法

常量管理

  1. - CacheConstant 数据存储的key值,密钥等
  2. - Constants 通用常量
  3. - EventConstant 事件总线常量
  4. - RoomConstant 数据库相关的常量
  5. - RouterConstant 路由相关的常量

图片加载

  1. - ImageLoader :封装Glide 以及常用方法

三方库的管理:

  1. - 首先控件效果的组件或者自定义View,尽量抽成本地的文件放到View包下。
  2. - 一些比较复杂的效果,可以放到lib下,以3lib-xxx命名,此时要注意此lib的依赖库的问题,不要与项目依赖冲突,**也不要使用低版本比如Support等或者比较难以修改维护的依赖库。也要注意三方库的支持版本号要与项目吻合。要写好注释或者使用说明,切勿自己没有知其原理而拿到项目里,这样其他成员无法知晓和使用。**

其他备注

  1. - 获取ApplicationContext ` FUtils.getApp()`
  2. - 网络图片加载:`ImageLoader.load() `
  3. - 关于TextView样式问题:中黑体,中粗体 都需要加粗。
  4. - 图片iconSVG格式,大图和比较复杂的图用png格式。

性能管理(内存优化,电量优化,体积优化,布局优化)


项目相关配置和说明

数据存储

  1. FCache:用于存储用户信息,车辆信息相关的一些字段,标志状态等。例如名字,电话,Token,是否加载过,是否初始化过等。根据不同的业务,可以新建不同的业务表来存储,方便做修改和删除。(key-value均已加密)
  2. DB:用于存储大量业务数据。数据库的打开在登录成功之后,关闭在用户主动退出登录或者Token异常的情况。存储的数据例如用户的所有信息,车辆的所有信息,经纬度,首页数据,列表数据等。(数据库名称已加密)

    网络请求

  3. 动态配置域名:在Gradle中已经按照构建环境配置好各自的域名。只需要在FAWUrl中获取即可实现动态切换

  4. 动态配置业务域名:在HttpClientManager中已经将各个业务的域名配置方法暴漏出来,在重载方法中getClient(),可填写业务域名,不填则视为基础域名。
  5. 在Common - AppTask :实现全局数据仓库,可把多处使用的请求放在该类,并把数据存储模型等一并放到Common下。如果该请求和界面相关,那么请用LiveData作为数据包装类,反之,可使用Call。
  6. Token异常处理:

— 在Http请求原数据回调中,拦截509,并做路由跳转处理
LoginInterceptor 路由登陆拦截器,处理是否需要登录,可配置优先级

  1. 配置自签名证书(待实现):可根据不同的域名自动切换本地证书并做校验。
  2. 通用后台请求并更新缓存实现(无需界面,待实现)
  3. 如果有先读取本地缓存友好展示到页面然后在进行网络请求的需求,那么需要用到NetworkBoundResource类,编写数据库存储方法、发起网络请求方法、数据库获取方法。
  4. shouldFetch():该方法作用为当数据库本地没有数据的时候,是否从网络请求拉取数据。一般写法为:data == null。

    数据逻辑处理

    • 一般情况下,一个Activity对应一个ViewModel,一个ViewModel可以对应多个Fragment,以用作数据共享而不必通过Activity。多个Fragment通过ViewModel数据共享可参考该链接
    • 一般情况下,Repository只为数据库或者ViewModel提供原始数据或纯净数据,相关的数据转换和逻辑操作需放在ViewModel下操作。
    • 注意ViewModel的生命周期要比Activity长,注意数据的回收操作和可用作Activity销毁后的数据恢复。

图片处理

  1. - 普通的ICON,例如返回箭头,添加按钮等,切图格式为**SVG,**下载到本地目录,然后如下操作

右键项目 - New - Vector Asset - Local file - 输入正确格式的名字,选择Common 下的drawable文件夹下即可。

  1. - 背景图,大图,复杂的图形等,切图格式为**PNG**,放到 **drawable - xxhdpi && drawable -- xxxhdpi **
  2. - 图片加载:Common ImageLoader 管理GlideApp加载的方法,如果不满足要求,可自行在该类添加

UI框架逻辑说明

主页面采用底部Tab和Fragment的切换。其中Fragment配备有懒加载,初始化的时候加载四个Fragment中所有的布局创建。可按需实施各个页面的数据初始化时机。底部Tab扩展性非常高,包括小红点和未读消息等。
卡友模块采用TabLayout+ViewPager2+Fragment,其中卡友在页面可见的时候初始化卡友里面的所有Fragment的布局。理由为程序启动进入主页面的时候无需加载太多内存。但是数据依然是懒加载实现。
卡友模块的上方Tab为动态获取,此时注意获取字典的时机。

数据流转核心类说明

  1. package com.mapbar.qingqi.base.net;
  2. import com.mapbar.qingqi.base.global.ErrorCode;
  3. import com.mapbar.qingqi.base.global.NetConstant;
  4. import com.mapbar.qingqi.base.model.Resource;
  5. import com.mapbar.qingqi.base.model.Result;
  6. import com.mapbar.qingqi.base.utils.AppThreadManager;
  7. import java.util.Objects;
  8. import androidx.annotation.MainThread;
  9. import androidx.annotation.NonNull;
  10. import androidx.annotation.Nullable;
  11. import androidx.annotation.WorkerThread;
  12. import androidx.lifecycle.LiveData;
  13. import androidx.lifecycle.MediatorLiveData;
  14. import androidx.lifecycle.MutableLiveData;
  15. /**
  16. * author : Ivan
  17. * time : 2021/05/30
  18. * desc : 数据流转核心类
  19. * ResultType : 该类返回的最终数据类型
  20. * RequestType : 网络请求返回的数据类型
  21. */
  22. public abstract class NetworkBoundResource<ResultType, RequestType> {
  23. //可以监听LiveData的数据变化
  24. private final MediatorLiveData<Resource<ResultType>> result = new MediatorLiveData<>();
  25. @MainThread
  26. public NetworkBoundResource() {
  27. //在主线程初始化
  28. if (AppThreadManager.isMainThread()) {
  29. init();
  30. } else {
  31. AppThreadManager.runOnUiThread(() -> init());
  32. }
  33. }
  34. private void init() {
  35. //首次发送一个Loading状态
  36. result.setValue(Resource.loading(null));
  37. //从数据库获取数据,注意过程是异步的
  38. LiveData<ResultType> dbSource = safeLoadFromDb();
  39. //把数据库获取到的LiveData数据源加入到监听
  40. result.addSource(dbSource, data -> {
  41. //移除该监听,目的为只进入一次该方法,后续再有数据变化,不再监听
  42. result.removeSource(dbSource);
  43. //判断是否需要从网络抓取数据
  44. if (shouldFetch(data)) {
  45. //Api获取数据
  46. fetchFromNetwork(dbSource);
  47. } else {
  48. //如果不需要Api获取数据,那么只返回最新的数据库数据,这里没有remove的目的是即使多次数据变化,最终还是发送最新的数据
  49. result.addSource(dbSource, newData -> setValue(Resource.success(newData)));
  50. }
  51. });
  52. }
  53. @MainThread
  54. private void setValue(Resource<ResultType> newValue) {
  55. if (!Objects.equals(result.getValue(), newValue)) {
  56. result.setValue(newValue);
  57. }
  58. }
  59. private void fetchFromNetwork(final LiveData<ResultType> dbSource) {
  60. //创建网络请求实例
  61. LiveData<RequestType> apiResponse = createCall();
  62. //监听数据库源的数据变化
  63. result.addSource(dbSource, newData -> setValue(Resource.loading(newData)));
  64. //监听Api源数据变化
  65. result.addSource(apiResponse, response -> {
  66. //移除监听,只处理一次Api数据
  67. result.removeSource(apiResponse);
  68. result.removeSource(dbSource);
  69. if (response != null) {
  70. if (response instanceof Result) {
  71. int code = ((Result) response).code;
  72. if (code != NetConstant.REQUEST_SUCCESS_CODE) {
  73. onFetchFailed();
  74. //如果请求失败,那么返回当前数据库的数据
  75. result.addSource(dbSource,
  76. newData -> setValue(Resource.error(code, newData)));
  77. return;
  78. } else {
  79. AppThreadManager.executeByIo(new AppThreadManager.SimpleTask<Object>() {
  80. @Override
  81. public Object doInBackground() throws Throwable {
  82. try {
  83. //保存Api数据到DB
  84. saveCallResult(processResponse(response));
  85. } catch (Exception e) {
  86. e.printStackTrace();
  87. }
  88. return null;
  89. }
  90. @Override
  91. public void onSuccess(Object object) {
  92. //保存到DB成功之后,发送最新的DB数据
  93. result.addSource(safeLoadFromDb(),
  94. newData -> setValue(Resource.success(newData)));
  95. }
  96. });
  97. }
  98. }
  99. } else {
  100. onFetchFailed();
  101. result.addSource(dbSource,
  102. newData -> setValue(Resource.error(ErrorCode.API_ERR_OTHER.getCode(), newData)));
  103. }
  104. });
  105. }
  106. private LiveData<ResultType> safeLoadFromDb() {
  107. LiveData<ResultType> dbSource;
  108. try {
  109. dbSource = loadFromDb();
  110. } catch (Exception e) {
  111. e.printStackTrace();
  112. dbSource = new MutableLiveData<>();
  113. }
  114. return dbSource;
  115. }
  116. protected void onFetchFailed() {
  117. }
  118. /**
  119. * 返回结果数据
  120. *
  121. * @return
  122. */
  123. public LiveData<Resource<ResultType>> asLiveData() {
  124. return result;
  125. }
  126. /**
  127. * 过滤处理网络请求
  128. *
  129. * @param response
  130. * @return
  131. */
  132. @WorkerThread
  133. protected RequestType processResponse(RequestType response) {
  134. return response;
  135. }
  136. /**
  137. * 保存网络请求结果
  138. *
  139. * @param item
  140. */
  141. @WorkerThread
  142. protected abstract void saveCallResult(@NonNull RequestType item);
  143. /**
  144. * 通过请求结果判断是否需要请求网络
  145. *
  146. * @param data
  147. * @return
  148. */
  149. @MainThread
  150. protected abstract boolean shouldFetch(@Nullable ResultType data);
  151. /**
  152. * 从数据库中取数据
  153. *
  154. * @return
  155. */
  156. @NonNull
  157. @MainThread
  158. protected abstract LiveData<ResultType> loadFromDb();
  159. /**
  160. * 创建网络请求
  161. *
  162. * @return
  163. */
  164. @NonNull
  165. @MainThread
  166. protected abstract LiveData<RequestType> createCall();
  167. }

登陆处理

登陆状态:
1.token正常,请求正常
2.token异常,拦截token异常状态码,跳转登陆页面,登陆成功后只回到主页面。
未登录状态:
当跳转到需要登录的页面:路由拦截器拦截,直接跳转到登录页面。登录成功后返回原页面
当某个请求需要token,根据请求返回的状态码,跳转到登录页面。

网络请求状态码的处理

a.当HttpCode =200时

  • Result
    1. code = 509 ,Token异常,跳转登陆
    2. code = 200,返回success,并传递code值和msg值
    3. code != 200 ,返回Error,并传递code和msg值
  • !Result
    1. 1. 自定义transformRequestType转换类型
  • body = null
    1. 1. new Result 并把 httpcode赋值

b.当HttpCode !=200时
new Result 并把 httpCode赋值,交给ErrorCode统一管理提示。

c.如果请求异常,比如超时等,会对返回的异常进行处理,并给予对应的错误码和提示信息

遇到接口有code业务操作失败的情况,要对Resource.error()进行判断处理。
2021-7-13 12:39:42 更新
取消统一错误码和错误信息的提示,改为每一个接口返回都要单独处理Message的情况,有的接口不需要处理那么可以不写

用户信息刷新处理

在登录之后,页面需要刷新时,需要用事件总线刷新用户信息,详情可见userInfo的获取逻辑。


版本管理

  1. 准备commit前,格式化代码,如果只修改一小部分,那么格式化这部分代码。
  2. 如果是组件化开发,注意切换到App壳工程整体运行,如果不能运行,则调整自己的组件直到可以整体运行为止。注意自己提交的代码切勿影响其他组件或整体运行的配置。这里需要注意三方库冲突和清单文件合并的问题。
  3. Update。
  4. 更新完有冲突,要逐个解决冲突,有必要时要找冲突文件的开发人员配合一起解决, 切勿乱动不熟悉的模块。
  5. commit可使用Android Studio 插件的commit,但是不要勾选commit and push
  6. 编写commit message ,语言简练,强调重点:
    1. commit message
    2. 1.修复首页点击浮标Token异常问题
    3. 2.User表增加字段age
    e. 然后Android Studio - Terminal 中输入 如下命令:或安装 Gerrit插件,勾选push gerrit即可。
    1. git push origin 本地分支:refs/for/远端分支
    f. 登陆Gerrit ,reviewed代码。

附git 常用命令集合:

  1. git push origin 本地分支:refs/for/远端分支 push
  2. git merge develop_wifi_pay_2 --no-ff 分支合并
  3. git reset --soft HEAD~1 回退上一版本
  4. git reset --hard 版本号 强制回退,会丢失修改
  5. git log 查看日志 (常用,可查看提交记录)
  6. git branch 列出所有本地分支
  7. git branch -r 列出所有远程分支
  8. git tag 列出所有tag
  9. git status 显示有变更的文件
  10. git reflog 记录每一次命令

Note

  1. 相关开源三方框架文档链接

](https://github.com/barteksc/AndroidPdfViewer)

2.技术规范相关

code_rules.md 该附件已经放到项目里