IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Architecture(5)电商APP组件化探索,快点来白嫖 -> 正文阅读

[移动开发]Architecture(5)电商APP组件化探索,快点来白嫖

组件化缘由

记得刚开始接触Android开发的时候,只知道MVC分层架构,而且感觉Model,View以及Controller太简单了,也能称之为分层架构,随便写就是MVC。就像在接触设计模式之前,你可能已经写了无数个单例模式,只是那个时候你可能并不知道,你已经在用设计模式了,你不会去想是用DCL还是使用内部类实现的单例优雅。

后来当一个类中的代码上千行之后,就开始想着抽取公共方法作为工具类,使用封装、继承以及多态来优化自己的代码,直到随着业务的发展,在View层的逻辑越来越多,无法抽取时,发现MVC的天花板其实很低,Activity跟Fragment作为View层经常会跟Model层纠缠不清,及时进行抽取之后,也还是很臃肿。MVP的出现,彻底解决了这个问题,解耦Model层跟View层,使得整个项目的代码显得更加简洁。

在项目初期的的时候,感觉MVP还是很不错的,当项目逐渐变大的时候,每次你改动了很小的一部分,你也需要重新编译整个APP,举个例子,就拿购物车来说,我修改了数量框的样式,我需要重新编译整个APP,为了加快速度,我可能要开启InstantRun,可能要使用Freeline来加速编译,这并不是我想要的,而且在使用InstantRun之后,output目录下生成的apk是差量包,只能供开发调试,给测试是无法安装的,我要是想通过脚本上传到fir给测试人员,那又得打一个全量的包,并且InstantRun也不是很稳定。

组件化效果

