version :v1.0 - 2021-6-05
version :v1.1 - 2021-6-27
分离关注点 适应新需求 更高的协作要求
规划
1.维护性
- -输出**有效**文档,技术文档保证代码规范,组织架构清晰- -注释和统一模板- -框架稳定性(大厂、优秀、start多的三方框架)- -封装好基础类,保证灵活性- -MVVM针对耦合- -所有三方框架必须封装- -组件分包(模块独立,入口单一,产品业务独立性强)
2.易用性
- -上手程度- -代码可读性- -学习成本- -技术文档,官方文档
3.扩展性
- -新业务新模块,模块单独运行,每个组件都可以独立打包- -组件化优势:- 业务拆分,把人力变动风险降到最低- 降低决策风险,如果业务变动较大,及时响应- 大幅度减少编译速度- -利用设计模式- -三方开源框架的扩展性选择
4.安全性
- -混淆- -数据加密存储和传输- -操作安全性(统一规范避免程序出现一些非崩溃性的异常等)- -减少依赖,避免交叉
设计
架构示意
- 组件化

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

MVVM
- Model- ViewModel 处理数据,处理业务逻辑,一个Activity最好对应一个ViewModel,以便VM及时清除数据- View 视图层:处理界面交互逻辑,根据业务处理滑动事件,吸顶事件等,一般根据数据逻辑来
- Repository层 - 为各个ViewModel提供纯净原始数据,可复用。
- 单一数据源获取方式
1.直接从网络获取数据
2.直接从数据库获取数据
3.网络获取数据并做缓存
项目结构(按照功能分包)
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方式
工具类整合
- 已把开发中非常多常用的工具类整合到本地,并且文末有链接,有详细说明。- 如果需要编写业务相关工具类,需要归类到common下,并写好注释,方便维护和其他模块使用。
网络请求封装
- FAWUrl : 包含域名和接口名- RetrofitClient :网络请求的基本配置(域名,log拦截器,header拦截器,超时时间等)- HttpClientManager : 网络请求封装(动态切换域名)- NetworkBoundResource : 数据获取,存储,状态支持的核心类
网络结果处理
- LiveDataCallAdapter : LiveData数据转换适配器- Resource : 资源获取的状态和结果- Result : 网络请求获取的结果基础类
数据存储
- MmkvHelper : key-value组件,MMKV封装,加密存储- FCache : 封装业务相关的存储,方便获取和删除某一表- DatabaseManager :数据库管理(初始化和关闭数据库的方法,访问数据库的方法等)
权限管理
- Normal权限统一common管理- Dangerour权限在Module中管理
组件化
- 组件间通信- 清单文件合并- 统一管理依赖库的版本号- gradle配置- 资源名冲突控制:前期resourcePrefix控制- SDK初始化时机- 公共组件- 混淆:固定底层三方库在common统一管理;Module库独有的三方库独立管理。shrinkResources 使用等
事件总线
- FEventBus : 封装了 LiveEventBus 和一些常用方法
常量管理
- CacheConstant : 数据存储的key值,密钥等- Constants : 通用常量- EventConstant : 事件总线常量- RoomConstant : 数据库相关的常量- RouterConstant : 路由相关的常量
图片加载
- ImageLoader :封装Glide 以及常用方法
三方库的管理:
- 首先控件效果的组件或者自定义View,尽量抽成本地的文件放到View包下。- 一些比较复杂的效果,可以放到lib下,以3lib-xxx命名,此时要注意此lib的依赖库的问题,不要与项目依赖冲突,**也不要使用低版本比如Support等或者比较难以修改维护的依赖库。也要注意三方库的支持版本号要与项目吻合。要写好注释或者使用说明,切勿自己没有知其原理而拿到项目里,这样其他成员无法知晓和使用。**
其他备注
- 获取ApplicationContext :` FUtils.getApp()`- 网络图片加载:`ImageLoader.load() `- 关于TextView样式问题:中黑体,中粗体 都需要加粗。- 图片icon用SVG格式,大图和比较复杂的图用png格式。
性能管理(内存优化,电量优化,体积优化,布局优化)
项目相关配置和说明
数据存储
- FCache:用于存储用户信息,车辆信息相关的一些字段,标志状态等。例如名字,电话,Token,是否加载过,是否初始化过等。根据不同的业务,可以新建不同的业务表来存储,方便做修改和删除。(key-value均已加密)
DB:用于存储大量业务数据。数据库的打开在登录成功之后,关闭在用户主动退出登录或者Token异常的情况。存储的数据例如用户的所有信息,车辆的所有信息,经纬度,首页数据,列表数据等。(数据库名称已加密)
网络请求
动态配置域名:在Gradle中已经按照构建环境配置好各自的域名。只需要在FAWUrl中获取即可实现动态切换
- 动态配置业务域名:在HttpClientManager中已经将各个业务的域名配置方法暴漏出来,在重载方法中getClient(),可填写业务域名,不填则视为基础域名。
- 在Common - AppTask :实现全局数据仓库,可把多处使用的请求放在该类,并把数据存储模型等一并放到Common下。如果该请求和界面相关,那么请用LiveData作为数据包装类,反之,可使用Call。
- Token异常处理:
— 在Http请求原数据回调中,拦截509,并做路由跳转处理
— LoginInterceptor 路由登陆拦截器,处理是否需要登录,可配置优先级
- 配置自签名证书(待实现):可根据不同的域名自动切换本地证书并做校验。
- 通用后台请求并更新缓存实现(无需界面,待实现)
- 如果有先读取本地缓存友好展示到页面然后在进行网络请求的需求,那么需要用到NetworkBoundResource类,编写数据库存储方法、发起网络请求方法、数据库获取方法。
shouldFetch():该方法作用为当数据库本地没有数据的时候,是否从网络请求拉取数据。一般写法为:data == null。
数据逻辑处理
- 一般情况下,一个Activity对应一个ViewModel,一个ViewModel可以对应多个Fragment,以用作数据共享而不必通过Activity。多个Fragment通过ViewModel数据共享可参考该链接。
- 一般情况下,Repository只为数据库或者ViewModel提供原始数据或纯净数据,相关的数据转换和逻辑操作需放在ViewModel下操作。
- 注意ViewModel的生命周期要比Activity长,注意数据的回收操作和可用作Activity销毁后的数据恢复。
图片处理
- 普通的ICON,例如返回箭头,添加按钮等,切图格式为**SVG,**下载到本地目录,然后如下操作
右键项目 - New - Vector Asset - Local file - 输入正确格式的名字,选择Common 下的drawable文件夹下即可。
- 背景图,大图,复杂的图形等,切图格式为**PNG**,放到 **drawable - xxhdpi && drawable -- xxxhdpi **- 图片加载:Common下 ImageLoader 管理GlideApp加载的方法,如果不满足要求,可自行在该类添加
UI框架逻辑说明
主页面采用底部Tab和Fragment的切换。其中Fragment配备有懒加载,初始化的时候加载四个Fragment中所有的布局创建。可按需实施各个页面的数据初始化时机。底部Tab扩展性非常高,包括小红点和未读消息等。
卡友模块采用TabLayout+ViewPager2+Fragment,其中卡友在页面可见的时候初始化卡友里面的所有Fragment的布局。理由为程序启动进入主页面的时候无需加载太多内存。但是数据依然是懒加载实现。
卡友模块的上方Tab为动态获取,此时注意获取字典的时机。
数据流转核心类说明
package com.mapbar.qingqi.base.net;import com.mapbar.qingqi.base.global.ErrorCode;import com.mapbar.qingqi.base.global.NetConstant;import com.mapbar.qingqi.base.model.Resource;import com.mapbar.qingqi.base.model.Result;import com.mapbar.qingqi.base.utils.AppThreadManager;import java.util.Objects;import androidx.annotation.MainThread;import androidx.annotation.NonNull;import androidx.annotation.Nullable;import androidx.annotation.WorkerThread;import androidx.lifecycle.LiveData;import androidx.lifecycle.MediatorLiveData;import androidx.lifecycle.MutableLiveData;/*** author : Ivan* time : 2021/05/30* desc : 数据流转核心类* ResultType : 该类返回的最终数据类型* RequestType : 网络请求返回的数据类型*/public abstract class NetworkBoundResource<ResultType, RequestType> {//可以监听LiveData的数据变化private final MediatorLiveData<Resource<ResultType>> result = new MediatorLiveData<>();@MainThreadpublic NetworkBoundResource() {//在主线程初始化if (AppThreadManager.isMainThread()) {init();} else {AppThreadManager.runOnUiThread(() -> init());}}private void init() {//首次发送一个Loading状态result.setValue(Resource.loading(null));//从数据库获取数据,注意过程是异步的LiveData<ResultType> dbSource = safeLoadFromDb();//把数据库获取到的LiveData数据源加入到监听result.addSource(dbSource, data -> {//移除该监听,目的为只进入一次该方法,后续再有数据变化,不再监听result.removeSource(dbSource);//判断是否需要从网络抓取数据if (shouldFetch(data)) {//Api获取数据fetchFromNetwork(dbSource);} else {//如果不需要Api获取数据,那么只返回最新的数据库数据,这里没有remove的目的是即使多次数据变化,最终还是发送最新的数据result.addSource(dbSource, newData -> setValue(Resource.success(newData)));}});}@MainThreadprivate void setValue(Resource<ResultType> newValue) {if (!Objects.equals(result.getValue(), newValue)) {result.setValue(newValue);}}private void fetchFromNetwork(final LiveData<ResultType> dbSource) {//创建网络请求实例LiveData<RequestType> apiResponse = createCall();//监听数据库源的数据变化result.addSource(dbSource, newData -> setValue(Resource.loading(newData)));//监听Api源数据变化result.addSource(apiResponse, response -> {//移除监听,只处理一次Api数据result.removeSource(apiResponse);result.removeSource(dbSource);if (response != null) {if (response instanceof Result) {int code = ((Result) response).code;if (code != NetConstant.REQUEST_SUCCESS_CODE) {onFetchFailed();//如果请求失败,那么返回当前数据库的数据result.addSource(dbSource,newData -> setValue(Resource.error(code, newData)));return;} else {AppThreadManager.executeByIo(new AppThreadManager.SimpleTask<Object>() {@Overridepublic Object doInBackground() throws Throwable {try {//保存Api数据到DBsaveCallResult(processResponse(response));} catch (Exception e) {e.printStackTrace();}return null;}@Overridepublic void onSuccess(Object object) {//保存到DB成功之后,发送最新的DB数据result.addSource(safeLoadFromDb(),newData -> setValue(Resource.success(newData)));}});}}} else {onFetchFailed();result.addSource(dbSource,newData -> setValue(Resource.error(ErrorCode.API_ERR_OTHER.getCode(), newData)));}});}private LiveData<ResultType> safeLoadFromDb() {LiveData<ResultType> dbSource;try {dbSource = loadFromDb();} catch (Exception e) {e.printStackTrace();dbSource = new MutableLiveData<>();}return dbSource;}protected void onFetchFailed() {}/*** 返回结果数据** @return*/public LiveData<Resource<ResultType>> asLiveData() {return result;}/*** 过滤处理网络请求** @param response* @return*/@WorkerThreadprotected RequestType processResponse(RequestType response) {return response;}/*** 保存网络请求结果** @param item*/@WorkerThreadprotected abstract void saveCallResult(@NonNull RequestType item);/*** 通过请求结果判断是否需要请求网络** @param data* @return*/@MainThreadprotected abstract boolean shouldFetch(@Nullable ResultType data);/*** 从数据库中取数据** @return*/@NonNull@MainThreadprotected abstract LiveData<ResultType> loadFromDb();/*** 创建网络请求** @return*/@NonNull@MainThreadprotected abstract LiveData<RequestType> createCall();}
登陆处理
登陆状态:
1.token正常,请求正常
2.token异常,拦截token异常状态码,跳转登陆页面,登陆成功后只回到主页面。
未登录状态:
当跳转到需要登录的页面:路由拦截器拦截,直接跳转到登录页面。登录成功后返回原页面
当某个请求需要token,根据请求返回的状态码,跳转到登录页面。
网络请求状态码的处理
a.当HttpCode =200时
- Result
- code = 509 ,Token异常,跳转登陆
- code = 200,返回success,并传递code值和msg值
- code != 200 ,返回Error,并传递code和msg值
- !Result
1. 自定义transformRequestType转换类型
- body = null
1. new Result 并把 httpcode赋值
b.当HttpCode !=200时
new Result 并把 httpCode赋值,交给ErrorCode统一管理提示。
c.如果请求异常,比如超时等,会对返回的异常进行处理,并给予对应的错误码和提示信息
遇到接口有code业务操作失败的情况,要对Resource.error()进行判断处理。
2021-7-13 12:39:42 更新
取消统一错误码和错误信息的提示,改为每一个接口返回都要单独处理Message的情况,有的接口不需要处理那么可以不写
用户信息刷新处理
在登录之后,页面需要刷新时,需要用事件总线刷新用户信息,详情可见userInfo的获取逻辑。
版本管理
- 准备commit前,格式化代码,如果只修改一小部分,那么格式化这部分代码。
- 如果是组件化开发,注意切换到App壳工程整体运行,如果不能运行,则调整自己的组件直到可以整体运行为止。注意自己提交的代码切勿影响其他组件或整体运行的配置。这里需要注意三方库冲突和清单文件合并的问题。
- Update。
- 更新完有冲突,要逐个解决冲突,有必要时要找冲突文件的开发人员配合一起解决, 切勿乱动不熟悉的模块。
- commit可使用Android Studio 插件的commit,但是不要勾选commit and push
- 编写commit message ,语言简练,强调重点:
e. 然后Android Studio - Terminal 中输入 如下命令:或安装 Gerrit插件,勾选push gerrit即可。commit message:1.修复首页点击浮标Token异常问题2.User表增加字段age
f. 登陆Gerrit ,reviewed代码。git push origin 本地分支:refs/for/远端分支
附git 常用命令集合:
git push origin 本地分支:refs/for/远端分支 pushgit merge develop_wifi_pay_2 --no-ff 分支合并git reset --soft HEAD~1 回退上一版本git reset --hard 版本号 强制回退,会丢失修改git log 查看日志 (常用,可查看提交记录)git branch 列出所有本地分支git branch -r 列出所有远程分支git tag 列出所有taggit status 显示有变更的文件git reflog 记录每一次命令
Note
- 相关开源三方框架文档链接
- Android JetPack - https://developer.android.google.cn/jetpack/guide
- LiveData
https://developer.android.google.cn/topic/libraries/architecture/livedata?hl=zh_cn
- ViewModel https://developer.android.google.cn/topic/libraries/architecture/viewmodel?hl=zh_cn
- Room https://developer.android.google.cn/training/data-storage/room?hl=zh_cn
- key-value组件-MMKV
https://github.com/Tencent/MMKV
- 页面路由-Arouter
https://github.com/alibaba/ARouter
- 网络请求-Retrofit
https://square.github.io/retrofit/
- 工具类集合说明-AndroidUtilsCode
https://github.com/Blankj/AndroidUtilCode/blob/master/lib/utilcode/README-CN.md
- 事件总线-LiveEventBus
https://github.com/JeremyLiao/LiveEventBus
- 图片加载-Glide
https://github.com/bumptech/glide
- RecyclerView适配器-BaseRecyclerViewAdapterHelper
https://github.com/CymChad/BaseRecyclerViewAdapterHelper
- View注入-ButterKnife
https://github.com/JakeWharton/butterknife
- 沉浸式-ImmersionBar
https://github.com/gyf-dev/ImmersionBar
- 适配方案-AndroidAutoSize
https://github.com/JessYanCoding/AndroidAutoSize
- PDF查看方案:
https://github.com/barteksc/AndroidPdfViewer
- 下拉刷新
https://github.com/scwang90/SmartRefreshLayout 属性文档 https://github.com/scwang90/SmartRefreshLayout/blob/master/art/md_property.md
- 日历组件
https://github.com/huanghaibin-dev/CalendarView
- 指示器
https://github.com/zhpanvip/viewpagerindicator
- 选择器(时间选择,联动,自定义选择等)
https://github.com/Bigkoo/Android-PickerView
- 异步初始化启动框架
https://github.com/alibaba/alpha
[
](https://github.com/barteksc/AndroidPdfViewer)
2.技术规范相关
code_rules.md 该附件已经放到项目里