毫无悬念,组件化势在必行,在网上看了很多相关的资料,对组件化有一个初步的了解,然后就开始组件化了,下面1以我自己的项目为例,放两张组件化之前跟之后的图对比一下。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tl4NVlL8-1630660709055)(https://user-gold-cdn.xitu.io/2018/1/21/161189f8e013e5f5?imageView2/0/w/1280/h/960/ignore-error/1)]

可以明显的发现我们的Module变多了,就像MVC切换到MVP之后,需要写很多的Presenter组件化最大的好处就是可以模块可以单独开发调试,这样效率一下子就上来了,还是拿购物车举例,购物车实际上就只有一个界面,也就是一个Fragment,加上启动页跟Fragment的父Activity,也就两个界面,可以说想慢都慢不下来,下面就我在组件化过程中遇到的一些问题进行总结一下。

正文

指导思想

组件拆分

组件化的目的在于将一个project划分成业务组件、基础组件、路由组件。其中业务组件是相互隔离的,可以单独调试,基础组件提供业务组件所公用的功能,路由组件为业务组件之间通信提供支持。

一般来讲,一个APP可以由一个app壳,然后集成多个Module,这是理想的情况,但是从运营的需求到产品的设计到UI出图,可能你就会对组件化很绝望,并不是那么的理想,很多时候我们程序入口所在的Module实际上跟其它很多Module是关联的,实际上没法拆分,本文将会以这种比较复杂的情况进行组件化分析。

组件隔离

组件化的一个很大的特性在于可以单独调试,但是由于业务组件之间的隔离,所以导致了多个组件之间无法进行通信,其实我觉得是很正常的,既然是单独调试,就必然不应该跟其它的Module间进行依赖,不管是编译期还是运行期都应如此,不然组件化就没有任何意义了,但是由于我们的业务组件都是相互关联的,如果不依赖其他的组件的话,作为一个单独的APP运行有时候是需要参数的,鉴于此,我们可以在Application初始化的时候,新增一个页面作为参数配置,或者直接在Application中固定写死。

核心法则

不管我们如何划分,如何依赖,组件间的关系都要严格遵守一个准则:编译器隔离,运行期按需依赖

整体架构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rInEBvxH-1630660709057)(https://user-gold-cdn.xitu.io/2018/1/21/161189f8e037580c?imageView2/0/w/1280/h/960/ignore-error/1)]

通过组件化将项目按照业务进行化分成GoodsModuleCartModuleUserModuleOrderModule四个模块,模块间通过RouterModule进行通信,也就是说业务组件依赖于路由组件,RouterModule依赖于Base,也就是BaseModuleLibraryModule,基础库跟第三方库,然后MainModule实际上相当于程序的入口跟容器,通过MainModule依赖上述四个Module,完成整个APP的打包。

当然在单独调试的时候,GoodsModuleCartModuleUserModuleOrderModule又各自成为一个APP,可以单独进行调试,这样就实现了APP的组件化,下面就组件化过程中遇到的一些问题总结一下。

组件化分析

在组件化的过程中,由于Module之间是隔离的,所以就产生了一系列问题,现在就组件化前后的遇到的问题总结如下:

  • 组件划分:如何根据业务对项目进行Module划分
  • 模式切换:如何使得APP在单独调试跟整体调试自由切换
  • 资源冲突:当我们创建了多个Module的时候,如何解决相同资源文件名合并的冲突
  • 依赖关系:多个Module之间如何引用一些共同的library以及工具类
  • 组件通信:组件化之后,Module之间是相互隔离的,如何进行UI跳转以及方法调用
  • 入口参数:我们知道组件之间是有联系的,所以在单独调试的时候如何拿到其它的Module传递过来的参数

接下来会根据这几个问题,提出对应的解决方法

组件划分

业务划分

由于我们做的是一个电商项目,网上也查找了很多资料,感觉他们举的例子都有些过于简单,因为模块间基本上没有什么耦合,所以很好拆分,不过还是很感谢他们提供了一种解决思路。玩过京东,淘宝都知道,大致分为几个大的模块:商品模块,购物车模块,订单模块,用户模块。没错,我也是这么拆分我们APP的。但是拆着拆着就发现问题了,模块间耦合性太高,我们过了SplashActivity之后就是MainActivity,看图说话

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rvbRGnON-1630660709059)(https://user-gold-cdn.xitu.io/2018/1/21/161189f8e020de07?imageView2/0/w/1280/h/960/ignore-error/1)]

所以网上的一些一进来就是一个空的APP壳的方法并不适用,从一开始就遇到了这个棘手的问题,有点尴尬,按照之前的模块划分,在用户登陆的情况下MainModule一进来就必须拿到GoodsModuleCartModule以及UserModule中的三个Fragment。所以首先必须得解决这个问题,很显然之前的使用一个APP壳来合并多个Module的情况并不适用,起初我直接定义了一个MainModule,然后让他直接引用多个Module,那么MainModule就承担了APP壳的功能,这样一来,就可以解决MainModule对其它Module的引用问题,但是违背了组件化的业务组件隔离的原则。

所以不能让MainModule依赖另外三个Module,但是如果我不引用其他的Module,那么很显然我无法拿到这四个Fragment的引用,有一点可以很明确,那就是编译期业务Module之间必须不可见,这点是毫无疑问的。但是运行期是可见的,因为所有的Module在运行期间肯定都是通过直接或者间接依赖,不然有些Module就没用了,在运行时获取实例,那么很自然地就会想到反射了,没错就是反射。

依赖划分

除了业务模块之外,我们还会有一些公用的工具类以及资源文件,也就是Base类,比如说多个Module共同使用的资源文件,我们都可以放在一个Module里面,另外就是还有第三方依赖,这里我新建了两个Module一个是BaseModule,一个是LibraryModule。整体关系如下

业务组件——>路由组件——>基础组件 

模式切换

定义开关

切换的时候需要一个开关,来表示是单个Module间运行还是多个Module间运行,很容易想到是一个布boolean类型的标志,可能你也想到了,在gradle.properties中来定义,网上好像都是这么做的,实际上我们还可以在BaseModule以及LibraryModule定义,原因很简单,只需要所有的Module中都能够访问就行了,只要遵循这个原则都是OK的,只是在gradle.properties中定义跟使用都比较方便。

isDebug=false//Debug还是Release
isModuleRun=true//是否单Module运行 

这里我不仅仅定义了isModuleRun,还定义了isDebug,是不是感觉有些奇怪,不是可以通过BuildConfig.Debug来判断当前是否是Debug模式么,因为我们的url配置信息都是写在BaseModule中以便于所有的Module调用,他是一个Library,关于Library这里还有一个问题注意下,由于Library的Module打包方式是使用release模式打包的,所以BuildConfig.Debug永远是false,所以我们需要额外定义一个变量isDebug,然后手动在Debug跟Release中进行切换,然后在BaseModule的gradle中进行判断

if (isDebug.toBoolean()) {
    //debug模式
    buildConfigField "String", "AlphaUrl", "\"${url["debug"]}\""

} else {
    //release模式
    buildConfigField "String", "AlphaUrl", "\"${url["release"]}\""

} 
使用开关
Application

isModuleRun为false的时候,Application跟AndroidManifest都是以Library的形式参与编译,不需要启动的Activity以及自定义的Application反之则需要。

isModuleRun=false

无序修改

<application
    android:allowBackup="true"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
 </application> 

isModuleRun=false

在main/debug目录下新建一个AndroidManifest.xml文件

<application
    android:name=".debug.GoodsApplication"
    android:allowBackup="true"
    android:label="@string/goods_name"
    android:supportsRtl="true"
    tools:replace="android:label"
    android:theme="@style/AppTheme">
    <activity android:name=".GoodsActivity">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />

            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
 </application> 

引用方式

在Module的gradle目录下进行引用

修改插件

if (isModuleRun.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
} 

新增applicationId

if (isModuleRun.toBoolean()) {
    applicationId "com.wustor.cartmoudle"
} 

切换AndroidManifest文件

sourceSets {
    main {
        if (isModuleRun.toBoolean()) {
            manifest.srcFile 'src/main/debug/AndroidManifest.xml'
        } else {
            manifest.srcFile 'src/main/AndroidManifest.xml'
            java {
                //全部Module一起编译的时候剔除debug目录
                exclude '**/debug/**'
            }
        }
    }
} 

资源冲突

假如我们在CartModule中定义了一个Application,然后在当前Module中的strings.xml中定义了app_name,同时在OrderModule中的strings.xml中也定义了这个app_name,那么合并你的时候就会出现冲突,我们只可以通过将上述字段分别改成cart_name跟order_name来解决这个问题,在严格的开发规范下,可以通过这种差异化命名来解决,因为不同的Module基本上资源文件的名称基本都不一样,即时冲突也是少量的冲突,很容易解决。

当然除了这种方式之外可以在build.gradle中给资源文件名添加前缀

resourcePrefix "cart_" 

可以强行检查,命名都需要价格前缀,这样反而违背了组件化的初衷,使得操作变麻烦了,不过感觉这种方式不是很有必要,当然有时候还可能出现图片名字相同,这个其实可以还原到组件化之前的项目中分析,是不可能发生的事情,所以归根到底还是没有良好的开发规范跟开发习惯造成,没必要为这种去做一些修改,毕竟约定大于配置

依赖配置

通过最开始的整体架构图可以看出来,凡是能够在Library跟Application之间进行切换的Module毫无疑问是需要依赖我们Base的两个Module的,其实可以合并成一个Module,我这里分了两个,一个是BaseModule,一个是LibraryModule。下面通过build.gradle中的配置来梳理一下他们的依赖关系:

MainModule
dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    compile project(':routermodule')
} 

编译期间组件进行隔离,所以MainModule只依赖了RouterModule,刚才说的还有在运行期按需依赖,这里是通过gradle的脚本实现控制的

//编译期组件隔离,运行期组件按需依赖
//mainModule需要跟cartModule,goodsModule,usersModule进行交互,所以在运行期添加了依赖
def tasks = project.gradle.startParameter.taskNames
for (String task : tasks) {
    def upperName = task.toUpperCase()
    if (upperName.contains("ASSEMBLE") || upperName.contains("INSTALL")) {
        dependencies.add("compile", project.project(':' + 'cartmodule'))
        dependencies.add("compile", project.project(':' + 'goodsmodule'))
        dependencies.add("compile", project.project(':' + 'usermodule'))
        dependencies.add("compile", project.project(':' + 'ordermodule'))
    }
} 
BusinessModule

这里指的是Goods/Cart/User/OrderModule,其实是平行的

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    compile project(':routermodule')
} 

业务Module依赖于RouterModule

RouterModule
dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    compile project(':modulelib')
    compile 'com.alibaba:arouter-api:1.2.1.1'
} 

RouterModule依赖了LibraryModule

BaseModule
dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    testCompile 'junit:junit:4.12'
    compile project(':librarymodule')
} 

BaseModule作为一个基础库,依赖了LibraryModule

LibraryModule

这个作为最底层的劳苦大众,实际上就是提供了一个依赖,所以就没有什么好依赖,只能自己跟自己玩儿。

所以到这里的话,基本的依赖关系已经很清楚了,知道了整个架构图,接下来进行施工也就很简单了

组件通信

其实在当初进行模块划分的时候,是根据业务来的,所以当我们进入到一个模块之后,大部分逻辑应该还是在这个模块内进行处理的,但是偶尔还是会跟别的Module进行打交道,看一个界面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UhB63xrb-1630660709062)(https://user-gold-cdn.xitu.io/2018/1/21/161189f8e008ae32?imageView2/0/w/1280/h/960/ignore-error/1)]

就拿GoodsModuleCartModule来说,这两个Module是可以进行相互跳转的,在GoodsModule的列表页面点击购物车图标可以进入到CartModule的购物车列表,购物车列表点击商品也可以进入GoodsModule的商品详情页。除了这个跳转实际上还有变量的获取,比如在首页,我需要同时获取到GoodsModule中的HomeFragment、SortFragment,CartModule中的CartFragment,UserModule中的MineFragment。我是在MainModule中直接依赖了四个业务Module,实际上可以不这样,我们也可以使用Arouter来进行获取Fragment的实例。

获取实例

其实这里的实例大多数情况下指的就是Fragment,下面以Fragment为例,别的实例如法炮制即可

  • 反射获取

由于模块间是隔离的,所以我们没办法直接创建Fragment的实例,那么这个时候其实很容易想到的就是反射,发射可谓无所不能,下面贴一下代码。

//获取Fragment实例
public static Fragment getFragment(String className) {
    Fragment fragment;
    try {
        Class fragmentClass = Class.forName(className);
        fragment = (Fragment) fragmentClass.newInstance();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
    return fragment;
} 
  • Arouter

Arouter是阿里巴巴退出的一款路由框架,在组件中进行路由操作表方便,下面举例说明

目标Fragment中加入注解

@Route(path = "cart/fragment")
public class CartFragement extends BaseFragment{
} 

在任何地方获取实例

Fragmetn fragment = (Fragment) ARouter.getInstance().build("/cart/fragment").navigation(); 
方法调用

在不同的Module之间都存在方法的调用,我们可以在每个Module里面定义一个接口,并且实现这个接口,然后在需要调用的地方获取到这个接口,然后进行方法调用即可。为了统一管理,我们把每个Module的接口都定义在RouterModule里面,然后由于各个业务Module都依赖于这个RouteModule,然后只需要通过反射获取到这个接口,进行方法调用就可以了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0NeZmnD5-1630660709064)(https://user-gold-cdn.xitu.io/2018/1/21/161189f8e03411d1?imageView2/0/w/1280/h/960/ignore-error/1)]

ModuleCall

Module之间回调的接口

public interface ModuleCall {
   //调用init方法可以传递Context参数
    void initContext(Context context);
} 

Service接口继承自ModuleCall可以定义一些回调方法供本身之外的其他Module进行调用

public interface AppService extends ModuleCall {
    //TODO 调用方法自定义
    void showHome();
    void finish();

} 

Impl实现类则是对应在每个Module中的具体回调,是实现Service接口的直接子类

public class AppServiceImpl implements AppService {
    @Override
    public void showHome() {
    }
    @Override
    public void finish() {
    }
    @Override
    public void initContext(Context context) {
    }
} 

下面还是通过反射跟Arouter两种方式进行说明

学习分享,共勉

Android高级架构师进阶之路

CodeChina开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》

题外话,我在阿里工作多年,深知技术改革和创新的方向,Android开发以其美观、快速、高效、开放等优势迅速俘获人心,但很多Android兴趣爱好者所需的进阶学习资料确实不太系统,完整。今天我把我搜集和整理的这份学习资料分享给有需要的人

  • Android进阶知识体系学习脑图

  • Android进阶高级工程师学习全套手册

  • 对标Android阿里P7,年薪50w+学习视频

  • 大厂内部Android高频面试题,以及面试经历

外话,我在阿里工作多年,深知技术改革和创新的方向,Android开发以其美观、快速、高效、开放等优势迅速俘获人心,但很多Android兴趣爱好者所需的进阶学习资料确实不太系统,完整。今天我把我搜集和整理的这份学习资料分享给有需要的人

  • Android进阶知识体系学习脑图

[外链图片转存中…(img-wZKNvg0i-1630660709064)]

  • Android进阶高级工程师学习全套手册

[外链图片转存中…(img-W9GJVpLE-1630660709065)]

  • 对标Android阿里P7,年薪50w+学习视频

[外链图片转存中…(img-TtF29xzh-1630660709066)]

  • 大厂内部Android高频面试题,以及面试经历

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-09-04 17:39:38  更:2021-09-04 17:40:37 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 12:51:49-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码